diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d8954fc40..5bba2b7760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Unreleased * Feat: Add Culture Context (#491) + +## Breaking Changes: + +* Feat: Support envelope based transport for events (#391) + * The method signature of `Transport` changed from `Future send(SentryEvent event)` to `Future send(SentryEnvelope envelope)` * Remove `Sentry.currentHub` (#490) # 5.1.0 @@ -32,6 +37,10 @@ * Feature: Add `withScope` callback to capture methods (#463) * Fix: Add missing properties `language`, `screenHeightPixels` and `screenWidthPixels` to `SentryDevice` (#465) +## Sentry Self Hosted Compatibility + +* This version of the `sentry` Dart package requires [Sentry server >= v20.6.0](https://github.com/getsentry/onpremise/releases). This only applies to on-premise Sentry, if you are using sentry.io no action is needed. + # 5.0.0 * Sound null safety diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 156a9d0752..dfdb9ccc0d 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -13,6 +13,7 @@ export 'src/noop_isolate_error_integration.dart' export 'src/protocol.dart'; export 'src/scope.dart'; export 'src/sentry.dart'; +export 'src/sentry_envelope.dart'; export 'src/sentry_client.dart'; export 'src/sentry_options.dart'; // useful for integrations diff --git a/dart/lib/src/noop_sentry_client.dart b/dart/lib/src/noop_sentry_client.dart index be34d6a18f..40eb864597 100644 --- a/dart/lib/src/noop_sentry_client.dart +++ b/dart/lib/src/noop_sentry_client.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'protocol.dart'; import 'scope.dart'; import 'sentry_client.dart'; +import 'sentry_envelope.dart'; class NoOpSentryClient implements SentryClient { NoOpSentryClient._(); @@ -42,6 +43,10 @@ class NoOpSentryClient implements SentryClient { }) => Future.value(SentryId.empty()); + @override + Future captureEnvelope(SentryEnvelope envelope) => + Future.value(SentryId.empty()); + @override Future close() async { return; diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index b07460ce8f..cbbfa7cbd7 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -76,6 +76,20 @@ class Breadcrumb { /// The value is submitted to Sentry with second precision. final DateTime timestamp; + /// Deserializes a [Breadcrumb] from JSON [Map]. + factory Breadcrumb.fromJson(Map json) { + final levelName = json['level']; + final timestamp = json['timestamp']; + return Breadcrumb( + timestamp: timestamp != null ? DateTime.tryParse(timestamp) : null, + message: json['message'], + category: json['category'], + data: json['data'], + level: levelName != null ? SentryLevel.fromName(levelName) : null, + type: json['type'], + ); + } + /// Converts this breadcrumb to a map that can be serialized to JSON according /// to the Sentry protocol. Map toJson() { diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index de4f34befc..880e3bbc04 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -27,6 +27,7 @@ class Contexts extends MapView { SentryCulture.type: culture, }); + /// Deserializes [Contexts] from JSON [Map]. factory Contexts.fromJson(Map data) { final contexts = Contexts( device: data[SentryDevice.type] != null diff --git a/dart/lib/src/protocol/debug_image.dart b/dart/lib/src/protocol/debug_image.dart index 4fd3eb1972..b75dea5bb0 100644 --- a/dart/lib/src/protocol/debug_image.dart +++ b/dart/lib/src/protocol/debug_image.dart @@ -50,6 +50,22 @@ class DebugImage { this.codeId, }); + /// Deserializes a [DebugImage] from JSON [Map]. + factory DebugImage.fromJson(Map json) { + return DebugImage( + type: json['type'], + imageAddr: json['image_addr'], + debugId: json['debug_id'], + debugFile: json['debug_file'], + imageSize: json['image_size'], + uuid: json['uuid'], + codeFile: json['code_file'], + arch: json['arch'], + codeId: json['code_id'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/debug_meta.dart b/dart/lib/src/protocol/debug_meta.dart index 1b6aebd993..24d8183d54 100644 --- a/dart/lib/src/protocol/debug_meta.dart +++ b/dart/lib/src/protocol/debug_meta.dart @@ -18,6 +18,20 @@ class DebugMeta { DebugMeta({this.sdk, List? images}) : _images = images; + /// Deserializes a [DebugMeta] from JSON [Map]. + factory DebugMeta.fromJson(Map json) { + final sdkInfoJson = json['sdk_info']; + final debugImagesJson = json['images'] as List?; + return DebugMeta( + sdk: sdkInfoJson != null ? SdkInfo.fromJson(sdkInfoJson) : null, + images: debugImagesJson + ?.map((debugImageJson) => + DebugImage.fromJson(debugImageJson as Map)) + .toList(), + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/dsn.dart b/dart/lib/src/protocol/dsn.dart index 9f60755b40..c3ec5093c8 100644 --- a/dart/lib/src/protocol/dsn.dart +++ b/dart/lib/src/protocol/dsn.dart @@ -43,7 +43,7 @@ class Dsn { apiPath = 'api'; } return Uri.parse( - '${uriCopy.scheme}://${uriCopy.host}$port/$apiPath/$projectId/store/', + '${uriCopy.scheme}://${uriCopy.host}$port/$apiPath/$projectId/envelope/', ); } diff --git a/dart/lib/src/protocol/mechanism.dart b/dart/lib/src/protocol/mechanism.dart index 69b6e0320b..8d8977b97c 100644 --- a/dart/lib/src/protocol/mechanism.dart +++ b/dart/lib/src/protocol/mechanism.dart @@ -74,6 +74,20 @@ class Mechanism { synthetic: synthetic ?? this.synthetic, ); + /// Deserializes a [Mechanism] from JSON [Map]. + factory Mechanism.fromJson(Map json) { + return Mechanism( + type: json['type'], + description: json['description'], + helpLink: json['help_link'], + handled: json['handled'], + meta: json['meta'], + data: json['data'], + synthetic: json['synthetic'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sdk_info.dart b/dart/lib/src/protocol/sdk_info.dart index a58ab63e73..a3434afd83 100644 --- a/dart/lib/src/protocol/sdk_info.dart +++ b/dart/lib/src/protocol/sdk_info.dart @@ -15,6 +15,17 @@ class SdkInfo { this.versionPatchlevel, }); + /// Deserializes a [SdkInfo] from JSON [Map]. + factory SdkInfo.fromJson(Map json) { + return SdkInfo( + sdkName: json['sdk_name'], + versionMajor: json['version_major'], + versionMinor: json['version_minor'], + versionPatchlevel: json['version_patchlevel'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; if (sdkName != null) { diff --git a/dart/lib/src/protocol/sdk_version.dart b/dart/lib/src/protocol/sdk_version.dart index dc4a7c9ad3..b69ca3c130 100644 --- a/dart/lib/src/protocol/sdk_version.dart +++ b/dart/lib/src/protocol/sdk_version.dart @@ -63,6 +63,20 @@ class SdkVersion { String get identifier => '$name/$version'; + /// Deserializes a [SdkVersion] from JSON [Map]. + factory SdkVersion.fromJson(Map json) { + final packagesJson = json['packages'] as List?; + final integrationsJson = json['integrations'] as List?; + return SdkVersion( + name: json['name'], + version: json['version'], + packages: packagesJson + ?.map((e) => SentryPackage.fromJson(e as Map)) + .toList(), + integrations: integrationsJson?.map((e) => e as String).toList(), + ); + } + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_app.dart b/dart/lib/src/protocol/sentry_app.dart index 1e4c87ecbe..90581e1fb4 100644 --- a/dart/lib/src/protocol/sentry_app.dart +++ b/dart/lib/src/protocol/sentry_app.dart @@ -18,18 +18,6 @@ class SentryApp { this.deviceAppHash, }); - factory SentryApp.fromJson(Map data) => SentryApp( - name: data['app_name'], - version: data['app_version'], - identifier: data['app_identifier'], - build: data['app_build'], - buildType: data['build_type'], - startTime: data['app_start_time'] != null - ? DateTime.tryParse(data['app_start_time']) - : null, - deviceAppHash: data['device_app_hash'], - ); - /// Human readable application name, as it appears on the platform. final String? name; @@ -51,6 +39,19 @@ class SentryApp { /// Application specific device identifier. final String? deviceAppHash; + /// Deserializes a [SentryApp] from JSON [Map]. + factory SentryApp.fromJson(Map data) => SentryApp( + name: data['app_name'], + version: data['app_version'], + identifier: data['app_identifier'], + build: data['app_build'], + buildType: data['build_type'], + startTime: data['app_start_time'] != null + ? DateTime.tryParse(data['app_start_time']) + : null, + deviceAppHash: data['device_app_hash'], + ); + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_browser.dart b/dart/lib/src/protocol/sentry_browser.dart index 32038bf2a4..5c536d5819 100644 --- a/dart/lib/src/protocol/sentry_browser.dart +++ b/dart/lib/src/protocol/sentry_browser.dart @@ -11,17 +11,18 @@ class SentryBrowser { /// Creates an instance of [SentryBrowser]. const SentryBrowser({this.name, this.version}); - factory SentryBrowser.fromJson(Map data) => SentryBrowser( - name: data['name'], - version: data['version'], - ); - /// 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; + /// Deserializes a [SentryBrowser] from JSON [Map]. + factory SentryBrowser.fromJson(Map data) => SentryBrowser( + name: data['name'], + version: data['version'], + ); + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_device.dart b/dart/lib/src/protocol/sentry_device.dart index 517593f01a..2630b8d6c1 100644 --- a/dart/lib/src/protocol/sentry_device.dart +++ b/dart/lib/src/protocol/sentry_device.dart @@ -41,39 +41,6 @@ class SentryDevice { batteryLevel == null || (batteryLevel >= 0 && batteryLevel <= 100), ); - factory SentryDevice.fromJson(Map data) => SentryDevice( - name: data['name'], - family: data['family'], - model: data['model'], - modelId: data['model_id'], - arch: data['arch'], - batteryLevel: data['battery_level'], - orientation: data['orientation'], - manufacturer: data['manufacturer'], - brand: data['brand'], - screenResolution: data['screen_resolution'], - screenHeightPixels: data['screen_height_pixels'], - screenWidthPixels: data['screen_width_pixels'], - screenDensity: data['screen_density'], - screenDpi: data['screen_dpi'], - online: data['online'], - charging: data['charging'], - lowMemory: data['low_memory'], - simulator: data['simulator'], - memorySize: data['memory_size'], - freeMemory: data['free_memory'], - usableMemory: data['usable_memory'], - storageSize: data['storage_size'], - freeStorage: data['free_storage'], - externalStorageSize: data['external_storage_size'], - externalFreeStorage: data['external_free_storage'], - bootTime: data['boot_time'] != null - ? DateTime.tryParse(data['boot_time']) - : null, - timezone: data['timezone'], - language: data['language'], - ); - /// The name of the device. This is typically a hostname. final String? name; @@ -165,6 +132,44 @@ class SentryDevice { /// The language of the device, e.g.: `en_US`. final String? language; + /// Deserializes a [SentryDevice] from JSON [Map]. + factory SentryDevice.fromJson(Map data) => SentryDevice( + name: data['name'], + family: data['family'], + model: data['model'], + modelId: data['model_id'], + arch: data['arch'], + batteryLevel: data['battery_level'], + orientation: data['orientation'] == 'portrait' + ? SentryOrientation.portrait + : data['orientation'] == 'landscape' + ? SentryOrientation.landscape + : null, + manufacturer: data['manufacturer'], + brand: data['brand'], + screenResolution: data['screen_resolution'], + screenHeightPixels: data['screen_height_pixels'], + screenWidthPixels: data['screen_width_pixels'], + screenDensity: data['screen_density'], + screenDpi: data['screen_dpi'], + online: data['online'], + charging: data['charging'], + lowMemory: data['low_memory'], + simulator: data['simulator'], + memorySize: data['memory_size'], + freeMemory: data['free_memory'], + usableMemory: data['usable_memory'], + storageSize: data['storage_size'], + freeStorage: data['free_storage'], + externalStorageSize: data['external_storage_size'], + externalFreeStorage: data['external_free_storage'], + bootTime: data['boot_time'] != null + ? DateTime.tryParse(data['boot_time']) + : null, + timezone: data['timezone'], + language: data['language'], + ); + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; @@ -299,32 +304,34 @@ class SentryDevice { } SentryDevice clone() => SentryDevice( - name: name, - family: family, - model: model, - modelId: modelId, - arch: arch, - batteryLevel: batteryLevel, - orientation: orientation, - manufacturer: manufacturer, - brand: brand, - screenResolution: screenResolution, - screenDensity: screenDensity, - screenDpi: screenDpi, - online: online, - charging: charging, - lowMemory: lowMemory, - simulator: simulator, - memorySize: memorySize, - freeMemory: freeMemory, - usableMemory: usableMemory, - storageSize: storageSize, - freeStorage: freeStorage, - externalStorageSize: externalStorageSize, - externalFreeStorage: externalFreeStorage, - bootTime: bootTime, - timezone: timezone, - ); + name: name, + family: family, + model: model, + modelId: modelId, + arch: arch, + batteryLevel: batteryLevel, + orientation: orientation, + manufacturer: manufacturer, + brand: brand, + screenResolution: screenResolution, + screenHeightPixels: screenHeightPixels, + screenWidthPixels: screenWidthPixels, + screenDensity: screenDensity, + screenDpi: screenDpi, + online: online, + charging: charging, + lowMemory: lowMemory, + simulator: simulator, + memorySize: memorySize, + freeMemory: freeMemory, + usableMemory: usableMemory, + storageSize: storageSize, + freeStorage: freeStorage, + externalStorageSize: externalStorageSize, + externalFreeStorage: externalFreeStorage, + bootTime: bootTime, + timezone: timezone, + language: language); SentryDevice copyWith({ String? name, diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 7472ce1950..c7bf163415 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -226,6 +226,88 @@ class SentryEvent { debugMeta: debugMeta ?? this.debugMeta, ); + /// Deserializes a [SentryEvent] from JSON [Map]. + factory SentryEvent.fromJson(Map json) { + final breadcrumbsJson = json['breadcrumbs'] as List?; + final breadcrumbs = breadcrumbsJson?.map((e) { + return Breadcrumb.fromJson(e); + }).toList(); + + final stackTraceValuesJson = json['threads']?['values']; + Map? stackTraceValuesStacktraceJson; + if (stackTraceValuesJson?.isNotEmpty == true) { + stackTraceValuesStacktraceJson = {}; + stackTraceValuesStacktraceJson = + stackTraceValuesJson?.first['stacktrace']; + } + + final exceptionValuesJson = json['exception']?['values']; + Map? exceptionValuesItemJson; + if (exceptionValuesJson?.isNotEmpty == true) { + exceptionValuesItemJson = {}; + exceptionValuesItemJson = exceptionValuesJson?.first; + } + + final modules = json['modules']?.cast(); + final tags = json['tags']?.cast(); + + final timestampJson = json['timestamp']; + final levelJson = json['level']; + final fingerprintJson = json['fingerprint'] as List?; + final sdkVersionJson = json['sdk'] as Map?; + final messageJson = json['message'] as Map?; + final userJson = json['user'] as Map?; + final contextsJson = json['contexts'] as Map?; + final requestJson = json['request'] as Map?; + final debugMetaJson = json['debug_meta'] as Map?; + + return SentryEvent( + eventId: SentryId.fromId(json['event_id']), + timestamp: + timestampJson != null ? DateTime.tryParse(timestampJson) : null, + modules: modules, + tags: tags, + extra: json['extra'], + fingerprint: fingerprintJson?.map((e) => e as String).toList(), + breadcrumbs: breadcrumbs, + sdk: sdkVersionJson != null && sdkVersionJson.isNotEmpty + ? SdkVersion.fromJson(sdkVersionJson) + : null, + platform: json['platform'], + logger: json['logger'], + serverName: json['server_name'], + release: json['release'], + dist: json['dist'], + environment: json['environment'], + message: messageJson != null && messageJson.isNotEmpty + ? SentryMessage.fromJson(messageJson) + : null, + transaction: json['transaction'], + stackTrace: stackTraceValuesStacktraceJson != null && + stackTraceValuesStacktraceJson.isNotEmpty + ? SentryStackTrace.fromJson(stackTraceValuesStacktraceJson) + : null, + exception: + exceptionValuesItemJson != null && exceptionValuesItemJson.isNotEmpty + ? SentryException.fromJson(exceptionValuesItemJson) + : null, + level: levelJson != null ? SentryLevel.fromName(levelJson) : null, + culprit: json['culprit'], + user: userJson != null && userJson.isNotEmpty + ? SentryUser.fromJson(userJson) + : null, + contexts: contextsJson != null && contextsJson.isNotEmpty + ? Contexts.fromJson(contextsJson) + : null, + request: requestJson != null && requestJson.isNotEmpty + ? SentryRequest.fromJson(requestJson) + : null, + debugMeta: debugMetaJson != null && debugMetaJson.isNotEmpty + ? DebugMeta.fromJson(debugMetaJson) + : null, + ); + } + /// Serializes this event to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index c4d2558240..8359bfd1a3 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -32,6 +32,24 @@ class SentryException { this.threadId, }); + /// Deserializes a [SentryException] from JSON [Map]. + factory SentryException.fromJson(Map json) { + final stackTraceJson = json['stacktrace']; + final mechanismJson = json['mechanism']; + return SentryException( + type: json['type'], + value: json['value'], + module: json['module'], + stackTrace: stackTraceJson != null + ? SentryStackTrace.fromJson(stackTraceJson) + : null, + mechanism: + mechanismJson != null ? Mechanism.fromJson(mechanismJson) : null, + threadId: json['thread_id'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_gpu.dart b/dart/lib/src/protocol/sentry_gpu.dart index bba5cfe07b..6696f7b4f7 100644 --- a/dart/lib/src/protocol/sentry_gpu.dart +++ b/dart/lib/src/protocol/sentry_gpu.dart @@ -56,6 +56,7 @@ class SentryGpu { this.npotSupport, }); + /// Deserializes a [SentryGpu] from JSON [Map]. factory SentryGpu.fromJson(Map data) => SentryGpu( name: data['name'], id: data['id'], diff --git a/dart/lib/src/protocol/sentry_level.dart b/dart/lib/src/protocol/sentry_level.dart index a2524ce1ac..264a7c43ab 100644 --- a/dart/lib/src/protocol/sentry_level.dart +++ b/dart/lib/src/protocol/sentry_level.dart @@ -15,6 +15,20 @@ class SentryLevel { final String name; final int ordinal; + factory SentryLevel.fromName(String name) { + switch (name) { + case 'fatal': + return SentryLevel.fatal; + case 'error': + return SentryLevel.error; + case 'warning': + return SentryLevel.warning; + case 'info': + return SentryLevel.info; + } + return SentryLevel.debug; + } + /// For use with Dart's /// [`log`](https://api.dart.dev/stable/2.12.4/dart-developer/log.html) /// function. diff --git a/dart/lib/src/protocol/sentry_message.dart b/dart/lib/src/protocol/sentry_message.dart index 3842e35b1d..e234de5584 100644 --- a/dart/lib/src/protocol/sentry_message.dart +++ b/dart/lib/src/protocol/sentry_message.dart @@ -22,6 +22,16 @@ class SentryMessage { const SentryMessage(this.formatted, {this.template, this.params}); + /// Deserializes a [SentryMessage] from JSON [Map]. + factory SentryMessage.fromJson(Map json) { + return SentryMessage( + json['formatted'], + template: json['message'], + params: json['params'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_operating_system.dart b/dart/lib/src/protocol/sentry_operating_system.dart index 60d7ce6848..4b6376a405 100644 --- a/dart/lib/src/protocol/sentry_operating_system.dart +++ b/dart/lib/src/protocol/sentry_operating_system.dart @@ -17,16 +17,6 @@ class SentryOperatingSystem { this.rawDescription, }); - factory SentryOperatingSystem.fromJson(Map data) => - SentryOperatingSystem( - name: data['name'], - version: data['version'], - build: data['build'], - kernelVersion: data['kernel_version'], - rooted: data['rooted'], - rawDescription: data['raw_description'], - ); - /// The name of the operating system. final String? name; @@ -50,6 +40,17 @@ class SentryOperatingSystem { /// version from this string, if they are not explicitly given. final String? rawDescription; + /// Deserializes a [SentryOperatingSystem] from JSON [Map]. + factory SentryOperatingSystem.fromJson(Map data) => + SentryOperatingSystem( + name: data['name'], + version: data['version'], + build: data['build'], + kernelVersion: data['kernel_version'], + rooted: data['rooted'], + rawDescription: data['raw_description'], + ); + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_package.dart b/dart/lib/src/protocol/sentry_package.dart index 57e5e4a9f4..51fad73377 100644 --- a/dart/lib/src/protocol/sentry_package.dart +++ b/dart/lib/src/protocol/sentry_package.dart @@ -12,6 +12,14 @@ class SentryPackage { /// The version of the SDK. final String version; + /// Deserializes a [SentryPackage] from JSON [Map]. + factory SentryPackage.fromJson(Map json) { + return SentryPackage( + json['name'], + json['version'], + ); + } + /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { diff --git a/dart/lib/src/protocol/sentry_request.dart b/dart/lib/src/protocol/sentry_request.dart index acaef3370e..7c326df727 100644 --- a/dart/lib/src/protocol/sentry_request.dart +++ b/dart/lib/src/protocol/sentry_request.dart @@ -69,6 +69,21 @@ class SentryRequest { _env = env != null ? Map.from(env) : null, _other = other != null ? Map.from(other) : null; + /// Deserializes a [SentryRequest] from JSON [Map]. + factory SentryRequest.fromJson(Map json) { + return SentryRequest( + url: json['url'], + method: json['method'], + queryString: json['query_string'], + cookies: json['cookies'], + data: json['data'], + headers: json['headers'], + env: json['env'], + other: json['other'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_runtime.dart b/dart/lib/src/protocol/sentry_runtime.dart index 54bf50a266..6bba34ff8c 100644 --- a/dart/lib/src/protocol/sentry_runtime.dart +++ b/dart/lib/src/protocol/sentry_runtime.dart @@ -13,12 +13,6 @@ class SentryRuntime { const SentryRuntime({this.key, this.name, this.version, this.rawDescription}) : assert(key == null || key.length >= 1); - factory SentryRuntime.fromJson(Map data) => SentryRuntime( - name: data['name'], - version: data['version'], - rawDescription: data['raw_description'], - ); - /// Key used in the JSON and which will be displayed /// in the Sentry UI. Defaults to lower case version of [name]. /// @@ -37,6 +31,13 @@ class SentryRuntime { /// and version from this string, if they are not explicitly given. final String? rawDescription; + /// Deserializes a [SentryRuntime] from JSON [Map]. + factory SentryRuntime.fromJson(Map data) => SentryRuntime( + name: data['name'], + version: data['version'], + rawDescription: data['raw_description'], + ); + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_stack_frame.dart b/dart/lib/src/protocol/sentry_stack_frame.dart index 685cac4df2..27085f8522 100644 --- a/dart/lib/src/protocol/sentry_stack_frame.dart +++ b/dart/lib/src/protocol/sentry_stack_frame.dart @@ -108,6 +108,32 @@ class SentryStackFrame { /// 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; + /// Deserializes a [SentryStackFrame] from JSON [Map]. + factory SentryStackFrame.fromJson(Map json) { + return SentryStackFrame( + absPath: json['abs_path'], + fileName: json['filename'], + function: json['function'], + module: json['module'], + lineNo: json['lineno'], + colNo: json['colno'], + contextLine: json['context_line'], + inApp: json['in_app'], + package: json['package'], + native: json['native'], + platform: json['platform'], + imageAddr: json['image_addr'], + symbolAddr: json['symbol_addr'], + instructionAddr: json['instruction_addr'], + rawFunction: json['raw_function'], + framesOmitted: json['frames_omitted'], + preContext: json['pre_context'], + postContext: json['post_context'], + vars: json['vars'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; @@ -163,10 +189,6 @@ class SentryStackFrame { json['in_app'] = inApp; } - if (package != null) { - json['package'] = package; - } - if (native != null) { json['native'] = native; } diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart index 112ecd81c4..abb74fdcca 100644 --- a/dart/lib/src/protocol/sentry_stack_trace.dart +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -25,6 +25,20 @@ class SentryStackTrace { /// thus mapping to the last frame in the list. Map get registers => Map.unmodifiable(_registers ?? const {}); + /// Deserializes a [SentryStackTrace] from JSON [Map]. + factory SentryStackTrace.fromJson(Map json) { + final framesJson = json['frames'] as List?; + return SentryStackTrace( + frames: framesJson != null + ? framesJson + .map((frameJson) => SentryStackFrame.fromJson(frameJson)) + .toList() + : [], + registers: json['registers'], + ); + } + + /// Produces a [Map] that can be serialized to JSON. Map toJson() { final json = {}; diff --git a/dart/lib/src/protocol/sentry_user.dart b/dart/lib/src/protocol/sentry_user.dart index 34ff14080d..54d363e0b7 100644 --- a/dart/lib/src/protocol/sentry_user.dart +++ b/dart/lib/src/protocol/sentry_user.dart @@ -53,6 +53,17 @@ class SentryUser { /// by Sentry. final Map? extras; + /// Deserializes a [SentryUser] from JSON [Map]. + factory SentryUser.fromJson(Map json) { + return SentryUser( + id: json['id'], + username: json['username'], + email: json['email'], + ipAddress: json['ip_address'], + extras: json['extras'], + ); + } + /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index fb20d6ad2f..b193ffec9b 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:math'; +import 'transport/rate_limiter.dart'; + import 'protocol.dart'; import 'scope.dart'; import 'sentry_exception_factory.dart'; @@ -9,6 +11,7 @@ import 'sentry_stack_trace_factory.dart'; import 'transport/http_transport.dart'; import 'transport/noop_transport.dart'; import 'version.dart'; +import 'sentry_envelope.dart'; /// Default value for [User.ipAddress]. It gets set when an event does not have /// a user and IP address. Only applies if [SentryOptions.sendDefaultPii] is set @@ -30,7 +33,7 @@ class SentryClient { /// Instantiates a client using [SentryOptions] factory SentryClient(SentryOptions options) { if (options.transport is NoOpTransport) { - options.transport = HttpTransport(options); + options.transport = HttpTransport(options, RateLimiter(options.clock)); } return SentryClient._(options); @@ -103,8 +106,8 @@ class SentryClient { return _sentryId; } } - - return _options.transport.send(preparedEvent); + final envelope = SentryEnvelope.fromEvent(preparedEvent, _options.sdk); + return await _options.transport.send(envelope); } SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) { @@ -195,6 +198,11 @@ class SentryClient { return captureEvent(event, scope: scope, hint: hint); } + /// Reports the [envelope] to Sentry.io. + Future captureEnvelope(SentryEnvelope envelope) { + return _options.transport.send(envelope); + } + void close() => _options.httpClient.close(); Future _processEvent( diff --git a/dart/lib/src/sentry_envelope.dart b/dart/lib/src/sentry_envelope.dart new file mode 100644 index 0000000000..18a1835231 --- /dev/null +++ b/dart/lib/src/sentry_envelope.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +import 'sentry_envelope_header.dart'; +import 'sentry_envelope_item.dart'; +import 'protocol/sentry_event.dart'; +import 'protocol/sdk_version.dart'; + +/// Class representation of `Envelope` file. +class SentryEnvelope { + SentryEnvelope(this.header, this.items); + + /// Header descriping envelope content. + final SentryEnvelopeHeader header; + + /// All items contained in the envelope. + final List items; + + /// Create an `SentryEnvelope` with containing one `SentryEnvelopeItem` which holds the `SentyEvent` data. + factory SentryEnvelope.fromEvent(SentryEvent event, SdkVersion sdkVersion) { + return SentryEnvelope(SentryEnvelopeHeader(event.eventId, sdkVersion), + [SentryEnvelopeItem.fromEvent(event)]); + } + + /// Stream binary data representation of `Envelope` file encoded. + Stream> envelopeStream() async* { + yield utf8.encode(jsonEncode(header.toJson())); + final newLineData = utf8.encode('\n'); + for (final item in items) { + yield newLineData; + await for (final chunk in item.envelopeItemStream()) { + yield chunk; + } + } + } +} diff --git a/dart/lib/src/sentry_envelope_header.dart b/dart/lib/src/sentry_envelope_header.dart new file mode 100644 index 0000000000..426bccc910 --- /dev/null +++ b/dart/lib/src/sentry_envelope_header.dart @@ -0,0 +1,30 @@ +import 'protocol/sentry_id.dart'; +import 'protocol/sdk_version.dart'; + +/// Header containing `SentryId` and `SdkVersion`. +class SentryEnvelopeHeader { + SentryEnvelopeHeader(this.eventId, this.sdkVersion); + SentryEnvelopeHeader.newEventId() + : eventId = SentryId.newId(), + sdkVersion = null; + + /// The identifier of encoded `SentryEvent`. + final SentryId? eventId; + + /// The `SdkVersion` with which the envelope was send. + final SdkVersion? sdkVersion; + + /// Header encoded as JSON + Map toJson() { + final json = {}; + final tempEventId = eventId; + if (tempEventId != null) { + json['event_id'] = tempEventId.toString(); + } + final tempSdkVersion = sdkVersion; + if (tempSdkVersion != null) { + json['sdk'] = tempSdkVersion.toJson(); + } + return json; + } +} diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart new file mode 100644 index 0000000000..5c00a4e1e6 --- /dev/null +++ b/dart/lib/src/sentry_envelope_item.dart @@ -0,0 +1,52 @@ +import 'dart:convert'; + +import 'sentry_item_type.dart'; +import 'protocol/sentry_event.dart'; +import 'sentry_envelope_item_header.dart'; + +/// Item holding header information and JSON encoded data. +class SentryEnvelopeItem { + SentryEnvelopeItem(this.header, this.dataFactory); + + /// Header with info about type and length of data in bytes. + final SentryEnvelopeItemHeader header; + + /// Create binary data representation of item data. + final Future> Function() dataFactory; + + /// Create an `SentryEnvelopeItem` which holds the `SentyEvent` data. + factory SentryEnvelopeItem.fromEvent(SentryEvent event) { + final cachedItem = _CachedItem(() async { + final jsonEncoded = jsonEncode(event.toJson()); + return utf8.encode(jsonEncoded); + }); + + final getLength = () async { + return (await cachedItem.getData()).length; + }; + + return SentryEnvelopeItem( + SentryEnvelopeItemHeader(SentryItemType.event, getLength, + contentType: 'application/json'), + cachedItem.getData); + } + + /// Stream binary data of `Envelope` item. + Stream> envelopeItemStream() async* { + yield utf8.encode(jsonEncode(await header.toJson())); + yield utf8.encode('\n'); + yield await dataFactory(); + } +} + +class _CachedItem { + _CachedItem(this._dataFactory); + + final Future> Function() _dataFactory; + List? _data; + + Future> getData() async { + _data ??= await _dataFactory(); + return _data!; + } +} diff --git a/dart/lib/src/sentry_envelope_item_header.dart b/dart/lib/src/sentry_envelope_item_header.dart new file mode 100644 index 0000000000..013a62d35b --- /dev/null +++ b/dart/lib/src/sentry_envelope_item_header.dart @@ -0,0 +1,29 @@ +/// Header with item info about type and length of data in bytes. +class SentryEnvelopeItemHeader { + SentryEnvelopeItemHeader(this.type, this.length, + {this.contentType, this.fileName}); + + /// Type of encoded data. + final String type; + + /// The number of bytes of the encoded item JSON. + final Future Function() length; + + final String? contentType; + + final String? fileName; + + /// Item header encoded as JSON + Future> toJson() async { + final json = {}; + if (contentType != null) { + json['content_type'] = contentType; + } + if (fileName != null) { + json['filename'] = fileName; + } + json['type'] = type; + json['length'] = await length(); + return json; + } +} diff --git a/dart/lib/src/sentry_item_type.dart b/dart/lib/src/sentry_item_type.dart new file mode 100644 index 0000000000..b90dff8538 --- /dev/null +++ b/dart/lib/src/sentry_item_type.dart @@ -0,0 +1,7 @@ +class SentryItemType { + static const String event = 'event'; + static const String userFeedback = 'user_report'; + static const String attachment = 'attachment'; + static const String transaction = 'transaction'; + static const String unknown = '__unknown__'; +} diff --git a/dart/lib/src/transport/encode.dart b/dart/lib/src/transport/encode.dart index 03256130f9..645aaf02cd 100644 --- a/dart/lib/src/transport/encode.dart +++ b/dart/lib/src/transport/encode.dart @@ -5,3 +5,10 @@ List compressBody(List body, Map headers) { headers['Content-Encoding'] = 'gzip'; return gzip.encode(body); } + +/// Encodes bytes in sink using Gzip compression +Sink> compressInSink( + Sink> sink, Map headers) { + headers['Content-Encoding'] = 'gzip'; + return GZipCodec().encoder.startChunkedConversion(sink); +} diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index bb5b05b8e4..3a93da51f1 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -3,11 +3,13 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; import '../noop_client.dart'; import '../protocol.dart'; import '../sentry_options.dart'; -import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; +import '../sentry_envelope.dart'; import 'transport.dart'; +import 'rate_limiter.dart'; /// A transport is in charge of sending the event to the Sentry server. class HttpTransport implements Transport { @@ -15,22 +17,26 @@ class HttpTransport implements Transport { final Dsn _dsn; + final RateLimiter _rateLimiter; + late _CredentialBuilder _credentialBuilder; final Map _headers; - factory HttpTransport(SentryOptions options) { + factory HttpTransport(SentryOptions options, RateLimiter rateLimiter) { if (options.httpClient is NoOpClient) { options.httpClient = Client(); } - return HttpTransport._(options); + return HttpTransport._(options, rateLimiter); } - HttpTransport._(this._options) + HttpTransport._(this._options, this._rateLimiter) : _dsn = Dsn.parse(_options.dsn!), _headers = _buildHeaders( - _options.platformChecker.isWeb, _options.sdk.identifier) { + _options.platformChecker.isWeb, + _options.sdk.identifier, + ) { _credentialBuilder = _CredentialBuilder( _dsn, _options.sdk.identifier, @@ -39,20 +45,18 @@ class HttpTransport implements Transport { } @override - Future send(SentryEvent event) async { - final data = event.toJson(); + Future send(SentryEnvelope envelope) async { + final filteredEnvelope = _rateLimiter.filter(envelope); + if (filteredEnvelope == null) { + return SentryId.empty(); + } - final body = _bodyEncoder( - data, - _headers, - compressPayload: _options.compressPayload, - ); + final streamedRequest = await _createStreamedRequest(filteredEnvelope); + final response = await _options.httpClient + .send(streamedRequest) + .then(Response.fromStream); - final response = await _options.httpClient.post( - _dsn.postUri, - headers: _credentialBuilder.configure(_headers), - body: body, - ); + _updateRetryAfterLimits(response); if (response.statusCode != 200) { // body guard to not log the error as it has performance impact to allocate @@ -68,7 +72,7 @@ class HttpTransport implements Transport { } else { _options.logger( SentryLevel.debug, - 'Event ${event.eventId} was sent successfully.', + 'Envelope ${envelope.header.eventId ?? "--"} was sent successfully.', ); } @@ -76,18 +80,40 @@ class HttpTransport implements Transport { return eventId != null ? SentryId.fromId(eventId) : SentryId.empty(); } - List _bodyEncoder( - Map data, - Map headers, { - required bool compressPayload, - }) { - // [SentryIOClient] implement gzip compression - // gzip compression is not available on browser - var body = utf8.encode(json.encode(data)); - if (compressPayload) { - body = compressBody(body, headers); + Future _createStreamedRequest( + SentryEnvelope envelope) async { + final streamedRequest = StreamedRequest('POST', _dsn.postUri); + + if (_options.compressPayload) { + final compressionSink = compressInSink(streamedRequest.sink, _headers); + envelope + .envelopeStream() + .listen(compressionSink.add) + .onDone(compressionSink.close); + } else { + envelope + .envelopeStream() + .listen(streamedRequest.sink.add) + .onDone(streamedRequest.sink.close); } - return body; + streamedRequest.headers.addAll(_credentialBuilder.configure(_headers)); + + return streamedRequest; + } + + void _updateRetryAfterLimits(Response response) { + // seconds + final retryAfterHeader = response.headers['Retry-After']; + + // X-Sentry-Rate-Limits looks like: seconds:categories:scope + // it could have more than one scope so it looks like: + // quota_limit, quota_limit, quota_limit + + // a real example: 50:transaction:key, 2700:default;error;security:organization + // 50::key is also a valid case, it means no categories and it should apply to all of them + final sentryRateLimitHeader = response.headers['X-Sentry-Rate-Limits']; + _rateLimiter.updateRetryAfterLimits( + sentryRateLimitHeader, retryAfterHeader, response.statusCode); } } @@ -139,7 +165,7 @@ class _CredentialBuilder { } Map _buildHeaders(bool isWeb, String sdkIdentifier) { - final headers = {'Content-Type': 'application/json'}; + final headers = {'Content-Type': 'application/x-sentry-envelope'}; // NOTE(lejard_h) overriding user agent on VM and Flutter not sure why // for web it use browser user agent if (!isWeb) { diff --git a/dart/lib/src/transport/noop_encode.dart b/dart/lib/src/transport/noop_encode.dart index 73f92b2fa0..fa1f9c5547 100644 --- a/dart/lib/src/transport/noop_encode.dart +++ b/dart/lib/src/transport/noop_encode.dart @@ -1,2 +1,7 @@ /// gzip compression is not available on browser List compressBody(List body, Map headers) => body; + +/// gzip compression is not available on browser +Sink> compressInSink( + Sink> sink, Map headers) => + sink; diff --git a/dart/lib/src/transport/noop_transport.dart b/dart/lib/src/transport/noop_transport.dart index 9c657ba0a7..705dfc3a9c 100644 --- a/dart/lib/src/transport/noop_transport.dart +++ b/dart/lib/src/transport/noop_transport.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import '../sentry_envelope.dart'; + import '../protocol.dart'; import 'transport.dart'; class NoOpTransport implements Transport { @override - Future send(SentryEvent event) => Future.value(SentryId.empty()); + Future send(SentryEnvelope envelope) => + Future.value(SentryId.empty()); } diff --git a/dart/lib/src/transport/rate_limit.dart b/dart/lib/src/transport/rate_limit.dart new file mode 100644 index 0000000000..e95285f716 --- /dev/null +++ b/dart/lib/src/transport/rate_limit.dart @@ -0,0 +1,9 @@ +import 'rate_limit_category.dart'; + +/// `RateLimit` containing limited `RateLimitCategory` and duration in milliseconds. +class RateLimit { + RateLimit(this.category, this.duration); + + final RateLimitCategory category; + final Duration duration; +} diff --git a/dart/lib/src/transport/rate_limit_category.dart b/dart/lib/src/transport/rate_limit_category.dart new file mode 100644 index 0000000000..cb0040a0e6 --- /dev/null +++ b/dart/lib/src/transport/rate_limit_category.dart @@ -0,0 +1,54 @@ +/// Different category types of data sent to Sentry. Used for rate limiting. +enum RateLimitCategory { + all, + rate_limit_default, // default + error, + session, + transaction, + attachment, + security, + unknown +} + +extension RateLimitCategoryExtension on RateLimitCategory { + static RateLimitCategory fromStringValue(String stringValue) { + switch (stringValue) { + case '__all__': + return RateLimitCategory.all; + case 'default': + return RateLimitCategory.rate_limit_default; + case 'error': + return RateLimitCategory.error; + case 'session': + return RateLimitCategory.session; + case 'transaction': + return RateLimitCategory.transaction; + case 'attachment': + return RateLimitCategory.attachment; + case 'security': + return RateLimitCategory.security; + } + return RateLimitCategory.unknown; + } + + String toStringValue() { + switch (this) { + case RateLimitCategory.all: + return '__all__'; + case RateLimitCategory.rate_limit_default: + return 'default'; + case RateLimitCategory.error: + return 'error'; + case RateLimitCategory.session: + return 'session'; + case RateLimitCategory.transaction: + return 'transaction'; + case RateLimitCategory.attachment: + return 'attachment'; + case RateLimitCategory.security: + return 'security'; + case RateLimitCategory.unknown: + return 'unknown'; + } + } +} diff --git a/dart/lib/src/transport/rate_limit_parser.dart b/dart/lib/src/transport/rate_limit_parser.dart new file mode 100644 index 0000000000..fe30121162 --- /dev/null +++ b/dart/lib/src/transport/rate_limit_parser.dart @@ -0,0 +1,61 @@ +import 'rate_limit_category.dart'; +import 'rate_limit.dart'; + +/// Parse rate limit categories and times from response header payloads. +class RateLimitParser { + RateLimitParser(this._header); + + static const httpRetryAfterDefaultDelay = Duration(milliseconds: 60000); + + final String? _header; + + List parseRateLimitHeader() { + final rateLimitHeader = _header; + if (rateLimitHeader == null) { + return []; + } + final rateLimits = []; + final rateLimitValues = rateLimitHeader.toLowerCase().split(','); + for (final rateLimitValue in rateLimitValues) { + final durationAndCategories = rateLimitValue.trim().split(':'); + if (durationAndCategories.isEmpty) { + continue; + } + final duration = _parseRetryAfterOrDefault(durationAndCategories[0]); + if (durationAndCategories.length <= 1) { + continue; + } + final allCategories = durationAndCategories[1]; + if (allCategories.isNotEmpty) { + final categoryValues = allCategories.split(';'); + for (final categoryValue in categoryValues) { + final category = + RateLimitCategoryExtension.fromStringValue(categoryValue); + if (category != RateLimitCategory.unknown) { + rateLimits.add(RateLimit(category, duration)); + } + } + } else { + rateLimits.add(RateLimit(RateLimitCategory.all, duration)); + } + } + return rateLimits; + } + + List parseRetryAfterHeader() { + return [ + RateLimit(RateLimitCategory.all, _parseRetryAfterOrDefault(_header)) + ]; + } + + // Helper + + static Duration _parseRetryAfterOrDefault(String? value) { + final durationInSeconds = int.tryParse(value ?? ''); + if (durationInSeconds != null) { + return Duration(seconds: durationInSeconds); + } else { + return RateLimitParser.httpRetryAfterDefaultDelay; + } + } +} diff --git a/dart/lib/src/transport/rate_limiter.dart b/dart/lib/src/transport/rate_limiter.dart new file mode 100644 index 0000000000..0ce91b3b59 --- /dev/null +++ b/dart/lib/src/transport/rate_limiter.dart @@ -0,0 +1,124 @@ +import '../transport/rate_limit_parser.dart'; + +import '../sentry_options.dart'; +import '../sentry_envelope.dart'; +import '../sentry_envelope_item.dart'; +import 'rate_limit.dart'; +import 'rate_limit_category.dart'; + +/// Controls retry limits on different category types sent to Sentry. +class RateLimiter { + RateLimiter(this._clockProvider); + + final ClockProvider _clockProvider; + final _rateLimitedUntil = {}; + + /// Filter out envelopes that are rate limited. + SentryEnvelope? filter(SentryEnvelope envelope) { + // Optimize for/No allocations if no items are under 429 + List? dropItems; + for (final item in envelope.items) { + // using the raw value of the enum to not expose SentryEnvelopeItemType + if (_isRetryAfter(item.header.type)) { + dropItems ??= []; + dropItems.add(item); + } + } + + if (dropItems != null) { + // Need a new envelope + final toSend = []; + for (final item in envelope.items) { + if (!dropItems.contains(item)) { + toSend.add(item); + } + } + + // no reason to continue + if (toSend.isEmpty) { + return null; + } + + return SentryEnvelope(envelope.header, toSend); + } else { + return envelope; + } + } + + /// Update rate limited categories + void updateRetryAfterLimits( + String? sentryRateLimitHeader, String? retryAfterHeader, int errorCode) { + final currentDateTime = _clockProvider().millisecondsSinceEpoch; + var rateLimits = []; + + if (sentryRateLimitHeader != null) { + rateLimits = + RateLimitParser(sentryRateLimitHeader).parseRateLimitHeader(); + } else if (errorCode == 429) { + rateLimits = + RateLimitParser(sentryRateLimitHeader).parseRetryAfterHeader(); + } + + for (final rateLimit in rateLimits) { + _applyRetryAfterOnlyIfLonger( + rateLimit.category, + DateTime.fromMillisecondsSinceEpoch( + currentDateTime + rateLimit.duration.inMilliseconds), + ); + } + } + + // Private + + bool _isRetryAfter(String itemType) { + final dataCategory = _categoryFromItemType(itemType); + final currentDate = DateTime.fromMillisecondsSinceEpoch( + _clockProvider().millisecondsSinceEpoch); + + // check all categories + final dateAllCategories = _rateLimitedUntil[RateLimitCategory.all]; + if (dateAllCategories != null) { + if (!currentDate.isAfter(dateAllCategories)) { + return true; + } + } + + // Unknown should not be rate limited + if (RateLimitCategory.unknown == dataCategory) { + return false; + } + + // check for specific dataCategory + final dateCategory = _rateLimitedUntil[dataCategory]; + if (dateCategory != null) { + return !currentDate.isAfter(dateCategory); + } + + return false; + } + + RateLimitCategory _categoryFromItemType(String itemType) { + switch (itemType) { + case 'event': + return RateLimitCategory.error; + case 'session': + return RateLimitCategory.session; + case 'attachment': + return RateLimitCategory.attachment; + case 'transaction': + return RateLimitCategory.transaction; + default: + return RateLimitCategory.unknown; + } + } + + void _applyRetryAfterOnlyIfLonger( + RateLimitCategory rateLimitCategory, DateTime date) { + final oldDate = _rateLimitedUntil[rateLimitCategory]; + + // only overwrite its previous date if the limit is even longer + if (oldDate == null || date.isAfter(oldDate)) { + _rateLimitedUntil[rateLimitCategory] = date; + } + } +} diff --git a/dart/lib/src/transport/transport.dart b/dart/lib/src/transport/transport.dart index 295181cbc2..08de9e9322 100644 --- a/dart/lib/src/transport/transport.dart +++ b/dart/lib/src/transport/transport.dart @@ -1,9 +1,10 @@ import 'dart:async'; +import '../sentry_envelope.dart'; import '../protocol.dart'; -/// A transport is in charge of sending the event either via http +/// A transport is in charge of sending the event/envelope either via http /// or caching in the disk. abstract class Transport { - Future send(SentryEvent event); + Future send(SentryEnvelope envelope); } diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index a11e2cd307..582aae1661 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: uuid: ^3.0.0 dev_dependencies: - mockito: ^5.0.0 + mockito: ^5.0.3 pedantic: ^1.11.0 test: ^1.16.5 yaml: ^3.1.0 # needed for version match (code and pubspec) diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index 5647f03144..5bd97189d0 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -61,50 +61,59 @@ void main() { ..['theme'] = {'value': 'material'} ..['version'] = {'value': 9}; + final contextsJson = { + 'device': { + 'name': 'testDevice', + 'family': 'testFamily', + 'model': 'testModel', + 'model_id': 'testModelId', + 'arch': 'testArch', + 'battery_level': 23.0, + 'orientation': 'landscape', + 'manufacturer': 'testOEM', + 'brand': 'testBrand', + 'screen_resolution': '123x345', + 'screen_density': 99.1, + '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', + }, + 'app': {'app_version': '1.2.3'}, + 'browser': {'version': '12.3.4'}, + 'gpu': {'name': 'Radeon', 'version': '1'}, + 'testrt1': {'name': 'testRT1', 'type': 'runtime', 'version': '1.0'}, + 'testrt2': {'name': 'testRT2', 'type': 'runtime', 'version': '2.3.1'}, + 'theme': {'value': 'material'}, + 'version': {'value': 9}, + }; + test('serializes to JSON', () { final event = SentryEvent(contexts: contexts); + expect(event.toJson()['contexts'], contextsJson); + }); + + test('deserializes/serializes JSON', () { + final contexts = Contexts.fromJson(contextsJson); + final json = contexts.toJson(); + 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.1, - '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'}, - 'gpu': {'name': 'Radeon', 'version': '1'}, - 'theme': {'value': 'material'}, - 'version': {'value': 9}, - }, + DeepCollectionEquality().equals(contextsJson, json), + true, ); }); diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index e1d8b87d5c..f57470436f 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -1,5 +1,6 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/protocol.dart'; +import 'package:sentry/src/transport/rate_limiter.dart'; final fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; @@ -91,3 +92,33 @@ final fakeEvent = SentryEvent( ), ), ); + +var fakeEnvelope = SentryEnvelope.fromEvent( + fakeEvent, SdkVersion(name: 'sdk1', version: '1.0.0')); + +class MockRateLimiter implements RateLimiter { + bool filterReturnsNull = false; + SentryEnvelope? filteredEnvelope; + SentryEnvelope? envelopeToFilter; + + String? sentryRateLimitHeader; + String? retryAfterHeader; + int? errorCode; + + @override + SentryEnvelope? filter(SentryEnvelope envelope) { + if (filterReturnsNull) { + return null; + } + envelopeToFilter = envelope; + return filteredEnvelope ?? envelope; + } + + @override + void updateRetryAfterLimits( + String? sentryRateLimitHeader, String? retryAfterHeader, int errorCode) { + this.sentryRateLimitHeader = sentryRateLimitHeader; + this.retryAfterHeader = retryAfterHeader; + this.errorCode = errorCode; + } +} diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index d7e54fca23..9b6f4175fe 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -1,9 +1,11 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_envelope.dart'; class MockSentryClient implements SentryClient { List captureEventCalls = []; List captureExceptionCalls = []; List captureMessageCalls = []; + List captureEnvelopeCalls = []; int closeCalls = 0; @override @@ -58,6 +60,12 @@ class MockSentryClient implements SentryClient { return SentryId.newId(); } + @override + Future captureEnvelope(SentryEnvelope envelope) async { + captureEnvelopeCalls.add(CaptureEnvelopeCall(envelope)); + return SentryId.newId(); + } + @override void close() { closeCalls = closeCalls + 1; @@ -109,3 +117,9 @@ class CaptureMessageCall { this.hint, ); } + +class CaptureEnvelopeCall { + final SentryEnvelope envelope; + + CaptureEnvelopeCall(this.envelope); +} diff --git a/dart/test/mocks/mock_transport.dart b/dart/test/mocks/mock_transport.dart index 313ed7eb72..64ed6902c5 100644 --- a/dart/test/mocks/mock_transport.dart +++ b/dart/test/mocks/mock_transport.dart @@ -1,15 +1,15 @@ import 'package:sentry/sentry.dart'; class MockTransport implements Transport { - List events = []; + List envelopes = []; bool called(int calls) { - return events.length == calls; + return envelopes.length == calls; } @override - Future send(SentryEvent event) async { - events.add(event); - return event.eventId; + Future send(SentryEnvelope envelope) async { + envelopes.add(envelope); + return envelope.header.eventId ?? SentryId.empty(); } } diff --git a/dart/test/protocol/app_test.dart b/dart/test/protocol/app_test.dart deleted file mode 100644 index fe2abb0957..0000000000 --- a/dart/test/protocol/app_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final startTime = DateTime.now(); - - final copy = data.copyWith( - name: 'name1', - version: 'version1', - identifier: 'identifier1', - build: 'build1', - buildType: 'buildType1', - startTime: startTime, - deviceAppHash: 'hash1', - ); - - expect('name1', copy.name); - expect('version1', copy.version); - expect('identifier1', copy.identifier); - expect('build1', copy.build); - expect('buildType1', copy.buildType); - expect(startTime, copy.startTime); - expect('hash1', copy.deviceAppHash); - }); -} - -SentryApp _generate({DateTime? startTime}) => SentryApp( - name: 'name', - version: 'version', - identifier: 'identifier', - build: 'build', - buildType: 'buildType', - startTime: startTime ?? DateTime.now(), - deviceAppHash: 'hash', - ); diff --git a/dart/test/protocol/breadcrumb_test.dart b/dart/test/protocol/breadcrumb_test.dart index 2c0f6a97a7..e34b8d2553 100644 --- a/dart/test/protocol/breadcrumb_test.dart +++ b/dart/test/protocol/breadcrumb_test.dart @@ -1,47 +1,80 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import 'package:sentry/src/utils.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final timestamp = DateTime.now(); - final copy = data.copyWith(); + final breadcrumb = Breadcrumb( + message: 'message', + timestamp: timestamp, + data: {'key': 'value'}, + level: SentryLevel.warning, + category: 'category', + type: 'type', + ); - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); + final breadcrumbJson = { + 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), + 'message': 'message', + 'category': 'category', + 'data': {'key': 'value'}, + 'level': 'warning', + 'type': 'type', + }; + + group('json', () { + test('toJson', () { + final json = breadcrumb.toJson(); + + expect( + DeepCollectionEquality().equals(breadcrumbJson, json), + true, + ); + }); + test('fromJson', () { + final breadcrumb = Breadcrumb.fromJson(breadcrumbJson); + final json = breadcrumb.toJson(); + + expect( + DeepCollectionEquality().equals(breadcrumbJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - - final timestamp = DateTime.now(); - - final copy = data.copyWith( - message: 'message1', - timestamp: timestamp, - data: {'key1': 'value1'}, - level: SentryLevel.fatal, - category: 'category1', - type: 'type1', - ); - - expect('message1', copy.message); - expect(timestamp, copy.timestamp); - expect({'key1': 'value1'}, copy.data); - expect(SentryLevel.fatal, copy.level); - expect('category1', copy.category); - expect('type1', copy.type); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = breadcrumb; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + test('copyWith takes new values', () { + final data = breadcrumb; + + final timestamp = DateTime.now(); + + final copy = data.copyWith( + message: 'message1', + timestamp: timestamp, + data: {'key1': 'value1'}, + level: SentryLevel.fatal, + category: 'category1', + type: 'type1', + ); + + expect('message1', copy.message); + expect(timestamp, copy.timestamp); + expect({'key1': 'value1'}, copy.data); + expect(SentryLevel.fatal, copy.level); + expect('category1', copy.category); + expect('type1', copy.type); + }); }); } - -Breadcrumb _generate({DateTime? timestamp}) => Breadcrumb( - message: 'message', - timestamp: timestamp ?? DateTime.now(), - data: {'key': 'value'}, - level: SentryLevel.warning, - category: 'category', - type: 'type', - ); diff --git a/dart/test/protocol/browser_test.dart b/dart/test/protocol/browser_test.dart deleted file mode 100644 index d3123fb638..0000000000 --- a/dart/test/protocol/browser_test.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - name: 'name1', - version: 'version1', - ); - - expect('name1', copy.name); - expect('version1', copy.version); - }); -} - -SentryBrowser _generate() => SentryBrowser( - name: 'name', - version: 'version', - ); diff --git a/dart/test/protocol/contexts_test.dart b/dart/test/protocol/contexts_test.dart index b85d34b7db..b420fbdb74 100644 --- a/dart/test/protocol/contexts_test.dart +++ b/dart/test/protocol/contexts_test.dart @@ -3,57 +3,122 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final contexts = Contexts( + device: SentryDevice(batteryLevel: 90), + operatingSystem: SentryOperatingSystem(name: 'name'), + runtimes: [SentryRuntime(name: 'name')], + app: SentryApp(name: 'name'), + browser: SentryBrowser(name: 'name'), + gpu: SentryGpu(id: 1), + culture: SentryCulture(locale: 'foo-bar'), + ); - final copy = data.copyWith(); + final contextsJson = { + 'device': {'battery_level': 90.0}, + 'os': {'name': 'name'}, + 'runtime': {'name': 'name'}, + 'app': {'app_name': 'name'}, + 'browser': {'name': 'name'}, + 'gpu': {'id': 1}, + 'culture': {'locale': 'foo-bar'}, + }; - // MapEquality fails for some reason, it probably check the instances equality too - expect(data.toJson(), copy.toJson()); + final contextsMutlipleRuntimes = Contexts( + runtimes: [ + SentryRuntime(name: 'name'), + SentryRuntime(name: 'name'), + SentryRuntime(key: 'key') + ], + ); + + final contextsMutlipleRuntimesJson = { + 'name': {'name': 'name', 'type': 'runtime'}, + 'name0': {'name': 'name', 'type': 'runtime'}, + }; + + group('json', () { + test('toJson', () { + final json = contexts.toJson(); + + expect( + DeepCollectionEquality().equals(contextsJson, json), + true, + ); + }); + test('toJson multiple runtimes', () { + final json = contextsMutlipleRuntimes.toJson(); + + expect( + DeepCollectionEquality().equals(contextsMutlipleRuntimesJson, json), + true, + ); + }); + test('fromJson', () { + final contexts = Contexts.fromJson(contextsJson); + final json = contexts.toJson(); + + expect( + DeepCollectionEquality().equals(contextsJson, json), + true, + ); + }); + test('fromJson multiple runtimes', () { + final contextsMutlipleRuntimes = + Contexts.fromJson(contextsMutlipleRuntimesJson); + final json = contextsMutlipleRuntimes.toJson(); + + expect( + DeepCollectionEquality().equals(contextsMutlipleRuntimesJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - data['extra'] = 'value'; - - final device = SentryDevice(batteryLevel: 100); - final os = SentryOperatingSystem(name: 'name1'); - final runtimes = [SentryRuntime(name: 'name1')]; - final app = SentryApp(name: 'name1'); - final browser = SentryBrowser(name: 'name1'); - final gpu = SentryGpu(id: 2); - final culture = SentryCulture(locale: 'foo-bar'); - - final copy = data.copyWith( - device: device, - operatingSystem: os, - runtimes: runtimes, - app: app, - browser: browser, - gpu: gpu, - culture: culture, - ); - - expect(device.toJson(), copy.device!.toJson()); - expect(os.toJson(), copy.operatingSystem!.toJson()); - expect( - ListEquality().equals(runtimes, copy.runtimes), - true, - ); - expect(app.toJson(), copy.app!.toJson()); - expect(browser.toJson(), copy.browser!.toJson()); - expect(culture.toJson(), copy.culture!.toJson()); - expect(gpu.toJson(), copy.gpu!.toJson()); - expect('value', copy['extra']); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = contexts; + + final copy = data.copyWith(); + + expect( + DeepCollectionEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = contexts; + data['extra'] = 'value'; + + final device = SentryDevice(batteryLevel: 100); + final os = SentryOperatingSystem(name: 'name1'); + final runtimes = [SentryRuntime(name: 'name1')]; + final app = SentryApp(name: 'name1'); + final browser = SentryBrowser(name: 'name1'); + final gpu = SentryGpu(id: 2); + final culture = SentryCulture(locale: 'foo-bar'); + + final copy = data.copyWith( + device: device, + operatingSystem: os, + runtimes: runtimes, + app: app, + browser: browser, + gpu: gpu, + culture: culture, + ); + + expect(device.toJson(), copy.device!.toJson()); + expect(os.toJson(), copy.operatingSystem!.toJson()); + expect( + ListEquality().equals(runtimes, copy.runtimes), + true, + ); + expect(app.toJson(), copy.app!.toJson()); + expect(browser.toJson(), copy.browser!.toJson()); + expect(culture.toJson(), copy.culture!.toJson()); + expect(gpu.toJson(), copy.gpu!.toJson()); + expect('value', copy['extra']); + }); }); } - -Contexts _generate() => Contexts( - device: SentryDevice(batteryLevel: 90), - operatingSystem: SentryOperatingSystem(name: 'name'), - runtimes: [SentryRuntime(name: 'name')], - app: SentryApp(name: 'name'), - browser: SentryBrowser(name: 'name'), - gpu: SentryGpu(id: 1), - culture: SentryCulture(locale: 'foo-bar'), - ); diff --git a/dart/test/protocol/debug_image_test.dart b/dart/test/protocol/debug_image_test.dart index a22b947b8d..427eb19b8a 100644 --- a/dart/test/protocol/debug_image_test.dart +++ b/dart/test/protocol/debug_image_test.dart @@ -3,52 +3,86 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final debugImage = DebugImage( + type: 'type', + imageAddr: 'imageAddr', + debugId: 'debugId', + debugFile: 'debugFile', + imageSize: 1, + uuid: 'uuid', + codeFile: 'codeFile', + arch: 'arch', + codeId: 'codeId', + ); - final copy = data.copyWith(); + final debugImageJson = { + 'uuid': 'uuid', + 'type': 'type', + 'debug_id': 'debugId', + 'debug_file': 'debugFile', + 'code_file': 'codeFile', + 'image_addr': 'imageAddr', + 'image_size': 1, + 'arch': 'arch', + 'code_id': 'codeId', + }; - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); + group('json', () { + test('toJson', () { + final json = debugImage.toJson(); + + expect( + MapEquality().equals(debugImageJson, json), + true, + ); + }); + test('fromJson', () { + final debugImage = DebugImage.fromJson(debugImageJson); + final json = debugImage.toJson(); + + expect( + MapEquality().equals(debugImageJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - type: 'type1', - imageAddr: 'imageAddr1', - debugId: 'debugId1', - debugFile: 'debugFile1', - imageSize: 2, - uuid: 'uuid1', - codeFile: 'codeFile1', - arch: 'arch1', - codeId: 'codeId1', - ); - - expect('type1', copy.type); - expect('imageAddr1', copy.imageAddr); - expect('debugId1', copy.debugId); - expect('debugFile1', copy.debugFile); - expect(2, copy.imageSize); - expect('uuid1', copy.uuid); - expect('codeFile1', copy.codeFile); - expect('arch1', copy.arch); - expect('codeId1', copy.codeId); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = debugImage; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = debugImage; + + final copy = data.copyWith( + type: 'type1', + imageAddr: 'imageAddr1', + debugId: 'debugId1', + debugFile: 'debugFile1', + imageSize: 2, + uuid: 'uuid1', + codeFile: 'codeFile1', + arch: 'arch1', + codeId: 'codeId1', + ); + + expect('type1', copy.type); + expect('imageAddr1', copy.imageAddr); + expect('debugId1', copy.debugId); + expect('debugFile1', copy.debugFile); + expect(2, copy.imageSize); + expect('uuid1', copy.uuid); + expect('codeFile1', copy.codeFile); + expect('arch1', copy.arch); + expect('codeId1', copy.codeId); + }); }); } - -DebugImage _generate() => DebugImage( - type: 'type', - imageAddr: 'imageAddr', - debugId: 'debugId', - debugFile: 'debugFile', - imageSize: 1, - uuid: 'uuid', - codeFile: 'codeFile', - arch: 'arch', - codeId: 'codeId', - ); diff --git a/dart/test/protocol/debug_meta_test.dart b/dart/test/protocol/debug_meta_test.dart index b611bc9828..21caf02747 100644 --- a/dart/test/protocol/debug_meta_test.dart +++ b/dart/test/protocol/debug_meta_test.dart @@ -3,42 +3,70 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final debugMeta = DebugMeta( + sdk: SdkInfo( + sdkName: 'sdkName', + ), + images: [DebugImage(type: 'macho', uuid: 'uuid')], + ); - final copy = data.copyWith(); + final debugMetaJson = { + 'sdk_info': {'sdk_name': 'sdkName'}, + 'images': [ + {'uuid': 'uuid', 'type': 'macho'} + ] + }; - // MapEquality fails for some reason, it probably check the instances equality too - expect(data.toJson(), copy.toJson()); + group('json', () { + test('toJson', () { + final json = debugMeta.toJson(); + + expect( + DeepCollectionEquality().equals(debugMetaJson, json), + true, + ); + }); + test('fromJson', () { + final debugMeta = DebugMeta.fromJson(debugMetaJson); + final json = debugMeta.toJson(); + + expect( + DeepCollectionEquality().equals(debugMetaJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - - final newSdkInfo = SdkInfo( - sdkName: 'sdkName1', - ); - final newImageList = [DebugImage(type: 'macho', uuid: 'uuid1')]; - - final copy = data.copyWith( - sdk: newSdkInfo, - images: newImageList, - ); - - expect( - ListEquality().equals(newImageList, copy.images), - true, - ); - expect( - MapEquality().equals(newSdkInfo.toJson(), copy.sdk!.toJson()), - true, - ); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = debugMeta; + + final copy = data.copyWith(); + + // MapEquality fails for some reason, it probably check the instances equality too + expect(data.toJson(), copy.toJson()); + }); + test('copyWith takes new values', () { + final data = debugMeta; + + final newSdkInfo = SdkInfo( + sdkName: 'sdkName1', + ); + final newImageList = [DebugImage(type: 'macho', uuid: 'uuid1')]; + + final copy = data.copyWith( + sdk: newSdkInfo, + images: newImageList, + ); + + expect( + ListEquality().equals(newImageList, copy.images), + true, + ); + expect( + MapEquality().equals(newSdkInfo.toJson(), copy.sdk!.toJson()), + true, + ); + }); }); } - -DebugMeta _generate() => DebugMeta( - sdk: SdkInfo( - sdkName: 'sdkName', - ), - images: [DebugImage(type: 'macho', uuid: 'uuid')], - ); diff --git a/dart/test/protocol/gpu_test.dart b/dart/test/protocol/gpu_test.dart deleted file mode 100644 index 872632e4c0..0000000000 --- a/dart/test/protocol/gpu_test.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - name: 'name1', - id: 11, - vendorId: 22, - vendorName: 'vendorName1', - memorySize: 33, - apiType: 'apiType1', - multiThreadedRendering: false, - version: 'version1', - npotSupport: 'npotSupport1', - ); - - expect('name1', copy.name); - expect(11, copy.id); - expect(22, copy.vendorId); - expect('vendorName1', copy.vendorName); - expect(33, copy.memorySize); - expect('apiType1', copy.apiType); - expect(false, copy.multiThreadedRendering); - expect('version1', copy.version); - expect('npotSupport1', copy.npotSupport); - }); -} - -SentryGpu _generate() => SentryGpu( - name: 'name', - id: 1, - vendorId: 2, - vendorName: 'vendorName', - memorySize: 3, - apiType: 'apiType', - multiThreadedRendering: true, - version: 'version', - npotSupport: 'npotSupport', - ); diff --git a/dart/test/protocol/mechanism_test.dart b/dart/test/protocol/mechanism_test.dart index dc940dfc47..4b484d2ca3 100644 --- a/dart/test/protocol/mechanism_test.dart +++ b/dart/test/protocol/mechanism_test.dart @@ -1,44 +1,76 @@ +import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final mechanism = Mechanism( + type: 'type', + description: 'description', + helpLink: 'helpLink', + handled: true, + synthetic: true, + meta: {'key': 'value'}, + data: {'keyb': 'valueb'}, + ); - final copy = data.copyWith(); + final mechanismJson = { + 'type': 'type', + 'description': 'description', + 'help_link': 'helpLink', + 'handled': true, + 'meta': {'key': 'value'}, + 'data': {'keyb': 'valueb'}, + 'synthetic': true, + }; - expect(data.toJson(), copy.toJson()); + group('json', () { + test('toJson', () { + final json = mechanism.toJson(); + + expect( + DeepCollectionEquality().equals(mechanismJson, json), + true, + ); + }); + test('fromJson', () { + final mechanism = Mechanism.fromJson(mechanismJson); + final json = mechanism.toJson(); + + expect( + DeepCollectionEquality().equals(mechanismJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - type: 'type1', - description: 'description1', - helpLink: 'helpLink1', - handled: false, - synthetic: false, - meta: {'key1': 'value1'}, - data: {'keyb1': 'valueb1'}, - ); - - expect('type1', copy.type); - expect('description1', copy.description); - expect('helpLink1', copy.helpLink); - expect(false, copy.handled); - expect(false, copy.synthetic); - expect({'key1': 'value1'}, copy.meta); - expect({'keyb1': 'valueb1'}, copy.data); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = mechanism; + + final copy = data.copyWith(); + + expect(data.toJson(), copy.toJson()); + }); + test('copyWith takes new values', () { + final data = mechanism; + + final copy = data.copyWith( + type: 'type1', + description: 'description1', + helpLink: 'helpLink1', + handled: false, + synthetic: false, + meta: {'key1': 'value1'}, + data: {'keyb1': 'valueb1'}, + ); + + expect('type1', copy.type); + expect('description1', copy.description); + expect('helpLink1', copy.helpLink); + expect(false, copy.handled); + expect(false, copy.synthetic); + expect({'key1': 'value1'}, copy.meta); + expect({'keyb1': 'valueb1'}, copy.data); + }); }); } - -Mechanism _generate() => Mechanism( - type: 'type', - description: 'description', - helpLink: 'helpLink', - handled: true, - synthetic: true, - meta: {'key': 'value'}, - data: {'keyb': 'valueb'}, - ); diff --git a/dart/test/protocol/message_test.dart b/dart/test/protocol/message_test.dart deleted file mode 100644 index 7705b82673..0000000000 --- a/dart/test/protocol/message_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - formatted: 'message 21', - template: 'message 2 %d', - params: ['2'], - ); - - expect('message 21', copy.formatted); - expect('message 2 %d', copy.template); - expect(['2'], copy.params); - }); -} - -SentryMessage _generate() => SentryMessage( - 'message 1', - template: 'message %d', - params: ['1'], - ); diff --git a/dart/test/protocol/operating_system_test.dart b/dart/test/protocol/operating_system_test.dart deleted file mode 100644 index 33d51d5df1..0000000000 --- a/dart/test/protocol/operating_system_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - name: 'name1', - version: 'version1', - build: 'build1', - kernelVersion: 'kernelVersion1', - rooted: true, - ); - - expect('name1', copy.name); - expect('version1', copy.version); - expect('build1', copy.build); - expect('kernelVersion1', copy.kernelVersion); - expect(true, copy.rooted); - }); -} - -SentryOperatingSystem _generate() => SentryOperatingSystem( - name: 'name', - version: 'version', - build: 'build', - kernelVersion: 'kernelVersion', - rooted: false, - ); diff --git a/dart/test/protocol/rate_limit_parser_test.dart b/dart/test/protocol/rate_limit_parser_test.dart new file mode 100644 index 0000000000..a8d5905c0e --- /dev/null +++ b/dart/test/protocol/rate_limit_parser_test.dart @@ -0,0 +1,107 @@ +import 'package:sentry/src/transport/rate_limit_parser.dart'; +import 'package:sentry/src/transport/rate_limit_category.dart'; +import 'package:test/test.dart'; + +void main() { + group('parseRateLimitHeader', () { + test('single rate limit with single category', () { + final sut = RateLimitParser('50:transaction').parseRateLimitHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].duration.inMilliseconds, 50000); + }); + + test('single rate limit with multiple categories', () { + final sut = + RateLimitParser('50:transaction;session').parseRateLimitHeader(); + + expect(sut.length, 2); + expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].duration.inMilliseconds, 50000); + expect(sut[1].category, RateLimitCategory.session); + expect(sut[1].duration.inMilliseconds, 50000); + }); + + test('don`t apply rate limit for unknown categories ', () { + final sut = RateLimitParser('50:somethingunknown').parseRateLimitHeader(); + + expect(sut.length, 0); + }); + + test('apply all if there are no categories', () { + final sut = RateLimitParser('50::key').parseRateLimitHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].duration.inMilliseconds, 50000); + }); + + test('multiple rate limits', () { + final sut = + RateLimitParser('50:transaction, 70:session').parseRateLimitHeader(); + + expect(sut.length, 2); + expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].duration.inMilliseconds, 50000); + expect(sut[1].category, RateLimitCategory.session); + expect(sut[1].duration.inMilliseconds, 70000); + }); + + test('multiple rate limits with same category', () { + final sut = RateLimitParser('50:transaction, 70:transaction') + .parseRateLimitHeader(); + + expect(sut.length, 2); + expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].duration.inMilliseconds, 50000); + expect(sut[1].category, RateLimitCategory.transaction); + expect(sut[1].duration.inMilliseconds, 70000); + }); + + test('ignore case', () { + final sut = RateLimitParser('50:TRANSACTION').parseRateLimitHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].duration.inMilliseconds, 50000); + }); + + test('un-parseable returns default duration', () { + final sut = RateLimitParser('foobar:transaction').parseRateLimitHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.transaction); + expect(sut[0].duration.inMilliseconds, + RateLimitParser.httpRetryAfterDefaultDelay.inMilliseconds); + }); + }); + + group('parseRetryAfterHeader', () { + test('null returns default category all with default duration', () { + final sut = RateLimitParser(null).parseRetryAfterHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].duration.inMilliseconds, + RateLimitParser.httpRetryAfterDefaultDelay.inMilliseconds); + }); + + test('parseable returns default category with duration in millis', () { + final sut = RateLimitParser('8').parseRetryAfterHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].duration.inMilliseconds, 8000); + }); + + test('un-parseable returns default category with default duration', () { + final sut = RateLimitParser('foobar').parseRetryAfterHeader(); + + expect(sut.length, 1); + expect(sut[0].category, RateLimitCategory.all); + expect(sut[0].duration.inMilliseconds, + RateLimitParser.httpRetryAfterDefaultDelay.inMilliseconds); + }); + }); +} diff --git a/dart/test/protocol/rate_limiter_test.dart b/dart/test/protocol/rate_limiter_test.dart new file mode 100644 index 0000000000..81775e0b40 --- /dev/null +++ b/dart/test/protocol/rate_limiter_test.dart @@ -0,0 +1,181 @@ +import 'package:test/test.dart'; + +import 'package:sentry/src/transport/rate_limiter.dart'; +import 'package:sentry/src/protocol/sentry_event.dart'; +import 'package:sentry/src/sentry_envelope.dart'; +import 'package:sentry/src/sentry_envelope_header.dart'; +import 'package:sentry/src/sentry_envelope_item.dart'; + +void main() { + var fixture = Fixture(); + + setUp(() { + fixture = Fixture(); + }); + + test('uses X-Sentry-Rate-Limit and allows sending if time has passed', () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '50:transaction:key, 1:default;error;security:organization', null, 1); + + fixture.dateTimeToReturn = 1001; + + final result = rateLimiter.filter(envelope); + expect(result, isNotNull); + expect(result!.items.length, 1); + }); + + test( + 'parse X-Sentry-Rate-Limit and set its values and retry after should be true', + () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '50:transaction:key, 2700:default;error;security:organization', + null, + 1); + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); + + test( + 'parse X-Sentry-Rate-Limit and set its values and retry after should be false', + () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '1:transaction:key, 1:default;error;security:organization', null, 1); + + fixture.dateTimeToReturn = 1001; + + final result = rateLimiter.filter(envelope); + expect(result, isNotNull); + expect(1, result!.items.length); + }); + + test( + 'When X-Sentry-Rate-Limit categories are empty, applies to all the categories', + () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits('50::key', null, 1); + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); + + test( + 'When all categories is set but expired, applies only for specific category', + () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '1::key, 60:default;error;security:organization', null, 1); + + fixture.dateTimeToReturn = 1001; + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); + + test('When category has shorter rate limiting, do not apply new timestamp', + () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '60:error:key, 1:error:organization', null, 1); + + fixture.dateTimeToReturn = 1001; + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); + + test('When category has longer rate limiting, apply new timestamp', () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits( + '1:error:key, 5:error:organization', null, 1); + + fixture.dateTimeToReturn = 1001; + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); + + test('When both retry headers are not present, default delay is set', () { + final rateLimiter = fixture.getSUT(); + fixture.dateTimeToReturn = 0; + final eventItem = SentryEnvelopeItem.fromEvent(SentryEvent()); + final envelope = SentryEnvelope( + SentryEnvelopeHeader.newEventId(), + [eventItem], + ); + + rateLimiter.updateRetryAfterLimits(null, null, 429); + + fixture.dateTimeToReturn = 1001; + + final result = rateLimiter.filter(envelope); + expect(result, isNull); + }); +} + +class Fixture { + var dateTimeToReturn = 0; + + RateLimiter getSUT() { + return RateLimiter(_currentDateTime); + } + + DateTime _currentDateTime() { + return DateTime.fromMillisecondsSinceEpoch(dateTimeToReturn); + } +} diff --git a/dart/test/protocol/request_test.dart b/dart/test/protocol/request_test.dart deleted file mode 100644 index ef5e68fa45..0000000000 --- a/dart/test/protocol/request_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - url: 'url1', - method: 'method1', - queryString: 'queryString1', - cookies: 'cookies1', - data: {'key1': 'value1'}, - ); - - expect('url1', copy.url); - expect('method1', copy.method); - expect('queryString1', copy.queryString); - expect('cookies1', copy.cookies); - expect({'key1': 'value1'}, copy.data); - }); -} - -SentryRequest _generate() => SentryRequest( - url: 'url', - method: 'method', - queryString: 'queryString', - cookies: 'cookies', - data: {'key': 'value'}, - ); diff --git a/dart/test/protocol/sdk_info_test.dart b/dart/test/protocol/sdk_info_test.dart index bee6f36ff8..50e3c3fdcd 100644 --- a/dart/test/protocol/sdk_info_test.dart +++ b/dart/test/protocol/sdk_info_test.dart @@ -3,37 +3,65 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final sdkInfo = SdkInfo( + sdkName: 'sdkName', + versionMajor: 1, + versionMinor: 2, + versionPatchlevel: 3, + ); - final copy = data.copyWith(); + final sdkInfoJson = { + 'sdk_name': 'sdkName', + 'version_major': 1, + 'version_minor': 2, + 'version_patchlevel': 3, + }; - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); + group('json', () { + test('toJson', () { + final json = sdkInfo.toJson(); + + expect( + MapEquality().equals(sdkInfoJson, json), + true, + ); + }); + test('fromJson', () { + final sdkInfo = SdkInfo.fromJson(sdkInfoJson); + final json = sdkInfo.toJson(); + + expect( + MapEquality().equals(sdkInfoJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sdkInfo; + + final copy = data.copyWith(); - final copy = data.copyWith( - sdkName: 'sdkName1', - versionMajor: 11, - versionMinor: 22, - versionPatchlevel: 33, - ); + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + test('copyWith takes new values', () { + final data = sdkInfo; - expect('sdkName1', copy.sdkName); - expect(11, copy.versionMajor); - expect(22, copy.versionMinor); - expect(33, copy.versionPatchlevel); + final copy = data.copyWith( + sdkName: 'sdkName1', + versionMajor: 11, + versionMinor: 22, + versionPatchlevel: 33, + ); + + expect('sdkName1', copy.sdkName); + expect(11, copy.versionMajor); + expect(22, copy.versionMinor); + expect(33, copy.versionPatchlevel); + }); }); } - -SdkInfo _generate() => SdkInfo( - sdkName: 'sdkName', - versionMajor: 1, - versionMinor: 2, - versionPatchlevel: 3, - ); diff --git a/dart/test/protocol/sdk_version_test.dart b/dart/test/protocol/sdk_version_test.dart index 35d6afcdfc..218ca72410 100644 --- a/dart/test/protocol/sdk_version_test.dart +++ b/dart/test/protocol/sdk_version_test.dart @@ -3,43 +3,77 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final sdkVersion = SdkVersion( + name: 'name', + version: 'version', + integrations: ['test'], + packages: [SentryPackage('name', 'version')], + ); - final copy = data.copyWith(); + final sdkVersionJson = { + 'name': 'name', + 'version': 'version', + 'integrations': ['test'], + 'packages': [ + { + 'name': 'name', + 'version': 'version', + } + ], + }; - expect(data.toJson(), copy.toJson()); + group('json', () { + test('toJson', () { + final json = sdkVersion.toJson(); + + expect( + DeepCollectionEquality().equals(sdkVersionJson, json), + true, + ); + }); + test('fromJson', () { + final sdkVersion = SdkVersion.fromJson(sdkVersionJson); + final json = sdkVersion.toJson(); + + expect( + DeepCollectionEquality().equals(sdkVersionJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - - final packages = [SentryPackage('name1', 'version1')]; - final integrations = ['test1']; - - final copy = data.copyWith( - name: 'name1', - version: 'version1', - integrations: integrations, - packages: packages, - ); - - expect( - ListEquality().equals(integrations, copy.integrations), - true, - ); - expect( - ListEquality().equals(packages, copy.packages), - true, - ); - expect('name1', copy.name); - expect('version1', copy.version); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sdkVersion; + + final copy = data.copyWith(); + + expect(data.toJson(), copy.toJson()); + }); + + test('copyWith takes new values', () { + final data = sdkVersion; + + final packages = [SentryPackage('name1', 'version1')]; + final integrations = ['test1']; + + final copy = data.copyWith( + name: 'name1', + version: 'version1', + integrations: integrations, + packages: packages, + ); + + expect( + ListEquality().equals(integrations, copy.integrations), + true, + ); + expect( + ListEquality().equals(packages, copy.packages), + true, + ); + expect('name1', copy.name); + expect('version1', copy.version); + }); }); } - -SdkVersion _generate() => SdkVersion( - name: 'name', - version: 'version', - integrations: ['test'], - packages: [SentryPackage('name', 'version')], - ); diff --git a/dart/test/protocol/sentry_app_test.dart b/dart/test/protocol/sentry_app_test.dart new file mode 100644 index 0000000000..11defbcdb6 --- /dev/null +++ b/dart/test/protocol/sentry_app_test.dart @@ -0,0 +1,83 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final testStartTime = DateTime.fromMicrosecondsSinceEpoch(0); + + final sentryApp = SentryApp( + name: 'fixture-name', + version: 'fixture-version', + identifier: 'fixture-identifier', + build: 'fixture-build', + buildType: 'fixture-buildType', + startTime: testStartTime, + deviceAppHash: 'fixture-deviceAppHash'); + + final sentryAppJson = { + 'app_name': 'fixture-name', + 'app_version': 'fixture-version', + 'app_identifier': 'fixture-identifier', + 'app_build': 'fixture-build', + 'build_type': 'fixture-buildType', + 'app_start_time': testStartTime.toIso8601String(), + 'device_app_hash': 'fixture-deviceAppHash' + }; + + group('json', () { + test('toJson', () { + final json = sentryApp.toJson(); + + expect( + MapEquality().equals(sentryAppJson, json), + true, + ); + }); + test('fromJson', () { + final sentryApp = SentryApp.fromJson(sentryAppJson); + final json = sentryApp.toJson(); + + expect( + MapEquality().equals(sentryAppJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryApp; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = sentryApp; + + final startTime = DateTime.now(); + + final copy = data.copyWith( + name: 'name1', + version: 'version1', + identifier: 'identifier1', + build: 'build1', + buildType: 'buildType1', + startTime: startTime, + deviceAppHash: 'hash1', + ); + + expect('name1', copy.name); + expect('version1', copy.version); + expect('identifier1', copy.identifier); + expect('build1', copy.build); + expect('buildType1', copy.buildType); + expect(startTime, copy.startTime); + expect('hash1', copy.deviceAppHash); + }); + }); +} diff --git a/dart/test/protocol/sentry_browser_test.dart b/dart/test/protocol/sentry_browser_test.dart new file mode 100644 index 0000000000..a388361ecd --- /dev/null +++ b/dart/test/protocol/sentry_browser_test.dart @@ -0,0 +1,60 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final sentryBrowser = SentryBrowser( + name: 'fixture-name', + version: 'fixture-version', + ); + + final sentryBrowserJson = { + 'name': 'fixture-name', + 'version': 'fixture-version', + }; + + group('json', () { + test('toJson', () { + final json = sentryBrowser.toJson(); + + expect( + MapEquality().equals(sentryBrowserJson, json), + true, + ); + }); + test('fromJson', () { + final sentryBrowser = SentryBrowser.fromJson(sentryBrowserJson); + final json = sentryBrowser.toJson(); + + expect( + MapEquality().equals(sentryBrowserJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryBrowser; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = sentryBrowser; + + final copy = data.copyWith( + name: 'name1', + version: 'version1', + ); + + expect('name1', copy.name); + expect('version1', copy.version); + }); + }); +} diff --git a/dart/test/protocol/sentry_device_test.dart b/dart/test/protocol/sentry_device_test.dart new file mode 100644 index 0000000000..e704fbc1a5 --- /dev/null +++ b/dart/test/protocol/sentry_device_test.dart @@ -0,0 +1,156 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final testBootTime = DateTime.fromMicrosecondsSinceEpoch(0); + + final sentryDevice = SentryDevice( + name: 'testDevice', + family: 'testFamily', + model: 'testModel', + modelId: 'testModelId', + arch: 'testArch', + batteryLevel: 23.0, + orientation: SentryOrientation.landscape, + manufacturer: 'testOEM', + brand: 'testBrand', + screenResolution: '123x345', + screenDensity: 99.1, + 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 sentryDeviceJson = { + 'name': 'testDevice', + 'family': 'testFamily', + 'model': 'testModel', + 'model_id': 'testModelId', + 'arch': 'testArch', + 'battery_level': 23.0, + 'orientation': 'landscape', + 'manufacturer': 'testOEM', + 'brand': 'testBrand', + 'screen_resolution': '123x345', + 'screen_density': 99.1, + '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', + }; + + group('json', () { + test('toJson', () { + final json = sentryDevice.toJson(); + + expect( + MapEquality().equals(sentryDeviceJson, json), + true, + ); + }); + test('fromJson', () { + final sentryDevice = SentryDevice.fromJson(sentryDeviceJson); + final json = sentryDevice.toJson(); + + expect( + MapEquality().equals(sentryDeviceJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryDevice; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = sentryDevice; + + final bootTime = DateTime.now(); + + final copy = data.copyWith( + name: 'name1', + family: 'family1', + model: 'model1', + modelId: 'modelId1', + arch: 'arch1', + batteryLevel: 2, + orientation: SentryOrientation.portrait, + manufacturer: 'manufacturer1', + brand: 'brand1', + screenResolution: '123x3451', + screenDensity: 99.2, + screenDpi: 99, + online: true, + charging: false, + lowMemory: true, + simulator: false, + memorySize: 12345678, + freeMemory: 123456, + usableMemory: 98765, + storageSize: 12345678, + freeStorage: 12345678, + externalStorageSize: 987654, + externalFreeStorage: 987654, + bootTime: bootTime, + timezone: 'Austria/Vienna', + ); + + expect('name1', copy.name); + expect('family1', copy.family); + expect('model1', copy.model); + expect('modelId1', copy.modelId); + expect('arch1', copy.arch); + expect(2, copy.batteryLevel); + expect(SentryOrientation.portrait, copy.orientation); + expect('manufacturer1', copy.manufacturer); + expect('brand1', copy.brand); + expect('123x3451', copy.screenResolution); + expect(99.2, copy.screenDensity); + expect(99, copy.screenDpi); + expect(true, copy.online); + expect(false, copy.charging); + expect(true, copy.lowMemory); + expect(false, copy.simulator); + expect(12345678, copy.memorySize); + expect(123456, copy.freeMemory); + expect(98765, copy.usableMemory); + expect(12345678, copy.storageSize); + expect(12345678, copy.freeStorage); + expect(987654, copy.externalStorageSize); + expect(987654, copy.externalFreeStorage); + expect(bootTime, copy.bootTime); + expect('Austria/Vienna', copy.timezone); + }); + }); +} diff --git a/dart/test/protocol/sentry_exception_test.dart b/dart/test/protocol/sentry_exception_test.dart index cad8cd4cba..15c75b0191 100644 --- a/dart/test/protocol/sentry_exception_test.dart +++ b/dart/test/protocol/sentry_exception_test.dart @@ -1,126 +1,154 @@ +import 'package:collection/collection.dart'; 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: { + final sentryException = SentryException( + type: 'type', + value: 'value', + module: 'module', + stackTrace: SentryStackTrace(frames: [SentryStackFrame(absPath: 'abs')]), + mechanism: Mechanism(type: 'type'), + threadId: 1, + ); + + final sentryExceptionJson = { + 'type': 'type', + 'value': 'value', + 'module': 'module', + 'stacktrace': { + 'frames': [ + {'abs_path': 'abs'} + ] + }, + 'mechanism': {'type': 'type'}, + 'thread_id': 1, + }; + + group('json', () { + test('fromJson', () { + final sentryException = SentryException.fromJson(sentryExceptionJson); + final json = sentryException.toJson(); + + expect( + DeepCollectionEquality().equals(sentryExceptionJson, json), + true, + ); + }); + + 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 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]); + 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]); + }); }); - test('copyWith keeps unchanged', () { - final data = _generate(); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryException; - final copy = data.copyWith(); + final copy = data.copyWith(); - expect(data.toJson(), copy.toJson()); - }); + expect(data.toJson(), copy.toJson()); + }); + + test('copyWith takes new values', () { + final data = sentryException; - test('copyWith takes new values', () { - final data = _generate(); - - final stackTrace = - SentryStackTrace(frames: [SentryStackFrame(absPath: 'abs1')]); - final mechanism = Mechanism(type: 'type1'); - - final copy = data.copyWith( - type: 'type1', - value: 'value1', - module: 'module1', - stackTrace: stackTrace, - mechanism: mechanism, - threadId: 2, - ); - - expect('type1', copy.type); - expect('value1', copy.value); - expect('module1', copy.module); - expect(2, copy.threadId); - expect(mechanism.toJson(), copy.mechanism!.toJson()); - expect(stackTrace.toJson(), copy.stackTrace!.toJson()); + final stackTrace = + SentryStackTrace(frames: [SentryStackFrame(absPath: 'abs1')]); + final mechanism = Mechanism(type: 'type1'); + + final copy = data.copyWith( + type: 'type1', + value: 'value1', + module: 'module1', + stackTrace: stackTrace, + mechanism: mechanism, + threadId: 2, + ); + + expect('type1', copy.type); + expect('value1', copy.value); + expect('module1', copy.module); + expect(2, copy.threadId); + expect(mechanism.toJson(), copy.mechanism!.toJson()); + expect(stackTrace.toJson(), copy.stackTrace!.toJson()); + }); }); } - -SentryException _generate() => SentryException( - type: 'type', - value: 'value', - module: 'module', - stackTrace: SentryStackTrace(frames: [SentryStackFrame(absPath: 'abs')]), - mechanism: Mechanism(type: 'type'), - threadId: 1, - ); diff --git a/dart/test/protocol/sentry_gpu_test.dart b/dart/test/protocol/sentry_gpu_test.dart new file mode 100644 index 0000000000..c1e1c363b8 --- /dev/null +++ b/dart/test/protocol/sentry_gpu_test.dart @@ -0,0 +1,86 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final sentryGpu = SentryGpu( + name: 'fixture-name', + id: 1, + vendorId: 2, + vendorName: 'fixture-vendorName', + memorySize: 3, + apiType: 'fixture-apiType', + multiThreadedRendering: true, + version: '4', + npotSupport: 'fixture-npotSupport'); + + final sentryGpuJson = { + 'name': 'fixture-name', + 'id': 1, + 'vendor_id': 2, + 'vendor_name': 'fixture-vendorName', + 'memory_size': 3, + 'api_type': 'fixture-apiType', + 'multi_threaded_rendering': true, + 'version': '4', + 'npot_support': 'fixture-npotSupport' + }; + + group('json', () { + test('toJson', () { + final json = sentryGpu.toJson(); + + expect( + MapEquality().equals(sentryGpuJson, json), + true, + ); + }); + test('fromJson', () { + final sentryGpu = SentryGpu.fromJson(sentryGpuJson); + final json = sentryGpu.toJson(); + + expect( + MapEquality().equals(sentryGpuJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryGpu; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + test('copyWith takes new values', () { + final data = sentryGpu; + + final copy = data.copyWith( + name: 'name1', + id: 11, + vendorId: 22, + vendorName: 'vendorName1', + memorySize: 33, + apiType: 'apiType1', + multiThreadedRendering: false, + version: 'version1', + npotSupport: 'npotSupport1', + ); + + expect('name1', copy.name); + expect(11, copy.id); + expect(22, copy.vendorId); + expect('vendorName1', copy.vendorName); + expect(33, copy.memorySize); + expect('apiType1', copy.apiType); + expect(false, copy.multiThreadedRendering); + expect('version1', copy.version); + expect('npotSupport1', copy.npotSupport); + }); + }); +} diff --git a/dart/test/protocol/sentry_message_test.dart b/dart/test/protocol/sentry_message_test.dart new file mode 100644 index 0000000000..7112642ed2 --- /dev/null +++ b/dart/test/protocol/sentry_message_test.dart @@ -0,0 +1,64 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final sentryMessage = SentryMessage( + 'message 1', + template: 'message %d', + params: ['1'], + ); + + final sentryMessageJson = { + 'formatted': 'message 1', + 'message': 'message %d', + 'params': ['1'], + }; + + group('json', () { + test('toJson', () { + final json = sentryMessage.toJson(); + + expect( + DeepCollectionEquality().equals(sentryMessageJson, json), + true, + ); + }); + test('fromJson', () { + final sentryMessage = SentryMessage.fromJson(sentryMessageJson); + final json = sentryMessage.toJson(); + + expect( + DeepCollectionEquality().equals(sentryMessageJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryMessage; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = sentryMessage; + + final copy = data.copyWith( + formatted: 'message 21', + template: 'message 2 %d', + params: ['2'], + ); + + expect('message 21', copy.formatted); + expect('message 2 %d', copy.template); + expect(['2'], copy.params); + }); + }); +} diff --git a/dart/test/protocol/sentry_operating_system_test.dart b/dart/test/protocol/sentry_operating_system_test.dart new file mode 100644 index 0000000000..dee235712d --- /dev/null +++ b/dart/test/protocol/sentry_operating_system_test.dart @@ -0,0 +1,74 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final sentryOperatingSystem = SentryOperatingSystem( + name: 'fixture-name', + version: 'fixture-version', + build: 'fixture-build', + kernelVersion: 'fixture-kernelVersion', + rooted: true, + rawDescription: 'fixture-rawDescription'); + + final sentryOperatingSystemJson = { + 'name': 'fixture-name', + 'version': 'fixture-version', + 'build': 'fixture-build', + 'kernel_version': 'fixture-kernelVersion', + 'rooted': true, + 'raw_description': 'fixture-rawDescription' + }; + + group('json', () { + test('toJson', () { + final json = sentryOperatingSystem.toJson(); + + expect( + MapEquality().equals(sentryOperatingSystemJson, json), + true, + ); + }); + test('fromJson', () { + final sentryOperatingSystem = + SentryOperatingSystem.fromJson(sentryOperatingSystemJson); + final json = sentryOperatingSystem.toJson(); + + expect( + MapEquality().equals(sentryOperatingSystemJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryOperatingSystem; + + final copy = data.copyWith(); + + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = sentryOperatingSystem; + + final copy = data.copyWith( + name: 'name1', + version: 'version1', + build: 'build1', + kernelVersion: 'kernelVersion1', + rooted: true, + ); + + expect('name1', copy.name); + expect('version1', copy.version); + expect('build1', copy.build); + expect('kernelVersion1', copy.kernelVersion); + expect(true, copy.rooted); + }); + }); +} diff --git a/dart/test/protocol/sentry_package_test.dart b/dart/test/protocol/sentry_package_test.dart index 35e1ffd75e..b8c1d40e71 100644 --- a/dart/test/protocol/sentry_package_test.dart +++ b/dart/test/protocol/sentry_package_test.dart @@ -3,31 +3,57 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final sentryPackage = SentryPackage( + 'name', + 'version', + ); - final copy = data.copyWith(); + final sentryPackageJson = { + 'name': 'name', + 'version': 'version', + }; - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); + group('json', () { + test('toJson', () { + final json = sentryPackage.toJson(); + + expect( + MapEquality().equals(sentryPackageJson, json), + true, + ); + }); + test('fromJson', () { + final sentryPackage = SdkVersion.fromJson(sentryPackageJson); + final json = sentryPackage.toJson(); + + expect( + MapEquality().equals(sentryPackageJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryPackage; + + final copy = data.copyWith(); - final copy = data.copyWith( - name: 'name1', - version: 'version1', - ); + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + test('copyWith takes new values', () { + final data = sentryPackage; - expect('name1', copy.name); - expect('version1', copy.version); + final copy = data.copyWith( + name: 'name1', + version: 'version1', + ); + + expect('name1', copy.name); + expect('version1', copy.version); + }); }); } - -SentryPackage _generate() => SentryPackage( - 'name', - 'version', - ); diff --git a/dart/test/protocol/sentry_request_test.dart b/dart/test/protocol/sentry_request_test.dart new file mode 100644 index 0000000000..ae78f75fdb --- /dev/null +++ b/dart/test/protocol/sentry_request_test.dart @@ -0,0 +1,78 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final sentryRequest = SentryRequest( + url: 'url', + method: 'method', + queryString: 'queryString', + cookies: 'cookies', + data: {'key': 'value'}, + headers: {'header_key': 'header_value'}, + env: {'env_key': 'env_value'}, + other: {'other_key': 'other_value'}, + ); + + final sentryRequestJson = { + 'url': 'url', + 'method': 'method', + 'query_string': 'queryString', + 'cookies': 'cookies', + 'data': {'key': 'value'}, + 'headers': {'header_key': 'header_value'}, + 'env': {'env_key': 'env_value'}, + 'other': {'other_key': 'other_value'}, + }; + + group('json', () { + test('toJson', () { + final json = sentryRequest.toJson(); + + expect( + DeepCollectionEquality().equals(sentryRequestJson, json), + true, + ); + }); + test('fromJson', () { + final sentryRequest = SentryRequest.fromJson(sentryRequestJson); + final json = sentryRequest.toJson(); + + expect( + DeepCollectionEquality().equals(sentryRequestJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryRequest; + + final copy = data.copyWith(); + + expect( + DeepCollectionEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); + + test('copyWith takes new values', () { + final data = sentryRequest; + + final copy = data.copyWith( + url: 'url1', + method: 'method1', + queryString: 'queryString1', + cookies: 'cookies1', + data: {'key1': 'value1'}, + ); + + expect('url1', copy.url); + expect('method1', copy.method); + expect('queryString1', copy.queryString); + expect('cookies1', copy.cookies); + expect({'key1': 'value1'}, copy.data); + }); + }); +} diff --git a/dart/test/protocol/sentry_runtime_test.dart b/dart/test/protocol/sentry_runtime_test.dart index 75878c6d2c..578eb8c9a3 100644 --- a/dart/test/protocol/sentry_runtime_test.dart +++ b/dart/test/protocol/sentry_runtime_test.dart @@ -3,37 +3,65 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final sentryRuntime = SentryRuntime( + key: 'key', + name: 'name', + version: 'version', + rawDescription: 'rawDescription', + ); - final copy = data.copyWith(); + final sentryRuntimeJson = { + 'name': 'name', + 'version': 'version', + 'raw_description': 'rawDescription', + }; - expect( - MapEquality().equals(data.toJson(), copy.toJson()), - true, - ); + group('json', () { + test('toJson', () { + final json = sentryRuntime.toJson(); + + expect( + MapEquality().equals(sentryRuntimeJson, json), + true, + ); + }); + test('fromJson', () { + final sentryRuntime = SentryRuntime.fromJson(sentryRuntimeJson); + final json = sentryRuntime.toJson(); + + expect( + MapEquality().equals(sentryRuntimeJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryRuntime; + + final copy = data.copyWith(); - final copy = data.copyWith( - key: 'key1', - name: 'name1', - version: 'version1', - rawDescription: 'rawDescription1', - ); + expect( + MapEquality().equals(data.toJson(), copy.toJson()), + true, + ); + }); - expect('key1', copy.key); - expect('name1', copy.name); - expect('version1', copy.version); - expect('rawDescription1', copy.rawDescription); + test('copyWith takes new values', () { + final data = sentryRuntime; + + final copy = data.copyWith( + key: 'key1', + name: 'name1', + version: 'version1', + rawDescription: 'rawDescription1', + ); + + expect('key1', copy.key); + expect('name1', copy.name); + expect('version1', copy.version); + expect('rawDescription1', copy.rawDescription); + }); }); } - -SentryRuntime _generate() => SentryRuntime( - key: 'key', - name: 'name', - version: 'version', - rawDescription: 'rawDescription', - ); diff --git a/dart/test/protocol/sentry_stack_frame_test.dart b/dart/test/protocol/sentry_stack_frame_test.dart index 3690e77f53..69b5ad30fa 100644 --- a/dart/test/protocol/sentry_stack_frame_test.dart +++ b/dart/test/protocol/sentry_stack_frame_test.dart @@ -1,79 +1,123 @@ +import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final sentryStackFrame = SentryStackFrame( + absPath: 'absPath', + fileName: 'fileName', + function: 'function', + module: 'module', + lineNo: 1, + colNo: 2, + contextLine: 'contextLine', + inApp: true, + package: 'package', + native: false, + platform: 'platform', + imageAddr: 'imageAddr', + symbolAddr: 'symbolAddr', + instructionAddr: 'instructionAddr', + rawFunction: 'rawFunction', + framesOmitted: [1], + preContext: ['a'], + postContext: ['b'], + vars: {'key': 'value'}, + ); - final copy = data.copyWith(); + final sentryStackFrameJson = { + 'pre_context': ['a'], + 'post_context': ['b'], + 'vars': {'key': 'value'}, + 'frames_omitted': [1], + 'filename': 'fileName', + 'package': 'package', + 'function': 'function', + 'module': 'module', + 'lineno': 1, + 'colno': 2, + 'abs_path': 'absPath', + 'context_line': 'contextLine', + 'in_app': true, + 'native': false, + 'platform': 'platform', + 'image_addr': 'imageAddr', + 'symbol_addr': 'symbolAddr', + 'instruction_addr': 'instructionAddr', + 'raw_function': 'rawFunction', + }; - expect(data.toJson(), copy.toJson()); + group('json', () { + test('toJson', () { + final json = sentryStackFrame.toJson(); + + expect( + DeepCollectionEquality().equals(sentryStackFrameJson, json), + true, + ); + }); + test('fromJson', () { + final sentryStackFrame = SentryStackFrame.fromJson(sentryStackFrameJson); + final json = sentryStackFrame.toJson(); + + expect( + DeepCollectionEquality().equals(sentryStackFrameJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryStackFrame; + + final copy = data.copyWith(); - final copy = data.copyWith( - absPath: 'absPath1', - fileName: 'fileName1', - function: 'function1', - module: 'module1', - lineNo: 11, - colNo: 22, - contextLine: 'contextLine1', - inApp: false, - package: 'package1', - native: true, - platform: 'platform1', - imageAddr: 'imageAddr1', - symbolAddr: 'symbolAddr1', - instructionAddr: 'instructionAddr1', - rawFunction: 'rawFunction1', - framesOmitted: [11], - preContext: ['ab'], - postContext: ['bb'], - vars: {'key1': 'value1'}, - ); + expect(data.toJson(), copy.toJson()); + }); + test('copyWith takes new values', () { + final data = sentryStackFrame; - expect('absPath1', copy.absPath); - expect('fileName1', copy.fileName); - expect('function1', copy.function); - expect('module1', copy.module); - expect(11, copy.lineNo); - expect(22, copy.colNo); - expect(false, copy.inApp); - expect('package1', copy.package); - expect(true, copy.native); - expect('platform1', copy.platform); - expect('imageAddr1', copy.imageAddr); - expect('symbolAddr1', copy.symbolAddr); - expect('instructionAddr1', copy.instructionAddr); - expect('rawFunction1', copy.rawFunction); - expect([11], copy.framesOmitted); - expect(['ab'], copy.preContext); - expect(['bb'], copy.postContext); - expect({'key1': 'value1'}, copy.vars); + final copy = data.copyWith( + absPath: 'absPath1', + fileName: 'fileName1', + function: 'function1', + module: 'module1', + lineNo: 11, + colNo: 22, + contextLine: 'contextLine1', + inApp: false, + package: 'package1', + native: true, + platform: 'platform1', + imageAddr: 'imageAddr1', + symbolAddr: 'symbolAddr1', + instructionAddr: 'instructionAddr1', + rawFunction: 'rawFunction1', + framesOmitted: [11], + preContext: ['ab'], + postContext: ['bb'], + vars: {'key1': 'value1'}, + ); + + expect('absPath1', copy.absPath); + expect('fileName1', copy.fileName); + expect('function1', copy.function); + expect('module1', copy.module); + expect(11, copy.lineNo); + expect(22, copy.colNo); + expect(false, copy.inApp); + expect('package1', copy.package); + expect(true, copy.native); + expect('platform1', copy.platform); + expect('imageAddr1', copy.imageAddr); + expect('symbolAddr1', copy.symbolAddr); + expect('instructionAddr1', copy.instructionAddr); + expect('rawFunction1', copy.rawFunction); + expect([11], copy.framesOmitted); + expect(['ab'], copy.preContext); + expect(['bb'], copy.postContext); + expect({'key1': 'value1'}, copy.vars); + }); }); } - -SentryStackFrame _generate() => SentryStackFrame( - absPath: 'absPath', - fileName: 'fileName', - function: 'function', - module: 'module', - lineNo: 1, - colNo: 2, - contextLine: 'contextLine', - inApp: true, - package: 'package', - native: false, - platform: 'platform', - imageAddr: 'imageAddr', - symbolAddr: 'symbolAddr', - instructionAddr: 'instructionAddr', - rawFunction: 'rawFunction', - framesOmitted: [1], - preContext: ['a'], - postContext: ['b'], - vars: {'key': 'value'}, - ); diff --git a/dart/test/protocol/sentry_stack_trace_test.dart b/dart/test/protocol/sentry_stack_trace_test.dart index 8f74f3c59e..ce3f4817d6 100644 --- a/dart/test/protocol/sentry_stack_trace_test.dart +++ b/dart/test/protocol/sentry_stack_trace_test.dart @@ -3,37 +3,66 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); + final sentryStackTrace = SentryStackTrace( + frames: [SentryStackFrame(absPath: 'abs')], + registers: {'key': 'value'}, + ); - final copy = data.copyWith(); + final sentryStackTraceJson = { + 'frames': [ + {'abs_path': 'abs'} + ], + 'registers': {'key': 'value'}, + }; - expect(data.toJson(), copy.toJson()); + group('json', () { + test('toJson', () { + final json = sentryStackTrace.toJson(); + + expect( + DeepCollectionEquality().equals(sentryStackTraceJson, json), + true, + ); + }); + test('fromJson', () { + final sentryStackTrace = SentryStackTrace.fromJson(sentryStackTraceJson); + final json = sentryStackTrace.toJson(); + + expect( + DeepCollectionEquality().equals(sentryStackTraceJson, json), + true, + ); + }); }); - test('copyWith takes new values', () { - final data = _generate(); - - final frames = [SentryStackFrame(absPath: 'abs1')]; - final registers = {'key1': 'value1'}; - - final copy = data.copyWith( - frames: frames, - registers: registers, - ); - - expect( - ListEquality().equals(frames, copy.frames), - true, - ); - expect( - MapEquality().equals(registers, copy.registers), - true, - ); + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryStackTrace; + + final copy = data.copyWith(); + + expect(data.toJson(), copy.toJson()); + }); + + test('copyWith takes new values', () { + final data = sentryStackTrace; + + final frames = [SentryStackFrame(absPath: 'abs1')]; + final registers = {'key1': 'value1'}; + + final copy = data.copyWith( + frames: frames, + registers: registers, + ); + + expect( + ListEquality().equals(frames, copy.frames), + true, + ); + expect( + MapEquality().equals(registers, copy.registers), + true, + ); + }); }); } - -SentryStackTrace _generate() => SentryStackTrace( - frames: [SentryStackFrame(absPath: 'abs')], - registers: {'key': 'value'}, - ); diff --git a/dart/test/protocol/sentry_user_test.dart b/dart/test/protocol/sentry_user_test.dart new file mode 100644 index 0000000000..297a949099 --- /dev/null +++ b/dart/test/protocol/sentry_user_test.dart @@ -0,0 +1,95 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final sentryUser = SentryUser( + id: 'id', + username: 'username', + email: 'email', + ipAddress: 'ipAddress', + extras: {'key': 'value'}, + ); + + final sentryUserJson = { + 'id': 'id', + 'username': 'username', + 'email': 'email', + 'ip_address': 'ipAddress', + 'extras': {'key': 'value'}, + }; + + group('json', () { + test('toJson', () { + final json = sentryUser.toJson(); + + expect( + DeepCollectionEquality().equals(sentryUserJson, json), + true, + ); + }); + test('fromJson', () { + final sentryUser = SentryUser.fromJson(sentryUserJson); + final json = sentryUser.toJson(); + + expect( + DeepCollectionEquality().equals(sentryUserJson, json), + true, + ); + }); + + test('toJson only serialises non-null values', () { + var data = SentryUser( + id: 'id', + ); + + var json = data.toJson(); + + expect(json.containsKey('id'), true); + expect(json.containsKey('username'), false); + expect(json.containsKey('email'), false); + expect(json.containsKey('ip_address'), false); + expect(json.containsKey('extras'), false); + + data = SentryUser( + ipAddress: 'ip', + ); + + json = data.toJson(); + + expect(json.containsKey('id'), false); + expect(json.containsKey('username'), false); + expect(json.containsKey('email'), false); + expect(json.containsKey('ip_address'), true); + expect(json.containsKey('extras'), false); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = sentryUser; + + final copy = data.copyWith(); + + expect(data.toJson(), copy.toJson()); + }); + + test('copyWith takes new values', () { + final data = sentryUser; + + final copy = data.copyWith( + id: 'id1', + username: 'username1', + email: 'email1', + ipAddress: 'ipAddress1', + extras: {'key1': 'value1'}, + ); + + expect('id1', copy.id); + expect('username1', copy.username); + expect('email1', copy.email); + expect('ipAddress1', copy.ipAddress); + expect({'key1': 'value1'}, copy.extras); + }); + }); +} diff --git a/dart/test/protocol/user_test.dart b/dart/test/protocol/user_test.dart deleted file mode 100644 index 8f0a080d52..0000000000 --- a/dart/test/protocol/user_test.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; - -void main() { - test('copyWith keeps unchanged', () { - final data = _generate(); - - final copy = data.copyWith(); - - expect(data.toJson(), copy.toJson()); - }); - - test('copyWith takes new values', () { - final data = _generate(); - - final copy = data.copyWith( - id: 'id1', - username: 'username1', - email: 'email1', - ipAddress: 'ipAddress1', - extras: {'key1': 'value1'}, - ); - - expect('id1', copy.id); - expect('username1', copy.username); - expect('email1', copy.email); - expect('ipAddress1', copy.ipAddress); - expect({'key1': 'value1'}, copy.extras); - }); - - test('toJson only serialises non-null values', () { - var data = SentryUser( - id: 'id', - ); - - var json = data.toJson(); - - expect(json.containsKey('id'), true); - expect(json.containsKey('username'), false); - expect(json.containsKey('email'), false); - expect(json.containsKey('ip_address'), false); - expect(json.containsKey('extras'), false); - - data = SentryUser( - ipAddress: 'ip', - ); - - json = data.toJson(); - - expect(json.containsKey('id'), false); - expect(json.containsKey('username'), false); - expect(json.containsKey('email'), false); - expect(json.containsKey('ip_address'), true); - expect(json.containsKey('extras'), false); - }); -} - -SentryUser _generate() => SentryUser( - id: 'id', - username: 'username', - email: 'email', - ipAddress: 'ipAddress', - extras: {'key': 'value'}, - ); diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 86f2645a95..6d16cbdb84 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:sentry/sentry.dart'; import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:test/test.dart'; @@ -22,7 +24,9 @@ void main() { stackTrace: '#0 baz (file:///pathto/test.dart:50:3)', ); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.stackTrace is SentryStackTrace, true); }); @@ -32,7 +36,9 @@ void main() { final event = SentryEvent(); await client.captureEvent(event); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.stackTrace is SentryStackTrace, true); }); @@ -42,7 +48,9 @@ void main() { final event = SentryEvent(); await client.captureEvent(event); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.stackTrace, isNull); }); @@ -62,7 +70,9 @@ void main() { stackTrace: '#0 baz (file:///pathto/test.dart:50:3)', ); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.stackTrace, isNull); expect(capturedEvent.exception!.stackTrace, isNotNull); @@ -86,7 +96,9 @@ void main() { stackTrace: '#0 baz (file:///pathto/test.dart:50:3)', ); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.stackTrace, isNull); expect(capturedEvent.exception!.stackTrace, isNotNull); @@ -101,7 +113,9 @@ void main() { level: SentryLevel.error, ); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.message!.formatted, 'simple message 1'); expect(capturedEvent.message!.template, 'simple message %d'); @@ -115,7 +129,10 @@ void main() { 'simple message 1', ); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + expect(capturedEvent.level, SentryLevel.info); }); @@ -123,7 +140,9 @@ void main() { final client = SentryClient(options..attachStacktrace = false); await client.captureMessage('message', level: SentryLevel.error); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.stackTrace, isNull); }); @@ -151,9 +170,10 @@ void main() { final client = SentryClient(options); await client.captureException(error, stackTrace: stackTrace); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); - expect(capturedEvent.throwable, error); expect(capturedEvent.exception is SentryException, true); expect(capturedEvent.exception!.stackTrace, isNotNull); }); @@ -185,9 +205,10 @@ void main() { final client = SentryClient(options); await client.captureException(error, stackTrace: stacktrace); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); - expect(capturedEvent.throwable, error); expect(capturedEvent.exception is SentryException, true); expect(capturedEvent.exception!.stackTrace, isNotNull); expect(capturedEvent.exception!.stackTrace!.frames.first.fileName, @@ -223,9 +244,10 @@ void main() { final client = SentryClient(options); await client.captureException(exception, stackTrace: stacktrace); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); - expect(capturedEvent.throwable, exception); expect(capturedEvent.exception is SentryException, true); expect(capturedEvent.exception!.stackTrace!.frames.first.fileName, 'test.dart'); @@ -243,7 +265,9 @@ void main() { final client = SentryClient(options); await client.captureException(exception); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.exception!.stackTrace, isNotNull); }); @@ -258,7 +282,9 @@ void main() { final client = SentryClient(options..attachStacktrace = false); await client.captureException(exception); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.exception!.stackTrace, isNull); }); @@ -280,7 +306,9 @@ void main() { final client = SentryClient(options); await client.captureException(exception, stackTrace: stacktrace); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect( capturedEvent.exception!.stackTrace!.frames @@ -333,13 +361,15 @@ void main() { final client = SentryClient(options); await client.captureEvent(event, scope: scope); - final capturedEvent = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.user?.id, user.id); expect(capturedEvent.level!.name, SentryLevel.error.name); expect(capturedEvent.transaction, transaction); expect(capturedEvent.fingerprint, fingerprint); - expect(capturedEvent.breadcrumbs?.first, crumb); + expect(capturedEvent.breadcrumbs?.first.toJson(), crumb.toJson()); expect(capturedEvent.tags, { scopeTagKey: scopeTagValue, eventTagKey: eventTagValue, @@ -384,21 +414,24 @@ void main() { }); test('should not apply the scope to non null event fields ', () async { - final client = fixture.getSut(true); + final client = fixture.getSut(sendDefaultPii: true); final scope = createScope(fixture.options); await client.captureEvent(event, scope: scope); - final capturedEvent = fixture.transport.events.first; + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + expect(capturedEvent.user!.id, eventUser.id); expect(capturedEvent.level!.name, SentryLevel.warning.name); expect(capturedEvent.transaction, eventTransaction); expect(capturedEvent.fingerprint, eventFingerprint); - expect(capturedEvent.breadcrumbs, eventCrumbs); + expect(capturedEvent.breadcrumbs?.map((e) => e.toJson()), + eventCrumbs.map((e) => e.toJson())); }); test('should apply the scope user to null event user fields ', () async { - final client = fixture.getSut(true); + final client = fixture.getSut(sendDefaultPii: true); final scope = createScope(fixture.options); scope.user = SentryUser(id: '987'); @@ -408,18 +441,20 @@ void main() { ); await client.captureEvent(eventWithUser, scope: scope); - final capturedEvent = fixture.transport.events.first; + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.user!.id, '123'); expect(capturedEvent.user!.username, 'foo bar'); expect(capturedEvent.level!.name, SentryLevel.warning.name); expect(capturedEvent.transaction, eventTransaction); expect(capturedEvent.fingerprint, eventFingerprint); - expect(capturedEvent.breadcrumbs, eventCrumbs); + expect(capturedEvent.breadcrumbs?.map((e) => e.toJson()), + eventCrumbs.map((e) => e.toJson())); }); test('merge scope user and event user extra', () async { - final client = fixture.getSut(true); + final client = fixture.getSut(sendDefaultPii: true); final scope = createScope(fixture.options); scope.user = SentryUser( @@ -441,7 +476,8 @@ void main() { ); await client.captureEvent(eventWithUser, scope: scope); - final capturedEvent = fixture.transport.events.first; + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); expect(capturedEvent.user?.extras?['foo'], 'this bar is more important'); expect(capturedEvent.user?.extras?['bar'], 'foo'); @@ -457,52 +493,63 @@ void main() { }); test('sendDefaultPii is disabled', () async { - final client = fixture.getSut(false); + final client = fixture.getSut(sendDefaultPii: false); await client.captureEvent(fakeEvent); - expect(fixture.transport.events.first.user, fakeEvent.user); + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.user?.toJson(), fakeEvent.user?.toJson()); }); test('sendDefaultPii is enabled and event has no user', () async { - final client = fixture.getSut(true); + final client = fixture.getSut(sendDefaultPii: true); var fakeEvent = SentryEvent(); await client.captureEvent(fakeEvent); - expect(fixture.transport.events.length, 1); - expect(fixture.transport.events.first.user, isNotNull); - expect(fixture.transport.events.first.user?.ipAddress, '{{auto}}'); + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(fixture.transport.envelopes.length, 1); + expect(capturedEvent.user, isNotNull); + expect(capturedEvent.user?.ipAddress, '{{auto}}'); }); test('sendDefaultPii is enabled and event has a user with IP address', () async { - final client = fixture.getSut(true); + final client = fixture.getSut(sendDefaultPii: true); await client.captureEvent(fakeEvent); - expect(fixture.transport.events.length, 1); - expect(fixture.transport.events.first.user, isNotNull); + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(fixture.transport.envelopes.length, 1); + expect(capturedEvent.user, isNotNull); // fakeEvent has a user which is not null - expect(fixture.transport.events.first.user?.ipAddress, - fakeEvent.user!.ipAddress); - expect(fixture.transport.events.first.user?.id, fakeEvent.user!.id); - expect(fixture.transport.events.first.user?.email, fakeEvent.user!.email); + expect(capturedEvent.user?.ipAddress, fakeEvent.user!.ipAddress); + expect(capturedEvent.user?.id, fakeEvent.user!.id); + expect(capturedEvent.user?.email, fakeEvent.user!.email); }); test('sendDefaultPii is enabled and event has a user without IP address', () async { - final client = fixture.getSut(true); + final client = fixture.getSut(sendDefaultPii: true); final event = fakeEvent.copyWith(user: fakeUser); await client.captureEvent(event); - expect(fixture.transport.events.length, 1); - expect(fixture.transport.events.first.user, isNotNull); - expect(fixture.transport.events.first.user?.ipAddress, '{{auto}}'); - expect(fixture.transport.events.first.user?.id, fakeUser.id); - expect(fixture.transport.events.first.user?.email, fakeUser.email); + final capturedEnvelope = fixture.transport.envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(fixture.transport.envelopes.length, 1); + expect(capturedEvent.user, isNotNull); + expect(capturedEvent.user?.ipAddress, '{{auto}}'); + expect(capturedEvent.user?.id, fakeUser.id); + expect(capturedEvent.user?.email, fakeUser.email); }); }); @@ -560,7 +607,9 @@ void main() { final client = SentryClient(options); await client.captureEvent(fakeEvent); - final event = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final event = await eventFromEnvelope(capturedEnvelope); expect(event.tags!.containsKey('theme'), true); expect(event.extra!.containsKey('host'), true); @@ -601,7 +650,10 @@ void main() { final client = SentryClient(options); await client.captureEvent(fakeEvent); - final event = (options.transport as MockTransport).events.first; + final capturedEnvelope = + (options.transport as MockTransport).envelopes.first; + final event = await eventFromEnvelope(capturedEnvelope); + expect(event.tags!.containsKey('theme'), true); expect(event.extra!.containsKey('host'), true); expect(event.modules!.containsKey('core'), true); @@ -642,6 +694,34 @@ void main() { expect((options.transport as MockTransport).called(0), true); }); }); + + group('SentryClient captures envelope', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('should capture envelope', () async { + final client = fixture.getSut(); + await client.captureEnvelope(fakeEnvelope); + + final capturedEnvelope = + (fixture.options.transport as MockTransport).envelopes.first; + + expect(capturedEnvelope, fakeEnvelope); + }); + }); +} + +Future eventFromEnvelope(SentryEnvelope envelope) async { + final envelopeItemData = []; + await envelope.items.first + .envelopeItemStream() + .forEach(envelopeItemData.addAll); + final envelopeItem = utf8.decode(envelopeItemData); + final envelopeItemJson = jsonDecode(envelopeItem.split('\n').last); + return SentryEvent.fromJson(envelopeItemJson as Map); } SentryEvent? beforeSendCallbackDropEvent(SentryEvent event, {dynamic hint}) => @@ -667,8 +747,7 @@ class Fixture { late SentryOptions options; - /// Test Fixture for tests with [SentryOptions.sendDefaultPii] - SentryClient getSut(bool sendDefaultPii) { + SentryClient getSut({bool sendDefaultPii = false}) { options = SentryOptions(dsn: fakeDsn); options.sendDefaultPii = sendDefaultPii; options.transport = transport; diff --git a/dart/test/sentry_envelope_header_test.dart b/dart/test/sentry_envelope_header_test.dart new file mode 100644 index 0000000000..28f894e766 --- /dev/null +++ b/dart/test/sentry_envelope_header_test.dart @@ -0,0 +1,28 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_envelope_header.dart'; +import 'package:test/test.dart'; + +void main() { + group('SentryEnvelopeHeader', () { + test('toJson empty', () { + final sut = SentryEnvelopeHeader(null, null); + final expected = {}; + expect(sut.toJson(), expected); + }); + + test('toJson', () async { + final eventId = SentryId.newId(); + final sdkVersion = SdkVersion( + name: 'fixture-sdkName', + version: 'fixture-version', + ); + final sut = SentryEnvelopeHeader(eventId, sdkVersion); + final expextedSkd = sdkVersion.toJson(); + final expected = { + 'event_id': eventId.toString(), + 'sdk': expextedSkd + }; + expect(sut.toJson(), expected); + }); + }); +} diff --git a/dart/test/sentry_envelope_item_header_test.dart b/dart/test/sentry_envelope_item_header_test.dart new file mode 100644 index 0000000000..8aca9316a3 --- /dev/null +++ b/dart/test/sentry_envelope_item_header_test.dart @@ -0,0 +1,19 @@ +import 'package:sentry/src/sentry_envelope_item_header.dart'; +import 'package:sentry/src/sentry_item_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('SentryEnvelopeItemHeader', () { + test('serialize', () async { + final sut = SentryEnvelopeItemHeader(SentryItemType.event, () async { + return 3; + }, contentType: 'application/json'); + final expected = { + 'content_type': 'application/json', + 'type': 'event', + 'length': 3 + }; + expect(await sut.toJson(), expected); + }); + }); +} diff --git a/dart/test/sentry_envelope_item_test.dart b/dart/test/sentry_envelope_item_test.dart new file mode 100644 index 0000000000..3e000cf953 --- /dev/null +++ b/dart/test/sentry_envelope_item_test.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_envelope_item_header.dart'; +import 'package:sentry/src/sentry_envelope_item.dart'; +import 'package:sentry/src/sentry_item_type.dart'; +import 'package:sentry/src/protocol/sentry_id.dart'; +import 'package:test/test.dart'; + +void main() { + group('SentryEnvelopeItem', () { + test('serialize', () async { + final header = SentryEnvelopeItemHeader(SentryItemType.event, () async { + return 9; + }, contentType: 'application/json'); + + final dataFactory = () async { + return utf8.encode('{fixture}'); + }; + + final sut = SentryEnvelopeItem(header, dataFactory); + + final headerJson = await header.toJson(); + final headerJsonEncoded = jsonEncode(headerJson); + final expected = utf8.encode('$headerJsonEncoded\n{fixture}'); + + final actualItem = []; + await sut.envelopeItemStream().forEach(actualItem.addAll); + + expect(actualItem, expected); + }); + + test('fromEvent', () async { + final eventId = SentryId.newId(); + final sentryEvent = SentryEvent(eventId: eventId); + final sut = SentryEnvelopeItem.fromEvent(sentryEvent); + + final expectedData = utf8.encode(jsonEncode(sentryEvent.toJson())); + final actualData = await sut.dataFactory(); + + final expectedLength = expectedData.length; + final actualLength = await sut.header.length(); + + expect(sut.header.contentType, 'application/json'); + expect(sut.header.type, SentryItemType.event); + expect(actualLength, expectedLength); + expect(actualData, expectedData); + }); + }); +} diff --git a/dart/test/sentry_envelope_test.dart b/dart/test/sentry_envelope_test.dart new file mode 100644 index 0000000000..624e4b0992 --- /dev/null +++ b/dart/test/sentry_envelope_test.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_envelope.dart'; +import 'package:sentry/src/sentry_envelope_header.dart'; +import 'package:sentry/src/sentry_envelope_item_header.dart'; +import 'package:sentry/src/sentry_envelope_item.dart'; +import 'package:sentry/src/sentry_item_type.dart'; +import 'package:sentry/src/protocol/sentry_id.dart'; +import 'package:test/test.dart'; + +void main() { + group('SentryEnvelope', () { + test('serialize', () async { + final eventId = SentryId.newId(); + + final itemHeader = + SentryEnvelopeItemHeader(SentryItemType.event, () async { + return 9; + }, contentType: 'application/json'); + + final dataFactory = () async { + return utf8.encode('{fixture}'); + }; + + final item = SentryEnvelopeItem(itemHeader, dataFactory); + + final header = SentryEnvelopeHeader(eventId, null); + final sut = SentryEnvelope(header, [item, item]); + + final expectedHeaderJson = header.toJson(); + final expectedHeaderJsonSerialized = jsonEncode(expectedHeaderJson); + + final expectedItem = []; + await item.envelopeItemStream().forEach(expectedItem.addAll); + final expectedItemSerialized = utf8.decode(expectedItem); + + final expected = utf8.encode( + '$expectedHeaderJsonSerialized\n$expectedItemSerialized\n$expectedItemSerialized'); + + final envelopeData = []; + await sut.envelopeStream().forEach(envelopeData.addAll); + expect(envelopeData, expected); + }); + + test('fromEvent', () async { + final eventId = SentryId.newId(); + final sentryEvent = SentryEvent(eventId: eventId); + final sdkVersion = + SdkVersion(name: 'fixture-name', version: 'fixture-version'); + final sut = SentryEnvelope.fromEvent(sentryEvent, sdkVersion); + + final expectedEnvelopeItem = SentryEnvelopeItem.fromEvent(sentryEvent); + + expect(sut.header.eventId, eventId); + expect(sut.header.sdkVersion, sdkVersion); + expect(sut.items[0].header.contentType, + expectedEnvelopeItem.header.contentType); + expect(sut.items[0].header.type, expectedEnvelopeItem.header.type); + expect(await sut.items[0].header.length(), + await expectedEnvelopeItem.header.length()); + + final actualItem = []; + await sut.items[0].envelopeItemStream().forEach(actualItem.addAll); + + final expectedItem = []; + await expectedEnvelopeItem + .envelopeItemStream() + .forEach(expectedItem.addAll); + + expect(actualItem, expectedItem); + }); + }); +} diff --git a/dart/test/sentry_envelope_vm_test.dart b/dart/test/sentry_envelope_vm_test.dart new file mode 100644 index 0000000000..ffbdd14e08 --- /dev/null +++ b/dart/test/sentry_envelope_vm_test.dart @@ -0,0 +1,46 @@ +@TestOn('vm') +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_envelope.dart'; +import 'package:sentry/src/sentry_envelope_header.dart'; +import 'package:sentry/src/sentry_envelope_item_header.dart'; +import 'package:sentry/src/sentry_envelope_item.dart'; +import 'package:sentry/src/protocol/sentry_id.dart'; +import 'package:test/test.dart'; + +void main() { + group('SentryEnvelopeItem', () { + test('item with binary payload', () async { + // Attachment + + final length = () async { + return 3535; + }; + final dataFactory = () async { + final file = File('test_resources/sentry.png'); + final bytes = await file.readAsBytes(); + return bytes; + }; + final attachmentHeader = SentryEnvelopeItemHeader('attachment', length, + contentType: 'image/png', fileName: 'sentry.png'); + final attachmentItem = SentryEnvelopeItem(attachmentHeader, dataFactory); + + // Envelope + + final eventId = SentryId.fromId('3b382f22ee67491f80f7dee18016a7b1'); + final sdkVersion = SdkVersion(name: 'test', version: 'version'); + final header = SentryEnvelopeHeader(eventId, sdkVersion); + final envelope = SentryEnvelope(header, [attachmentItem]); + + final envelopeData = []; + await envelope.envelopeStream().forEach(envelopeData.addAll); + + final expectedEnvelopeFile = + File('test_resources/envelope-with-image.envelope'); + final expectedEnvelopeData = await expectedEnvelopeFile.readAsBytes(); + + expect(expectedEnvelopeData, envelopeData); + }); + }); +} diff --git a/dart/test/sentry_event_test.dart b/dart/test/sentry_event_test.dart index 51847556f7..6fd52cf673 100644 --- a/dart/test/sentry_event_test.dart +++ b/dart/test/sentry_event_test.dart @@ -2,15 +2,109 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/protocol/sentry_request.dart'; import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:sentry/src/version.dart'; +import 'package:sentry/src/utils.dart'; import 'package:test/test.dart'; import 'mocks.dart'; void main() { + group('deserialize', () { + final sentryId = SentryId.empty(); + final timestamp = DateTime.fromMillisecondsSinceEpoch(0); + final sentryEventJson = { + 'event_id': sentryId.toString(), + 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), + 'platform': 'platform', + 'logger': 'logger', + 'server_name': 'serverName', + 'release': 'release', + 'dist': 'dist', + 'environment': 'environment', + 'modules': {'key': 'value'}, + 'message': {'formatted': 'formatted'}, + 'transaction': 'transaction', + 'exception': { + 'values': [ + {'type': 'type', 'value': 'value'} + ] + }, + 'level': 'debug', + 'culprit': 'culprit', + 'tags': {'key': 'value'}, + 'extra': {'key': 'value'}, + 'contexts': { + 'device': {'name': 'name'} + }, + 'user': { + 'id': 'id', + 'username': 'username', + 'ip_address': '192.168.0.0.1' + }, + 'fingerprint': ['fingerprint'], + 'breadcrumbs': [ + { + 'message': 'message', + 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), + 'level': 'info' + } + ], + 'sdk': {'name': 'name', 'version': 'version'}, + 'request': {'url': 'url'}, + 'debug_meta': { + 'sdk_info': {'sdk_name': 'sdkName'} + }, + }; + + final emptyFieldsSentryEventJson = { + 'event_id': sentryId.toString(), + 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), + 'contexts': { + 'device': {'name': 'name'} + }, + }; + + test('fromJson', () { + final sentryEvent = SentryEvent.fromJson(sentryEventJson); + final json = sentryEvent.toJson(); + + expect( + DeepCollectionEquality().equals(sentryEventJson, json), + true, + ); + }); + + test('should not deserialize null or empty fields', () { + final sentryEvent = SentryEvent.fromJson(emptyFieldsSentryEventJson); + + expect(sentryEvent.platform, isNull); + expect(sentryEvent.logger, isNull); + expect(sentryEvent.serverName, isNull); + expect(sentryEvent.release, isNull); + expect(sentryEvent.dist, isNull); + expect(sentryEvent.environment, isNull); + expect(sentryEvent.modules, isNull); + expect(sentryEvent.message, isNull); + expect(sentryEvent.stackTrace, isNull); + expect(sentryEvent.exception, isNull); + expect(sentryEvent.transaction, isNull); + expect(sentryEvent.level, isNull); + expect(sentryEvent.culprit, isNull); + expect(sentryEvent.tags, isNull); + expect(sentryEvent.extra, isNull); + expect(sentryEvent.breadcrumbs, isNull); + expect(sentryEvent.user, isNull); + expect(sentryEvent.fingerprint, isNull); + expect(sentryEvent.sdk, isNull); + expect(sentryEvent.request, isNull); + expect(sentryEvent.debugMeta, isNull); + }); + }); + group(SentryEvent, () { test('$Breadcrumb serializes', () { expect( diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index cb000f9b11..fa0c410198 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -27,7 +27,7 @@ void testHeaders( bool withSecret = true, }) { final expectedHeaders = { - 'Content-Type': 'application/json', + 'Content-Type': 'application/x-sentry-envelope', 'X-Sentry-Auth': 'Sentry sentry_version=7, ' 'sentry_client=$sdkName/$sdkVersion, ' 'sentry_key=public, ' @@ -101,13 +101,14 @@ Future testCaptureException( sdkName: sdkName(isWeb), ); - Map? data; + String envelopeData; if (compressPayload) { - data = - json.decode(utf8.decode(gzip!.decode(body))) as Map?; + envelopeData = utf8.decode(gzip!.decode(body)); } else { - data = json.decode(utf8.decode(body!)) as Map?; + envelopeData = utf8.decode(body!); } + final eventJson = envelopeData.split('\n').last; + final data = json.decode(eventJson) as Map?; // so we assert the generated and returned id data!['event_id'] = sentryId.toString(); @@ -193,7 +194,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(testDsn)); expect( dsn.postUri, - Uri.parse('https://sentry.example.com/api/1/store/'), + Uri.parse('https://sentry.example.com/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); expect(dsn.secretKey, 'secret'); @@ -210,7 +211,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(_testDsnWithoutSecret)); expect( dsn.postUri, - Uri.parse('https://sentry.example.com/api/1/store/'), + Uri.parse('https://sentry.example.com/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); expect(dsn.secretKey, null); @@ -227,7 +228,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(_testDsnWithPath)); expect( dsn.postUri, - Uri.parse('https://sentry.example.com/path/api/1/store/'), + Uri.parse('https://sentry.example.com/path/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); expect(dsn.secretKey, 'secret'); @@ -243,7 +244,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { expect(dsn.uri, Uri.parse(_testDsnWithPort)); expect( dsn.postUri, - Uri.parse('https://sentry.example.com:8888/api/1/store/'), + Uri.parse('https://sentry.example.com:8888/api/1/envelope/'), ); expect(dsn.publicKey, 'public'); expect(dsn.secretKey, 'secret'); @@ -350,7 +351,9 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { if (request.method == 'POST') { final bodyData = request.bodyBytes; final decoded = const Utf8Codec().decode(bodyData); - final dynamic decodedJson = jsonDecode(decoded); + final eventJson = decoded.split('\n').last; + final dynamic decodedJson = json.decode(eventJson); + loggedUserId = decodedJson['user']['id'] as String?; return http.Response( '', diff --git a/dart/test/transport/http_transport_test.dart b/dart/test/transport/http_transport_test.dart new file mode 100644 index 0000000000..89e5613814 --- /dev/null +++ b/dart/test/transport/http_transport_test.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:sentry/src/sentry_envelope_header.dart'; +import 'package:sentry/src/sentry_envelope_item.dart'; +import 'package:sentry/src/sentry_envelope_item_header.dart'; +import 'package:sentry/src/sentry_item_type.dart'; +import 'package:sentry/src/transport/rate_limiter.dart'; +import 'package:test/test.dart'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/transport/http_transport.dart'; + +import '../mocks.dart'; + +void main() { + SentryEnvelope givenEnvelope() { + final filteredEnvelopeHeader = SentryEnvelopeHeader(SentryId.empty(), null); + final filteredItemHeader = + SentryEnvelopeItemHeader(SentryItemType.event, () async { + return 2; + }, contentType: 'application/json'); + final dataFactory = () async { + return utf8.encode('{}'); + }; + final filteredItem = SentryEnvelopeItem(filteredItemHeader, dataFactory); + return SentryEnvelope(filteredEnvelopeHeader, [filteredItem]); + } + + group('filter', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('filter called', () async { + final httpMock = MockClient((http.Request request) async { + return http.Response('{}', 200); + }); + + fixture.options.compressPayload = false; + final mockRateLimiter = MockRateLimiter(); + final sut = fixture.getSut(httpMock, mockRateLimiter); + + final sentryEnvelope = givenEnvelope(); + await sut.send(sentryEnvelope); + + expect(mockRateLimiter.envelopeToFilter, sentryEnvelope); + }); + + test('send filtered event', () async { + List? body; + + final httpMock = MockClient((http.Request request) async { + body = request.bodyBytes; + return http.Response('{}', 200); + }); + + final filteredEnvelope = givenEnvelope(); + + fixture.options.compressPayload = false; + final mockRateLimiter = MockRateLimiter() + ..filteredEnvelope = filteredEnvelope; + final sut = fixture.getSut(httpMock, mockRateLimiter); + + final sentryEvent = SentryEvent(); + final envelope = + SentryEnvelope.fromEvent(sentryEvent, fixture.options.sdk); + await sut.send(envelope); + + final envelopeData = []; + await filteredEnvelope.envelopeStream().forEach(envelopeData.addAll); + + expect(body, envelopeData); + }); + + test('send nothing when filtered event null', () async { + var httpCalled = false; + final httpMock = MockClient((http.Request request) async { + httpCalled = true; + return http.Response('{}', 200); + }); + + fixture.options.compressPayload = false; + final mockRateLimiter = MockRateLimiter()..filterReturnsNull = true; + final sut = fixture.getSut(httpMock, mockRateLimiter); + + final sentryEvent = SentryEvent(); + final envelope = + SentryEnvelope.fromEvent(sentryEvent, fixture.options.sdk); + final eventId = await sut.send(envelope); + + expect(eventId, SentryId.empty()); + expect(httpCalled, false); + }); + }); + + group('updateRetryAfterLimits', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('retryAfterHeader', () async { + final httpMock = MockClient((http.Request request) async { + return http.Response('{}', 429, headers: {'Retry-After': '1'}); + }); + final mockRateLimiter = MockRateLimiter(); + final sut = fixture.getSut(httpMock, mockRateLimiter); + + final sentryEvent = SentryEvent(); + final envelope = + SentryEnvelope.fromEvent(sentryEvent, fixture.options.sdk); + await sut.send(envelope); + + expect(mockRateLimiter.envelopeToFilter?.header.eventId, + sentryEvent.eventId); + + expect(mockRateLimiter.errorCode, 429); + expect(mockRateLimiter.retryAfterHeader, '1'); + expect(mockRateLimiter.sentryRateLimitHeader, isNull); + }); + + test('sentryRateLimitHeader', () async { + final httpMock = MockClient((http.Request request) async { + return http.Response('{}', 200, + headers: {'X-Sentry-Rate-Limits': 'fixture-sentryRateLimitHeader'}); + }); + final mockRateLimiter = MockRateLimiter(); + final sut = fixture.getSut(httpMock, mockRateLimiter); + + final sentryEvent = SentryEvent(); + final envelope = + SentryEnvelope.fromEvent(sentryEvent, fixture.options.sdk); + await sut.send(envelope); + + expect(mockRateLimiter.errorCode, 200); + expect(mockRateLimiter.retryAfterHeader, isNull); + expect(mockRateLimiter.sentryRateLimitHeader, + 'fixture-sentryRateLimitHeader'); + }); + }); +} + +class Fixture { + final options = SentryOptions( + dsn: 'https://public:secret@sentry.example.com/1', + ); + + HttpTransport getSut(http.Client client, RateLimiter rateLimiter) { + options.httpClient = client; + return HttpTransport(options, rateLimiter); + } +} diff --git a/dart/test_resources/envelope-with-image.envelope b/dart/test_resources/envelope-with-image.envelope new file mode 100644 index 0000000000..533f5e4f6f Binary files /dev/null and b/dart/test_resources/envelope-with-image.envelope differ diff --git a/dart/test_resources/sentry.png b/dart/test_resources/sentry.png new file mode 100644 index 0000000000..2225be472d Binary files /dev/null and b/dart/test_resources/sentry.png differ diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index c24f6be894..6004c2e75e 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -46,14 +46,14 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler { channel.setMethodCallHandler(null) } - private fun writeEnvelope(envelope: String): Boolean { + private fun writeEnvelope(envelope: ByteArray): Boolean { val options = HubAdapter.getInstance().options if (options.outboxPath.isNullOrEmpty()) { return false } val file = File(options.outboxPath, UUID.randomUUID().toString()) - file.writeText(envelope, Charsets.UTF_8) + file.writeBytes(envelope) return true } @@ -122,9 +122,9 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler { private fun captureEnvelope(call: MethodCall, result: Result) { val args = call.arguments() as List if (args.isNotEmpty()) { - val event = args.first() as String? + val event = args.first() as ByteArray? - if (!event.isNullOrEmpty()) { + if (event != null && event.size > 0) { if (!writeEnvelope(event)) { result.error("3", "SentryOptions or outboxPath are null or empty", null) } diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 2440f77cc7..1fb771ce38 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -258,65 +258,18 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { private func captureEnvelope(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [Any], !arguments.isEmpty, - let event = arguments.first as? String else { + let data = (arguments.first as? FlutterStandardTypedData)?.data else { print("Envelope is null or empty !") result(FlutterError(code: "2", message: "Envelope is null or empty", details: nil)) return } - - do { - let envelope = try parseJsonEnvelope(event) - PrivateSentrySDKOnly.capture(envelope) - result("") - } catch { - print("Cannot parse the envelope json !") - result(FlutterError(code: "3", message: "Cannot parse the envelope json", details: nil)) + guard let envelope = PrivateSentrySDKOnly.envelope(with: data) else { + print("Cannot parse the envelope data") + result(FlutterError(code: "3", message: "Cannot parse the envelope data", details: nil)) return } - } - - private func parseJsonEnvelope(_ data: String) throws -> SentryEnvelope { - let parts = data.split(separator: "\n") - - let envelopeParts: [[String: Any]] = try parts.map({ part in - guard let dict = parseJson(text: "\(part)") else { - throw NSError() - } - return dict - }) - - let rawEnvelopeHeader = envelopeParts[0] - guard let eventId = rawEnvelopeHeader["event_id"] as? String, - let itemType = envelopeParts[1]["type"] as? String else { - throw NSError() - } - - let sdkInfo = SentrySdkInfo(dict: rawEnvelopeHeader) - let sentryId = SentryId(uuidString: eventId) - let envelopeHeader = SentryEnvelopeHeader.init(id: sentryId, andSdkInfo: sdkInfo) - - let payload = envelopeParts[2] - - let data = try JSONSerialization.data(withJSONObject: payload, options: .init(rawValue: 0)) - - let itemHeader = SentryEnvelopeItemHeader(type: itemType, length: UInt(data.count)) - let sentryItem = SentryEnvelopeItem(header: itemHeader, data: data) - - return SentryEnvelope.init(header: envelopeHeader, singleItem: sentryItem) - } - - func parseJson(text: String) -> [String: Any]? { - guard let data = text.data(using: .utf8) else { - print("Invalid UTF8 String : \(text)") - return nil - } - - do { - let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - return json - } catch { - print("json parsing error !") - } - return nil + PrivateSentrySDKOnly.capture(envelope) + result("") + return } } diff --git a/flutter/lib/src/file_system_transport.dart b/flutter/lib/src/file_system_transport.dart index 188e896452..68e47d3cde 100644 --- a/flutter/lib/src/file_system_transport.dart +++ b/flutter/lib/src/file_system_transport.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:sentry/sentry.dart'; @@ -10,28 +10,11 @@ class FileSystemTransport implements Transport { final SentryOptions _options; @override - Future send(SentryEvent event) async { - final headerMap = { - 'event_id': event.eventId.toString(), - 'sdk': _options.sdk.toJson() - }; - - final eventMap = event.toJson(); - - final eventString = jsonEncode(eventMap); - final eventUtf8 = utf8.encode(eventString); - - final itemHeaderMap = { - 'content_type': 'application/json', - 'type': 'event', - 'length': eventUtf8.length, - }; - - final headerString = jsonEncode(headerMap); - final itemHeaderString = jsonEncode(itemHeaderMap); - final envelopeString = '$headerString\n$itemHeaderString\n$eventString'; - - final args = [envelopeString]; + Future send(SentryEnvelope envelope) async { + final envelopeData = []; + await envelope.envelopeStream().forEach(envelopeData.addAll); + // https://flutter.dev/docs/development/platform-integration/platform-channels#codec + final args = [Uint8List.fromList(envelopeData)]; try { await _channel.invokeMethod('captureEnvelope', args); } catch (error) { @@ -42,6 +25,6 @@ class FileSystemTransport implements Transport { return SentryId.empty(); } - return event.eventId; + return envelope.header.eventId ?? SentryId.empty(); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 263d3b46e9..e4cbae7a92 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -7,6 +7,7 @@ import 'sentry_flutter_options.dart'; import 'default_integrations.dart'; import 'file_system_transport.dart'; + import 'version.dart'; /// Configuration options callback diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 470b9b5290..dcb0913e65 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mockito: ^5.0.0 + mockito: ^5.0.3 yaml: ^3.0.0 # needed for version match (code and pubspec) pedantic: ^1.10.0 build_runner: ^1.11.5 diff --git a/flutter/test/file_system_transport_test.dart b/flutter/test/file_system_transport_test.dart index 41b88a5cab..680deb073d 100644 --- a/flutter/test/file_system_transport_test.dart +++ b/flutter/test/file_system_transport_test.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -25,8 +26,11 @@ void main() { final transport = fixture.getSut(_channel); final event = SentryEvent(); + final sdkVersion = + SdkVersion(name: 'fixture-sdkName', version: 'fixture-sdkVersion'); - final sentryId = await transport.send(event); + final envelope = SentryEnvelope.fromEvent(event, sdkVersion); + final sentryId = await transport.send(envelope); expect(sentryId, sentryId); }); @@ -37,8 +41,12 @@ void main() { }); final transport = fixture.getSut(_channel); + final event = SentryEvent(); + final sdkVersion = + SdkVersion(name: 'fixture-sdkName', version: 'fixture-sdkVersion'); - final sentryId = await transport.send(SentryEvent()); + final envelope = SentryEnvelope.fromEvent(event, sdkVersion); + final sentryId = await transport.send(envelope); expect(SentryId.empty(), sentryId); }); @@ -53,10 +61,14 @@ void main() { final event = SentryEvent(message: SentryMessage('hi I am a special char ◤')); - await transport.send(event); + final sdkVersion = + SdkVersion(name: 'fixture-sdkName', version: 'fixture-sdkVersion'); + final envelope = SentryEnvelope.fromEvent(event, sdkVersion); + await transport.send(envelope); final envelopeList = arguments as List; - final envelopeString = envelopeList.first as String; + final envelopeData = envelopeList.first as Uint8List; + final envelopeString = utf8.decode(envelopeData); final lines = envelopeString.split('\n'); final envelopeHeader = lines.first; final itemHeader = lines[1]; diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 94fe95fbe6..2ee6b6f00e 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1,5 +1,5 @@ -// Mocks generated by Mockito 5.0.1 from annotations -// in sentry_flutter/test/mocks.dart. +// Mocks generated by Mockito 5.0.7 from annotations +// in sentry_flutter/example/ios/.symlinks/plugins/sentry_flutter/test/mocks.dart. // Do not manually edit this file. import 'dart:async' as _i4; @@ -11,11 +11,16 @@ import 'package:sentry/src/protocol/sentry_event.dart' as _i5; import 'package:sentry/src/protocol/sentry_id.dart' as _i2; import 'package:sentry/src/protocol/sentry_level.dart' as _i6; import 'package:sentry/src/sentry_client.dart' as _i8; +import 'package:sentry/src/sentry_envelope.dart' as _i10; import 'package:sentry/src/transport/transport.dart' as _i9; // ignore_for_file: comment_references // ignore_for_file: unnecessary_parenthesis +// ignore_for_file: prefer_const_constructors + +// ignore_for_file: avoid_redundant_argument_values + class _FakeSentryId extends _i1.Fake implements _i2.SentryId {} class _FakeHub extends _i1.Fake implements _i3.Hub {} @@ -47,7 +52,7 @@ class MockHub extends _i1.Mock implements _i3.Hub { #hint: hint, #withScope: withScope }), - returnValue: Future.value(_FakeSentryId())) + returnValue: Future<_i2.SentryId>.value(_FakeSentryId())) as _i4.Future<_i2.SentryId>); @override _i4.Future<_i2.SentryId> captureException(dynamic throwable, @@ -60,7 +65,7 @@ class MockHub extends _i1.Mock implements _i3.Hub { #hint: hint, #withScope: withScope }), - returnValue: Future.value(_FakeSentryId())) + returnValue: Future<_i2.SentryId>.value(_FakeSentryId())) as _i4.Future<_i2.SentryId>); @override _i4.Future<_i2.SentryId> captureMessage(String? message, @@ -79,7 +84,7 @@ class MockHub extends _i1.Mock implements _i3.Hub { #hint: hint, #withScope: withScope }), - returnValue: Future.value(_FakeSentryId())) + returnValue: Future<_i2.SentryId>.value(_FakeSentryId())) as _i4.Future<_i2.SentryId>); @override void addBreadcrumb(_i7.Breadcrumb? crumb, {dynamic hint}) => super @@ -94,7 +99,7 @@ class MockHub extends _i1.Mock implements _i3.Hub { returnValue: _FakeHub()) as _i3.Hub); @override _i4.Future close() => (super.noSuchMethod(Invocation.method(#close, []), - returnValue: Future.value(null), + returnValue: Future.value(null), returnValueForMissingStub: Future.value()) as _i4.Future); @override void configureScope(_i3.ScopeCallback? callback) => @@ -111,7 +116,8 @@ class MockTransport extends _i1.Mock implements _i9.Transport { } @override - _i4.Future<_i2.SentryId> send(_i5.SentryEvent? event) => (super.noSuchMethod( - Invocation.method(#send, [event]), - returnValue: Future.value(_FakeSentryId())) as _i4.Future<_i2.SentryId>); + _i4.Future<_i2.SentryId> send(_i10.SentryEnvelope? envelope) => + (super.noSuchMethod(Invocation.method(#send, [envelope]), + returnValue: Future<_i2.SentryId>.value(_FakeSentryId())) + as _i4.Future<_i2.SentryId>); }