diff --git a/CHANGELOG.md b/CHANGELOG.md index c67bef25c..3634834d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Bring protocol up to date with latest Sentry protocol ([#1038](https://github.com/getsentry/sentry-dart/pull/1038)) + ## 6.12.2 ### Fixes diff --git a/dart/example_web/web/event.dart b/dart/example_web/web/event.dart index 1ba3ea04f..81550d938 100644 --- a/dart/example_web/web/event.dart +++ b/dart/example_web/web/event.dart @@ -14,7 +14,7 @@ final event = SentryEvent( username: 'first-user', email: 'first@user.lan', // ipAddress: '127.0.0.1', - extras: {'first-sign-in': '2020-01-01'}, + data: {'first-sign-in': '2020-01-01'}, ), breadcrumbs: [ Breadcrumb( diff --git a/dart/example_web/web/main.dart b/dart/example_web/web/main.dart index f405fc5a6..6e7d9ccb7 100644 --- a/dart/example_web/web/main.dart +++ b/dart/example_web/web/main.dart @@ -52,7 +52,7 @@ Future runApp() async { username: 'first-user', email: 'first@user.lan', // ipAddress: '127.0.0.1', - extras: {'first-sign-in': '2020-01-01'}, + data: {'first-sign-in': '2020-01-01'}, ), ); }); diff --git a/dart/lib/src/http_client/failed_request_client.dart b/dart/lib/src/http_client/failed_request_client.dart index bbffa2772..f3e8e4bc5 100644 --- a/dart/lib/src/http_client/failed_request_client.dart +++ b/dart/lib/src/http_client/failed_request_client.dart @@ -166,6 +166,7 @@ class FailedRequestClient extends BaseClient { queryString: query, cookies: sendDefaultPii ? request.headers['Cookie'] : null, data: _getDataFromRequest(request), + // ignore: deprecated_member_use_from_same_package other: { 'content_length': request.contentLength.toString(), 'duration': requestDuration.toString(), diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index f193e1968..26a4fc211 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -23,6 +23,7 @@ export 'protocol/sentry_runtime.dart'; export 'protocol/sentry_stack_frame.dart'; export 'protocol/sentry_stack_trace.dart'; export 'protocol/sentry_user.dart'; +export 'protocol/sentry_geo.dart'; export 'protocol/max_body_size.dart'; export 'protocol/sentry_culture.dart'; export 'protocol/sentry_thread.dart'; diff --git a/dart/lib/src/protocol/breadcrumb.dart b/dart/lib/src/protocol/breadcrumb.dart index 1a7234069..500e04f2a 100644 --- a/dart/lib/src/protocol/breadcrumb.dart +++ b/dart/lib/src/protocol/breadcrumb.dart @@ -148,27 +148,14 @@ class Breadcrumb { /// Converts this breadcrumb to a map that can be serialized to JSON according /// to the Sentry protocol. Map toJson() { - final json = { + return { 'timestamp': formatDateAsIso8601WithMillisPrecision(timestamp), + if (message != null) 'message': message, + if (category != null) 'category': category, + if (data?.isNotEmpty ?? false) 'data': data, + if (level != null) 'level': level!.name, + if (type != null) 'type': type, }; - - if (message != null) { - json['message'] = message; - } - if (category != null) { - json['category'] = category; - } - if (data?.isNotEmpty ?? false) { - json['data'] = data; - } - if (level != null) { - json['level'] = level!.name; - } - - if (type != null) { - json['type'] = type; - } - return json; } Breadcrumb copyWith({ diff --git a/dart/lib/src/protocol/debug_image.dart b/dart/lib/src/protocol/debug_image.dart index 9d5a0a441..df45a12fe 100644 --- a/dart/lib/src/protocol/debug_image.dart +++ b/dart/lib/src/protocol/debug_image.dart @@ -45,6 +45,12 @@ class DebugImage { /// Optional. Identifier of the dynamic library or executable. It is the value of the LC_UUID load command in the Mach header, formatted as UUID. Can be empty for Mach images, as it is equivalent to the debug identifier. final String? codeId; + /// MachO CPU subtype identifier. + final int? cpuSubtype; + + /// MachO CPU type identifier. + final int? cpuType; + const DebugImage({ required this.type, this.name, @@ -57,6 +63,8 @@ class DebugImage { this.codeFile, this.arch, this.codeId, + this.cpuType, + this.cpuSubtype, }); /// Deserializes a [DebugImage] from JSON [Map]. @@ -73,56 +81,28 @@ class DebugImage { codeFile: json['code_file'], arch: json['arch'], codeId: json['code_id'], + cpuType: json['cpu_type'], + cpuSubtype: json['cpu_subtype'], ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (uuid != null) { - json['uuid'] = uuid; - } - - json['type'] = type; - - if (debugId != null) { - json['debug_id'] = debugId; - } - - if (name != null) { - json['name'] = name; - } - - if (debugFile != null) { - json['debug_file'] = debugFile; - } - - if (codeFile != null) { - json['code_file'] = codeFile; - } - - if (imageAddr != null) { - json['image_addr'] = imageAddr; - } - - if (imageVmAddr != null) { - json['image_vmaddr'] = imageVmAddr; - } - - if (imageSize != null) { - json['image_size'] = imageSize; - } - - if (arch != null) { - json['arch'] = arch; - } - - if (codeId != null) { - json['code_id'] = codeId; - } - - return json; + return { + 'type': type, + if (uuid != null) 'uuid': uuid, + if (debugId != null) 'debug_id': debugId, + if (name != null) 'name': name, + if (debugFile != null) 'debug_file': debugFile, + if (codeFile != null) 'code_file': codeFile, + if (imageAddr != null) 'image_addr': imageAddr, + if (imageVmAddr != null) 'image_vmaddr': imageVmAddr, + if (imageSize != null) 'image_size': imageSize, + if (arch != null) 'arch': arch, + if (codeId != null) 'code_id': codeId, + if (cpuType != null) 'cpu_type': cpuType, + if (cpuSubtype != null) 'cpu_subtype': cpuSubtype, + }; } DebugImage copyWith({ @@ -137,6 +117,8 @@ class DebugImage { int? imageSize, String? arch, String? codeId, + int? cpuType, + int? cpuSubtype, }) => DebugImage( uuid: uuid ?? this.uuid, @@ -150,5 +132,7 @@ class DebugImage { imageSize: imageSize ?? this.imageSize, arch: arch ?? this.arch, codeId: codeId ?? this.codeId, + cpuType: cpuType ?? this.cpuType, + cpuSubtype: cpuSubtype ?? this.cpuSubtype, ); } diff --git a/dart/lib/src/protocol/debug_meta.dart b/dart/lib/src/protocol/debug_meta.dart index 24d8183d5..205b2cc13 100644 --- a/dart/lib/src/protocol/debug_meta.dart +++ b/dart/lib/src/protocol/debug_meta.dart @@ -33,21 +33,15 @@ class DebugMeta { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - final sdkInfo = sdk?.toJson(); - if (sdkInfo?.isNotEmpty ?? false) { - json['sdk_info'] = sdkInfo; - } - - if (_images?.isNotEmpty ?? false) { - json['images'] = _images! - .map((e) => e.toJson()) - .where((element) => element.isNotEmpty) - .toList(growable: false); - } - - return json; + return { + if (sdkInfo?.isNotEmpty ?? false) 'sdk_info': sdkInfo, + if (_images?.isNotEmpty ?? false) + 'images': _images! + .map((e) => e.toJson()) + .where((element) => element.isNotEmpty) + .toList(growable: false) + }; } DebugMeta copyWith({ diff --git a/dart/lib/src/protocol/mechanism.dart b/dart/lib/src/protocol/mechanism.dart index 2e5d08e1a..2c06c954f 100644 --- a/dart/lib/src/protocol/mechanism.dart +++ b/dart/lib/src/protocol/mechanism.dart @@ -99,34 +99,14 @@ class Mechanism { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - json['type'] = type; - - if (description != null) { - json['description'] = description; - } - - if (helpLink != null) { - json['help_link'] = helpLink; - } - - if (handled != null) { - json['handled'] = handled; - } - - if (_meta?.isNotEmpty ?? false) { - json['meta'] = _meta; - } - - if (_data?.isNotEmpty ?? false) { - json['data'] = _data; - } - - if (synthetic != null) { - json['synthetic'] = synthetic; - } - - return json; + return { + 'type': type, + if (description != null) 'description': description, + if (helpLink != null) 'help_link': helpLink, + if (handled != null) 'handled': handled, + if (_meta?.isNotEmpty ?? false) 'meta': _meta, + if (_data?.isNotEmpty ?? false) 'data': _data, + if (synthetic != null) 'synthetic': synthetic, + }; } } diff --git a/dart/lib/src/protocol/sdk_info.dart b/dart/lib/src/protocol/sdk_info.dart index a3434afd8..cf0b7d0f4 100644 --- a/dart/lib/src/protocol/sdk_info.dart +++ b/dart/lib/src/protocol/sdk_info.dart @@ -27,24 +27,12 @@ class SdkInfo { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - if (sdkName != null) { - json['sdk_name'] = sdkName; - } - - if (versionMajor != null) { - json['version_major'] = versionMajor; - } - - if (versionMinor != null) { - json['version_minor'] = versionMinor; - } - - if (versionPatchlevel != null) { - json['version_patchlevel'] = versionPatchlevel; - } - - return json; + return { + if (sdkName != null) 'sdk_name': sdkName, + if (versionMajor != null) 'version_major': versionMajor, + if (versionMinor != null) 'version_minor': versionMinor, + if (versionPatchlevel != null) 'version_patchlevel': versionPatchlevel, + }; } SdkInfo copyWith({ diff --git a/dart/lib/src/protocol/sdk_version.dart b/dart/lib/src/protocol/sdk_version.dart index b69ca3c13..37c68fe70 100644 --- a/dart/lib/src/protocol/sdk_version.dart +++ b/dart/lib/src/protocol/sdk_version.dart @@ -79,21 +79,13 @@ class SdkVersion { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - json['name'] = name; - - json['version'] = version; - - if (packages.isNotEmpty) { - json['packages'] = - packages.map((p) => p.toJson()).toList(growable: false); - } - - if (integrations.isNotEmpty) { - json['integrations'] = integrations; - } - return json; + return { + 'name': name, + 'version': version, + if (packages.isNotEmpty) + 'packages': packages.map((p) => p.toJson()).toList(growable: false), + if (integrations.isNotEmpty) 'integrations': integrations, + }; } /// Adds a package diff --git a/dart/lib/src/protocol/sentry_app.dart b/dart/lib/src/protocol/sentry_app.dart index 90581e1fb..960b7d0cb 100644 --- a/dart/lib/src/protocol/sentry_app.dart +++ b/dart/lib/src/protocol/sentry_app.dart @@ -16,6 +16,7 @@ class SentryApp { this.buildType, this.startTime, this.deviceAppHash, + this.appMemory, }); /// Human readable application name, as it appears on the platform. @@ -39,6 +40,9 @@ class SentryApp { /// Application specific device identifier. final String? deviceAppHash; + /// Amount of memory used by the application in bytes. + final int? appMemory; + /// Deserializes a [SentryApp] from JSON [Map]. factory SentryApp.fromJson(Map data) => SentryApp( name: data['app_name'], @@ -50,41 +54,21 @@ class SentryApp { ? DateTime.tryParse(data['app_start_time']) : null, deviceAppHash: data['device_app_hash'], + appMemory: data['app_memory'], ); /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (name != null) { - json['app_name'] = name!; - } - - if (version != null) { - json['app_version'] = version!; - } - - if (identifier != null) { - json['app_identifier'] = identifier!; - } - - if (build != null) { - json['app_build'] = build!; - } - - if (buildType != null) { - json['build_type'] = buildType!; - } - - if (startTime != null) { - json['app_start_time'] = startTime!.toIso8601String(); - } - - if (deviceAppHash != null) { - json['device_app_hash'] = deviceAppHash!; - } - - return json; + return { + if (name != null) 'app_name': name!, + if (version != null) 'app_version': version!, + if (identifier != null) 'app_identifier': identifier!, + if (build != null) 'app_build': build!, + if (buildType != null) 'build_type': buildType!, + if (deviceAppHash != null) 'device_app_hash': deviceAppHash!, + if (appMemory != null) 'app_memory': appMemory!, + if (startTime != null) 'app_start_time': startTime!.toIso8601String(), + }; } SentryApp clone() => SentryApp( @@ -95,6 +79,7 @@ class SentryApp { buildType: buildType, startTime: startTime, deviceAppHash: deviceAppHash, + appMemory: appMemory, ); SentryApp copyWith({ @@ -105,6 +90,7 @@ class SentryApp { String? buildType, DateTime? startTime, String? deviceAppHash, + int? appMemory, }) => SentryApp( name: name ?? this.name, @@ -114,5 +100,6 @@ class SentryApp { buildType: buildType ?? this.buildType, startTime: startTime ?? this.startTime, deviceAppHash: deviceAppHash ?? this.deviceAppHash, + appMemory: appMemory ?? this.appMemory, ); } diff --git a/dart/lib/src/protocol/sentry_browser.dart b/dart/lib/src/protocol/sentry_browser.dart index 5c536d581..f2807e109 100644 --- a/dart/lib/src/protocol/sentry_browser.dart +++ b/dart/lib/src/protocol/sentry_browser.dart @@ -25,17 +25,10 @@ class SentryBrowser { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (name != null) { - json['name'] = name; - } - - if (version != null) { - json['version'] = version; - } - - return json; + return { + if (name != null) 'name': name, + if (version != null) 'version': version, + }; } SentryBrowser clone() => SentryBrowser(name: name, version: version); diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index 767c38198..3b3bab792 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -51,33 +51,14 @@ class SentryException { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (type != null) { - json['type'] = type; - } - - if (value != null) { - json['value'] = value; - } - - if (module != null) { - json['module'] = module; - } - - if (stackTrace != null) { - json['stacktrace'] = stackTrace!.toJson(); - } - - if (mechanism != null) { - json['mechanism'] = mechanism!.toJson(); - } - - if (threadId != null) { - json['thread_id'] = threadId; - } - - return json; + return { + if (type != null) 'type': type, + if (value != null) 'value': value, + if (module != null) 'module': module, + if (stackTrace != null) 'stacktrace': stackTrace!.toJson(), + if (mechanism != null) 'mechanism': mechanism!.toJson(), + if (threadId != null) 'thread_id': threadId, + }; } SentryException copyWith({ diff --git a/dart/lib/src/protocol/sentry_geo.dart b/dart/lib/src/protocol/sentry_geo.dart new file mode 100644 index 000000000..e8221c153 --- /dev/null +++ b/dart/lib/src/protocol/sentry_geo.dart @@ -0,0 +1,29 @@ +/// Geographical location of the end user or device. +class SentryGeo { + SentryGeo({this.city, this.countryCode, this.region}); + + factory SentryGeo.fromJson(Map json) { + return SentryGeo( + city: json['city'], + countryCode: json['country_code'], + region: json['region'], + ); + } + + /// Human readable city name. + final String? city; + + /// Two-letter country code (ISO 3166-1 alpha-2). + final String? countryCode; + + /// Human readable region name or code. + final String? region; + + Map toJson() { + return { + if (city != null) 'city': city, + if (countryCode != null) 'country_code': countryCode, + if (region != null) 'region': region, + }; + } +} diff --git a/dart/lib/src/protocol/sentry_gpu.dart b/dart/lib/src/protocol/sentry_gpu.dart index 6696f7b4f..64747eb86 100644 --- a/dart/lib/src/protocol/sentry_gpu.dart +++ b/dart/lib/src/protocol/sentry_gpu.dart @@ -44,6 +44,27 @@ class SentryGpu { /// The Non-Power-Of-Two-Support support. final String? npotSupport; + /// Approximate "shader capability" level of the graphics device. + /// For Example: + /// Shader Model 2.0, OpenGL ES 3.0, Metal / OpenGL ES 3.1, 27 (unknown) + final String? graphicsShaderLevel; + + /// Largest size of a texture that is supported by the graphics hardware. + /// For Example: 16384 + final int? maxTextureSize; + + /// Whether compute shaders are available on the device. + final bool? supportsComputeShaders; + + /// Whether GPU draw call instancing is supported. + final bool? supportsDrawCallInstancing; + + /// Whether geometry shaders are available on the device. + final bool? supportsGeometryShaders; + + /// Whether ray tracing is available on the device. + final bool? supportsRayTracing; + const SentryGpu({ this.name, this.id, @@ -54,6 +75,12 @@ class SentryGpu { this.multiThreadedRendering, this.version, this.npotSupport, + this.graphicsShaderLevel, + this.maxTextureSize, + this.supportsComputeShaders, + this.supportsDrawCallInstancing, + this.supportsGeometryShaders, + this.supportsRayTracing, }); /// Deserializes a [SentryGpu] from JSON [Map]. @@ -67,6 +94,12 @@ class SentryGpu { 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'], ); SentryGpu clone() => SentryGpu( @@ -79,49 +112,39 @@ class SentryGpu { multiThreadedRendering: multiThreadedRendering, version: version, npotSupport: npotSupport, + graphicsShaderLevel: graphicsShaderLevel, + maxTextureSize: maxTextureSize, + supportsComputeShaders: supportsComputeShaders, + supportsDrawCallInstancing: supportsDrawCallInstancing, + supportsGeometryShaders: supportsGeometryShaders, + supportsRayTracing: supportsRayTracing, ); /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (name != null) { - json['name'] = name; - } - - if (id != null) { - json['id'] = id; - } - - if (vendorId != null) { - json['vendor_id'] = vendorId; - } - - if (vendorName != null) { - json['vendor_name'] = vendorName; - } - - if (memorySize != null) { - json['memory_size'] = memorySize; - } - - if (apiType != null) { - json['api_type'] = apiType; - } - - if (multiThreadedRendering != null) { - json['multi_threaded_rendering'] = multiThreadedRendering; - } - - if (version != null) { - json['version'] = version; - } - - if (npotSupport != null) { - json['npot_support'] = npotSupport; - } - - return json; + return { + if (name != null) 'name': name, + if (id != null) 'id': id, + if (vendorId != null) 'vendor_id': vendorId, + if (vendorName != null) 'vendor_name': vendorName, + if (memorySize != null) 'memory_size': memorySize, + if (apiType != null) 'api_type': apiType, + if (multiThreadedRendering != null) + 'multi_threaded_rendering': multiThreadedRendering, + if (version != null) 'version': version, + if (npotSupport != null) 'npot_support': npotSupport, + if (graphicsShaderLevel != null) + 'graphics_shader_level': graphicsShaderLevel, + if (maxTextureSize != null) 'max_texture_size': maxTextureSize, + if (supportsComputeShaders != null) + 'supports_compute_shaders': supportsComputeShaders, + if (supportsDrawCallInstancing != null) + 'supports_draw_call_instancing': supportsDrawCallInstancing, + if (supportsGeometryShaders != null) + 'supports_geometry_shaders': supportsGeometryShaders, + if (supportsRayTracing != null) + 'supports_ray_tracing': supportsRayTracing, + }; } SentryGpu copyWith({ @@ -134,6 +157,12 @@ class SentryGpu { bool? multiThreadedRendering, String? version, String? npotSupport, + String? graphicsShaderLevel, + int? maxTextureSize, + bool? supportsComputeShaders, + bool? supportsDrawCallInstancing, + bool? supportsGeometryShaders, + bool? supportsRayTracing, }) => SentryGpu( name: name ?? this.name, @@ -146,5 +175,14 @@ class SentryGpu { multiThreadedRendering ?? this.multiThreadedRendering, version: version ?? this.version, npotSupport: npotSupport ?? this.npotSupport, + graphicsShaderLevel: graphicsShaderLevel ?? this.graphicsShaderLevel, + maxTextureSize: maxTextureSize ?? this.maxTextureSize, + supportsComputeShaders: + supportsComputeShaders ?? this.supportsComputeShaders, + supportsDrawCallInstancing: + supportsDrawCallInstancing ?? this.supportsDrawCallInstancing, + supportsGeometryShaders: + supportsGeometryShaders ?? this.supportsGeometryShaders, + supportsRayTracing: supportsRayTracing ?? this.supportsRayTracing, ); } diff --git a/dart/lib/src/protocol/sentry_message.dart b/dart/lib/src/protocol/sentry_message.dart index e234de558..9d86c279c 100644 --- a/dart/lib/src/protocol/sentry_message.dart +++ b/dart/lib/src/protocol/sentry_message.dart @@ -33,19 +33,11 @@ class SentryMessage { /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - json['formatted'] = formatted; - - if (template != null) { - json['message'] = template; - } - - if (params?.isNotEmpty ?? false) { - json['params'] = params; - } - - return json; + return { + 'formatted': formatted, + if (template != null) 'message': template, + if (params?.isNotEmpty ?? false) 'params': params, + }; } SentryMessage copyWith({ diff --git a/dart/lib/src/protocol/sentry_request.dart b/dart/lib/src/protocol/sentry_request.dart index 7c326df72..c657b0326 100644 --- a/dart/lib/src/protocol/sentry_request.dart +++ b/dart/lib/src/protocol/sentry_request.dart @@ -53,17 +53,22 @@ class SentryRequest { final Map? _other; + @Deprecated('Will be removed in v7') Map get other => Map.unmodifiable(_other ?? const {}); + /// The fragment of the request URL. + final String? fragment; + SentryRequest({ this.url, this.method, this.queryString, this.cookies, + this.fragment, dynamic data, Map? headers, Map? env, - Map? other, + @Deprecated('Will be removed in v7.') Map? other, }) : _data = data, _headers = headers != null ? Map.from(headers) : null, _env = env != null ? Map.from(env) : null, @@ -79,47 +84,26 @@ class SentryRequest { data: json['data'], headers: json['headers'], env: json['env'], + // ignore: deprecated_member_use_from_same_package other: json['other'], + fragment: json['fragment'], ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (url != null) { - json['url'] = url; - } - - if (method != null) { - json['method'] = method; - } - - if (queryString != null) { - json['query_string'] = queryString; - } - - if (_data != null) { - json['data'] = _data; - } - - if (cookies != null) { - json['cookies'] = cookies; - } - - if (headers.isNotEmpty) { - json['headers'] = headers; - } - - if (env.isNotEmpty) { - json['env'] = env; - } - - if (other.isNotEmpty) { - json['other'] = other; - } - - return json; + return { + if (url != null) 'url': url, + if (method != null) 'method': method, + if (queryString != null) 'query_string': queryString, + if (_data != null) 'data': _data, + if (cookies != null) 'cookies': cookies, + if (headers.isNotEmpty) 'headers': headers, + if (env.isNotEmpty) 'env': env, + // ignore: deprecated_member_use_from_same_package + if (other.isNotEmpty) 'other': other, + if (fragment != null) 'fragment': fragment, + }; } SentryRequest copyWith({ @@ -127,10 +111,11 @@ class SentryRequest { String? method, String? queryString, String? cookies, + String? fragment, dynamic data, Map? headers, Map? env, - Map? other, + @Deprecated('Will be removed in v7.') Map? other, }) => SentryRequest( url: url ?? this.url, @@ -140,6 +125,8 @@ class SentryRequest { data: data ?? _data, headers: headers ?? _headers, env: env ?? _env, + // ignore: deprecated_member_use_from_same_package other: other ?? _other, + fragment: fragment, ); } diff --git a/dart/lib/src/protocol/sentry_runtime.dart b/dart/lib/src/protocol/sentry_runtime.dart index 245845d07..960f48c17 100644 --- a/dart/lib/src/protocol/sentry_runtime.dart +++ b/dart/lib/src/protocol/sentry_runtime.dart @@ -16,6 +16,7 @@ class SentryRuntime { this.version, this.compiler, this.rawDescription, + this.build, }) : assert(key == null || key.length >= 1); /// Key used in the JSON and which will be displayed @@ -40,12 +41,16 @@ class SentryRuntime { /// and version from this string, if they are not explicitly given. final String? rawDescription; + /// Application build string, if it is separate from the version. + final String? build; + /// 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'], ); /// Produces a [Map] that can be serialized to JSON. @@ -55,6 +60,7 @@ class SentryRuntime { if (compiler != null) 'compiler': compiler, if (version != null) 'version': version, if (rawDescription != null) 'raw_description': rawDescription, + if (build != null) 'build': build, }; } @@ -64,6 +70,7 @@ class SentryRuntime { version: version, compiler: compiler, rawDescription: rawDescription, + build: build, ); SentryRuntime copyWith({ @@ -72,6 +79,7 @@ class SentryRuntime { String? version, String? compiler, String? rawDescription, + String? build, }) => SentryRuntime( key: key ?? this.key, @@ -79,5 +87,6 @@ class SentryRuntime { version: version ?? this.version, compiler: compiler ?? this.compiler, rawDescription: rawDescription ?? this.rawDescription, + build: build ?? this.build, ); } diff --git a/dart/lib/src/protocol/sentry_stack_frame.dart b/dart/lib/src/protocol/sentry_stack_frame.dart index 27085f852..a2bfb47c4 100644 --- a/dart/lib/src/protocol/sentry_stack_frame.dart +++ b/dart/lib/src/protocol/sentry_stack_frame.dart @@ -23,10 +23,12 @@ class SentryStackFrame { this.symbolAddr, this.instructionAddr, this.rawFunction, + this.stackStart, + this.symbol, List? framesOmitted, List? preContext, List? postContext, - Map? vars, + Map? vars, }) : _framesOmitted = framesOmitted != null ? List.from(framesOmitted) : null, _preContext = preContext != null ? List.from(preContext) : null, @@ -46,10 +48,10 @@ class SentryStackFrame { /// An immutable list of source code lines after context_line (in order) – usually [lineno + 1:lineno + 5]. List get postContext => List.unmodifiable(_postContext ?? const []); - final Map? _vars; + final Map? _vars; /// An immutable mapping of variables which were available within this frame (usually context-locals). - Map get vars => Map.unmodifiable(_vars ?? const {}); + Map get vars => Map.unmodifiable(_vars ?? const {}); final List? _framesOmitted; @@ -108,6 +110,23 @@ class SentryStackFrame { /// The original function name, if the function name is shortened or demangled. Sentry shows the raw function when clicking on the shortened one in the UI. final String? rawFunction; + /// Marks this frame as the bottom of a chained stack trace. + /// + /// Stack traces from asynchronous code consist of several sub traces that + /// are chained together into one large list. This flag indicates the root + /// function of a chained stack trace. Depending on the runtime and thread, + /// this is either the main function or a thread base stub. + /// + /// This field should only be specified when true. + final bool? stackStart; + + /// Potentially mangled name of the symbol as it appears in an executable. + /// + /// This is different from a function name by generally being the mangled name + /// that appears natively in the binary. + /// This is relevant for languages like Swift, C++ or Rust. + final String? symbol; + /// Deserializes a [SentryStackFrame] from JSON [Map]. factory SentryStackFrame.fromJson(Map json) { return SentryStackFrame( @@ -130,90 +149,36 @@ class SentryStackFrame { preContext: json['pre_context'], postContext: json['post_context'], vars: json['vars'], + symbol: json['symbol'], + stackStart: json['stack_start'], ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (_preContext?.isNotEmpty ?? false) { - json['pre_context'] = _preContext; - } - - if (_postContext?.isNotEmpty ?? false) { - json['post_context'] = _postContext; - } - - if (_vars?.isNotEmpty ?? false) { - json['vars'] = _vars; - } - - if (_framesOmitted?.isNotEmpty ?? false) { - json['frames_omitted'] = _framesOmitted; - } - - if (fileName != null) { - json['filename'] = fileName; - } - - if (package != null) { - json['package'] = package; - } - - if (function != null) { - json['function'] = function; - } - - if (module != null) { - json['module'] = module; - } - - if (lineNo != null) { - json['lineno'] = lineNo; - } - - if (colNo != null) { - json['colno'] = colNo; - } - - if (absPath != null) { - json['abs_path'] = absPath; - } - - if (contextLine != null) { - json['context_line'] = contextLine; - } - - if (inApp != null) { - json['in_app'] = inApp; - } - - if (native != null) { - json['native'] = native; - } - - if (platform != null) { - json['platform'] = platform; - } - - if (imageAddr != null) { - json['image_addr'] = imageAddr; - } - - if (symbolAddr != null) { - json['symbol_addr'] = symbolAddr; - } - - if (instructionAddr != null) { - json['instruction_addr'] = instructionAddr; - } - - if (rawFunction != null) { - json['raw_function'] = rawFunction; - } - - return json; + return { + if (_preContext?.isNotEmpty ?? false) 'pre_context': _preContext, + if (_postContext?.isNotEmpty ?? false) 'post_context': _postContext, + if (_vars?.isNotEmpty ?? false) 'vars': _vars, + if (_framesOmitted?.isNotEmpty ?? false) 'frames_omitted': _framesOmitted, + if (fileName != null) 'filename': fileName, + if (package != null) 'package': package, + if (function != null) 'function': function, + if (module != null) 'module': module, + if (lineNo != null) 'lineno': lineNo, + if (colNo != null) 'colno': colNo, + if (absPath != null) 'abs_path': absPath, + if (contextLine != null) 'context_line': contextLine, + if (inApp != null) 'in_app': inApp, + if (native != null) 'native': native, + if (platform != null) 'platform': platform, + if (imageAddr != null) 'image_addr': imageAddr, + if (symbolAddr != null) 'symbol_addr': symbolAddr, + if (instructionAddr != null) 'instruction_addr': instructionAddr, + if (rawFunction != null) 'raw_function': rawFunction, + if (symbol != null) 'symbol': symbol, + if (stackStart != null) 'stack_start': stackStart, + }; } SentryStackFrame copyWith({ @@ -236,6 +201,8 @@ class SentryStackFrame { List? preContext, List? postContext, Map? vars, + bool? stackStart, + String? symbol, }) => SentryStackFrame( absPath: absPath ?? this.absPath, @@ -257,5 +224,7 @@ class SentryStackFrame { preContext: preContext ?? _preContext, postContext: postContext ?? _postContext, vars: vars ?? _vars, + symbol: symbol ?? symbol, + stackStart: stackStart ?? stackStart, ); } diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart index abb74fdcc..c8a607672 100644 --- a/dart/lib/src/protocol/sentry_stack_trace.dart +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -8,6 +8,8 @@ class SentryStackTrace { SentryStackTrace({ required List frames, Map? registers, + this.lang, + this.snapshot, }) : _frames = frames, _registers = Map.from(registers ?? {}); @@ -25,6 +27,23 @@ class SentryStackTrace { /// thus mapping to the last frame in the list. Map get registers => Map.unmodifiable(_registers ?? const {}); + /// The language of the stacktrace + final String? lang; + + /// Indicates that this stack trace is a snapshot triggered + /// by an external signal. + /// + /// If this field is false, then the stack trace points to the code that + /// caused this stack trace to be created. + /// This can be the location of a raised exception, as well as an exception or + /// signal handler. + /// + /// If this field is true, then the stack trace was captured as part + /// of creating an unrelated event. For example, a thread other than the + /// crashing thread, or a stack trace computed as a result of an external kill + /// signal. + final bool? snapshot; + /// Deserializes a [SentryStackTrace] from JSON [Map]. factory SentryStackTrace.fromJson(Map json) { final framesJson = json['frames'] as List?; @@ -35,23 +54,21 @@ class SentryStackTrace { .toList() : [], registers: json['registers'], + lang: json['lang'], + snapshot: json['snapshot'], ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { - final json = {}; - - if (_frames?.isNotEmpty ?? false) { - json['frames'] = - _frames?.map((frame) => frame.toJson()).toList(growable: false); - } - - if (_registers?.isNotEmpty ?? false) { - json['registers'] = _registers; - } - - return json; + return { + if (_frames?.isNotEmpty ?? false) + 'frames': + _frames?.map((frame) => frame.toJson()).toList(growable: false), + if (_registers?.isNotEmpty ?? false) 'registers': _registers, + if (lang != null) 'lang': lang, + if (snapshot != null) 'snapshot': snapshot, + }; } SentryStackTrace copyWith({ diff --git a/dart/lib/src/protocol/sentry_user.dart b/dart/lib/src/protocol/sentry_user.dart index 960ed8733..f0e4cb5e8 100644 --- a/dart/lib/src/protocol/sentry_user.dart +++ b/dart/lib/src/protocol/sentry_user.dart @@ -1,5 +1,7 @@ import 'package:meta/meta.dart'; +import '../../sentry.dart'; + /// Describes the current user associated with the application, such as the /// currently signed in user. /// @@ -37,12 +39,20 @@ class SentryUser { this.email, this.ipAddress, this.segment, - Map? extras, - }) : assert(id != null || - username != null || - email != null || - ipAddress != null || - segment != null), + this.geo, + this.name, + Map? data, + @Deprecated('Will be removed in v7. Use [data] instead') + Map? extras, + }) : assert( + id != null || + username != null || + email != null || + ipAddress != null || + segment != null, + ), + data = data == null ? null : Map.from(data), + // ignore: deprecated_member_use_from_same_package extras = extras == null ? null : Map.from(extras); /// A unique identifier of the user. @@ -64,13 +74,38 @@ class SentryUser { /// /// These keys are stored as extra information but not specifically processed /// by Sentry. + final Map? data; + + @Deprecated('Will be removed in v7. Use [data] instead') final Map? extras; + /// Approximate geographical location of the end user or device. + /// + /// The geolocation is automatically inferred by Sentry.io if the [ipAddress] is set. + /// Sentry however doesn't collect the [ipAddress] automatically because it is PII. + /// The geo location will currently not be synced to the native layer, if available. + // See https://github.com/getsentry/sentry-dart/issues/1065 + final SentryGeo? geo; + + /// Human readable name of the user. + final String? name; + /// Deserializes a [SentryUser] from JSON [Map]. factory SentryUser.fromJson(Map json) { var extras = json['extras']; if (extras != null) { - extras = Map.from(extras as Map); + extras = Map.from(extras); + } + + var data = json['data']; + if (data != null) { + data = Map.from(data); + } + + SentryGeo? geo; + final geoJson = json['geo']; + if (geoJson != null) { + geo = SentryGeo.fromJson(Map.from(geoJson)); } return SentryUser( id: json['id'], @@ -78,19 +113,28 @@ class SentryUser { email: json['email'], ipAddress: json['ip_address'], segment: json['segment'], + data: data, + geo: geo, + name: json['name'], + // ignore: deprecated_member_use_from_same_package extras: extras, ); } /// Produces a [Map] that can be serialized to JSON. Map toJson() { + final geoJson = geo?.toJson(); return { if (id != null) 'id': id, if (username != null) 'username': username, if (email != null) 'email': email, if (ipAddress != null) 'ip_address': ipAddress, if (segment != null) 'segment': segment, + if (data?.isNotEmpty ?? false) 'data': data, + // ignore: deprecated_member_use_from_same_package if (extras?.isNotEmpty ?? false) 'extras': extras, + if (name != null) 'name': name, + if (geoJson != null && geoJson.isNotEmpty) 'geo': geoJson, }; } @@ -100,14 +144,23 @@ class SentryUser { String? email, String? ipAddress, String? segment, - Map? extras, - }) => - SentryUser( - id: id ?? this.id, - username: username ?? this.username, - email: email ?? this.email, - ipAddress: ipAddress ?? this.ipAddress, - segment: segment ?? this.segment, - extras: extras ?? this.extras, - ); + @Deprecated('Will be removed in v7. Use [data] instead') + Map? extras, + String? name, + SentryGeo? geo, + Map? data, + }) { + return SentryUser( + id: id ?? this.id, + username: username ?? this.username, + email: email ?? this.email, + ipAddress: ipAddress ?? this.ipAddress, + segment: segment ?? this.segment, + data: data ?? this.data, + // ignore: deprecated_member_use_from_same_package + extras: extras ?? this.extras, + geo: geo ?? this.geo, + name: name ?? this.name, + ); + } } diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 5e63a0a8e..e5c9a36ed 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -389,24 +389,26 @@ class Scope { email: eventUser?.email, ipAddress: eventUser?.ipAddress, username: eventUser?.username, - extras: _mergeUserExtra(eventUser?.extras, scopeUser.extras), + data: _mergeUserData(eventUser?.data, scopeUser.data), + // ignore: deprecated_member_use_from_same_package + extras: _mergeUserData(eventUser?.extras, scopeUser.extras), ); } /// If the User on the scope and the user of an event have extra entries with /// the same key, the event user extra will be kept. - Map _mergeUserExtra( - Map? eventExtra, - Map? scopeExtra, + Map _mergeUserData( + Map? eventData, + Map? scopeData, ) { final map = {}; - if (eventExtra != null) { - map.addAll(eventExtra); + if (eventData != null) { + map.addAll(eventData); } - if (scopeExtra == null) { + if (scopeData == null) { return map; } - for (var value in scopeExtra.entries) { + for (var value in scopeData.entries) { map.putIfAbsent(value.key, () => value.value); } return map; diff --git a/dart/test/http_client/failed_request_client_test.dart b/dart/test/http_client/failed_request_client_test.dart index 342a7f00f..715ffc12d 100644 --- a/dart/test/http_client/failed_request_client_test.dart +++ b/dart/test/http_client/failed_request_client_test.dart @@ -57,7 +57,9 @@ void main() { expect(request?.queryString, 'foo=bar'); expect(request?.cookies, 'foo=bar'); expect(request?.headers, {'Cookie': 'foo=bar'}); + // ignore: deprecated_member_use_from_same_package expect(request?.other.keys.contains('duration'), true); + // ignore: deprecated_member_use_from_same_package expect(request?.other.keys.contains('content_length'), true); }); @@ -108,7 +110,9 @@ void main() { expect(request?.queryString, 'foo=bar'); expect(request?.cookies, 'foo=bar'); expect(request?.headers, {'Cookie': 'foo=bar'}); + // ignore: deprecated_member_use_from_same_package expect(request?.other.keys.contains('duration'), true); + // ignore: deprecated_member_use_from_same_package expect(request?.other.keys.contains('content_length'), true); }); diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index 836dccc76..9e0ea7ff6 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -33,7 +33,7 @@ final fakeEvent = SentryEvent( username: 'first-user', email: 'first@user.lan', ipAddress: '127.0.0.1', - extras: {'first-sign-in': '2020-01-01'}, + data: {'first-sign-in': '2020-01-01'}, ), breadcrumbs: [ Breadcrumb( diff --git a/dart/test/protocol/sentry_request_test.dart b/dart/test/protocol/sentry_request_test.dart index ae78f75fd..394da9119 100644 --- a/dart/test/protocol/sentry_request_test.dart +++ b/dart/test/protocol/sentry_request_test.dart @@ -11,6 +11,7 @@ void main() { data: {'key': 'value'}, headers: {'header_key': 'header_value'}, env: {'env_key': 'env_value'}, + // ignore: deprecated_member_use_from_same_package other: {'other_key': 'other_value'}, ); diff --git a/dart/test/protocol/sentry_user_test.dart b/dart/test/protocol/sentry_user_test.dart index 82a22ec7a..2a958624e 100644 --- a/dart/test/protocol/sentry_user_test.dart +++ b/dart/test/protocol/sentry_user_test.dart @@ -8,7 +8,7 @@ void main() { username: 'username', email: 'email', ipAddress: 'ipAddress', - extras: {'key': 'value'}, + data: {'key': 'value'}, segment: 'seg', ); @@ -17,7 +17,7 @@ void main() { 'username': 'username', 'email': 'email', 'ip_address': 'ipAddress', - 'extras': {'key': 'value'}, + 'data': {'key': 'value'}, 'segment': 'seg', }; @@ -41,9 +41,7 @@ void main() { }); test('toJson only serialises non-null values', () { - var data = SentryUser( - id: 'id', - ); + var data = SentryUser(id: 'id'); var json = data.toJson(); @@ -54,9 +52,7 @@ void main() { expect(json.containsKey('extras'), false); expect(json.containsKey('segment'), false); - data = SentryUser( - ipAddress: 'ip', - ); + data = SentryUser(ipAddress: 'ip'); json = data.toJson(); @@ -86,7 +82,7 @@ void main() { username: 'username1', email: 'email1', ipAddress: 'ipAddress1', - extras: {'key1': 'value1'}, + data: {'key1': 'value1'}, segment: 'seg1', ); @@ -94,7 +90,7 @@ void main() { expect('username1', copy.username); expect('email1', copy.email); expect('ipAddress1', copy.ipAddress); - expect({'key1': 'value1'}, copy.extras); + expect({'key1': 'value1'}, copy.data); expect('seg1', copy.segment); }); }); diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index de3d2f7d5..12f8b6a04 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -366,7 +366,7 @@ void main() { username: 'first-user', email: 'first@user.lan', ipAddress: '127.0.0.1', - extras: const {'first-sign-in': '2020-01-01'}, + data: const {'first-sign-in': '2020-01-01'}, ); final breadcrumb = Breadcrumb(message: 'Authenticated'); diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 2cc2d15e6..8ccaefd57 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -633,7 +633,7 @@ void main() { await scope.setUser( SentryUser( id: 'id', - extras: { + data: { 'foo': 'bar', 'bar': 'foo', }, @@ -643,7 +643,7 @@ void main() { var eventWithUser = event.copyWith( user: SentryUser( id: 'id', - extras: { + data: { 'foo': 'this bar is more important', 'event': 'Really important event' }, @@ -654,9 +654,9 @@ void main() { final capturedEnvelope = fixture.transport.envelopes.first; final capturedEvent = await eventFromEnvelope(capturedEnvelope); - expect(capturedEvent.user?.extras?['foo'], 'this bar is more important'); - expect(capturedEvent.user?.extras?['bar'], 'foo'); - expect(capturedEvent.user?.extras?['event'], 'Really important event'); + expect(capturedEvent.user?.data?['foo'], 'this bar is more important'); + expect(capturedEvent.user?.data?['bar'], 'foo'); + expect(capturedEvent.user?.data?['event'], 'Really important event'); }); }); diff --git a/dart/test/sentry_event_test.dart b/dart/test/sentry_event_test.dart index 104f68b54..2fb1a7d45 100644 --- a/dart/test/sentry_event_test.dart +++ b/dart/test/sentry_event_test.dart @@ -160,11 +160,12 @@ void main() { final timestamp = DateTime.utc(2019); final user = SentryUser( - id: 'user_id', - username: 'username', - email: 'email@email.com', - ipAddress: '127.0.0.1', - extras: const {'foo': 'bar'}); + id: 'user_id', + username: 'username', + email: 'email@email.com', + ipAddress: '127.0.0.1', + data: const {'foo': 'bar'}, + ); final breadcrumbs = [ Breadcrumb( @@ -248,7 +249,7 @@ void main() { 'username': 'username', 'email': 'email@email.com', 'ip_address': '127.0.0.1', - 'extras': {'foo': 'bar'} + 'data': {'foo': 'bar'} }, 'breadcrumbs': { { diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index c364e323d..f9c8d354d 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -379,7 +379,7 @@ void runTest({Codec, List?>? gzip, bool isWeb = false}) { username: 'username', email: 'email@email.com', ipAddress: '127.0.0.1', - extras: {'foo': 'bar'}, + data: {'foo': 'bar'}, ); final options = SentryOptions( diff --git a/dio/test/mocks.dart b/dio/test/mocks.dart index f12bc2c4a..9710fd962 100644 --- a/dio/test/mocks.dart +++ b/dio/test/mocks.dart @@ -33,7 +33,7 @@ final fakeEvent = SentryEvent( username: 'first-user', email: 'first@user.lan', ipAddress: '127.0.0.1', - extras: {'first-sign-in': '2020-01-01'}, + data: {'first-sign-in': '2020-01-01'}, ), breadcrumbs: [ Breadcrumb( diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index 043e19386..da2abe9db 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -275,6 +275,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { (user["id"] as? String)?.let { userInstance.id = it } (user["username"] as? String)?.let { userInstance.username = it } (user["ip_address"] as? String)?.let { userInstance.ipAddress = it } + (user["segment"] as? String)?.let { userInstance.segment = it } (user["extras"] as? Map)?.let { extras -> val others = mutableMapOf() for ((key, value) in extras.entries) { @@ -284,6 +285,14 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } userInstance.others = others } + (user["data"] as? Map)?.let { data -> + val others = mutableMapOf() + for ((key, value) in data.entries) { + if (value != null) { + others[key] = value.toString() + } + } + } Sentry.setUser(userInstance) diff --git a/flutter/example/android/app/build.gradle b/flutter/example/android/app/build.gradle index 9c708c5c3..f3cf4a9d9 100644 --- a/flutter/example/android/app/build.gradle +++ b/flutter/example/android/app/build.gradle @@ -66,7 +66,7 @@ android { // TODO: we need to fix CI as the version 21.1 (default) is not installed by default on // GH Actions. - ndkVersion "21.4.7075529" // windows requires 21.4.7075529 + ndkVersion "25.1.8937393" externalNativeBuild { cmake { diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index cf567f69f..c2208941f 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -516,9 +516,19 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { if let ipAddress = user["ip_address"] as? String { userInstance.ipAddress = ipAddress } + if let segment = user["segment"] as? String { + userInstance.segment = segment + } if let extras = user["extras"] as? [String: Any] { userInstance.data = extras } + if let data = user["data"] as? [String: Any] { + if let oldData = userInstance.data { + userInstance.data = oldData.reduce(into: data) { (first, second) in first[second.0] = second.1 } + } else { + userInstance.data = data + } + } SentrySDK.setUser(userInstance) } else { diff --git a/flutter/test/load_contexts_integrations_test.dart b/flutter/test/load_contexts_integrations_test.dart index 2c6b9fd00..d27c222cb 100644 --- a/flutter/test/load_contexts_integrations_test.dart +++ b/flutter/test/load_contexts_integrations_test.dart @@ -321,7 +321,7 @@ void main() { expect(event?.user?.username, 'fixture-username'); expect(event?.user?.email, 'fixture-email'); expect(event?.user?.ipAddress, 'fixture-ip_address'); - expect(event?.user?.extras?['key'], 'value'); + expect(event?.user?.data?['key'], 'value'); }); test('should not override user with native', () async { @@ -448,7 +448,7 @@ class Fixture { 'username': 'fixture-username', 'email': 'fixture-email', 'ip_address': 'fixture-ip_address', - 'extras': {'key': 'value'}, + 'data': {'key': 'value'}, }, 'tags': {'key-a': 'native', 'key-b': 'native'}, 'extra': {'key-a': 'native', 'key-b': 'native'},