From 7ec9238637dbe38b6170da172bc0650eaa157885 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 6 Aug 2024 13:03:48 +0200 Subject: [PATCH 1/4] feat: add span level measurements #1855 (#2214) * feat: add span level measurements * add changelog entry * add issue link to changelog * fix: correct changelog * moved the stored measurements from root span to tracer * changed issue number to pr number in changelog * fixed formatting * Update CHANGELOG.md Co-authored-by: Giancarlo Buenaflor --------- Co-authored-by: Martin Co-authored-by: Giancarlo Buenaflor --- CHANGELOG.md | 6 ++++++ dart/lib/src/protocol/sentry_span.dart | 7 ++++++- dart/lib/src/sentry_span_interface.dart | 2 +- dart/lib/src/sentry_tracer.dart | 19 +++++++++++++------ dart/test/sentry_span_test.dart | 15 +++++++++++++++ dart/test/sentry_tracer_test.dart | 16 ++++++++++++++++ 6 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3517cd150b..1f7ce06059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add support for span level measurements. ([#2214](https://github.com/getsentry/sentry-dart/pull/2214)) + ## 8.6.0 ### Improvements diff --git a/dart/lib/src/protocol/sentry_span.dart b/dart/lib/src/protocol/sentry_span.dart index 6358b4802a..00f6ec8f5a 100644 --- a/dart/lib/src/protocol/sentry_span.dart +++ b/dart/lib/src/protocol/sentry_span.dart @@ -242,7 +242,12 @@ class SentrySpan extends ISentrySpan { num value, { SentryMeasurementUnit? unit, }) { - _tracer.setMeasurement(name, value, unit: unit); + if (finished) { + _hub.options.logger(SentryLevel.debug, + "The span is already finished. Measurement $name cannot be set"); + return; + } + _tracer.setMeasurementFromChild(name, value, unit: unit); } @override diff --git a/dart/lib/src/sentry_span_interface.dart b/dart/lib/src/sentry_span_interface.dart index 1d142c45b9..979822c2ac 100644 --- a/dart/lib/src/sentry_span_interface.dart +++ b/dart/lib/src/sentry_span_interface.dart @@ -71,7 +71,7 @@ abstract class ISentrySpan { /// Returns the trace information that could be sent as a sentry-trace header. SentryTraceHeader toSentryTrace(); - /// Set observed measurement for this transaction. + /// Set observed measurement for this span or transaction. void setMeasurement( String name, num value, { diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 66c179e386..1507143d69 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -17,7 +17,9 @@ class SentryTracer extends ISentrySpan { late final SentrySpan _rootSpan; final List _children = []; final Map _extra = {}; + final Map _measurements = {}; + Map get measurements => _measurements; Timer? _autoFinishAfterTimer; Duration? _autoFinishAfter; @@ -320,10 +322,6 @@ class SentryTracer extends ISentrySpan { @override SentryTraceHeader toSentryTrace() => _rootSpan.toSentryTrace(); - @visibleForTesting - Map get measurements => - Map.unmodifiable(_measurements); - bool _haveAllChildrenFinished() { for (final child in children) { if (!child.finished) { @@ -341,10 +339,19 @@ class SentryTracer extends ISentrySpan { @override void setMeasurement(String name, num value, {SentryMeasurementUnit? unit}) { if (finished) { + _hub.options.logger(SentryLevel.debug, + "The tracer is already finished. Measurement $name cannot be set"); return; } - final measurement = SentryMeasurement(name, value, unit: unit); - _measurements[name] = measurement; + _measurements[name] = SentryMeasurement(name, value, unit: unit); + } + + void setMeasurementFromChild(String name, num value, + {SentryMeasurementUnit? unit}) { + // We don't want to overwrite span measurement, if it comes from a child. + if (!_measurements.containsKey(name)) { + setMeasurement(name, value, unit: unit); + } } @override diff --git a/dart/test/sentry_span_test.dart b/dart/test/sentry_span_test.dart index e161ceee2f..d063cf787e 100644 --- a/dart/test/sentry_span_test.dart +++ b/dart/test/sentry_span_test.dart @@ -310,6 +310,21 @@ void main() { expect(fixture.hub.options.enableSpanLocalMetricAggregation, false); expect(sut.localMetricsAggregator, null); }); + + test('setMeasurement sets a measurement', () async { + final sut = fixture.getSut(); + sut.setMeasurement("test", 1); + expect(sut.tracer.measurements.containsKey("test"), true); + expect(sut.tracer.measurements["test"]!.value, 1); + }); + + test('setMeasurement does not set a measurement if a span is finished', + () async { + final sut = fixture.getSut(); + await sut.finish(); + sut.setMeasurement("test", 1); + expect(sut.tracer.measurements.isEmpty, true); + }); } class Fixture { diff --git a/dart/test/sentry_tracer_test.dart b/dart/test/sentry_tracer_test.dart index a1251c224b..ba57aeb405 100644 --- a/dart/test/sentry_tracer_test.dart +++ b/dart/test/sentry_tracer_test.dart @@ -467,6 +467,22 @@ void main() { expect(fixture.hub.options.enableSpanLocalMetricAggregation, false); expect(sut.localMetricsAggregator, null); }); + + test('setMeasurement sets a measurement', () async { + final sut = fixture.getSut(); + sut.setMeasurement("test", 1); + expect(sut.measurements.containsKey("test"), true); + expect(sut.measurements["test"]!.value, 1); + }); + + test('setMeasurementFromChild does not override existing measurements', + () async { + final sut = fixture.getSut(); + sut.setMeasurement("test", 1); + sut.setMeasurementFromChild("test", 5); + expect(sut.measurements.containsKey("test"), true); + expect(sut.measurements["test"]!.value, 1); + }); }); group('$SentryBaggageHeader', () { From 2e1e4aebae075ca46960534a099de7081db831b6 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 6 Aug 2024 14:59:36 +0200 Subject: [PATCH 2/4] feat: add `ignoreTransactions` and ignoreErrors` #1391 (#2207) * Add ignoreTransactions and ignoreErrors #1391 * chore: add changelog entry * move methods from sentry_options to sentry_client and change to private * change discard reason to ignored Co-authored-by: Giancarlo Buenaflor * change iterable to list * add event recorder to ignoredTransactions * add tests for ignoreTransactions * set ignoreErrors list to empty list a default Co-authored-by: Giancarlo Buenaflor * change variables to final for ignoreTransaction Co-authored-by: Giancarlo Buenaflor * change var to final for ignoreErrors and adapt test * Update CHANGELOG.md Co-authored-by: Giancarlo Buenaflor * Add example for ignoreTransactions and ignoreErrors to changelog * fix: check for empty ignoreError and ignoreTransaction before handling regex * moved ignoreTransactions and ignoreErrors back to unreleased area in CHANGELOG.md * refactored implementation of ignoreErrors and ignoreTransactions and improved test cases * removed unnecessary backslash from tests --------- Co-authored-by: Martin <> Co-authored-by: Martin Co-authored-by: Giancarlo Buenaflor --- CHANGELOG.md | 12 +++ dart/lib/src/sentry_client.dart | 46 +++++++++++ dart/lib/src/sentry_options.dart | 8 ++ dart/test/sentry_client_test.dart | 124 ++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7ce06059..f04c01fb11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ ### Features - Add support for span level measurements. ([#2214](https://github.com/getsentry/sentry-dart/pull/2214)) +- Add `ignoreTransactions` and `ignoreErrors` to options ([#2207](https://github.com/getsentry/sentry-dart/pull/2207)) + ```dart + await SentryFlutter.init( + (options) { + options.dsn = 'https://examplePublicKey@o0.ingest.sentry.io/0'; + options.ignoreErrors = ["my-error", "^error-.*\$"]; + options.ignoreTransactions = ["my-transaction", "^transaction-.*\$"]; + ... + }, + appRunner: () => runApp(MyApp()), + ); + ``` ## 8.6.0 diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index c319677832..c4ebac3db5 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -84,6 +84,16 @@ class SentryClient { dynamic stackTrace, Hint? hint, }) async { + if (_isIgnoredError(event)) { + _options.logger( + SentryLevel.debug, + 'Error was ignored as specified in the ignoredErrors options.', + ); + _options.recorder + .recordLostEvent(DiscardReason.ignored, _getCategory(event)); + return _emptySentryId; + } + if (_options.containsIgnoredExceptionForType(event.throwable)) { _options.logger( SentryLevel.debug, @@ -180,6 +190,15 @@ class SentryClient { return id ?? SentryId.empty(); } + bool _isIgnoredError(SentryEvent event) { + if (event.message == null || _options.ignoreErrors.isEmpty) { + return false; + } + + var message = event.message!.formatted; + return _isMatchingRegexPattern(message, _options.ignoreErrors); + } + SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) { event = event.copyWith( serverName: event.serverName ?? _options.serverName, @@ -351,6 +370,17 @@ class SentryClient { return _emptySentryId; } + if (_isIgnoredTransaction(preparedTransaction)) { + _options.logger( + SentryLevel.debug, + 'Transaction was ignored as specified in the ignoredTransactions options.', + ); + + _options.recorder.recordLostEvent( + DiscardReason.ignored, _getCategory(preparedTransaction)); + return _emptySentryId; + } + preparedTransaction = await _runBeforeSend(preparedTransaction, hint) as SentryTransaction?; @@ -379,6 +409,15 @@ class SentryClient { return id ?? SentryId.empty(); } + bool _isIgnoredTransaction(SentryTransaction transaction) { + if (_options.ignoreTransactions.isEmpty) { + return false; + } + + var name = transaction.tracer.name; + return _isMatchingRegexPattern(name, _options.ignoreTransactions); + } + /// Reports the [envelope] to Sentry.io. Future captureEnvelope(SentryEnvelope envelope) { return _attachClientReportsAndSend(envelope); @@ -554,4 +593,11 @@ class SentryClient { SentryId.empty(), ); } + + bool _isMatchingRegexPattern(String value, List regexPattern, + {bool caseSensitive = false}) { + final combinedRegexPattern = regexPattern.join('|'); + final regExp = RegExp(combinedRegexPattern, caseSensitive: caseSensitive); + return regExp.hasMatch(value); + } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 636534fb5d..26cda852c4 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -184,6 +184,14 @@ class SentryOptions { /// sent. Events are picked randomly. Default is null (disabled) double? sampleRate; + /// The ignoreErrors tells the SDK which errors should be not sent to the sentry server. + /// If an null or an empty list is used, the SDK will send all transactions. + List ignoreErrors = []; + + /// The ignoreTransactions tells the SDK which transactions should be not sent to the sentry server. + /// If null or an empty list is used, the SDK will send all transactions. + List ignoreTransactions = []; + final List _inAppExcludes = []; /// A list of string prefixes of packages names that do not belong to the app, but rather third-party diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 5a2945a6ec..772699b14d 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -1033,6 +1033,130 @@ void main() { }); }); + group('SentryClient ignored errors', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + fixture.options.ignoreErrors = ["my-error", "^error-.*\$"]; + }); + + test('drop event if error message fully matches ignoreErrors value', + () async { + final event = SentryEvent(message: SentryMessage("my-error")); + + final client = fixture.getSut(); + await client.captureEvent(event); + + expect((fixture.transport).called(0), true); + }); + + test('drop event if error message partially matches ignoreErrors value', + () async { + final event = SentryEvent(message: SentryMessage("this is my-error-foo")); + + final client = fixture.getSut(); + await client.captureEvent(event); + + expect((fixture.transport).called(0), true); + }); + + test( + 'drop event if error message partially matches ignoreErrors regex value', + () async { + final event = SentryEvent(message: SentryMessage("error-test message")); + + final client = fixture.getSut(); + await client.captureEvent(event); + + expect((fixture.transport).called(0), true); + }); + + test('send event if error message does not match ignoreErrors value', + () async { + final event = SentryEvent(message: SentryMessage("warning")); + + final client = fixture.getSut(); + await client.captureEvent(event); + + expect((fixture.transport).called(1), true); + }); + + test('send event if no values are set for ignoreErrors', () async { + fixture.options.ignoreErrors = []; + final event = SentryEvent(message: SentryMessage("this is a test event")); + + final client = fixture.getSut(); + await client.captureEvent(event); + + expect((fixture.transport).called(1), true); + }); + }); + + group('SentryClient ignored transactions', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + fixture.options.ignoreTransactions = [ + "my-transaction", + "^transaction-.*\$" + ]; + }); + + test('drop transaction if name fully matches ignoreTransaction value', + () async { + final client = fixture.getSut(); + final fakeTransaction = fixture.fakeTransaction(); + fakeTransaction.tracer.name = "my-transaction"; + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(0), true); + }); + + test('drop transaction if name partially matches ignoreTransaction value', + () async { + final client = fixture.getSut(); + final fakeTransaction = fixture.fakeTransaction(); + fakeTransaction.tracer.name = "this is a transaction-test"; + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(0), true); + }); + + test( + 'drop transaction if name partially matches ignoreTransaction regex value', + () async { + final client = fixture.getSut(); + final fakeTransaction = fixture.fakeTransaction(); + fakeTransaction.tracer.name = "transaction-test message"; + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(0), true); + }); + + test('send transaction if name does not match ignoreTransaction value', + () async { + final client = fixture.getSut(); + final fakeTransaction = fixture.fakeTransaction(); + fakeTransaction.tracer.name = "capture"; + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(1), true); + }); + + test('send transaction if no values are set for ignoreTransaction', + () async { + fixture.options.ignoreTransactions = []; + final client = fixture.getSut(); + final fakeTransaction = fixture.fakeTransaction(); + fakeTransaction.tracer.name = "this is a test transaction"; + await client.captureTransaction(fakeTransaction); + + expect((fixture.transport).called(1), true); + }); + }); + group('SentryClient ignored exceptions', () { late Fixture fixture; From 6e9c5a2e82cbb6e1fa94f35e64670297a0e148c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Tue, 6 Aug 2024 15:44:16 +0200 Subject: [PATCH 3/4] Deserialize and serialize unknown fields (#2153) --- CHANGELOG.md | 4 + dart/lib/src/protocol/access_aware_map.dart | 53 ++++++++ dart/lib/src/protocol/breadcrumb.dart | 13 +- dart/lib/src/protocol/debug_image.dart | 12 +- dart/lib/src/protocol/debug_meta.dart | 15 ++- dart/lib/src/protocol/mechanism.dart | 12 +- dart/lib/src/protocol/metric_summary.dart | 32 +++-- dart/lib/src/protocol/sdk_info.dart | 12 +- dart/lib/src/protocol/sdk_version.dart | 11 +- dart/lib/src/protocol/sentry_app.dart | 35 ++++-- dart/lib/src/protocol/sentry_browser.dart | 29 +++-- dart/lib/src/protocol/sentry_culture.dart | 29 +++-- dart/lib/src/protocol/sentry_device.dart | 106 +++++++++------- dart/lib/src/protocol/sentry_event.dart | 14 ++- dart/lib/src/protocol/sentry_exception.dart | 14 ++- dart/lib/src/protocol/sentry_gpu.dart | 49 +++++--- dart/lib/src/protocol/sentry_message.dart | 18 ++- .../src/protocol/sentry_operating_system.dart | 34 +++-- dart/lib/src/protocol/sentry_package.dart | 15 ++- dart/lib/src/protocol/sentry_request.dart | 13 +- dart/lib/src/protocol/sentry_runtime.dart | 29 +++-- dart/lib/src/protocol/sentry_stack_frame.dart | 13 +- dart/lib/src/protocol/sentry_stack_trace.dart | 17 ++- dart/lib/src/protocol/sentry_thread.dart | 11 +- .../src/protocol/sentry_trace_context.dart | 11 +- .../src/protocol/sentry_transaction_info.dart | 15 ++- dart/lib/src/protocol/sentry_user.dart | 14 ++- dart/lib/src/sentry_trace_context_header.dart | 12 +- dart/lib/src/sentry_user_feedback.dart | 15 ++- dart/test/contexts_test.dart | 2 +- dart/test/mocks.dart | 6 + .../test/protocol/access_aware_map_tests.dart | 118 ++++++++++++++++++ dart/test/protocol/breadcrumb_test.dart | 4 + dart/test/protocol/debug_image_test.dart | 4 + dart/test/protocol/debug_meta_test.dart | 4 + dart/test/protocol/mechanism_test.dart | 4 + dart/test/protocol/sdk_info_test.dart | 7 ++ dart/test/protocol/sdk_version_test.dart | 7 ++ dart/test/protocol/sentry_app_test.dart | 4 + dart/test/protocol/sentry_device_test.dart | 4 + dart/test/protocol/sentry_exception_test.dart | 4 + dart/test/protocol/sentry_gpu_test.dart | 23 ++-- dart/test/protocol/sentry_message_test.dart | 4 + .../sentry_operating_system_test.dart | 17 ++- dart/test/protocol/sentry_package_test.dart | 4 + dart/test/protocol/sentry_request_test.dart | 4 + dart/test/protocol/sentry_runtime_test.dart | 4 + .../protocol/sentry_stack_frame_test.dart | 4 + .../protocol/sentry_stack_trace_test.dart | 8 ++ .../sentry_transaction_info_test.dart | 14 ++- dart/test/protocol/sentry_user_test.dart | 6 + dart/test/sentry_event_test.dart | 102 ++++++++------- .../sentry_trace_context_header_test.dart | 26 +++- dart/test/sentry_trace_context_test.dart | 19 +-- dart/test/sentry_user_feedback_test.dart | 61 +++++---- 55 files changed, 864 insertions(+), 257 deletions(-) create mode 100644 dart/lib/src/protocol/access_aware_map.dart create mode 100644 dart/test/protocol/access_aware_map_tests.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f04c01fb11..3a93cdd016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ ); ``` +### Improvements + +- Deserialize and serialize unknown fields ([#2153](https://github.com/getsentry/sentry-dart/pull/2153)) + ## 8.6.0 ### Improvements diff --git a/dart/lib/src/protocol/access_aware_map.dart b/dart/lib/src/protocol/access_aware_map.dart new file mode 100644 index 0000000000..c8ea7b2395 --- /dev/null +++ b/dart/lib/src/protocol/access_aware_map.dart @@ -0,0 +1,53 @@ +import 'dart:collection'; + +import 'package:meta/meta.dart'; + +@internal +class AccessAwareMap extends MapBase { + AccessAwareMap(this._map); + + final Map _map; + final Set _accessedKeysWithValues = {}; + + Set get accessedKeysWithValues => _accessedKeysWithValues; + + @override + V? operator [](Object? key) { + if (key is String && _map.containsKey(key)) { + _accessedKeysWithValues.add(key); + } + return _map[key]; + } + + @override + void operator []=(String key, V value) { + _map[key] = value; + } + + @override + void clear() { + _map.clear(); + _accessedKeysWithValues.clear(); + } + + @override + Iterable get keys => _map.keys; + + @override + V? remove(Object? key) { + return _map.remove(key); + } + + Map? notAccessed() { + if (_accessedKeysWithValues.length == _map.length) { + return null; + } + Map unknown = _map.keys + .where((key) => !_accessedKeysWithValues.contains(key)) + .fold>({}, (map, key) { + map[key] = _map[key]; + return map; + }); + return unknown.isNotEmpty ? unknown : null; + } +} diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index f7eea55358..86eabc2ffc 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -2,6 +2,7 @@ import 'package:meta/meta.dart'; import '../utils.dart'; import '../protocol.dart'; +import 'access_aware_map.dart'; /// Structured data to describe more information prior to the event captured. /// See `Sentry.captureEvent()`. @@ -30,6 +31,7 @@ class Breadcrumb { this.data, SentryLevel? level, this.type, + this.unknown, }) : timestamp = timestamp ?? getUtcDateTime(), level = level ?? SentryLevel.info; @@ -156,8 +158,13 @@ class Breadcrumb { /// The value is submitted to Sentry with second precision. final DateTime timestamp; + @internal + final Map? unknown; + /// Deserializes a [Breadcrumb] from JSON [Map]. - factory Breadcrumb.fromJson(Map json) { + factory Breadcrumb.fromJson(Map jsonData) { + final json = AccessAwareMap(jsonData); + final levelName = json['level']; final timestamp = json['timestamp']; @@ -165,7 +172,6 @@ class Breadcrumb { if (data != null) { data = Map.from(data as Map); } - return Breadcrumb( timestamp: timestamp != null ? DateTime.tryParse(timestamp) : null, message: json['message'], @@ -173,6 +179,7 @@ class Breadcrumb { data: data, level: levelName != null ? SentryLevel.fromName(levelName) : null, type: json['type'], + unknown: json.notAccessed(), ); } @@ -180,6 +187,7 @@ class Breadcrumb { /// to the Sentry protocol. Map toJson() { return { + ...?unknown, 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), if (message != null) 'message': message, if (category != null) 'category': category, @@ -204,5 +212,6 @@ class Breadcrumb { level: level ?? this.level, type: type ?? this.type, timestamp: timestamp ?? this.timestamp, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/debug_image.dart b/dart/lib/src/protocol/debug_image.dart index df45a12fed..6ff98cbf07 100644 --- a/dart/lib/src/protocol/debug_image.dart +++ b/dart/lib/src/protocol/debug_image.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// The list of debug images contains all dynamic libraries loaded into /// the process and their memory addresses. /// Instruction addresses in the Stack Trace are mapped into the list of debug @@ -51,6 +53,9 @@ class DebugImage { /// MachO CPU type identifier. final int? cpuType; + @internal + final Map? unknown; + const DebugImage({ required this.type, this.name, @@ -65,10 +70,12 @@ class DebugImage { this.codeId, this.cpuType, this.cpuSubtype, + this.unknown, }); /// Deserializes a [DebugImage] from JSON [Map]. - factory DebugImage.fromJson(Map json) { + factory DebugImage.fromJson(Map data) { + final json = AccessAwareMap(data); return DebugImage( type: json['type'], name: json['name'], @@ -83,12 +90,14 @@ class DebugImage { codeId: json['code_id'], cpuType: json['cpu_type'], cpuSubtype: json['cpu_subtype'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, 'type': type, if (uuid != null) 'uuid': uuid, if (debugId != null) 'debug_id': debugId, @@ -134,5 +143,6 @@ class DebugImage { codeId: codeId ?? this.codeId, cpuType: cpuType ?? this.cpuType, cpuSubtype: cpuSubtype ?? this.cpuSubtype, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/debug_meta.dart b/dart/lib/src/protocol/debug_meta.dart index 205b2cc13e..dd4e93c4e4 100644 --- a/dart/lib/src/protocol/debug_meta.dart +++ b/dart/lib/src/protocol/debug_meta.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import '../protocol.dart'; +import 'access_aware_map.dart'; /// The debug meta interface carries debug information for processing errors and crash reports. @immutable @@ -16,10 +17,15 @@ class DebugMeta { /// images in order to retrieve debug files for symbolication. List get images => List.unmodifiable(_images ?? const []); - DebugMeta({this.sdk, List? images}) : _images = images; + DebugMeta({this.sdk, List? images, this.unknown}) + : _images = images; + + @internal + final Map? unknown; /// Deserializes a [DebugMeta] from JSON [Map]. - factory DebugMeta.fromJson(Map json) { + factory DebugMeta.fromJson(Map data) { + final json = AccessAwareMap(data); final sdkInfoJson = json['sdk_info']; final debugImagesJson = json['images'] as List?; return DebugMeta( @@ -28,6 +34,7 @@ class DebugMeta { ?.map((debugImageJson) => DebugImage.fromJson(debugImageJson as Map)) .toList(), + unknown: json.notAccessed(), ); } @@ -35,12 +42,13 @@ class DebugMeta { Map toJson() { final sdkInfo = sdk?.toJson(); return { + ...?unknown, if (sdkInfo?.isNotEmpty ?? false) 'sdk_info': sdkInfo, if (_images?.isNotEmpty ?? false) 'images': _images! .map((e) => e.toJson()) .where((element) => element.isNotEmpty) - .toList(growable: false) + .toList(growable: false), }; } @@ -51,5 +59,6 @@ class DebugMeta { DebugMeta( sdk: sdk ?? this.sdk, images: images ?? _images, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/mechanism.dart b/dart/lib/src/protocol/mechanism.dart index 22a6356800..fe6500b21b 100644 --- a/dart/lib/src/protocol/mechanism.dart +++ b/dart/lib/src/protocol/mechanism.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// Sentry Exception Mechanism /// The exception mechanism is an optional field residing /// in the Exception Interface. It carries additional information about @@ -76,6 +78,9 @@ class Mechanism { /// (the last to be listed in the exception values). final int? parentId; + @internal + final Map? unknown; + Mechanism({ required this.type, this.description, @@ -88,6 +93,7 @@ class Mechanism { this.source, this.exceptionId, this.parentId, + this.unknown, }) : _meta = meta != null ? Map.from(meta) : null, _data = data != null ? Map.from(data) : null; @@ -116,10 +122,12 @@ class Mechanism { source: source ?? this.source, exceptionId: exceptionId ?? this.exceptionId, parentId: parentId ?? this.parentId, + unknown: unknown, ); /// Deserializes a [Mechanism] from JSON [Map]. - factory Mechanism.fromJson(Map json) { + factory Mechanism.fromJson(Map jsonData) { + final json = AccessAwareMap(jsonData); var data = json['data']; if (data != null) { data = Map.from(data as Map); @@ -142,12 +150,14 @@ class Mechanism { source: json['source'], exceptionId: json['exception_id'], parentId: json['parent_id'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, 'type': type, if (description != null) 'description': description, if (helpLink != null) 'help_link': helpLink, diff --git a/dart/lib/src/protocol/metric_summary.dart b/dart/lib/src/protocol/metric_summary.dart index b0c617fb30..f5354bfdf7 100644 --- a/dart/lib/src/protocol/metric_summary.dart +++ b/dart/lib/src/protocol/metric_summary.dart @@ -1,4 +1,7 @@ +import 'package:meta/meta.dart'; + import '../metrics/metric.dart'; +import 'access_aware_map.dart'; class MetricSummary { final num min; @@ -7,7 +10,10 @@ class MetricSummary { final int count; final Map? tags; - MetricSummary.fromGauge(GaugeMetric gauge) + @internal + final Map? unknown; + + MetricSummary.fromGauge(GaugeMetric gauge, {this.unknown}) : min = gauge.minimum, max = gauge.maximum, sum = gauge.sum, @@ -19,20 +25,26 @@ class MetricSummary { required this.max, required this.sum, required this.count, - required this.tags}); + required this.tags, + this.unknown}); /// Deserializes a [MetricSummary] from JSON [Map]. - factory MetricSummary.fromJson(Map data) => MetricSummary( - min: data['min'], - max: data['max'], - count: data['count'], - sum: data['sum'], - tags: data['tags']?.cast(), - ); + factory MetricSummary.fromJson(Map data) { + final json = AccessAwareMap(data); + return MetricSummary( + min: json['min'], + max: json['max'], + count: json['count'], + sum: json['sum'], + tags: json['tags']?.cast(), + unknown: json.notAccessed(), + ); + } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, 'min': min, 'max': max, 'count': count, diff --git a/dart/lib/src/protocol/sdk_info.dart b/dart/lib/src/protocol/sdk_info.dart index cf0b7d0f41..f5aec76efc 100644 --- a/dart/lib/src/protocol/sdk_info.dart +++ b/dart/lib/src/protocol/sdk_info.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// An object describing the system SDK. @immutable class SdkInfo { @@ -8,26 +10,33 @@ class SdkInfo { final int? versionMinor; final int? versionPatchlevel; + @internal + final Map? unknown; + const SdkInfo({ this.sdkName, this.versionMajor, this.versionMinor, this.versionPatchlevel, + this.unknown, }); /// Deserializes a [SdkInfo] from JSON [Map]. - factory SdkInfo.fromJson(Map json) { + factory SdkInfo.fromJson(Map data) { + final json = AccessAwareMap(data); return SdkInfo( sdkName: json['sdk_name'], versionMajor: json['version_major'], versionMinor: json['version_minor'], versionPatchlevel: json['version_patchlevel'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, if (sdkName != null) 'sdk_name': sdkName, if (versionMajor != null) 'version_major': versionMajor, if (versionMinor != null) 'version_minor': versionMinor, @@ -46,5 +55,6 @@ class SdkInfo { versionMajor: versionMajor ?? this.versionMajor, versionMinor: versionMinor ?? this.versionMinor, versionPatchlevel: versionPatchlevel ?? this.versionPatchlevel, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sdk_version.dart b/dart/lib/src/protocol/sdk_version.dart index e4b686a402..b915fbdde8 100644 --- a/dart/lib/src/protocol/sdk_version.dart +++ b/dart/lib/src/protocol/sdk_version.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import 'sentry_package.dart'; +import 'access_aware_map.dart'; /// Describes the SDK that is submitting events to Sentry. /// @@ -40,6 +41,7 @@ class SdkVersion { required this.version, List? integrations, List? packages, + this.unknown, }) : // List.from prevents from having immutable lists _integrations = List.from(integrations ?? []), @@ -61,8 +63,12 @@ class SdkVersion { /// An immutable list of packages that compose this SDK. List get packages => List.unmodifiable(_packages); + @internal + final Map? unknown; + /// Deserializes a [SdkVersion] from JSON [Map]. - factory SdkVersion.fromJson(Map json) { + factory SdkVersion.fromJson(Map data) { + final json = AccessAwareMap(data); final packagesJson = json['packages'] as List?; final integrationsJson = json['integrations'] as List?; return SdkVersion( @@ -72,12 +78,14 @@ class SdkVersion { ?.map((e) => SentryPackage.fromJson(e as Map)) .toList(), integrations: integrationsJson?.map((e) => e as String).toList(), + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, 'name': name, 'version': version, if (packages.isNotEmpty) @@ -117,5 +125,6 @@ class SdkVersion { version: version ?? this.version, integrations: integrations ?? _integrations, packages: packages ?? _packages, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_app.dart b/dart/lib/src/protocol/sentry_app.dart index 24501ce540..8ef217c690 100644 --- a/dart/lib/src/protocol/sentry_app.dart +++ b/dart/lib/src/protocol/sentry_app.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// App context describes the application. /// /// As opposed to the runtime, this is the actual application that was @@ -20,6 +22,7 @@ class SentryApp { this.inForeground, this.viewNames, this.textScale, + this.unknown, }); /// Human readable application name, as it appears on the platform. @@ -56,29 +59,35 @@ class SentryApp { /// The current text scale. Only available on Flutter. final double? textScale; + @internal + final Map? unknown; + /// Deserializes a [SentryApp] from JSON [Map]. factory SentryApp.fromJson(Map data) { - final viewNamesJson = data['view_names'] as List?; + final json = AccessAwareMap(data); + final viewNamesJson = json['view_names'] as List?; return 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']) + name: json['app_name'], + version: json['app_version'], + identifier: json['app_identifier'], + build: json['app_build'], + buildType: json['build_type'], + startTime: json['app_start_time'] != null + ? DateTime.tryParse(json['app_start_time']) : null, - deviceAppHash: data['device_app_hash'], - appMemory: data['app_memory'], - inForeground: data['in_foreground'], + deviceAppHash: json['device_app_hash'], + appMemory: json['app_memory'], + inForeground: json['in_foreground'], viewNames: viewNamesJson?.map((e) => e as String).toList(), - textScale: data['text_scale'], + textScale: json['text_scale'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, if (name != null) 'app_name': name!, if (version != null) 'app_version': version!, if (identifier != null) 'app_identifier': identifier!, @@ -105,6 +114,7 @@ class SentryApp { inForeground: inForeground, viewNames: viewNames, textScale: textScale, + unknown: unknown, ); SentryApp copyWith({ @@ -132,5 +142,6 @@ class SentryApp { inForeground: inForeground ?? this.inForeground, viewNames: viewNames ?? this.viewNames, textScale: textScale ?? this.textScale, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_browser.dart b/dart/lib/src/protocol/sentry_browser.dart index f2807e1092..a67e1abb7d 100644 --- a/dart/lib/src/protocol/sentry_browser.dart +++ b/dart/lib/src/protocol/sentry_browser.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// Carries information about the browser or user agent for web-related errors. /// /// This can either be the browser this event ocurred in, or the user @@ -9,7 +11,7 @@ class SentryBrowser { static const type = 'browser'; /// Creates an instance of [SentryBrowser]. - const SentryBrowser({this.name, this.version}); + const SentryBrowser({this.name, this.version, this.unknown}); /// Human readable application name, as it appears on the platform. final String? name; @@ -17,21 +19,33 @@ class SentryBrowser { /// Human readable application version, as it appears on the platform. final String? version; + @internal + final Map? unknown; + /// Deserializes a [SentryBrowser] from JSON [Map]. - factory SentryBrowser.fromJson(Map data) => SentryBrowser( - name: data['name'], - version: data['version'], - ); + factory SentryBrowser.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryBrowser( + name: json['name'], + version: json['version'], + unknown: json.notAccessed(), + ); + } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (name != null) 'name': name, if (version != null) 'version': version, }; } - SentryBrowser clone() => SentryBrowser(name: name, version: version); + SentryBrowser clone() => SentryBrowser( + name: name, + version: version, + unknown: unknown, + ); SentryBrowser copyWith({ String? name, @@ -40,5 +54,6 @@ class SentryBrowser { SentryBrowser( name: name ?? this.name, version: version ?? this.version, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_culture.dart b/dart/lib/src/protocol/sentry_culture.dart index e48a973131..68bcae5cd8 100644 --- a/dart/lib/src/protocol/sentry_culture.dart +++ b/dart/lib/src/protocol/sentry_culture.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// Culture Context describes certain properties of the culture in which the /// software is used. @immutable @@ -12,15 +14,20 @@ class SentryCulture { this.locale, this.is24HourFormat, this.timezone, + this.unknown, }); - factory SentryCulture.fromJson(Map data) => SentryCulture( - calendar: data['calendar'], - displayName: data['display_name'], - locale: data['locale'], - is24HourFormat: data['is_24_hour_format'], - timezone: data['timezone'], - ); + factory SentryCulture.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryCulture( + calendar: json['calendar'], + displayName: json['display_name'], + locale: json['locale'], + is24HourFormat: json['is_24_hour_format'], + timezone: json['timezone'], + unknown: json.notAccessed(), + ); + } /// Optional: For example `GregorianCalendar`. Free form string. final String? calendar; @@ -39,9 +46,13 @@ class SentryCulture { /// Optional. The timezone of the locale. For example, `Europe/Vienna`. final String? timezone; + @internal + final Map? unknown; + /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (calendar != null) 'calendar': calendar!, if (displayName != null) 'display_name': displayName!, if (locale != null) 'locale': locale!, @@ -56,6 +67,7 @@ class SentryCulture { locale: locale, is24HourFormat: is24HourFormat, timezone: timezone, + unknown: unknown, ); SentryCulture copyWith({ @@ -71,5 +83,6 @@ class SentryCulture { locale: locale ?? this.locale, is24HourFormat: is24HourFormat ?? this.is24HourFormat, timezone: timezone ?? this.timezone, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_device.dart b/dart/lib/src/protocol/sentry_device.dart index cad8c765f5..1bc5c89b78 100644 --- a/dart/lib/src/protocol/sentry_device.dart +++ b/dart/lib/src/protocol/sentry_device.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; import '../sentry_options.dart'; +import 'access_aware_map.dart'; /// If a device is on portrait or landscape mode enum SentryOrientation { portrait, landscape } @@ -46,6 +47,7 @@ class SentryDevice { this.supportsGyroscope, this.supportsAudio, this.supportsLocationService, + this.unknown, }) : assert( batteryLevel == null || (batteryLevel >= 0 && batteryLevel <= 100), ); @@ -171,57 +173,65 @@ class SentryDevice { /// Optional. Is the device capable of reporting its location? final bool? supportsLocationService; + @internal + final Map? unknown; + /// 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'] is num ? data['battery_level'] as num : null) - ?.toDouble(), - orientation: data['orientation'] == 'portrait' - ? SentryOrientation.portrait - : data['orientation'] == 'landscape' - ? SentryOrientation.landscape - : null, - manufacturer: data['manufacturer'], - brand: data['brand'], - screenHeightPixels: data['screen_height_pixels']?.toInt(), - screenWidthPixels: data['screen_width_pixels']?.toInt(), - 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, - processorCount: data['processor_count'], - cpuDescription: data['cpu_description'], - processorFrequency: data['processor_frequency'], - deviceType: data['device_type'], - batteryStatus: data['battery_status'], - deviceUniqueIdentifier: data['device_unique_identifier'], - supportsVibration: data['supports_vibration'], - supportsAccelerometer: data['supports_accelerometer'], - supportsGyroscope: data['supports_gyroscope'], - supportsAudio: data['supports_audio'], - supportsLocationService: data['supports_location_service'], - ); + factory SentryDevice.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryDevice( + name: json['name'], + family: json['family'], + model: json['model'], + modelId: json['model_id'], + arch: json['arch'], + batteryLevel: + (json['battery_level'] is num ? json['battery_level'] as num : null) + ?.toDouble(), + orientation: json['orientation'] == 'portrait' + ? SentryOrientation.portrait + : json['orientation'] == 'landscape' + ? SentryOrientation.landscape + : null, + manufacturer: json['manufacturer'], + brand: json['brand'], + screenHeightPixels: json['screen_height_pixels']?.toInt(), + screenWidthPixels: json['screen_width_pixels']?.toInt(), + screenDensity: json['screen_density'], + screenDpi: json['screen_dpi'], + online: json['online'], + charging: json['charging'], + lowMemory: json['low_memory'], + simulator: json['simulator'], + memorySize: json['memory_size'], + freeMemory: json['free_memory'], + usableMemory: json['usable_memory'], + storageSize: json['storage_size'], + freeStorage: json['free_storage'], + externalStorageSize: json['external_storage_size'], + externalFreeStorage: json['external_free_storage'], + bootTime: json['boot_time'] != null + ? DateTime.tryParse(json['boot_time']) + : null, + processorCount: json['processor_count'], + cpuDescription: json['cpu_description'], + processorFrequency: json['processor_frequency'], + deviceType: json['device_type'], + batteryStatus: json['battery_status'], + deviceUniqueIdentifier: json['device_unique_identifier'], + supportsVibration: json['supports_vibration'], + supportsAccelerometer: json['supports_accelerometer'], + supportsGyroscope: json['supports_gyroscope'], + supportsAudio: json['supports_audio'], + supportsLocationService: json['supports_location_service'], + unknown: json.notAccessed(), + ); + } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (name != null) 'name': name, if (family != null) 'family': family, if (model != null) 'model': model, @@ -304,6 +314,7 @@ class SentryDevice { supportsGyroscope: supportsGyroscope, supportsAudio: supportsAudio, supportsLocationService: supportsLocationService, + unknown: unknown, ); SentryDevice copyWith({ @@ -384,5 +395,6 @@ class SentryDevice { supportsAudio: supportsAudio ?? this.supportsAudio, supportsLocationService: supportsLocationService ?? this.supportsLocationService, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 32a76b9885..1b2765c426 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -3,6 +3,7 @@ import 'package:meta/meta.dart'; import '../protocol.dart'; import '../throwable_mechanism.dart'; import '../utils.dart'; +import 'access_aware_map.dart'; /// An event to be reported to Sentry.io. @immutable @@ -37,6 +38,7 @@ class SentryEvent with SentryEventLike { this.request, this.debugMeta, this.type, + this.unknown, }) : eventId = eventId ?? SentryId.newId(), timestamp = timestamp ?? getUtcDateTime(), contexts = contexts ?? Contexts(), @@ -189,6 +191,9 @@ class SentryEvent with SentryEventLike { /// defaults to 'default' final String? type; + @internal + final Map? unknown; + @override SentryEvent copyWith({ SentryId? eventId, @@ -251,10 +256,13 @@ class SentryEvent with SentryEventLike { this.exceptions, threads: (threads != null ? List.from(threads) : null) ?? this.threads, type: type ?? this.type, + unknown: unknown, ); /// Deserializes a [SentryEvent] from JSON [Map]. - factory SentryEvent.fromJson(Map json) { + factory SentryEvent.fromJson(Map data) { + final json = AccessAwareMap(data); + final breadcrumbsJson = json['breadcrumbs'] as List?; final breadcrumbs = breadcrumbsJson ?.map((e) => Breadcrumb.fromJson(e)) @@ -329,6 +337,7 @@ class SentryEvent with SentryEventLike { : null, exceptions: exceptions, type: json['type'], + unknown: json.notAccessed(), ); } @@ -368,7 +377,8 @@ class SentryEvent with SentryEventLike { .where((e) => e.isNotEmpty) .toList(growable: false); - return { + return { + ...?unknown, 'event_id': eventId.toString(), if (timestamp != null) 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp!), diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index 45de1b5c9c..9bf5f3fa13 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import '../protocol.dart'; +import 'access_aware_map.dart'; /// The Exception Interface specifies an exception or error that occurred in a program. @immutable @@ -25,6 +26,9 @@ class SentryException { final dynamic throwable; + @internal + final Map? unknown; + const SentryException({ required this.type, required this.value, @@ -33,10 +37,13 @@ class SentryException { this.mechanism, this.threadId, this.throwable, + this.unknown, }); /// Deserializes a [SentryException] from JSON [Map]. - factory SentryException.fromJson(Map json) { + factory SentryException.fromJson(Map data) { + final json = AccessAwareMap(data); + final stackTraceJson = json['stacktrace']; final mechanismJson = json['mechanism']; return SentryException( @@ -49,12 +56,14 @@ class SentryException { mechanism: mechanismJson != null ? Mechanism.fromJson(mechanismJson) : null, threadId: json['thread_id'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (type != null) 'type': type, if (value != null) 'value': value, if (module != null) 'module': module, @@ -81,5 +90,6 @@ class SentryException { mechanism: mechanism ?? this.mechanism, threadId: threadId ?? this.threadId, throwable: throwable ?? this.throwable, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_gpu.dart b/dart/lib/src/protocol/sentry_gpu.dart index f428e674bb..bb6168813e 100644 --- a/dart/lib/src/protocol/sentry_gpu.dart +++ b/dart/lib/src/protocol/sentry_gpu.dart @@ -12,6 +12,8 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// GPU context describes the GPU of the device. @immutable class SentryGpu { @@ -65,6 +67,9 @@ class SentryGpu { /// Whether ray tracing is available on the device. final bool? supportsRayTracing; + @internal + final Map? unknown; + const SentryGpu({ this.name, this.id, @@ -81,26 +86,31 @@ class SentryGpu { this.supportsDrawCallInstancing, this.supportsGeometryShaders, this.supportsRayTracing, + this.unknown, }); /// Deserializes a [SentryGpu] from JSON [Map]. - factory SentryGpu.fromJson(Map data) => SentryGpu( - name: data['name'], - id: data['id'], - vendorId: data['vendor_id'], - vendorName: data['vendor_name'], - memorySize: data['memory_size'], - apiType: data['api_type'], - multiThreadedRendering: data['multi_threaded_rendering'], - version: data['version'], - npotSupport: data['npot_support'], - graphicsShaderLevel: data['graphics_shader_level'], - maxTextureSize: data['max_texture_size'], - supportsComputeShaders: data['supports_compute_shaders'], - supportsDrawCallInstancing: data['supports_draw_call_instancing'], - supportsGeometryShaders: data['supports_geometry_shaders'], - supportsRayTracing: data['supports_ray_tracing'], - ); + factory SentryGpu.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryGpu( + name: json['name'], + id: json['id'], + vendorId: json['vendor_id'], + vendorName: json['vendor_name'], + memorySize: json['memory_size'], + apiType: json['api_type'], + multiThreadedRendering: json['multi_threaded_rendering'], + version: json['version'], + npotSupport: json['npot_support'], + graphicsShaderLevel: json['graphics_shader_level'], + maxTextureSize: json['max_texture_size'], + supportsComputeShaders: json['supports_compute_shaders'], + supportsDrawCallInstancing: json['supports_draw_call_instancing'], + supportsGeometryShaders: json['supports_geometry_shaders'], + supportsRayTracing: json['supports_ray_tracing'], + unknown: json.notAccessed(), + ); + } SentryGpu clone() => SentryGpu( name: name, @@ -118,11 +128,13 @@ class SentryGpu { supportsDrawCallInstancing: supportsDrawCallInstancing, supportsGeometryShaders: supportsGeometryShaders, supportsRayTracing: supportsRayTracing, + unknown: unknown, ); /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (name != null) 'name': name, if (id != null) 'id': id, if (vendorId != null) 'vendor_id': vendorId, @@ -184,5 +196,6 @@ class SentryGpu { supportsGeometryShaders: supportsGeometryShaders ?? this.supportsGeometryShaders, supportsRayTracing: supportsRayTracing ?? this.supportsRayTracing, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_message.dart b/dart/lib/src/protocol/sentry_message.dart index 6115458ef2..bb9193f714 100644 --- a/dart/lib/src/protocol/sentry_message.dart +++ b/dart/lib/src/protocol/sentry_message.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// The Message Interface carries a log message that describes an event or error. /// Optionally, it can carry a format string and structured parameters. This can help to group similar messages into the same issue. /// example of a serialized message: @@ -23,20 +25,31 @@ class SentryMessage { /// A list of formatting parameters, preferably strings. Non-strings will be coerced to strings. final List? params; - const SentryMessage(this.formatted, {this.template, this.params}); + @internal + final Map? unknown; + + const SentryMessage( + this.formatted, { + this.template, + this.params, + this.unknown, + }); /// Deserializes a [SentryMessage] from JSON [Map]. - factory SentryMessage.fromJson(Map json) { + factory SentryMessage.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryMessage( json['formatted'], template: json['message'], params: json['params'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, 'formatted': formatted, if (template != null) 'message': template, if (params?.isNotEmpty ?? false) 'params': params, @@ -52,5 +65,6 @@ class SentryMessage { formatted ?? this.formatted, template: template ?? this.template, params: params ?? this.params, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_operating_system.dart b/dart/lib/src/protocol/sentry_operating_system.dart index 89839e8da1..8854a4d87f 100644 --- a/dart/lib/src/protocol/sentry_operating_system.dart +++ b/dart/lib/src/protocol/sentry_operating_system.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// Describes the operating system on which the event was created. /// /// In web contexts, this is the operating system of the browse @@ -16,6 +18,7 @@ class SentryOperatingSystem { this.rooted, this.rawDescription, this.theme, + this.unknown, }); /// The name of the operating system. @@ -45,21 +48,28 @@ class SentryOperatingSystem { /// Describes whether the OS runs in dark mode or not. final String? theme; + @internal + final Map? unknown; + /// 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'], - theme: data['theme'], - ); + factory SentryOperatingSystem.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryOperatingSystem( + name: json['name'], + version: json['version'], + build: json['build'], + kernelVersion: json['kernel_version'], + rooted: json['rooted'], + rawDescription: json['raw_description'], + theme: json['theme'], + unknown: json.notAccessed(), + ); + } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (name != null) 'name': name, if (version != null) 'version': version, if (build != null) 'build': build, @@ -78,6 +88,7 @@ class SentryOperatingSystem { rooted: rooted, rawDescription: rawDescription, theme: theme, + unknown: unknown, ); SentryOperatingSystem copyWith({ @@ -97,5 +108,6 @@ class SentryOperatingSystem { rooted: rooted ?? this.rooted, rawDescription: rawDescription ?? this.rawDescription, theme: theme ?? this.theme, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_package.dart b/dart/lib/src/protocol/sentry_package.dart index 8157a50aeb..643ba910ed 100644 --- a/dart/lib/src/protocol/sentry_package.dart +++ b/dart/lib/src/protocol/sentry_package.dart @@ -1,10 +1,12 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// A [SentryPackage] part of the SDK. @immutable class SentryPackage { /// Creates an [SentryPackage] object that is part of the SDK. - const SentryPackage(this.name, this.version); + const SentryPackage(this.name, this.version, {this.unknown}); /// The name of the SDK. final String name; @@ -12,17 +14,23 @@ class SentryPackage { /// The version of the SDK. final String version; + @internal + final Map? unknown; + /// Deserializes a [SentryPackage] from JSON [Map]. - factory SentryPackage.fromJson(Map json) { + factory SentryPackage.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryPackage( json['name'], json['version'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, 'name': name, 'version': version, }; @@ -35,5 +43,6 @@ class SentryPackage { SentryPackage( name ?? this.name, version ?? this.version, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_request.dart b/dart/lib/src/protocol/sentry_request.dart index b5ee7d003c..6ad35cd34d 100644 --- a/dart/lib/src/protocol/sentry_request.dart +++ b/dart/lib/src/protocol/sentry_request.dart @@ -1,4 +1,5 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; import '../utils/iterable_utils.dart'; import '../utils/http_sanitizer.dart'; @@ -69,6 +70,9 @@ class SentryRequest { /// its target specification. final String? apiTarget; + @internal + final Map? unknown; + SentryRequest({ this.url, this.method, @@ -81,6 +85,7 @@ class SentryRequest { Map? env, @Deprecated('Will be removed in v8. Use [data] instead') Map? other, + this.unknown, }) : _data = data, _headers = headers != null ? Map.from(headers) : null, // Look for a 'Set-Cookie' header (case insensitive) if not given. @@ -119,7 +124,8 @@ class SentryRequest { } /// Deserializes a [SentryRequest] from JSON [Map]. - factory SentryRequest.fromJson(Map json) { + factory SentryRequest.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryRequest( url: json['url'], method: json['method'], @@ -132,12 +138,14 @@ class SentryRequest { other: json.containsKey('other') ? Map.from(json['other']) : null, fragment: json['fragment'], apiTarget: json['api_target'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (url != null) 'url': url, if (method != null) 'method': method, if (queryString != null) 'query_string': queryString, @@ -178,5 +186,6 @@ class SentryRequest { apiTarget: apiTarget ?? this.apiTarget, // ignore: deprecated_member_use_from_same_package other: other ?? _other, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_runtime.dart b/dart/lib/src/protocol/sentry_runtime.dart index 960f48c170..02f8e632e8 100644 --- a/dart/lib/src/protocol/sentry_runtime.dart +++ b/dart/lib/src/protocol/sentry_runtime.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; + /// Describes a runtime in more detail. /// /// Typically this context is used multiple times if multiple runtimes @@ -17,6 +19,7 @@ class SentryRuntime { this.compiler, this.rawDescription, this.build, + this.unknown, }) : assert(key == null || key.length >= 1); /// Key used in the JSON and which will be displayed @@ -44,18 +47,26 @@ class SentryRuntime { /// Application build string, if it is separate from the version. final String? build; + @internal + final Map? unknown; + /// Deserializes a [SentryRuntime] from JSON [Map]. - factory SentryRuntime.fromJson(Map data) => SentryRuntime( - name: data['name'], - version: data['version'], - compiler: data['compiler'], - rawDescription: data['raw_description'], - build: data['build'], - ); + factory SentryRuntime.fromJson(Map data) { + final json = AccessAwareMap(data); + return SentryRuntime( + name: json['name'], + version: json['version'], + compiler: json['compiler'], + rawDescription: json['raw_description'], + build: json['build'], + unknown: json.notAccessed(), + ); + } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (name != null) 'name': name, if (compiler != null) 'compiler': compiler, if (version != null) 'version': version, @@ -71,6 +82,7 @@ class SentryRuntime { compiler: compiler, rawDescription: rawDescription, build: build, + unknown: unknown, ); SentryRuntime copyWith({ @@ -88,5 +100,6 @@ class SentryRuntime { compiler: compiler ?? this.compiler, rawDescription: rawDescription ?? this.rawDescription, build: build ?? this.build, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_stack_frame.dart b/dart/lib/src/protocol/sentry_stack_frame.dart index 12eb5de4f6..edba949e9f 100644 --- a/dart/lib/src/protocol/sentry_stack_frame.dart +++ b/dart/lib/src/protocol/sentry_stack_frame.dart @@ -1,4 +1,5 @@ import 'package:meta/meta.dart'; +import 'access_aware_map.dart'; /// Frames belong to a StackTrace /// It should contain at least a filename, function or instruction_addr @@ -26,6 +27,7 @@ class SentryStackFrame { List? preContext, List? postContext, Map? vars, + this.unknown, }) : _framesOmitted = framesOmitted != null ? List.from(framesOmitted) : null, _preContext = preContext != null ? List.from(preContext) : null, @@ -124,8 +126,12 @@ class SentryStackFrame { /// This is relevant for languages like Swift, C++ or Rust. final String? symbol; + @internal + final Map? unknown; + /// Deserializes a [SentryStackFrame] from JSON [Map]. - factory SentryStackFrame.fromJson(Map json) { + factory SentryStackFrame.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryStackFrame( absPath: json['abs_path'], fileName: json['filename'], @@ -148,12 +154,14 @@ class SentryStackFrame { vars: json['vars'], symbol: json['symbol'], stackStart: json['stack_start'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (_preContext?.isNotEmpty ?? false) 'pre_context': _preContext, if (_postContext?.isNotEmpty ?? false) 'post_context': _postContext, if (_vars?.isNotEmpty ?? false) 'vars': _vars, @@ -223,5 +231,6 @@ class SentryStackFrame { vars: vars ?? _vars, symbol: symbol ?? symbol, stackStart: stackStart ?? stackStart, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart index c8a6076726..949318ec4c 100644 --- a/dart/lib/src/protocol/sentry_stack_trace.dart +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import 'sentry_stack_frame.dart'; +import 'access_aware_map.dart'; /// Stacktrace holds information about the frames of the stack. @immutable @@ -10,6 +11,7 @@ class SentryStackTrace { Map? registers, this.lang, this.snapshot, + this.unknown, }) : _frames = frames, _registers = Map.from(registers ?? {}); @@ -44,8 +46,12 @@ class SentryStackTrace { /// signal. final bool? snapshot; + @internal + final Map? unknown; + /// Deserializes a [SentryStackTrace] from JSON [Map]. - factory SentryStackTrace.fromJson(Map json) { + factory SentryStackTrace.fromJson(Map data) { + final json = AccessAwareMap(data); final framesJson = json['frames'] as List?; return SentryStackTrace( frames: framesJson != null @@ -56,12 +62,14 @@ class SentryStackTrace { registers: json['registers'], lang: json['lang'], snapshot: json['snapshot'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - return { + return { + ...?unknown, if (_frames?.isNotEmpty ?? false) 'frames': _frames?.map((frame) => frame.toJson()).toList(growable: false), @@ -74,9 +82,14 @@ class SentryStackTrace { SentryStackTrace copyWith({ List? frames, Map? registers, + String? lang, + bool? snapshot, }) => SentryStackTrace( frames: frames ?? this.frames, registers: registers ?? this.registers, + lang: lang ?? this.lang, + snapshot: snapshot ?? this.snapshot, + unknown: unknown, ); } diff --git a/dart/lib/src/protocol/sentry_thread.dart b/dart/lib/src/protocol/sentry_thread.dart index c6ce13c15a..49dcd284f9 100644 --- a/dart/lib/src/protocol/sentry_thread.dart +++ b/dart/lib/src/protocol/sentry_thread.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import 'sentry_stack_trace.dart'; +import 'access_aware_map.dart'; /// The Threads Interface specifies threads that were running at the time an /// event happened. These threads can also contain stack traces. @@ -13,9 +14,11 @@ class SentryThread { this.crashed, this.current, this.stacktrace, + this.unknown, }); - factory SentryThread.fromJson(Map json) { + factory SentryThread.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryThread( id: json['id'] as int?, name: json['name'] as String?, @@ -23,6 +26,7 @@ class SentryThread { current: json['current'] as bool?, stacktrace: json['stacktrace'] == null ? null : SentryStackTrace.fromJson(json), + unknown: json.notAccessed(), ); } @@ -44,9 +48,13 @@ class SentryThread { /// See https://develop.sentry.dev/sdk/event-payloads/stacktrace/ final SentryStackTrace? stacktrace; + @internal + final Map? unknown; + Map toJson() { final stacktrace = this.stacktrace; return { + ...?unknown, if (id != null) 'id': id, if (name != null) 'name': name, if (crashed != null) 'crashed': crashed, @@ -68,6 +76,7 @@ class SentryThread { crashed: crashed ?? this.crashed, current: current ?? this.current, stacktrace: stacktrace ?? this.stacktrace, + unknown: unknown, ); } } diff --git a/dart/lib/src/protocol/sentry_trace_context.dart b/dart/lib/src/protocol/sentry_trace_context.dart index 25c4ca7ad8..1d80541fd4 100644 --- a/dart/lib/src/protocol/sentry_trace_context.dart +++ b/dart/lib/src/protocol/sentry_trace_context.dart @@ -3,6 +3,7 @@ import 'package:meta/meta.dart'; import '../../sentry.dart'; import '../propagation_context.dart'; import '../protocol.dart'; +import 'access_aware_map.dart'; @immutable class SentryTraceContext { @@ -37,7 +38,11 @@ class SentryTraceContext { /// @see final String? origin; - factory SentryTraceContext.fromJson(Map json) { + @internal + final Map? unknown; + + factory SentryTraceContext.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryTraceContext( operation: json['op'] as String, spanId: SpanId.fromId(json['span_id'] as String), @@ -51,12 +56,14 @@ class SentryTraceContext { : SpanStatus.fromString(json['status'] as String), sampled: true, origin: json['origin'] == null ? null : json['origin'] as String?, + unknown: json.notAccessed(), ); } /// Item encoded as JSON Map toJson() { return { + ...?unknown, 'span_id': spanId.toString(), 'trace_id': traceId.toString(), 'op': operation, @@ -76,6 +83,7 @@ class SentryTraceContext { parentSpanId: parentSpanId, sampled: sampled, origin: origin, + unknown: unknown, ); SentryTraceContext({ @@ -87,6 +95,7 @@ class SentryTraceContext { this.description, this.status, this.origin, + this.unknown, }) : traceId = traceId ?? SentryId.newId(), spanId = spanId ?? SpanId.newId(); diff --git a/dart/lib/src/protocol/sentry_transaction_info.dart b/dart/lib/src/protocol/sentry_transaction_info.dart index 96b520eb5a..773b482646 100644 --- a/dart/lib/src/protocol/sentry_transaction_info.dart +++ b/dart/lib/src/protocol/sentry_transaction_info.dart @@ -1,10 +1,18 @@ +import 'package:meta/meta.dart'; + +import 'access_aware_map.dart'; + class SentryTransactionInfo { - SentryTransactionInfo(this.source); + SentryTransactionInfo(this.source, {this.unknown}); final String source; + @internal + final Map? unknown; + Map toJson() { return { + ...?unknown, 'source': source, }; } @@ -14,12 +22,15 @@ class SentryTransactionInfo { }) { return SentryTransactionInfo( source ?? this.source, + unknown: unknown, ); } - factory SentryTransactionInfo.fromJson(Map json) { + factory SentryTransactionInfo.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryTransactionInfo( json['source'], + unknown: json.notAccessed(), ); } } diff --git a/dart/lib/src/protocol/sentry_user.dart b/dart/lib/src/protocol/sentry_user.dart index c173432fcd..a62d7b9c58 100644 --- a/dart/lib/src/protocol/sentry_user.dart +++ b/dart/lib/src/protocol/sentry_user.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import '../../sentry.dart'; +import 'access_aware_map.dart'; /// Describes the current user associated with the application, such as the /// currently signed in user. @@ -44,6 +45,7 @@ class SentryUser { Map? data, @Deprecated('Will be removed in v8. Use [data] instead') Map? extras, + this.unknown, }) : assert( id != null || username != null || @@ -92,8 +94,13 @@ class SentryUser { /// Human readable name of the user. final String? name; + @internal + final Map? unknown; + /// Deserializes a [SentryUser] from JSON [Map]. - factory SentryUser.fromJson(Map json) { + factory SentryUser.fromJson(Map jsonData) { + final json = AccessAwareMap(jsonData); + var extras = json['extras']; if (extras != null) { extras = Map.from(extras); @@ -120,13 +127,15 @@ class SentryUser { name: json['name'], // ignore: deprecated_member_use_from_same_package extras: extras, + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { final geoJson = geo?.toJson(); - return { + return { + ...?unknown, if (id != null) 'id': id, if (username != null) 'username': username, if (email != null) 'email': email, @@ -165,6 +174,7 @@ class SentryUser { extras: extras ?? this.extras, geo: geo ?? this.geo, name: name ?? this.name, + unknown: unknown, ); } } diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index d1ee5368af..ed7ec53653 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -1,4 +1,7 @@ +import 'package:meta/meta.dart'; + import 'protocol/sentry_id.dart'; +import 'protocol/access_aware_map.dart'; import 'sentry_baggage.dart'; import 'sentry_options.dart'; @@ -13,6 +16,7 @@ class SentryTraceContextHeader { this.transaction, this.sampleRate, this.sampled, + this.unknown, }); final SentryId traceId; @@ -27,8 +31,12 @@ class SentryTraceContextHeader { final String? sampleRate; final String? sampled; + @internal + final Map? unknown; + /// Deserializes a [SentryTraceContextHeader] from JSON [Map]. - factory SentryTraceContextHeader.fromJson(Map json) { + factory SentryTraceContextHeader.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryTraceContextHeader( SentryId.fromId(json['trace_id']), json['public_key'], @@ -39,12 +47,14 @@ class SentryTraceContextHeader { transaction: json['transaction'], sampleRate: json['sample_rate'], sampled: json['sampled'], + unknown: json.notAccessed(), ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { return { + ...?unknown, 'trace_id': traceId.toString(), 'public_key': publicKey, if (release != null) 'release': release, diff --git a/dart/lib/src/sentry_user_feedback.dart b/dart/lib/src/sentry_user_feedback.dart index 7ba8c0d6f1..055199ed61 100644 --- a/dart/lib/src/sentry_user_feedback.dart +++ b/dart/lib/src/sentry_user_feedback.dart @@ -1,4 +1,7 @@ +import 'package:meta/meta.dart'; + import 'protocol.dart'; +import 'protocol/access_aware_map.dart'; class SentryUserFeedback { SentryUserFeedback({ @@ -6,17 +9,20 @@ class SentryUserFeedback { this.name, this.email, this.comments, + this.unknown, }) : assert(eventId != SentryId.empty() && (name?.isNotEmpty == true || email?.isNotEmpty == true || comments?.isNotEmpty == true)); - factory SentryUserFeedback.fromJson(Map json) { + factory SentryUserFeedback.fromJson(Map data) { + final json = AccessAwareMap(data); return SentryUserFeedback( eventId: SentryId.fromId(json['event_id']), name: json['name'], email: json['email'], comments: json['comments'], + unknown: json.notAccessed(), ); } @@ -32,8 +38,12 @@ class SentryUserFeedback { /// Recommended: Comments of the user about what happened. final String? comments; + @internal + final Map? unknown; + Map toJson() { - return { + return { + ...?unknown, 'event_id': eventId.toString(), if (name != null) 'name': name, if (email != null) 'email': email, @@ -52,6 +62,7 @@ class SentryUserFeedback { name: name ?? this.name, email: email ?? this.email, comments: comments ?? this.comments, + unknown: unknown, ); } } diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index 7cce3a79ea..31cc55b7cd 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -173,6 +173,7 @@ void main() { MapEquality().equals( contexts.app!.toJson(), { + 'app_id': 'D533244D-985D-3996-9FC2-9FA353D28586', 'app_name': 'sentry_flutter_example', 'app_version': '0.1.2', 'app_identifier': 'io.sentry.flutter.example', @@ -224,7 +225,6 @@ const jsonContexts = ''' "memory_size": 17179869184, "storage_size": 1023683072000, "boot_time": "2020-11-18T13:28:11Z", - "timezone": "GMT+1", "usable_memory": 17114120192 }, "app": { diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index fc68270295..679b9f73cd 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -203,6 +203,12 @@ class MockRateLimiter implements RateLimiter { } } +final Map testUnknown = { + 'unknown-string': 'foo', + 'unknown-bool': true, + 'unknown-num': 9001, +}; + @GenerateMocks([ SentryProfilerFactory, SentryProfiler, diff --git a/dart/test/protocol/access_aware_map_tests.dart b/dart/test/protocol/access_aware_map_tests.dart new file mode 100644 index 0000000000..b9c08f2b9a --- /dev/null +++ b/dart/test/protocol/access_aware_map_tests.dart @@ -0,0 +1,118 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/src/protocol/access_aware_map.dart'; +import 'package:test/test.dart'; + +void main() { + group('MapBase', () { + test('set/get value for key', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + }); + + sut['foo'] = 'bar'; + sut['bar'] = 'foo'; + + expect(sut['foo'], 'bar'); + expect(sut['bar'], 'foo'); + }); + + test('clear', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + }); + + sut.clear(); + + expect(sut.isEmpty, true); + }); + + test('keys', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + 'bar': 'bar', + }); + expect( + sut.keys.sortedBy((it) => it), ['bar', 'foo'].sortedBy((it) => it)); + }); + + test('remove', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + }); + + sut.remove('foo'); + + expect(sut.isEmpty, true); + }); + }); + + group('access aware', () { + test('collects accessedKeys', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + 'bar': 'bar', + }); + + sut['foo']; + sut['bar']; + sut['baz']; + + expect(sut.accessedKeysWithValues, {'foo', 'bar', 'baz'}); + }); + + test('returns notAccessed data', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + 'bar': 'bar', + }); + + sut['foo']; + + final notAccessed = sut.notAccessed(); + expect(notAccessed, isNotNull); + expect(notAccessed?.containsKey('foo'), false); + expect(notAccessed?.containsKey('bar'), true); + }); + }); + + group('map base functionality', () { + test('set value with []= operator', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + }); + + sut['foo'] = 'bar'; + sut['bar'] = 'foo'; + + expect(sut['foo'], 'bar'); + expect(sut['bar'], 'foo'); + }); + + test('clear', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + }); + + sut.clear(); + + expect(sut.accessedKeysWithValues.isEmpty, true); + expect(sut.isEmpty, true); + }); + + test('keys', () { + final sut = AccessAwareMap({ + 'foo': 'foo', + 'bar': 'bar', + }); + expect(sut.keys.toSet(), {'foo', 'bar'}); + }); + + test('remove', () { + final sut = AccessAwareMap({'foo': 'foo'}); + + sut.remove('foo'); + + expect(sut['foo'], isNull); + }); + }); +} diff --git a/dart/test/protocol/breadcrumb_test.dart b/dart/test/protocol/breadcrumb_test.dart index ddd4008d98..ddda79df6e 100644 --- a/dart/test/protocol/breadcrumb_test.dart +++ b/dart/test/protocol/breadcrumb_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final timestamp = DateTime.now(); @@ -12,6 +14,7 @@ void main() { level: SentryLevel.warning, category: 'category', type: 'type', + unknown: testUnknown, ); final breadcrumbJson = { @@ -22,6 +25,7 @@ void main() { 'level': 'warning', 'type': 'type', }; + breadcrumbJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/debug_image_test.dart b/dart/test/protocol/debug_image_test.dart index d06dad4b5e..5f3de134e1 100644 --- a/dart/test/protocol/debug_image_test.dart +++ b/dart/test/protocol/debug_image_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final debugImage = DebugImage( type: 'type', @@ -13,6 +15,7 @@ void main() { codeFile: 'codeFile', arch: 'arch', codeId: 'codeId', + unknown: testUnknown, ); final debugImageJson = { @@ -26,6 +29,7 @@ void main() { 'arch': 'arch', 'code_id': 'codeId', }; + debugImageJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/debug_meta_test.dart b/dart/test/protocol/debug_meta_test.dart index 21caf02747..4b5624f4a9 100644 --- a/dart/test/protocol/debug_meta_test.dart +++ b/dart/test/protocol/debug_meta_test.dart @@ -2,12 +2,15 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final debugMeta = DebugMeta( sdk: SdkInfo( sdkName: 'sdkName', ), images: [DebugImage(type: 'macho', uuid: 'uuid')], + unknown: testUnknown, ); final debugMetaJson = { @@ -16,6 +19,7 @@ void main() { {'uuid': 'uuid', 'type': 'macho'} ] }; + debugMetaJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/mechanism_test.dart b/dart/test/protocol/mechanism_test.dart index 857a0529a9..d9bdc3a7cf 100644 --- a/dart/test/protocol/mechanism_test.dart +++ b/dart/test/protocol/mechanism_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final mechanism = Mechanism( type: 'type', @@ -15,6 +17,7 @@ void main() { exceptionId: 0, parentId: 0, source: 'source', + unknown: testUnknown, ); final mechanismJson = { @@ -30,6 +33,7 @@ void main() { 'exception_id': 0, 'parent_id': 0, }; + mechanismJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sdk_info_test.dart b/dart/test/protocol/sdk_info_test.dart index 50e3c3fdcd..522638f238 100644 --- a/dart/test/protocol/sdk_info_test.dart +++ b/dart/test/protocol/sdk_info_test.dart @@ -2,12 +2,15 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sdkInfo = SdkInfo( sdkName: 'sdkName', versionMajor: 1, versionMinor: 2, versionPatchlevel: 3, + unknown: testUnknown, ); final sdkInfoJson = { @@ -16,6 +19,7 @@ void main() { 'version_minor': 2, 'version_patchlevel': 3, }; + sdkInfoJson.addAll(testUnknown); group('json', () { test('toJson', () { @@ -30,6 +34,9 @@ void main() { final sdkInfo = SdkInfo.fromJson(sdkInfoJson); final json = sdkInfo.toJson(); + print(sdkInfo); + print(json); + expect( MapEquality().equals(sdkInfoJson, json), true, diff --git a/dart/test/protocol/sdk_version_test.dart b/dart/test/protocol/sdk_version_test.dart index 92f799712c..b2e85a73c4 100644 --- a/dart/test/protocol/sdk_version_test.dart +++ b/dart/test/protocol/sdk_version_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { group('json', () { final fixture = Fixture(); @@ -95,10 +97,15 @@ class Fixture { ], }; + Fixture() { + sdkVersionJson.addAll(testUnknown); + } + SdkVersion getSut() => SdkVersion( name: 'name', version: 'version', integrations: ['test'], packages: [SentryPackage('name', 'version')], + unknown: testUnknown, ); } diff --git a/dart/test/protocol/sentry_app_test.dart b/dart/test/protocol/sentry_app_test.dart index 0fdef8a780..fbc468d3d9 100644 --- a/dart/test/protocol/sentry_app_test.dart +++ b/dart/test/protocol/sentry_app_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final testStartTime = DateTime.fromMicrosecondsSinceEpoch(0); @@ -16,6 +18,7 @@ void main() { inForeground: true, viewNames: ['fixture-viewName', 'fixture-viewName2'], textScale: 2.0, + unknown: testUnknown, ); final sentryAppJson = { @@ -30,6 +33,7 @@ void main() { 'view_names': ['fixture-viewName', 'fixture-viewName2'], 'text_scale': 2.0, }; + sentryAppJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_device_test.dart b/dart/test/protocol/sentry_device_test.dart index d7ed3e518f..7f405e09bf 100644 --- a/dart/test/protocol/sentry_device_test.dart +++ b/dart/test/protocol/sentry_device_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final testBootTime = DateTime.fromMicrosecondsSinceEpoch(0); @@ -42,6 +44,7 @@ void main() { supportsVibration: true, screenHeightPixels: 100, screenWidthPixels: 100, + unknown: testUnknown, ); final sentryDeviceJson = { @@ -82,6 +85,7 @@ void main() { 'screen_height_pixels': 100, 'screen_width_pixels': 100, }; + sentryDeviceJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_exception_test.dart b/dart/test/protocol/sentry_exception_test.dart index 15c75b0191..4541e67332 100644 --- a/dart/test/protocol/sentry_exception_test.dart +++ b/dart/test/protocol/sentry_exception_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryException = SentryException( type: 'type', @@ -10,6 +12,7 @@ void main() { stackTrace: SentryStackTrace(frames: [SentryStackFrame(absPath: 'abs')]), mechanism: Mechanism(type: 'type'), threadId: 1, + unknown: testUnknown, ); final sentryExceptionJson = { @@ -24,6 +27,7 @@ void main() { 'mechanism': {'type': 'type'}, 'thread_id': 1, }; + sentryExceptionJson.addAll(testUnknown); group('json', () { test('fromJson', () { diff --git a/dart/test/protocol/sentry_gpu_test.dart b/dart/test/protocol/sentry_gpu_test.dart index 443b50ea91..af2907a648 100644 --- a/dart/test/protocol/sentry_gpu_test.dart +++ b/dart/test/protocol/sentry_gpu_test.dart @@ -2,17 +2,21 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.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'); + name: 'fixture-name', + id: 1, + vendorId: '2', + vendorName: 'fixture-vendorName', + memorySize: 3, + apiType: 'fixture-apiType', + multiThreadedRendering: true, + version: '4', + npotSupport: 'fixture-npotSupport', + unknown: testUnknown, + ); final sentryGpuJson = { 'name': 'fixture-name', @@ -25,6 +29,7 @@ void main() { 'version': '4', 'npot_support': 'fixture-npotSupport' }; + sentryGpuJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_message_test.dart b/dart/test/protocol/sentry_message_test.dart index 7112642ed2..b89c0ef08f 100644 --- a/dart/test/protocol/sentry_message_test.dart +++ b/dart/test/protocol/sentry_message_test.dart @@ -2,11 +2,14 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryMessage = SentryMessage( 'message 1', template: 'message %d', params: ['1'], + unknown: testUnknown, ); final sentryMessageJson = { @@ -14,6 +17,7 @@ void main() { 'message': 'message %d', 'params': ['1'], }; + sentryMessageJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_operating_system_test.dart b/dart/test/protocol/sentry_operating_system_test.dart index dee235712d..9ca41e8590 100644 --- a/dart/test/protocol/sentry_operating_system_test.dart +++ b/dart/test/protocol/sentry_operating_system_test.dart @@ -2,14 +2,18 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryOperatingSystem = SentryOperatingSystem( - name: 'fixture-name', - version: 'fixture-version', - build: 'fixture-build', - kernelVersion: 'fixture-kernelVersion', - rooted: true, - rawDescription: 'fixture-rawDescription'); + name: 'fixture-name', + version: 'fixture-version', + build: 'fixture-build', + kernelVersion: 'fixture-kernelVersion', + rooted: true, + rawDescription: 'fixture-rawDescription', + unknown: testUnknown, + ); final sentryOperatingSystemJson = { 'name': 'fixture-name', @@ -19,6 +23,7 @@ void main() { 'rooted': true, 'raw_description': 'fixture-rawDescription' }; + sentryOperatingSystemJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_package_test.dart b/dart/test/protocol/sentry_package_test.dart index b8c1d40e71..7f9e82bfc4 100644 --- a/dart/test/protocol/sentry_package_test.dart +++ b/dart/test/protocol/sentry_package_test.dart @@ -2,16 +2,20 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryPackage = SentryPackage( 'name', 'version', + unknown: testUnknown, ); final sentryPackageJson = { 'name': 'name', 'version': 'version', }; + sentryPackageJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_request_test.dart b/dart/test/protocol/sentry_request_test.dart index a1d186632b..0aeb24dc1d 100644 --- a/dart/test/protocol/sentry_request_test.dart +++ b/dart/test/protocol/sentry_request_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryRequest = SentryRequest( url: 'url', @@ -14,6 +16,7 @@ void main() { apiTarget: 'GraphQL', // ignore: deprecated_member_use_from_same_package other: {'other_key': 'other_value'}, + unknown: testUnknown, ); final sentryRequestJson = { @@ -27,6 +30,7 @@ void main() { 'api_target': 'GraphQL', 'other': {'other_key': 'other_value'}, }; + sentryRequestJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_runtime_test.dart b/dart/test/protocol/sentry_runtime_test.dart index 578eb8c9a3..2537950d8e 100644 --- a/dart/test/protocol/sentry_runtime_test.dart +++ b/dart/test/protocol/sentry_runtime_test.dart @@ -2,12 +2,15 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryRuntime = SentryRuntime( key: 'key', name: 'name', version: 'version', rawDescription: 'rawDescription', + unknown: testUnknown, ); final sentryRuntimeJson = { @@ -15,6 +18,7 @@ void main() { 'version': 'version', 'raw_description': 'rawDescription', }; + sentryRuntimeJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_stack_frame_test.dart b/dart/test/protocol/sentry_stack_frame_test.dart index 69b5ad30fa..eb4619dfbc 100644 --- a/dart/test/protocol/sentry_stack_frame_test.dart +++ b/dart/test/protocol/sentry_stack_frame_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryStackFrame = SentryStackFrame( absPath: 'absPath', @@ -23,6 +25,7 @@ void main() { preContext: ['a'], postContext: ['b'], vars: {'key': 'value'}, + unknown: testUnknown, ); final sentryStackFrameJson = { @@ -46,6 +49,7 @@ void main() { 'instruction_addr': 'instructionAddr', 'raw_function': 'rawFunction', }; + sentryStackFrameJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_stack_trace_test.dart b/dart/test/protocol/sentry_stack_trace_test.dart index ce3f4817d6..4c40032381 100644 --- a/dart/test/protocol/sentry_stack_trace_test.dart +++ b/dart/test/protocol/sentry_stack_trace_test.dart @@ -2,10 +2,15 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryStackTrace = SentryStackTrace( frames: [SentryStackFrame(absPath: 'abs')], registers: {'key': 'value'}, + lang: 'de', + snapshot: true, + unknown: testUnknown, ); final sentryStackTraceJson = { @@ -13,7 +18,10 @@ void main() { {'abs_path': 'abs'} ], 'registers': {'key': 'value'}, + 'lang': 'de', + 'snapshot': true, }; + sentryStackTraceJson.addAll(testUnknown); group('json', () { test('toJson', () { diff --git a/dart/test/protocol/sentry_transaction_info_test.dart b/dart/test/protocol/sentry_transaction_info_test.dart index 951fb08453..31438d820c 100644 --- a/dart/test/protocol/sentry_transaction_info_test.dart +++ b/dart/test/protocol/sentry_transaction_info_test.dart @@ -1,16 +1,24 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { group('$SentryTransactionInfo', () { + final info = SentryTransactionInfo( + 'component', + unknown: testUnknown, + ); + + final json = {'source': 'component'}; + json.addAll(testUnknown); + test('returns source', () { - final info = SentryTransactionInfo('component'); expect(info.source, 'component'); }); test('toJson has source', () { - final info = SentryTransactionInfo('component'); - expect(info.toJson(), {'source': 'component'}); + expect(info.toJson(), json); }); test('fromJson has source', () { diff --git a/dart/test/protocol/sentry_user_test.dart b/dart/test/protocol/sentry_user_test.dart index d3c8e078ba..dbc6e6cd87 100644 --- a/dart/test/protocol/sentry_user_test.dart +++ b/dart/test/protocol/sentry_user_test.dart @@ -2,6 +2,8 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; + void main() { final sentryUser = SentryUser( id: 'id', @@ -10,6 +12,7 @@ void main() { ipAddress: 'ipAddress', data: {'key': 'value'}, segment: 'seg', + unknown: testUnknown, ); final sentryUserJson = { @@ -20,11 +23,14 @@ void main() { 'data': {'key': 'value'}, 'segment': 'seg', }; + sentryUserJson.addAll(testUnknown); group('json', () { test('toJson', () { final json = sentryUser.toJson(); + print("$json"); + expect( DeepCollectionEquality().equals(sentryUserJson, json), true, diff --git a/dart/test/sentry_event_test.dart b/dart/test/sentry_event_test.dart index 4e661638a8..fe9d3b4d48 100644 --- a/dart/test/sentry_event_test.dart +++ b/dart/test/sentry_event_test.dart @@ -7,6 +7,8 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/version.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; + void main() { group('deserialize', () { final sentryId = SentryId.empty(); @@ -60,6 +62,7 @@ void main() { }, 'type': 'type', }; + sentryEventJson.addAll(testUnknown); final emptyFieldsSentryEventJson = { 'event_id': sentryId.toString(), @@ -105,6 +108,7 @@ void main() { expect(sentryEvent.request, isNull); expect(sentryEvent.debugMeta, isNull); expect(sentryEvent.type, isNull); + expect(sentryEvent.unknown, isNull); }); }); @@ -184,52 +188,56 @@ void main() { expect( SentryEvent( - eventId: SentryId.empty(), - timestamp: timestamp, - platform: sdkPlatform(platformChecker.isWeb), - message: SentryMessage( - 'test-message 1 2', - template: 'test-message %d %d', - params: ['1', '2'], - ), - transaction: '/test/1', - level: SentryLevel.debug, - culprit: 'Professor Moriarty', - tags: const { - 'a': 'b', - 'c': 'd', - }, - // ignore: deprecated_member_use_from_same_package - extra: const { - 'e': 'f', - 'g': 2, - }, - fingerprint: const [SentryEvent.defaultFingerprint, 'foo'], - user: user, - breadcrumbs: breadcrumbs, - request: request, - debugMeta: DebugMeta( - sdk: SdkInfo( - sdkName: 'sentry.dart', - versionMajor: 4, - versionMinor: 1, - versionPatchlevel: 2, - ), - images: const [ - DebugImage( - type: 'macho', - debugId: '84a04d24-0e60-3810-a8c0-90a65e2df61a', - debugFile: 'libDiagnosticMessagesClient.dylib', - codeFile: '/usr/lib/libDiagnosticMessagesClient.dylib', - imageAddr: '0x7fffe668e000', - imageSize: 8192, - arch: 'x86_64', - codeId: '123', - ) - ], - ), - type: 'type', - ).toJson(), + eventId: SentryId.empty(), + timestamp: timestamp, + platform: sdkPlatform(platformChecker.isWeb), + message: SentryMessage( + 'test-message 1 2', + template: 'test-message %d %d', + params: ['1', '2'], + ), + transaction: '/test/1', + level: SentryLevel.debug, + culprit: 'Professor Moriarty', + tags: const { + 'a': 'b', + 'c': 'd', + }, + // ignore: deprecated_member_use_from_same_package + extra: const { + 'e': 'f', + 'g': 2, + }, + fingerprint: const [ + SentryEvent.defaultFingerprint, + 'foo' + ], + user: user, + breadcrumbs: breadcrumbs, + request: request, + debugMeta: DebugMeta( + sdk: SdkInfo( + sdkName: 'sentry.dart', + versionMajor: 4, + versionMinor: 1, + versionPatchlevel: 2, + ), + images: const [ + DebugImage( + type: 'macho', + debugId: '84a04d24-0e60-3810-a8c0-90a65e2df61a', + debugFile: 'libDiagnosticMessagesClient.dylib', + codeFile: '/usr/lib/libDiagnosticMessagesClient.dylib', + imageAddr: '0x7fffe668e000', + imageSize: 8192, + arch: 'x86_64', + codeId: '123', + ) + ], + ), + type: 'type', + unknown: testUnknown) + .toJson(), { 'platform': platformChecker.isWeb ? 'javascript' : 'other', 'event_id': '00000000000000000000000000000000', @@ -286,7 +294,7 @@ void main() { ] }, 'type': 'type', - }, + }..addAll(testUnknown), ); }); diff --git a/dart/test/sentry_trace_context_header_test.dart b/dart/test/sentry_trace_context_header_test.dart index c4f856f344..db9a2d3621 100644 --- a/dart/test/sentry_trace_context_header_test.dart +++ b/dart/test/sentry_trace_context_header_test.dart @@ -2,11 +2,27 @@ import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; + void main() { group('$SentryTraceContextHeader', () { - final id = SentryId.newId(); + final traceId = SentryId.newId(); + + final context = SentryTraceContextHeader( + traceId, + '123', + release: 'release', + environment: 'environment', + userId: 'user_id', + userSegment: 'user_segment', + transaction: 'transaction', + sampleRate: '1.0', + sampled: 'false', + unknown: testUnknown, + ); + final mapJson = { - 'trace_id': '$id', + 'trace_id': '$traceId', 'public_key': '123', 'release': 'release', 'environment': 'environment', @@ -16,10 +32,10 @@ void main() { 'sample_rate': '1.0', 'sampled': 'false' }; - final context = SentryTraceContextHeader.fromJson(mapJson); + mapJson.addAll(testUnknown); test('fromJson', () { - expect(context.traceId.toString(), id.toString()); + expect(context.traceId.toString(), traceId.toString()); expect(context.publicKey, '123'); expect(context.release, 'release'); expect(context.environment, 'environment'); @@ -41,7 +57,7 @@ void main() { final baggage = context.toBaggage(); expect(baggage.toHeaderString(), - 'sentry-trace_id=${id.toString()},sentry-public_key=123,sentry-release=release,sentry-environment=environment,sentry-user_id=user_id,sentry-user_segment=user_segment,sentry-transaction=transaction,sentry-sample_rate=1.0,sentry-sampled=false'); + 'sentry-trace_id=${traceId.toString()},sentry-public_key=123,sentry-release=release,sentry-environment=environment,sentry-user_id=user_id,sentry-user_segment=user_segment,sentry-transaction=transaction,sentry-sample_rate=1.0,sentry-sampled=false'); }); }); } diff --git a/dart/test/sentry_trace_context_test.dart b/dart/test/sentry_trace_context_test.dart index dde599bef1..9030b48182 100644 --- a/dart/test/sentry_trace_context_test.dart +++ b/dart/test/sentry_trace_context_test.dart @@ -1,6 +1,8 @@ import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; +import 'mocks.dart'; + void main() { final fixture = Fixture(); @@ -19,7 +21,7 @@ void main() { }); test('fromJson deserializes', () { - final map = { + final map = { 'op': 'op', 'span_id': '0000000000000000', 'trace_id': '00000000000000000000000000000000', @@ -28,6 +30,7 @@ void main() { 'status': 'aborted', 'origin': 'auto.ui' }; + map.addAll(testUnknown); final traceContext = SentryTraceContext.fromJson(map); expect(traceContext.description, 'desc'); @@ -43,11 +46,13 @@ void main() { class Fixture { SentryTraceContext getSut() { return SentryTraceContext( - operation: 'op', - parentSpanId: SpanId.newId(), - description: 'desc', - sampled: true, - status: SpanStatus.aborted(), - origin: 'auto.ui'); + operation: 'op', + parentSpanId: SpanId.newId(), + description: 'desc', + sampled: true, + status: SpanStatus.aborted(), + origin: 'auto.ui', + unknown: testUnknown, + ); } } diff --git a/dart/test/sentry_user_feedback_test.dart b/dart/test/sentry_user_feedback_test.dart index 6051798b65..cb406f65b1 100644 --- a/dart/test/sentry_user_feedback_test.dart +++ b/dart/test/sentry_user_feedback_test.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/sentry_item_type.dart'; import 'package:test/test.dart'; @@ -7,35 +8,39 @@ import 'mocks/mock_transport.dart'; void main() { group('$SentryUserFeedback', () { + final id = SentryId.newId(); + + final feedback = SentryUserFeedback( + eventId: id, + comments: 'this is awesome', + email: 'sentry@example.com', + name: 'Rockstar Developer', + unknown: testUnknown, + ); + final feedbackJson = { + 'event_id': id.toString(), + 'comments': 'this is awesome', + 'email': 'sentry@example.com', + 'name': 'Rockstar Developer', + }; + feedbackJson.addAll(testUnknown); + test('toJson', () { - final id = SentryId.newId(); - final feedback = SentryUserFeedback( - eventId: id, - comments: 'this is awesome', - email: 'sentry@example.com', - name: 'Rockstar Developer', + final json = feedback.toJson(); + expect( + MapEquality().equals(feedbackJson, json), + true, ); - expect(feedback.toJson(), { - 'event_id': id.toString(), - 'comments': 'this is awesome', - 'email': 'sentry@example.com', - 'name': 'Rockstar Developer', - }); }); test('fromJson', () { - final id = SentryId.newId(); - final feedback = SentryUserFeedback.fromJson({ - 'event_id': id.toString(), - 'comments': 'this is awesome', - 'email': 'sentry@example.com', - 'name': 'Rockstar Developer', - }); - - expect(feedback.eventId.toString(), id.toString()); - expect(feedback.comments, 'this is awesome'); - expect(feedback.email, 'sentry@example.com'); - expect(feedback.name, 'Rockstar Developer'); + final feedback = SentryRuntime.fromJson(feedbackJson); + final json = feedback.toJson(); + + expect( + MapEquality().equals(feedbackJson, json), + true, + ); }); test('copyWith', () { @@ -169,6 +174,7 @@ class SentryUserFeedbackWithoutAssert implements SentryUserFeedback { this.name, this.email, this.comments, + this.unknown, }); @override @@ -183,9 +189,13 @@ class SentryUserFeedbackWithoutAssert implements SentryUserFeedback { @override final String? comments; + @override + Map? unknown; + @override Map toJson() { - return { + return { + ...?unknown, 'event_id': eventId.toString(), if (name != null) 'name': name, if (email != null) 'email': email, @@ -205,6 +215,7 @@ class SentryUserFeedbackWithoutAssert implements SentryUserFeedback { name: name ?? this.name, email: email ?? this.email, comments: comments ?? this.comments, + unknown: unknown, ); } } From 33527b407fa3215a7497a0816c804acd85213992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Tue, 6 Aug 2024 17:04:03 +0200 Subject: [PATCH 4/4] Add proxy support (#2192) --- CHANGELOG.md | 23 +- dart/lib/sentry.dart | 2 + dart/lib/src/http_client/client_provider.dart | 16 ++ .../src/http_client/io_client_provider.dart | 67 +++++ dart/lib/src/protocol/sentry_proxy.dart | 62 ++++ dart/lib/src/sentry_options.dart | 13 + dart/lib/src/transport/http_transport.dart | 5 +- .../transport/spotlight_http_transport.dart | 4 +- .../http_client/io_client_provider_test.dart | 267 ++++++++++++++++++ dart/test/protocol/sentry_proxy_test.dart | 102 +++++++ .../kotlin/io/sentry/flutter/SentryFlutter.kt | 27 ++ .../io/sentry/flutter/SentryFlutterTest.kt | 20 +- .../ios/RunnerTests/SentryFlutterTests.swift | 48 +++- flutter/ios/Classes/SentryFlutter.swift | 35 +++ .../lib/src/native/sentry_native_channel.dart | 1 + .../integrations/init_native_sdk_test.dart | 16 +- 16 files changed, 699 insertions(+), 9 deletions(-) create mode 100644 dart/lib/src/http_client/client_provider.dart create mode 100644 dart/lib/src/http_client/io_client_provider.dart create mode 100644 dart/lib/src/protocol/sentry_proxy.dart create mode 100644 dart/test/http_client/io_client_provider_test.dart create mode 100644 dart/test/protocol/sentry_proxy_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a93cdd016..1137531955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,27 @@ appRunner: () => runApp(MyApp()), ); ``` +- Add proxy support ([#2192](https://github.com/getsentry/sentry-dart/pull/2192)) + - Configure a `SentryProxy` object and set it on `SentryFlutter.init` + ```dart + import 'package:flutter/widgets.dart'; + import 'package:sentry_flutter/sentry_flutter.dart'; + + Future main() async { + await SentryFlutter.init( + (options) { + options.dsn = 'https://example@sentry.io/add-your-dsn-here'; + options.proxy = SentryProxy( + type: SenryProxyType.http, + host: 'localhost', + port: 8080, + ); + }, + // Init your App. + appRunner: () => runApp(MyApp()), + ); + } + ``` ### Improvements @@ -72,7 +93,7 @@ SentryFlutter.init((options) => - This allows viewing the correct dart formatted raw stacktrace in the Sentry UI - Support `ignoredExceptionsForType` ([#2150](https://github.com/getsentry/sentry-dart/pull/2150)) - Filter out exception types by calling `SentryOptions.addExceptionFilterForType(Type exceptionType)` - + ### Fixes - Disable sff & frame delay detection on web, linux and windows ([#2182](https://github.com/getsentry/sentry-dart/pull/2182)) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index a43fc08fc6..e9cae9d666 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -56,3 +56,5 @@ export 'src/sentry_span_operations.dart'; export 'src/utils.dart'; // spotlight debugging export 'src/spotlight.dart'; +// proxy +export 'src/protocol/sentry_proxy.dart'; diff --git a/dart/lib/src/http_client/client_provider.dart b/dart/lib/src/http_client/client_provider.dart new file mode 100644 index 0000000000..201a559601 --- /dev/null +++ b/dart/lib/src/http_client/client_provider.dart @@ -0,0 +1,16 @@ +import 'package:meta/meta.dart'; +import 'package:http/http.dart'; + +import '../sentry_options.dart'; + +@internal +ClientProvider getClientProvider() { + return ClientProvider(); +} + +@internal +class ClientProvider { + Client getClient(SentryOptions options) { + return Client(); + } +} diff --git a/dart/lib/src/http_client/io_client_provider.dart b/dart/lib/src/http_client/io_client_provider.dart new file mode 100644 index 0000000000..b8ec366d06 --- /dev/null +++ b/dart/lib/src/http_client/io_client_provider.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:http/io_client.dart'; +import 'package:meta/meta.dart'; + +import '../protocol.dart'; +import '../protocol/sentry_proxy.dart'; +import '../sentry_options.dart'; +import 'client_provider.dart'; + +@internal +ClientProvider getClientProvider() { + return IoClientProvider( + () { + return HttpClient(); + }, + (user, pass) { + return HttpClientBasicCredentials(user, pass); + }, + ); +} + +@internal +class IoClientProvider implements ClientProvider { + final HttpClient Function() _httpClient; + final HttpClientCredentials Function(String, String) _httpClientCredentials; + + IoClientProvider(this._httpClient, this._httpClientCredentials); + + @override + Client getClient(SentryOptions options) { + final proxy = options.proxy; + if (proxy == null) { + return Client(); + } + final pac = proxy.toPacString(); + if (proxy.type == SentryProxyType.socks) { + options.logger( + SentryLevel.warning, + "Setting proxy '$pac' is not supported.", + ); + return Client(); + } + options.logger( + SentryLevel.info, + "Setting proxy '$pac'", + ); + final httpClient = _httpClient(); + httpClient.findProxy = (url) => pac; + + final host = proxy.host; + final port = proxy.port; + final user = proxy.user; + final pass = proxy.pass; + + if (host != null && port != null && user != null && pass != null) { + httpClient.addProxyCredentials( + host, + port, + '', + _httpClientCredentials(user, pass), + ); + } + return IOClient(httpClient); + } +} diff --git a/dart/lib/src/protocol/sentry_proxy.dart b/dart/lib/src/protocol/sentry_proxy.dart new file mode 100644 index 0000000000..7dcaefef22 --- /dev/null +++ b/dart/lib/src/protocol/sentry_proxy.dart @@ -0,0 +1,62 @@ +class SentryProxy { + final SentryProxyType type; + final String? host; + final int? port; + final String? user; + final String? pass; + + SentryProxy({required this.type, this.host, this.port, this.user, this.pass}); + + String toPacString() { + String type = 'DIRECT'; + switch (this.type) { + case SentryProxyType.direct: + return 'DIRECT'; + case SentryProxyType.http: + type = 'PROXY'; + break; + case SentryProxyType.socks: + type = 'SOCKS'; + break; + } + if (host != null && port != null) { + return '$type $host:$port'; + } else if (host != null) { + return '$type $host'; + } else { + return 'DIRECT'; + } + } + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + return { + if (host != null) 'host': host, + if (port != null) 'port': port, + 'type': type.toString().split('.').last.toUpperCase(), + if (user != null) 'user': user, + if (pass != null) 'pass': pass, + }; + } + + SentryProxy copyWith({ + String? host, + int? port, + SentryProxyType? type, + String? user, + String? pass, + }) => + SentryProxy( + host: host ?? this.host, + port: port ?? this.port, + type: type ?? this.type, + user: user ?? this.user, + pass: pass ?? this.pass, + ); +} + +enum SentryProxyType { + direct, + http, + socks; +} diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 26cda852c4..2b1771a2b5 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -496,6 +496,19 @@ class SentryOptions { /// ``` Spotlight spotlight = Spotlight(enabled: false); + /// Configure a proxy to use for SDK API calls. + /// + /// On io platforms without native SDKs (dart, linux, windows), this will use + /// an 'IOClient' with inner 'HTTPClient' for http communication. + /// A http proxy will be set in returned for 'HttpClient.findProxy' in the + /// form 'PROXY :'. + /// When setting 'user' and 'pass', the 'HttpClient.addProxyCredentials' + /// method will be called with empty 'realm'. + /// + /// On Android & iOS, the proxy settings are handled by the native SDK. + /// iOS only supports http proxies, while macOS also supports socks. + SentryProxy? proxy; + SentryOptions({this.dsn, PlatformChecker? checker}) { if (checker != null) { platformChecker = checker; diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 90dd8949ce..68950948a2 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -11,6 +11,8 @@ import '../sentry_options.dart'; import '../sentry_envelope.dart'; import 'transport.dart'; import 'rate_limiter.dart'; +import '../http_client/client_provider.dart' + if (dart.library.io) '../http_client/io_client_provider.dart'; /// A transport is in charge of sending the event to the Sentry server. class HttpTransport implements Transport { @@ -22,9 +24,8 @@ class HttpTransport implements Transport { factory HttpTransport(SentryOptions options, RateLimiter rateLimiter) { if (options.httpClient is NoOpClient) { - options.httpClient = Client(); + options.httpClient = getClientProvider().getClient(options); } - return HttpTransport._(options, rateLimiter); } diff --git a/dart/lib/src/transport/spotlight_http_transport.dart b/dart/lib/src/transport/spotlight_http_transport.dart index f51e77d478..7567039d82 100644 --- a/dart/lib/src/transport/spotlight_http_transport.dart +++ b/dart/lib/src/transport/spotlight_http_transport.dart @@ -4,6 +4,8 @@ import 'http_transport_request_handler.dart'; import '../../sentry.dart'; import '../noop_client.dart'; +import '../http_client/client_provider.dart' + if (dart.library.io) '../http_client/io_client_provider.dart'; /// Spotlight HTTP transport decorator that sends Sentry envelopes to both Sentry and Spotlight. class SpotlightHttpTransport extends Transport { @@ -13,7 +15,7 @@ class SpotlightHttpTransport extends Transport { factory SpotlightHttpTransport(SentryOptions options, Transport transport) { if (options.httpClient is NoOpClient) { - options.httpClient = Client(); + options.httpClient = getClientProvider().getClient(options); } return SpotlightHttpTransport._(options, transport); } diff --git a/dart/test/http_client/io_client_provider_test.dart b/dart/test/http_client/io_client_provider_test.dart new file mode 100644 index 0000000000..0672e84c08 --- /dev/null +++ b/dart/test/http_client/io_client_provider_test.dart @@ -0,0 +1,267 @@ +@TestOn('vm') +library dart_test; + +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/http_client/io_client_provider.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; + +void main() { + group('getClient', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('http proxy should call findProxyResult', () async { + fixture.options.proxy = SentryProxy( + type: SentryProxyType.http, + host: 'localhost', + port: 8080, + ); + + final sut = fixture.getSut(); + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.findProxyResult, + equals(fixture.options.proxy?.toPacString())); + }); + + test('direct proxy should call findProxyResult', () async { + fixture.options.proxy = SentryProxy(type: SentryProxyType.direct); + + final sut = fixture.getSut(); + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.findProxyResult, + equals(fixture.options.proxy?.toPacString())); + }); + + test('socks proxy should not call findProxyResult', () async { + fixture.options.proxy = SentryProxy( + type: SentryProxyType.socks, host: 'localhost', port: 8080); + + final sut = fixture.getSut(); + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.findProxyResult, isNull); + }); + + test('authenticated proxy http should call addProxyCredentials', () async { + fixture.options.proxy = SentryProxy( + type: SentryProxyType.http, + host: 'localhost', + port: 8080, + user: 'admin', + pass: '0000', + ); + + final sut = fixture.getSut(); + + sut.getClient(fixture.options); + + expect(fixture.mockHttpClient.addProxyCredentialsHost, + fixture.options.proxy?.host); + expect(fixture.mockHttpClient.addProxyCredentialsPort, + fixture.options.proxy?.port); + expect(fixture.mockHttpClient.addProxyCredentialsRealm, ''); + expect(fixture.mockUser, fixture.options.proxy?.user); + expect(fixture.mockPass, fixture.options.proxy?.pass); + expect(fixture.mockHttpClient.addProxyCredentialsBasic, isNotNull); + }); + }); +} + +class Fixture { + final options = SentryOptions(dsn: fakeDsn); + final mockHttpClient = MockHttpClient(); + + String? mockUser; + String? mockPass; + + IoClientProvider getSut() { + return IoClientProvider( + () { + return mockHttpClient; + }, + (user, pass) { + mockUser = user; + mockPass = pass; + return HttpClientBasicCredentials(user, pass); + }, + ); + } +} + +class MockHttpClient implements HttpClient { + @override + bool autoUncompress = false; + + @override + Duration? connectionTimeout; + + @override + Duration idleTimeout = Duration(seconds: 1); + + @override + int? maxConnectionsPerHost; + + @override + String? userAgent; + + @override + void addCredentials( + Uri url, String realm, HttpClientCredentials credentials) { + // TODO: implement addCredentials + } + + String? addProxyCredentialsHost; + int? addProxyCredentialsPort; + String? addProxyCredentialsRealm; + HttpClientBasicCredentials? addProxyCredentialsBasic; + + @override + void addProxyCredentials( + String host, int port, String realm, HttpClientCredentials credentials) { + addProxyCredentialsHost = host; + addProxyCredentialsPort = port; + addProxyCredentialsRealm = realm; + if (credentials is HttpClientBasicCredentials) { + addProxyCredentialsBasic = credentials; + } + } + + @override + set authenticate( + Future Function(Uri url, String scheme, String? realm)? f) { + // TODO: implement authenticate + } + + @override + set authenticateProxy( + Future Function( + String host, int port, String scheme, String? realm)? + f) { + // TODO: implement authenticateProxy + } + + @override + set badCertificateCallback( + bool Function(X509Certificate cert, String host, int port)? callback) { + // TODO: implement badCertificateCallback + } + + @override + void close({bool force = false}) { + // TODO: implement close + } + + @override + set connectionFactory( + Future> Function( + Uri url, String? proxyHost, int? proxyPort)? + f) { + // TODO: implement connectionFactory + } + + @override + Future delete(String host, int port, String path) { + // TODO: implement delete + throw UnimplementedError(); + } + + @override + Future deleteUrl(Uri url) { + // TODO: implement deleteUrl + throw UnimplementedError(); + } + + String? findProxyResult; + + @override + set findProxy(String Function(Uri url)? f) { + findProxyResult = f!(Uri(scheme: "http", host: "localhost", port: 8080)); + } + + @override + Future get(String host, int port, String path) { + // TODO: implement get + throw UnimplementedError(); + } + + @override + Future getUrl(Uri url) { + // TODO: implement getUrl + throw UnimplementedError(); + } + + @override + Future head(String host, int port, String path) { + // TODO: implement head + throw UnimplementedError(); + } + + @override + Future headUrl(Uri url) { + // TODO: implement headUrl + throw UnimplementedError(); + } + + @override + set keyLog(Function(String line)? callback) { + // TODO: implement keyLog + } + + @override + Future open( + String method, String host, int port, String path) { + // TODO: implement open + throw UnimplementedError(); + } + + @override + Future openUrl(String method, Uri url) { + // TODO: implement openUrl + throw UnimplementedError(); + } + + @override + Future patch(String host, int port, String path) { + // TODO: implement patch + throw UnimplementedError(); + } + + @override + Future patchUrl(Uri url) { + // TODO: implement patchUrl + throw UnimplementedError(); + } + + @override + Future post(String host, int port, String path) { + // TODO: implement post + throw UnimplementedError(); + } + + @override + Future postUrl(Uri url) { + // TODO: implement postUrl + throw UnimplementedError(); + } + + @override + Future put(String host, int port, String path) { + // TODO: implement put + throw UnimplementedError(); + } + + @override + Future putUrl(Uri url) { + // TODO: implement putUrl + throw UnimplementedError(); + } +} diff --git a/dart/test/protocol/sentry_proxy_test.dart b/dart/test/protocol/sentry_proxy_test.dart new file mode 100644 index 0000000000..795dc1793d --- /dev/null +++ b/dart/test/protocol/sentry_proxy_test.dart @@ -0,0 +1,102 @@ +import 'package:collection/collection.dart'; +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + final proxy = SentryProxy( + host: 'localhost', + port: 8080, + type: SentryProxyType.http, + user: 'admin', + pass: '0000', + ); + + final proxyJson = { + 'host': 'localhost', + 'port': 8080, + 'type': 'HTTP', + 'user': 'admin', + 'pass': '0000', + }; + + group('toPacString', () { + test('returns "DIRECT" for ProxyType.direct', () { + SentryProxy proxy = SentryProxy(type: SentryProxyType.direct); + expect(proxy.toPacString(), equals('DIRECT')); + }); + + test('returns "PROXY host:port" for ProxyType.http with host and port', () { + SentryProxy proxy = SentryProxy( + type: SentryProxyType.http, host: 'localhost', port: 8080); + expect(proxy.toPacString(), equals('PROXY localhost:8080')); + }); + + test('returns "PROXY host" for ProxyType.http with host only', () { + SentryProxy proxy = + SentryProxy(type: SentryProxyType.http, host: 'localhost'); + expect(proxy.toPacString(), equals('PROXY localhost')); + }); + + test('returns "SOCKS host:port" for ProxyType.socks with host and port', + () { + SentryProxy proxy = SentryProxy( + type: SentryProxyType.socks, host: 'localhost', port: 8080); + expect(proxy.toPacString(), equals('SOCKS localhost:8080')); + }); + + test('returns "SOCKS host" for ProxyType.socks with host only', () { + SentryProxy proxy = + SentryProxy(type: SentryProxyType.socks, host: 'localhost'); + expect(proxy.toPacString(), equals('SOCKS localhost')); + }); + + test('falls back to "DIRECT" if http is missing host', () { + SentryProxy proxy = SentryProxy(type: SentryProxyType.http); + expect(proxy.toPacString(), equals('DIRECT')); + }); + + test('falls back to "DIRECT" if socks is missing host', () { + SentryProxy proxy = SentryProxy(type: SentryProxyType.socks); + expect(proxy.toPacString(), equals('DIRECT')); + }); + }); + + group('json', () { + test('toJson', () { + final json = proxy.toJson(); + + expect( + DeepCollectionEquality().equals(proxyJson, json), + true, + ); + }); + }); + + group('copyWith', () { + test('copyWith keeps unchanged', () { + final data = proxy; + + final copy = data.copyWith(); + + expect(data.toJson(), copy.toJson()); + }); + + test('copyWith takes new values', () { + final data = proxy; + + final copy = data.copyWith( + host: 'localhost-2', + port: 9001, + type: SentryProxyType.socks, + user: 'user', + pass: '1234', + ); + + expect('localhost-2', copy.host); + expect(9001, copy.port); + expect(SentryProxyType.socks, copy.type); + expect('user', copy.user); + expect('1234', copy.pass); + }); + }); +} diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index c06a8b0dc2..123778e9be 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -1,9 +1,12 @@ package io.sentry.flutter +import android.util.Log import io.sentry.SentryLevel +import io.sentry.SentryOptions.Proxy import io.sentry.android.core.BuildConfig import io.sentry.android.core.SentryAndroidOptions import io.sentry.protocol.SdkVersion +import java.net.Proxy.Type import java.util.Locale class SentryFlutter( @@ -119,6 +122,30 @@ class SentryFlutter( data.getIfNotNull("readTimeoutMillis") { options.readTimeoutMillis = it } + data.getIfNotNull>("proxy") { proxyJson -> + options.proxy = + Proxy() + .apply { + host = proxyJson["host"] as? String + port = + (proxyJson["port"] as? Int) + ?.let { + "$it" + } + (proxyJson["type"] as? String) + ?.let { + type = + try { + Type.valueOf(it.toUpperCase(Locale.ROOT)) + } catch (_: IllegalArgumentException) { + Log.w("Sentry", "Could not parse `type` from proxy json: $proxyJson") + null + } + } + user = proxyJson["user"] as? String + pass = proxyJson["pass"] as? String + } + } } } diff --git a/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt b/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt index 724559bb76..9fa9183f33 100644 --- a/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt +++ b/flutter/android/src/test/kotlin/io/sentry/flutter/SentryFlutterTest.kt @@ -6,6 +6,7 @@ import io.sentry.android.core.SentryAndroidOptions import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import java.net.Proxy class SentryFlutterTest { private lateinit var fixture: Fixture @@ -60,6 +61,12 @@ class SentryFlutterTest { assertEquals(9006, fixture.options.connectionTimeoutMillis) assertEquals(9007, fixture.options.readTimeoutMillis) + + assertEquals("localhost", fixture.options.proxy?.host) + assertEquals("8080", fixture.options.proxy?.port) + assertEquals(Proxy.Type.HTTP, fixture.options.proxy?.type) + assertEquals("admin", fixture.options.proxy?.user) + assertEquals("0000", fixture.options.proxy?.pass) } @Test @@ -127,12 +134,19 @@ class Fixture { "enableAutoPerformanceTracing" to true, "connectionTimeoutMillis" to 9006, "readTimeoutMillis" to 9007, + "proxy" to + mapOf( + "host" to "localhost", + "port" to 8080, + "type" to "http", // lowercase to check enum mapping + "user" to "admin", + "pass" to "0000", + ), ) - fun getSut(): SentryFlutter { - return SentryFlutter( + fun getSut(): SentryFlutter = + SentryFlutter( androidSdk = "sentry.java.android.flutter", nativeSdk = "fixture-nativeSdk", ) - } } diff --git a/flutter/example/ios/RunnerTests/SentryFlutterTests.swift b/flutter/example/ios/RunnerTests/SentryFlutterTests.swift index 057b2363b5..4873388f2f 100644 --- a/flutter/example/ios/RunnerTests/SentryFlutterTests.swift +++ b/flutter/example/ios/RunnerTests/SentryFlutterTests.swift @@ -9,6 +9,8 @@ import XCTest import sentry_flutter import Sentry +// swiftlint:disable function_body_length line_length + final class SentryFlutterTests: XCTestCase { private var fixture: Fixture! @@ -43,7 +45,14 @@ final class SentryFlutterTests: XCTestCase { "maxAttachmentSize": NSNumber(value: 9004), "captureFailedRequests": false, "enableAppHangTracking": false, - "appHangTimeoutIntervalMillis": NSNumber(value: 10000) + "appHangTimeoutIntervalMillis": NSNumber(value: 10000), + "proxy": [ + "host": "localhost", + "port": NSNumber(value: 8080), + "type": "hTtP", // mixed case to check enum mapping + "user": "admin", + "pass": "0000" + ] ] ) @@ -68,6 +77,41 @@ final class SentryFlutterTests: XCTestCase { XCTAssertEqual(false, fixture.options.enableCaptureFailedRequests) XCTAssertEqual(false, fixture.options.enableAppHangTracking) XCTAssertEqual(10, fixture.options.appHangTimeoutInterval) + + XCTAssertNotNil(fixture.options.urlSession) + XCTAssertEqual(true, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] as? Bool) + XCTAssertEqual("localhost", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] as? String) + XCTAssertEqual(8080, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] as? Int) + XCTAssertEqual("admin", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyUsernameKey as String] as? String) + XCTAssertEqual("0000", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyPasswordKey as String] as? String) + } + + func testUpdateSocksProxy() { + let sut = fixture.getSut() + + sut.update( + options: fixture.options, + with: [ + "proxy": [ + "host": "localhost", + "port": 8080, + "type": "sOcKs", // mixed case to check enum mapping + "user": "admin", + "pass": "0000" + ] + ] + ) + + #if os(macOS) + XCTAssertNotNil(fixture.options.urlSession) + XCTAssertEqual(true, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesSOCKSEnable as String] as? Bool) + XCTAssertEqual("localhost", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesSOCKSProxy as String] as? String) + XCTAssertEqual(8080, fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFNetworkProxiesSOCKSPort as String] as? Int) + XCTAssertEqual("admin", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyUsernameKey as String] as? String) + XCTAssertEqual("0000", fixture.options.urlSession?.configuration.connectionProxyDictionary?[kCFProxyPasswordKey as String] as? String) + #else + XCTAssertNil(fixture.options.urlSession) + #endif } } @@ -81,3 +125,5 @@ extension SentryFlutterTests { } } } + +// swiftlint:enable function_body_length line_length diff --git a/flutter/ios/Classes/SentryFlutter.swift b/flutter/ios/Classes/SentryFlutter.swift index b26bcfc30d..769f595fba 100644 --- a/flutter/ios/Classes/SentryFlutter.swift +++ b/flutter/ios/Classes/SentryFlutter.swift @@ -70,6 +70,41 @@ public final class SentryFlutter { if let appHangTimeoutIntervalMillis = data["appHangTimeoutIntervalMillis"] as? NSNumber { options.appHangTimeoutInterval = appHangTimeoutIntervalMillis.doubleValue / 1000 } + if let proxy = data["proxy"] as? [String: Any] { + guard let host = proxy["host"] as? String, + let port = proxy["port"] as? Int, + let type = proxy["type"] as? String else { + print("Could not read proxy data") + return + } + + var connectionProxyDictionary: [String: Any] = [:] + if type.lowercased() == "http" { + connectionProxyDictionary[kCFNetworkProxiesHTTPEnable as String] = true + connectionProxyDictionary[kCFNetworkProxiesHTTPProxy as String] = host + connectionProxyDictionary[kCFNetworkProxiesHTTPPort as String] = port + } else if type.lowercased() == "socks" { + #if os(macOS) + connectionProxyDictionary[kCFNetworkProxiesSOCKSEnable as String] = true + connectionProxyDictionary[kCFNetworkProxiesSOCKSProxy as String] = host + connectionProxyDictionary[kCFNetworkProxiesSOCKSPort as String] = port + #else + return + #endif + } else { + return + } + + if let user = proxy["user"] as? String, let pass = proxy["pass"] { + connectionProxyDictionary[kCFProxyUsernameKey as String] = user + connectionProxyDictionary[kCFProxyPasswordKey as String] = pass + } + + let configuration = URLSessionConfiguration.default + configuration.connectionProxyDictionary = connectionProxyDictionary + + options.urlSession = URLSession(configuration: configuration) + } } private func logLevelFrom(diagnosticLevel: String) -> SentryLevel { diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 0a3b97820d..623111bb9e 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -66,6 +66,7 @@ class SentryNativeChannel 'readTimeoutMillis': options.readTimeout.inMilliseconds, 'appHangTimeoutIntervalMillis': options.appHangTimeoutInterval.inMilliseconds, + if (options.proxy != null) 'proxy': options.proxy?.toJson(), }); } diff --git a/flutter/test/integrations/init_native_sdk_test.dart b/flutter/test/integrations/init_native_sdk_test.dart index 0dcf3af502..d3faa93346 100644 --- a/flutter/test/integrations/init_native_sdk_test.dart +++ b/flutter/test/integrations/init_native_sdk_test.dart @@ -104,7 +104,14 @@ void main() { ..enableAppHangTracking = false ..connectionTimeout = Duration(milliseconds: 9001) ..readTimeout = Duration(milliseconds: 9002) - ..appHangTimeoutInterval = Duration(milliseconds: 9003); + ..appHangTimeoutInterval = Duration(milliseconds: 9003) + ..proxy = SentryProxy( + host: "localhost", + port: 8080, + type: SentryProxyType.http, + user: 'admin', + pass: '0000', + ); fixture.options.sdk.addIntegration('foo'); fixture.options.sdk.addPackage('bar', '1'); @@ -149,6 +156,13 @@ void main() { 'connectionTimeoutMillis': 9001, 'readTimeoutMillis': 9002, 'appHangTimeoutIntervalMillis': 9003, + 'proxy': { + 'host': 'localhost', + 'port': 8080, + 'type': 'HTTP', + 'user': 'admin', + 'pass': '0000', + } }); }); }