diff --git a/CHANGELOG.md b/CHANGELOG.md index b5871b6734..0d780a546d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10380) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.6.0...10.38.0) +
+Internal Changes + +- Add SDK features metadata for SPM vs CocoaPods tracking ([#3508](https://github.com/getsentry/sentry-dart/pull/3508)) + +
+ ## 9.12.0 ### Dependencies diff --git a/packages/dart/lib/src/protocol/sdk_version.dart b/packages/dart/lib/src/protocol/sdk_version.dart index b80483accd..5b059a0ea4 100644 --- a/packages/dart/lib/src/protocol/sdk_version.dart +++ b/packages/dart/lib/src/protocol/sdk_version.dart @@ -21,6 +21,9 @@ import 'access_aware_map.dart'; /// "integrations": [ /// "tracing" /// ], +/// "features": [ +/// "SwiftPackageManager" +/// ], /// "packages": [ /// { /// "name": "git:https://github.com/getsentry/sentry-cocoa.git", @@ -39,11 +42,13 @@ class SdkVersion { required this.name, required this.version, List? integrations, + List? features, List? packages, this.unknown, }) : // List.from prevents from having immutable lists _integrations = List.from(integrations ?? []), + _features = List.from(features ?? []), _packages = List.from(packages ?? []); /// The name of the SDK. @@ -57,6 +62,11 @@ class SdkVersion { /// An immutable list of integrations enabled in the SDK that created the [Event]. List get integrations => List.unmodifiable(_integrations); + List _features; + + /// An immutable list of features enabled in the SDK. + List get features => List.unmodifiable(_features); + List _packages; /// An immutable list of packages that compose this SDK. @@ -70,6 +80,7 @@ class SdkVersion { final json = AccessAwareMap(data); final packagesJson = json['packages'] as List?; final integrationsJson = json['integrations'] as List?; + final featuresJson = json['features'] as List?; return SdkVersion( name: json['name'], @@ -78,6 +89,7 @@ class SdkVersion { ?.map((e) => SentryPackage.fromJson(e as Map)) .toList(), integrations: integrationsJson?.map((e) => e as String).toList(), + features: featuresJson?.map((e) => e as String).toList(), unknown: json.notAccessed(), ); } @@ -91,6 +103,7 @@ class SdkVersion { if (packages.isNotEmpty) 'packages': packages.map((p) => p.toJson()).toList(growable: false), if (integrations.isNotEmpty) 'integrations': integrations, + if (features.isNotEmpty) 'features': features, }; } @@ -106,7 +119,6 @@ class SdkVersion { _packages.add(package); } - // Adds an integration if not already added void addIntegration(String integration) { if (_integrations.contains(integration)) { return; @@ -114,17 +126,26 @@ class SdkVersion { _integrations.add(integration); } + void addFeature(String feature) { + if (_features.contains(feature)) { + return; + } + _features.add(feature); + } + @Deprecated('Assign values directly to the instance.') SdkVersion copyWith({ String? name, String? version, List? integrations, + List? features, List? packages, }) => SdkVersion( name: name ?? this.name, version: version ?? this.version, integrations: integrations ?? _integrations, + features: features ?? _features, packages: packages ?? _packages, unknown: unknown, ); diff --git a/packages/dart/test/protocol/sdk_version_test.dart b/packages/dart/test/protocol/sdk_version_test.dart index 26ddddbb40..6be608f0f8 100644 --- a/packages/dart/test/protocol/sdk_version_test.dart +++ b/packages/dart/test/protocol/sdk_version_test.dart @@ -82,6 +82,24 @@ void main() { expect(1, sut.packages.length); }); }); + + group('addFeature', () { + final fixture = Fixture(); + + test('add feature if not already present', () { + final sut = fixture.getSut(); + sut.addFeature('newFeature'); + + expect(sut.features.last, 'newFeature'); + }); + + test('does not add feature if already present', () { + final sut = fixture.getSut(); + sut.addFeature('testFeature'); + + expect(sut.features.where((f) => f == 'testFeature').length, 1); + }); + }); } class Fixture { @@ -95,6 +113,7 @@ class Fixture { 'version': 'version', } ], + 'features': ['testFeature'], }; Fixture() { @@ -106,6 +125,7 @@ class Fixture { version: 'version', integrations: ['test'], packages: [SentryPackage('name', 'version')], + features: ['testFeature'], unknown: testUnknown, ); } diff --git a/packages/flutter/ios/sentry_flutter/Package.swift b/packages/flutter/ios/sentry_flutter/Package.swift index 6f9bf24d3c..89b109d62b 100644 --- a/packages/flutter/ios/sentry_flutter/Package.swift +++ b/packages/flutter/ios/sentry_flutter/Package.swift @@ -21,6 +21,9 @@ let package = Package( dependencies: [ "sentry_flutter_objc", .product(name: "Sentry", package: "sentry-cocoa") + ], + swiftSettings: [ + .define("SENTRY_FLUTTER_SPM") ] ), // SPM does not support mixed-language targets, so we need to move the ObjC files into a separate one diff --git a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift index e2b370d29e..14042314a5 100644 --- a/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift +++ b/packages/flutter/ios/sentry_flutter/Sources/sentry_flutter/SentryFlutterPlugin.swift @@ -213,6 +213,12 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { infos["integrations"] = integrations.filter { $0 != "SentrySessionReplayIntegration" } } + #if SENTRY_FLUTTER_SPM + infos["features"] = ["SwiftPackageManager"] + #else + infos["features"] = ["CocoaPods"] + #endif + let deviceStr = "device" let appStr = "app" if let extraContext = PrivateSentrySDKOnly.getExtraContext() as? [String: Any] { @@ -241,8 +247,13 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { // Not reading the name from PrivateSentrySDKOnly.getSdkName because // this is added as a package and packages should follow the sentry-release-registry format + #if SENTRY_FLUTTER_SPM + infos["package"] = ["version": PrivateSentrySDKOnly.getSdkVersionString(), + "sdk_name": "spm:sentry-cocoa"] + #else infos["package"] = ["version": PrivateSentrySDKOnly.getSdkVersionString(), "sdk_name": "cocoapods:sentry-cocoa"] + #endif result(infos) } @@ -276,6 +287,7 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { result(debugImages.map { $0.serialize() }) } + // swiftlint:disable:next cyclomatic_complexity private func initNativeSdk(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String: Any], !arguments.isEmpty else { print("Arguments is null or empty") @@ -319,6 +331,13 @@ public class SentryFlutterPlugin: NSObject, FlutterPlugin { sdk["integrations"] = integrations } } + if let features = flutterSdk!["features"] as? [String] { + if let sdkFeatures = sdk["features"] as? [String] { + sdk["features"] = sdkFeatures + features + } else { + sdk["features"] = features + } + } event.sdk = sdk } } diff --git a/packages/flutter/lib/src/integrations/load_contexts_integration.dart b/packages/flutter/lib/src/integrations/load_contexts_integration.dart index b46a7a7494..b50669b686 100644 --- a/packages/flutter/lib/src/integrations/load_contexts_integration.dart +++ b/packages/flutter/lib/src/integrations/load_contexts_integration.dart @@ -260,6 +260,18 @@ class _LoadContextsIntegrationEventProcessor implements EventProcessor { event.sdk = sdk; } + final featuresList = infos['features'] as List?; + if (featuresList != null && featuresList.isNotEmpty) { + final features = List.from(featuresList); + final sdk = event.sdk ?? _options.sdk; + + for (final feature in features) { + sdk.addFeature(feature); + } + + event.sdk = sdk; + } + final packageMap = infos['package'] as Map?; if (packageMap != null && packageMap.isNotEmpty) { final package = Map.from(packageMap); diff --git a/packages/flutter/lib/src/sentry_flutter.dart b/packages/flutter/lib/src/sentry_flutter.dart index 3ec4d43cd9..9fef9ce29d 100644 --- a/packages/flutter/lib/src/sentry_flutter.dart +++ b/packages/flutter/lib/src/sentry_flutter.dart @@ -260,6 +260,7 @@ mixin SentryFlutter { version: sdkVersion, integrations: options.sdk.integrations, packages: options.sdk.packages, + features: options.sdk.features, ); sdk.addPackage('pub:sentry_flutter', sdkVersion); options.sdk = sdk; diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index f74f261bad..3ad809c363 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -14,6 +14,7 @@ import 'fixture.dart'; void main() { final defaultContexts = { 'integrations': ['NativeIntegration'], + 'features': ['SwiftPackageManager'], 'package': {'sdk_name': 'native-package', 'version': '1.0'}, 'contexts': { 'device': { @@ -291,6 +292,37 @@ void main() { ); }); + group('features', () { + test('merges features from native into sdk', () async { + mockLoadContexts(); + await fixture.registerIntegration(); + + final e = getEvent(); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect(event?.sdk?.features.contains('SwiftPackageManager'), true); + }); + + test('does not duplicate feature if already present', () async { + mockLoadContexts({ + 'features': ['EventFeature'] + }); + await fixture.registerIntegration(); + + final sdk = getSdkVersion(); + sdk.addFeature('EventFeature'); + final e = getEvent(sdk: sdk); + final event = + await fixture.options.eventProcessors.first.apply(e, Hint()); + + expect( + event?.sdk?.features.where((f) => f == 'EventFeature').length, + 1, + ); + }); + }); + group('breadcrumbs', () { test('takes breadcrumbs from native if scope sync is enabled', () async { await fixture.registerIntegration();