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();