diff --git a/.gitignore b/.gitignore
index 9e3ff1c66ae..140bb8bf791 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,6 +87,7 @@ Samples/iOS-ObjectiveC/iOS-ObjectiveC.xcodeproj
Samples/iOS-Swift/iOS-Swift.xcodeproj
Samples/iOS-Swift6/iOS-Swift6.xcodeproj
Samples/iOS-SwiftUI/iOS-SwiftUI.xcodeproj
+Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.xcodeproj
Samples/iOS15-SwiftUI/iOS15-SwiftUI.xcodeproj
Samples/macOS-Swift/macOS-Swift.xcodeproj
Samples/macOS-SwiftUI/macOS-SwiftUI.xcodeproj
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04bb2c5e2e1..93da1296c0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## Unreleased
+
+### Breaking Changes
+
+- App hang tracking is now automatically disabled for Widgets, Live Activities, Action Extensions, (Siri) Intent Extensions, and Share Extensions (#6670).
+ These components run in separate processes or sandboxes with different execution characteristics, which can cause false positive app hang reports.
+
## 9.0.0-alpha.1
### Breaking Changes
diff --git a/Makefile b/Makefile
index 302ea0981e5..3e33e601bc1 100644
--- a/Makefile
+++ b/Makefile
@@ -190,6 +190,7 @@ xcode-ci:
xcodegen --spec Samples/iOS-Swift/iOS-Swift.yml
xcodegen --spec Samples/iOS-Swift6/iOS-Swift6.yml
xcodegen --spec Samples/iOS-SwiftUI/iOS-SwiftUI.yml
+ xcodegen --spec Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.yml
xcodegen --spec Samples/iOS15-SwiftUI/iOS15-SwiftUI.yml
xcodegen --spec Samples/macOS-SwiftUI/macOS-SwiftUI.yml
xcodegen --spec Samples/macOS-Swift/macOS-Swift.yml
diff --git a/Samples/iOS-Swift/iOS-Swift-ActionExtension.xcconfig b/Samples/iOS-Swift/iOS-Swift-ActionExtension.xcconfig
new file mode 100644
index 00000000000..061f3edfaba
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-ActionExtension.xcconfig
@@ -0,0 +1,20 @@
+#include "../Shared/Config/_Common.xcconfig"
+
+PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sample.iOS-Swift.ActionExtension
+INFOPLIST_FILE = iOS-Swift-ActionExtension/Resources/Info.plist
+
+PROVISIONING_PROFILE_SPECIFIER_Debug = match Development io.sentry.sample.iOS-Swift.ActionExtension
+PROVISIONING_PROFILE_SPECIFIER_Test = match Development io.sentry.sample.iOS-Swift.ActionExtension
+PROVISIONING_PROFILE_SPECIFIER_TestCI = match Development io.sentry.sample.iOS-Swift.ActionExtension
+PROVISIONING_PROFILE_SPECIFIER_Release = match AppStore io.sentry.sample.iOS-Swift.ActionExtension
+PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = $(PROVISIONING_PROFILE_SPECIFIER_$(CONFIGURATION))
+PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] =
+
+CODE_SIGN_STYLE = Manual
+
+SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+
+SWIFT_OBJC_BRIDGING_HEADER = iOS-Swift/Tools/iOS-Swift-Bridging-Header.h
+
+// Runtime search paths for app extensions to find frameworks in parent app
+LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks
diff --git a/Samples/iOS-Swift/iOS-Swift-ActionExtension/Resources/Info.plist b/Samples/iOS-Swift/iOS-Swift-ActionExtension/Resources/Info.plist
new file mode 100644
index 00000000000..b201ce3b033
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-ActionExtension/Resources/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ $(PRODUCT_NAME)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ GIT_BRANCH
+ <branch>
+ GIT_COMMIT_HASH
+ <sha>
+ GIT_STATUS_CLEAN
+ <status>
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.ui-services
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ActionViewController
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsText
+
+ NSExtensionActivationSupportsWebURLWithMaxCount
+ 1
+ NSExtensionActivationSupportsImageWithMaxCount
+ 1
+
+
+
+
+
diff --git a/Samples/iOS-Swift/iOS-Swift-ActionExtension/Sources/ActionViewController.swift b/Samples/iOS-Swift/iOS-Swift-ActionExtension/Sources/ActionViewController.swift
new file mode 100644
index 00000000000..f11aa05216f
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-ActionExtension/Sources/ActionViewController.swift
@@ -0,0 +1,106 @@
+import Sentry
+@_spi(Private) import Sentry
+import SentrySampleShared
+import UIKit
+import UniformTypeIdentifiers
+
+class ActionViewController: UIViewController {
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupSentry()
+ }
+
+ override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
+ super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+ setupSentry()
+ }
+
+ private func setupSentry() {
+ // Prevent double initialization - SentrySDK.start() can be called multiple times
+ // but we want to avoid unnecessary re-initialization
+ guard !SentrySDK.isEnabled else {
+ return
+ }
+
+ // For this extension we need a specific configuration set, therefore we do not use the shared sample initializer
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+
+ // App Hang Tracking must be enabled, but should not be installed
+ options.enableAppHangTracking = true
+ }
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ setupUI()
+ }
+
+ func setupUI() {
+ view.backgroundColor = .systemBackground
+
+ setupDoneButton()
+ setupStatusChecklist()
+ }
+
+ var isANRInstalled: Bool {
+ return isSentryEnabled && SentrySDKInternal.trimmedInstalledIntegrationNames().contains("ANRTracking")
+ }
+
+ var isSentryEnabled: Bool {
+ SentrySDK.isEnabled
+ }
+
+ // MARK: - UI
+
+ func setupDoneButton() {
+ var configuration = UIButton.Configuration.borderedProminent()
+ configuration.title = "Done"
+ configuration.baseBackgroundColor = .systemBlue
+ configuration.buttonSize = .large
+
+ let button = UIButton(configuration: configuration)
+ button.addTarget(self, action: #selector(doneAction(_:)), for: .touchUpInside)
+ view.addSubview(button)
+
+ button.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
+ button.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
+ button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16)
+ ])
+ }
+
+ @objc func doneAction(_ sender: UIButton) {
+ SentrySDK.capture(message: "iOS-Swift-ActionExtension: done called")
+ let returnItems = extensionContext?.inputItems as? [NSExtensionItem] ?? []
+ extensionContext?.completeRequest(returningItems: returnItems, completionHandler: nil)
+ }
+
+ func setupStatusChecklist() {
+ let stack = UIStackView()
+ stack.axis = .vertical
+ stack.spacing = 8
+ view.addSubview(stack)
+
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ stack.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+
+ let sdkStatusLabel = UILabel()
+ sdkStatusLabel.text = isSentryEnabled ? "✅ Sentry is enabled" : "❌ Sentry is not enabled"
+ sdkStatusLabel.textAlignment = .center
+ stack.addArrangedSubview(sdkStatusLabel)
+
+ let anrStatusLabel = UILabel()
+ // We want the ANR integration to be disabled for share extensions due to false-positives
+ anrStatusLabel.text = !isANRInstalled ? "✅ ANR Tracking not installed" : "❌ ANR Tracking installed"
+ anrStatusLabel.textAlignment = .center
+ stack.addArrangedSubview(anrStatusLabel)
+ }
+}
diff --git a/Samples/iOS-Swift/iOS-Swift-IntentExtension.xcconfig b/Samples/iOS-Swift/iOS-Swift-IntentExtension.xcconfig
new file mode 100644
index 00000000000..4f037e1bbc2
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-IntentExtension.xcconfig
@@ -0,0 +1,20 @@
+#include "../Shared/Config/_Common.xcconfig"
+
+PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sample.iOS-Swift.IntentExtension
+INFOPLIST_FILE = iOS-Swift-IntentExtension/Resources/Info.plist
+
+PROVISIONING_PROFILE_SPECIFIER_Debug = match Development io.sentry.sample.iOS-Swift.IntentExtension
+PROVISIONING_PROFILE_SPECIFIER_Test = match Development io.sentry.sample.iOS-Swift.IntentExtension
+PROVISIONING_PROFILE_SPECIFIER_TestCI = match Development io.sentry.sample.iOS-Swift.IntentExtension
+PROVISIONING_PROFILE_SPECIFIER_Release = match AppStore io.sentry.sample.iOS-Swift.IntentExtension
+PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*] = $(PROVISIONING_PROFILE_SPECIFIER_$(CONFIGURATION))
+PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] =
+
+CODE_SIGN_STYLE = Manual
+
+SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+
+SWIFT_OBJC_BRIDGING_HEADER = iOS-Swift/Tools/iOS-Swift-Bridging-Header.h
+
+// Runtime search paths for app extensions to find frameworks in parent app
+LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks
diff --git a/Samples/iOS-Swift/iOS-Swift-IntentExtension/Resources/Info.plist b/Samples/iOS-Swift/iOS-Swift-IntentExtension/Resources/Info.plist
new file mode 100644
index 00000000000..5a16ea5f567
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-IntentExtension/Resources/Info.plist
@@ -0,0 +1,48 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ $(PRODUCT_NAME)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ GIT_BRANCH
+ <branch>
+ GIT_COMMIT_HASH
+ <sha>
+ GIT_STATUS_CLEAN
+ <status>
+ NSExtension
+
+ NSExtensionAttributes
+
+ IntentsRestrictedWhileLocked
+
+ IntentsSupported
+
+ INSendMessageIntent
+ INSearchForMessagesIntent
+ INSetMessageAttributeIntent
+
+
+ NSExtensionPointIdentifier
+ com.apple.intents-service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).IntentHandler
+
+
+
diff --git a/Samples/iOS-Swift/iOS-Swift-IntentExtension/Sources/IntentHandler.swift b/Samples/iOS-Swift/iOS-Swift-IntentExtension/Sources/IntentHandler.swift
new file mode 100644
index 00000000000..2a90af36828
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-IntentExtension/Sources/IntentHandler.swift
@@ -0,0 +1,78 @@
+import Intents
+import Sentry
+@_spi(Private) import Sentry
+import SentrySampleShared
+
+class IntentHandler: INExtension, INSendMessageIntentHandling {
+
+ override init() {
+ super.init()
+ setupSentry()
+ }
+
+ override func handler(for intent: INIntent) -> Any {
+ setupSentry()
+ return self
+ }
+
+ private func setupSentry() {
+ // Prevent double initialization - SentrySDK.start() can be called multiple times
+ // but we want to avoid unnecessary re-initialization
+ guard !SentrySDK.isEnabled else {
+ return
+ }
+
+ // For this extension we need a specific configuration set, therefore we do not use the shared sample initializer
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+
+ // App Hang Tracking must be enabled, but should not be installed
+ options.enableAppHangTracking = true
+ }
+ }
+
+ // MARK: - INSendMessageIntentHandling
+
+ func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) {
+ let person = INPerson(
+ personHandle: INPersonHandle(value: "john-snow", type: .unknown),
+ nameComponents: PersonNameComponents(givenName: "John", familyName: "Snow"),
+ displayName: "John Snow",
+ image: nil,
+ contactIdentifier: nil,
+ customIdentifier: nil
+ )
+ completion([INSendMessageRecipientResolutionResult.success(with: person)])
+ }
+
+ func resolveContent(for intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
+ let message = """
+ Sentry Enabled? \(isSentryEnabled ? "✅" : "❌")
+ ANR Disabled? \(!isANRInstalled ? "✅" : "❌")
+ """
+ completion(INStringResolutionResult.success(with: message))
+ }
+
+ func confirm(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
+ let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
+ completion(INSendMessageIntentResponse(code: .ready, userActivity: userActivity))
+ }
+
+ func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
+ SentrySDK.capture(message: "iOS-Swift-IntentExtension: handle intent called")
+
+ let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
+ completion(INSendMessageIntentResponse(code: .success, userActivity: userActivity))
+ }
+
+ // MARK: - Helpers
+
+ var isANRInstalled: Bool {
+ return isSentryEnabled && SentrySDKInternal.trimmedInstalledIntegrationNames().contains("ANRTracking")
+ }
+
+ var isSentryEnabled: Bool {
+ SentrySDK.isEnabled
+ }
+}
diff --git a/Samples/iOS-Swift/iOS-Swift-ShareExtension.xcconfig b/Samples/iOS-Swift/iOS-Swift-ShareExtension.xcconfig
index ccdf55160e8..c10225b7503 100644
--- a/Samples/iOS-Swift/iOS-Swift-ShareExtension.xcconfig
+++ b/Samples/iOS-Swift/iOS-Swift-ShareExtension.xcconfig
@@ -13,3 +13,8 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] =
CODE_SIGN_STYLE = Manual
SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+
+SWIFT_OBJC_BRIDGING_HEADER = iOS-Swift/Tools/iOS-Swift-Bridging-Header.h
+
+// Runtime search paths for app extensions to find frameworks in parent app
+LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks
diff --git a/Samples/iOS-Swift/iOS-Swift-ShareExtension/Base.lproj/MainInterface.storyboard b/Samples/iOS-Swift/iOS-Swift-ShareExtension/Resources/Base.lproj/MainInterface.storyboard
similarity index 100%
rename from Samples/iOS-Swift/iOS-Swift-ShareExtension/Base.lproj/MainInterface.storyboard
rename to Samples/iOS-Swift/iOS-Swift-ShareExtension/Resources/Base.lproj/MainInterface.storyboard
diff --git a/Samples/iOS-Swift/iOS-Swift-ShareExtension/Info.plist b/Samples/iOS-Swift/iOS-Swift-ShareExtension/Resources/Info.plist
similarity index 100%
rename from Samples/iOS-Swift/iOS-Swift-ShareExtension/Info.plist
rename to Samples/iOS-Swift/iOS-Swift-ShareExtension/Resources/Info.plist
diff --git a/Samples/iOS-Swift/iOS-Swift-ShareExtension/ShareViewController.swift b/Samples/iOS-Swift/iOS-Swift-ShareExtension/ShareViewController.swift
deleted file mode 100644
index cd3dd15885a..00000000000
--- a/Samples/iOS-Swift/iOS-Swift-ShareExtension/ShareViewController.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-import Sentry
-import SentrySampleShared
-import Social
-import UIKit
-
-class ShareViewController: SLComposeServiceViewController {
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupSentry()
- }
-
- override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
- super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
- setupSentry()
- }
-
- private func setupSentry() {
- SentrySDKWrapper.shared.startSentry()
- }
-
- override func isContentValid() -> Bool {
- return true
- }
-
- override func didSelectPost() {
- SentrySDK.capture(message: "iOS-Swift-ShareExtension: didSelectPost called")
-
- self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
- }
-
- override func configurationItems() -> [Any]! {
- return []
- }
-}
diff --git a/Samples/iOS-Swift/iOS-Swift-ShareExtension/Sources/ShareViewController.swift b/Samples/iOS-Swift/iOS-Swift-ShareExtension/Sources/ShareViewController.swift
new file mode 100644
index 00000000000..666883156c0
--- /dev/null
+++ b/Samples/iOS-Swift/iOS-Swift-ShareExtension/Sources/ShareViewController.swift
@@ -0,0 +1,73 @@
+@_spi(Private) import Sentry
+import SentrySampleShared
+import Social
+import UIKit
+
+class ShareViewController: SLComposeServiceViewController {
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupSentry()
+ }
+
+ override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
+ super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
+ setupSentry()
+ }
+
+ private func setupSentry() {
+ // Prevent double initialization - SentrySDK.start() can be called multiple times
+ // but we want to avoid unnecessary re-initialization
+ guard !SentrySDK.isEnabled else {
+ return
+ }
+
+ // For this extension we need a specific configuration set, therefore we do not use the shared sample initializer
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+
+ // App Hang Tracking must be enabled, but should not be installed
+ options.enableAppHangTracking = true
+ }
+ }
+
+ override func isContentValid() -> Bool {
+ // We are not actually processing any information, therefore just allow all content
+ return true
+ }
+
+ override func didSelectPost() {
+ SentrySDK.capture(message: "iOS-Swift-ShareExtension: didSelectPost called")
+
+ self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
+ }
+
+ override func configurationItems() -> [Any]! {
+ guard let isSDKEnabledItem = SLComposeSheetConfigurationItem() else {
+ return []
+ }
+ isSDKEnabledItem.title = "Sentry Enabled?"
+ isSDKEnabledItem.value = isSentryEnabled ? "✅" : "❌"
+
+ guard let isANRActiveItem = SLComposeSheetConfigurationItem() else {
+ return []
+ }
+ isANRActiveItem.title = "ANR Disabled?"
+ // We want the ANR integration to be disabled for share extensions due to false-positives
+ isANRActiveItem.value = !isANRInstalled ? "✅" : "❌"
+
+ return [
+ isSDKEnabledItem,
+ isANRActiveItem
+ ]
+ }
+
+ var isANRInstalled: Bool {
+ return isSentryEnabled && SentrySDKInternal.trimmedInstalledIntegrationNames().contains("ANRTracking")
+ }
+
+ var isSentryEnabled: Bool {
+ SentrySDK.isEnabled
+ }
+}
diff --git a/Samples/iOS-Swift/iOS-Swift.yml b/Samples/iOS-Swift/iOS-Swift.yml
index 3c021922eba..ea658b3cc51 100644
--- a/Samples/iOS-Swift/iOS-Swift.yml
+++ b/Samples/iOS-Swift/iOS-Swift.yml
@@ -34,6 +34,8 @@ targets:
- target: iOS-SwiftClip
- target: SentrySampleShared/SentrySampleShared
- target: iOS-Swift-ShareExtension
+ - target: iOS-Swift-ActionExtension
+ - target: iOS-Swift-IntentExtension
configFiles:
Debug: iOS-Swift.xcconfig
Release: iOS-Swift.xcconfig
@@ -108,6 +110,32 @@ targets:
Release: iOS-Swift-ShareExtension.xcconfig
Test: iOS-Swift-ShareExtension.xcconfig
TestCI: iOS-Swift-ShareExtension.xcconfig
+ iOS-Swift-ActionExtension:
+ type: app-extension
+ platform: auto
+ sources:
+ - iOS-Swift-ActionExtension
+ dependencies:
+ - target: Sentry/Sentry
+ - target: SentrySampleShared/SentrySampleShared
+ configFiles:
+ Debug: iOS-Swift-ActionExtension.xcconfig
+ Release: iOS-Swift-ActionExtension.xcconfig
+ Test: iOS-Swift-ActionExtension.xcconfig
+ TestCI: iOS-Swift-ActionExtension.xcconfig
+ iOS-Swift-IntentExtension:
+ type: app-extension
+ platform: auto
+ sources:
+ - iOS-Swift-IntentExtension
+ dependencies:
+ - target: Sentry/Sentry
+ - target: SentrySampleShared/SentrySampleShared
+ configFiles:
+ Debug: iOS-Swift-IntentExtension.xcconfig
+ Release: iOS-Swift-IntentExtension.xcconfig
+ Test: iOS-Swift-IntentExtension.xcconfig
+ TestCI: iOS-Swift-IntentExtension.xcconfig
schemes:
iOS-Swift:
templates:
@@ -128,3 +156,24 @@ schemes:
config: Debug
testPlans:
- path: ../../Plans/iOS-Benchmarking_Base.xctestplan
+ iOS-Swift-IntentExtension:
+ build:
+ targets:
+ iOS-Swift: all
+ iOS-Swift-IntentExtension: all
+ run:
+ config: Debug
+ iOS-Swift-ActionExtension:
+ build:
+ targets:
+ iOS-Swift: all
+ iOS-Swift-ActionExtension: all
+ run:
+ config: Debug
+ iOS-Swift-ShareExtension:
+ build:
+ targets:
+ iOS-Swift: all
+ iOS-Swift-ShareExtension: all
+ run:
+ config: Debug
diff --git a/Samples/iOS-Swift/iOS-Swift/Info.plist b/Samples/iOS-Swift/iOS-Swift/Info.plist
index 438de9757e8..d412f5199a6 100644
--- a/Samples/iOS-Swift/iOS-Swift/Info.plist
+++ b/Samples/iOS-Swift/iOS-Swift/Info.plist
@@ -38,6 +38,10 @@
Testing location permissions
NSLocationWhenInUseUsageDescription
Testing location permissions
+ NSSiriUsageDescription
+ Testing app intents
+ NSSupportsSiriKit
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
diff --git a/Samples/iOS-Swift/iOS-Swift/Tools/SentryExposure.h b/Samples/iOS-Swift/iOS-Swift/Tools/SentryExposure.h
index 7712f4be68e..66643ef8816 100644
--- a/Samples/iOS-Swift/iOS-Swift/Tools/SentryExposure.h
+++ b/Samples/iOS-Swift/iOS-Swift/Tools/SentryExposure.h
@@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable SentryClientInternal *)getClient;
+- (NSArray *)trimmedInstalledIntegrationNames;
+
@end
@interface SentrySDKInternal : NSObject
@@ -31,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN
+ (SentryHubInternal *)currentHub;
++ (NSArray *)trimmedInstalledIntegrationNames;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Samples/iOS-Swift/iOS-Swift/iOS-Swift.entitlements b/Samples/iOS-Swift/iOS-Swift/iOS-Swift.entitlements
index ee95ab7e582..bda9be410a0 100644
--- a/Samples/iOS-Swift/iOS-Swift/iOS-Swift.entitlements
+++ b/Samples/iOS-Swift/iOS-Swift/iOS-Swift.entitlements
@@ -2,6 +2,10 @@
+ aps-environment
+ development
+ com.apple.developer.siri
+
com.apple.security.app-sandbox
com.apple.security.network.client
diff --git a/Samples/iOS-SwiftUI-Widgets/Shared/Data/LiveActivityAttributes.swift b/Samples/iOS-SwiftUI-Widgets/Shared/Data/LiveActivityAttributes.swift
new file mode 100644
index 00000000000..4df86e2a7f5
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/Shared/Data/LiveActivityAttributes.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+#if canImport(ActivityKit)
+
+import ActivityKit
+
+struct LiveActivityAttributes: ActivityAttributes {
+ struct ContentState: Codable & Hashable {
+ let timestamp: Date
+ }
+
+ let id: String
+}
+
+#endif // canImport(ActivityKit)
diff --git a/Samples/iOS-SwiftUI-Widgets/Shared/Headers/SentryExposure.h b/Samples/iOS-SwiftUI-Widgets/Shared/Headers/SentryExposure.h
new file mode 100644
index 00000000000..f1dde7f4639
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/Shared/Headers/SentryExposure.h
@@ -0,0 +1,12 @@
+#import
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SentrySDKInternal : NSObject
+
++ (NSArray *)trimmedInstalledIntegrationNames;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Samples/iOS-SwiftUI-Widgets/Shared/Headers/iOS-Swift-Widget-Bridging-Header.h b/Samples/iOS-SwiftUI-Widgets/Shared/Headers/iOS-Swift-Widget-Bridging-Header.h
new file mode 100644
index 00000000000..c88c075d563
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/Shared/Headers/iOS-Swift-Widget-Bridging-Header.h
@@ -0,0 +1 @@
+#import "SentryExposure.h"
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity.xcconfig b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity.xcconfig
new file mode 100644
index 00000000000..432c73d3907
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity.xcconfig
@@ -0,0 +1,34 @@
+#include "../Shared/Config/_Common.xcconfig"
+
+PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sentry-cocoa.samples.iOS-SwiftUI-Widgets.LiveActivity
+INFOPLIST_FILE = iOS-SwiftUI-Widgets-LiveActivity/Resources/Info.plist
+CODE_SIGN_ENTITLEMENTS = iOS-SwiftUI-Widgets-LiveActivity/Resources/Entitlements.entitlements
+
+GENERATE_INFOPLIST_FILE = NO
+
+IPHONEOS_DEPLOYMENT_TARGET = 18.0
+TARGETED_DEVICE_FAMILY = 1,2
+
+CODE_SIGN_STYLE = Manual
+
+SWIFT_OBJC_BRIDGING_HEADER = Shared/Headers/iOS-Swift-Widget-Bridging-Header.h
+
+SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+SUPPORTS_MACCATALYST = NO
+
+ENABLE_USER_SCRIPT_SANDBOXING = YES
+
+// Force rebuild when main app changes - helps with incremental build issues
+ALWAYS_SEARCH_USER_PATHS = NO
+
+// Asset Catalog
+ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES
+ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
+ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground
+
+// Swift Settings
+SWIFT_VERSION = 5.0
+
+LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../Frameworks @executable_path/../../Frameworks
+STRING_CATALOG_GENERATE_SYMBOLS = YES
+SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000000..0afb3cf0eec
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000000..c70a5bff185
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 00000000000..74d6a722cf3
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 00000000000..0afb3cf0eec
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Entitlements.entitlements b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Entitlements.entitlements
new file mode 100644
index 00000000000..6631ffa6f24
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Entitlements.entitlements
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Info.plist b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Info.plist
new file mode 100644
index 00000000000..26511c011b9
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Resources/Info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Live Activity
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+ NSHumanReadableCopyright
+
+
+
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Sources/LiveActivityBundle.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Sources/LiveActivityBundle.swift
new file mode 100644
index 00000000000..9d86c007b6f
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Sources/LiveActivityBundle.swift
@@ -0,0 +1,9 @@
+import SwiftUI
+import WidgetKit
+
+@main
+struct LiveActivityBundle: WidgetBundle {
+ var body: some Widget {
+ LiveActivityWidget()
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Sources/LiveActivityWidget.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Sources/LiveActivityWidget.swift
new file mode 100644
index 00000000000..0468afebab5
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-LiveActivity/Sources/LiveActivityWidget.swift
@@ -0,0 +1,89 @@
+import ActivityKit
+import Sentry
+import SentrySampleShared
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityWidget: Widget {
+ init() {
+ setupSentrySDK()
+ }
+
+ private func setupSentrySDK() {
+ guard !SentrySDK.isEnabled else {
+ return
+ }
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+ options.enableAppHangTracking = true
+ }
+ }
+
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Sentry")
+ .font(.headline)
+
+ Text("SDK Enabled? \(isSentryEnabled ? "✅" : "❌")")
+ .font(.caption)
+ .foregroundColor(isSentryEnabled ? .green : .red)
+ .bold()
+
+ Text("ANR Disabled? \(!isANRTrackingEnabled ? "✅" : "❌")")
+ .font(.caption2)
+ .foregroundColor(!isANRTrackingEnabled ? .green : .red)
+ .bold()
+
+ Text(context.state.timestamp, style: .time)
+ .font(.caption2)
+ }
+ .padding()
+
+ } dynamicIsland: { _ in
+ DynamicIsland {
+ // Expanded UI goes here. Compose the expanded UI through
+ // various regions, like leading/trailing/center/bottom
+ DynamicIslandExpandedRegion(.leading) {
+ Text("SDK Enabled? \(isSentryEnabled ? "✅" : "❌")")
+ .font(.caption)
+ .foregroundColor(isSentryEnabled ? .green : .red)
+ .bold()
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ Text("ANR Disabled? \(!isANRTrackingEnabled ? "✅" : "❌")")
+ .font(.caption2)
+ .foregroundColor(!isANRTrackingEnabled ? .green : .red)
+ .bold()
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ Text("SDK Enabled? \(isSentryEnabled ? "✅" : "❌")")
+ .font(.caption)
+ .foregroundColor(isSentryEnabled ? .green : .red)
+ .bold()
+ Text("ANR Disabled? \(!isANRTrackingEnabled ? "✅" : "❌")")
+ .font(.caption2)
+ .foregroundColor(!isANRTrackingEnabled ? .green : .red)
+ .bold()
+ }
+ } compactLeading: {
+ Text(isSentryEnabled ? "✅" : "❌")
+ } compactTrailing: {
+ Text(!isANRTrackingEnabled ? "✅" : "❌")
+ } minimal: {
+ Text("\(isSentryEnabled ? "✅" : "❌") - \(!isANRTrackingEnabled ? "✅" : "❌")")
+ }
+ .widgetURL(URL(string: "http://sentry.io"))
+ .keylineTint(isSentryEnabled && !isANRTrackingEnabled ? .green : .red)
+ }
+ }
+
+ var isSentryEnabled: Bool {
+ return SentrySDK.isEnabled
+ }
+
+ var isANRTrackingEnabled: Bool {
+ return isSentryEnabled && SentrySDKInternal.trimmedInstalledIntegrationNames().contains("ANRTracking")
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension.xcconfig b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension.xcconfig
new file mode 100644
index 00000000000..58c8bb462d3
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension.xcconfig
@@ -0,0 +1,30 @@
+#include "../Shared/Config/_Common.xcconfig"
+
+PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sentry-cocoa.samples.iOS-SwiftUI-Widgets.Widget
+INFOPLIST_FILE = iOS-SwiftUI-Widgets-WidgetExtension/Resources/Info.plist
+CODE_SIGN_ENTITLEMENTS = iOS-SwiftUI-Widgets-WidgetExtension/Resources/Entitlements.entitlements
+
+GENERATE_INFOPLIST_FILE = NO
+
+IPHONEOS_DEPLOYMENT_TARGET = 18.0
+TARGETED_DEVICE_FAMILY = 1,2
+
+CODE_SIGN_STYLE = Manual
+
+SWIFT_OBJC_BRIDGING_HEADER = Shared/Headers/iOS-Swift-Widget-Bridging-Header.h
+
+SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+SUPPORTS_MACCATALYST = NO
+
+ENABLE_USER_SCRIPT_SANDBOXING = YES
+
+// Force rebuild when main app changes - helps with incremental build issues
+ALWAYS_SEARCH_USER_PATHS = NO
+
+// Asset Catalog
+ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES
+ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
+ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground
+
+// Swift Settings
+SWIFT_VERSION = 5.0
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000000..0afb3cf0eec
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000000..c70a5bff185
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ },
+ {
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "tinted"
+ }
+ ],
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 00000000000..74d6a722cf3
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 00000000000..0afb3cf0eec
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Entitlements.entitlements b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Entitlements.entitlements
new file mode 100644
index 00000000000..6631ffa6f24
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Entitlements.entitlements
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Info.plist b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Info.plist
new file mode 100644
index 00000000000..0d0089fd4ca
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Resources/Info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ iOS-SwiftUI-Widget
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+ NSHumanReadableCopyright
+
+
+
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleConfigurationAppIntent.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleConfigurationAppIntent.swift
new file mode 100644
index 00000000000..cc265900ed9
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleConfigurationAppIntent.swift
@@ -0,0 +1,7 @@
+import AppIntents
+import WidgetKit
+
+struct SampleConfigurationAppIntent: WidgetConfigurationIntent {
+ static var title: LocalizedStringResource { "Configuration" }
+ static var description: IntentDescription { "This is a Sentry widget." }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidget.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidget.swift
new file mode 100644
index 00000000000..cdf0b1f7450
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidget.swift
@@ -0,0 +1,87 @@
+import Sentry
+import SentrySampleShared
+import SwiftUI
+import WidgetKit
+
+struct SampleWidget: Widget {
+ let kind: String = "SampleWidget"
+
+ var body: some WidgetConfiguration {
+ AppIntentConfiguration(kind: kind, intent: SampleConfigurationAppIntent.self, provider: Provider()) { entry in
+ SampleWidgetEntryView(entry: entry)
+ .containerBackground(.fill.tertiary, for: .widget)
+ }
+ }
+}
+
+fileprivate struct SampleWidgetEntryView: View {
+ var entry: Provider.Entry
+
+ var body: some View {
+ VStack(spacing: 8) {
+ Text("Sentry")
+ .font(.headline)
+
+ Text("SDK Enabled? \(isSentryEnabled ? "✅" : "❌")")
+ .font(.caption)
+ .foregroundColor(isSentryEnabled ? .green : .red)
+ .bold()
+ Text("ANR Disabled? \(!isANRInstalled ? "✅" : "❌")")
+ .font(.caption)
+ .foregroundColor(!isANRInstalled ? .green : .red)
+ .bold()
+
+ Text(entry.date, style: .time)
+ .font(.caption2)
+ }
+ }
+
+ var isANRInstalled: Bool {
+ return isSentryEnabled && SentrySDKInternal.trimmedInstalledIntegrationNames().contains("ANRTracking")
+ }
+
+ var isSentryEnabled: Bool {
+ SentrySDK.isEnabled
+ }
+}
+
+private struct Provider: AppIntentTimelineProvider {
+ func placeholder(in context: Context) -> SimpleEntry {
+ SimpleEntry(date: Date(), configuration: SampleConfigurationAppIntent())
+ }
+
+ func snapshot(for configuration: SampleConfigurationAppIntent, in context: Context) async -> SimpleEntry {
+ setupSentrySDK()
+
+ return SimpleEntry(date: Date(), configuration: configuration)
+ }
+
+ func timeline(for configuration: SampleConfigurationAppIntent, in context: Context) async -> Timeline {
+ setupSentrySDK()
+
+ let entry = SimpleEntry(date: Date(), configuration: configuration)
+ return Timeline(entries: [entry], policy: .atEnd)
+ }
+
+ private func setupSentrySDK() {
+ // Prevent double initialization - SentrySDK.start() can be called multiple times
+ // but we want to avoid unnecessary re-initialization
+ guard !SentrySDK.isEnabled else {
+ return
+ }
+
+ // For this extension we need a specific configuration set, therefore we do not use the shared sample initializer
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+
+ // App Hang Tracking must be enabled, but should not be installed
+ options.enableAppHangTracking = true
+ }
+ }
+}
+
+struct SimpleEntry: TimelineEntry {
+ let date: Date
+ let configuration: SampleConfigurationAppIntent
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidgetBundle.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidgetBundle.swift
new file mode 100644
index 00000000000..a9d653e1355
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidgetBundle.swift
@@ -0,0 +1,10 @@
+import SwiftUI
+import WidgetKit
+
+@main
+struct SampleWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ SampleWidget()
+ SampleWidgetControl()
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidgetControl.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidgetControl.swift
new file mode 100644
index 00000000000..4e5292ce5fe
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets-WidgetExtension/Sources/SampleWidgetControl.swift
@@ -0,0 +1,77 @@
+import AppIntents
+import Sentry
+import SentrySampleShared
+import SwiftUI
+import WidgetKit
+
+struct SampleWidgetControl: ControlWidget {
+ static let kind: String = "SampleWidgetControl"
+
+ init() {
+ setupSentrySDK()
+ }
+
+ private func setupSentrySDK() {
+ guard !SentrySDK.isEnabled else {
+ return
+ }
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+ options.enableAppHangTracking = true
+ }
+ }
+
+ var body: some ControlWidgetConfiguration {
+ AppIntentControlConfiguration(
+ kind: Self.kind,
+ provider: Provider()
+ ) { value in
+ ControlWidgetButton(
+ action: RefreshStatusIntent()
+ ) {
+ Label("ANR Tracking", systemImage: value.isOn ? "checkmark.circle.fill" : "xmark.circle.fill")
+ }
+ }
+ .displayName("Sentry ANR")
+ .description("Refresh ANR status")
+ }
+}
+
+extension SampleWidgetControl {
+ struct Value {
+ var isOn: Bool
+ }
+
+ struct Provider: AppIntentControlValueProvider {
+ func previewValue(configuration: ANRConfiguration) -> Value {
+ Value(isOn: true) // Preview shows checkmark (ANR disabled, which is good)
+ }
+
+ func currentValue(configuration: ANRConfiguration) async throws -> Value {
+ // Check if ANR tracking is installed
+ let anrInstalled = SentrySDK.isEnabled && SentrySDKInternal.trimmedInstalledIntegrationNames()
+ .contains("ANRTracking")
+
+ // isOn = true means ANR is disabled (good for widgets)
+ // isOn = false means ANR is enabled (bad for widgets)
+ return Value(isOn: !anrInstalled)
+ }
+ }
+}
+
+struct ANRConfiguration: ControlConfigurationIntent {
+ static var title: LocalizedStringResource { "ANR Status" }
+ static var description: IntentDescription { "Configure ANR tracking status" }
+}
+
+struct RefreshStatusIntent: AppIntent {
+ static let title: LocalizedStringResource = "Refresh Status"
+
+ init() {}
+
+ func perform() async throws -> some IntentResult {
+ await WidgetCenter.shared.reloadTimelines(ofKind: SampleWidgetControl.kind)
+ return .result()
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.xcconfig b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.xcconfig
new file mode 100644
index 00000000000..b1d8579510c
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.xcconfig
@@ -0,0 +1,29 @@
+#include "../Shared/Config/_Common.xcconfig"
+
+PRODUCT_BUNDLE_IDENTIFIER = io.sentry.sentry-cocoa.samples.iOS-SwiftUI-Widgets
+CODE_SIGN_ENTITLEMENTS = iOS-SwiftUI-Widgets/Resources/Entitlements.entitlements
+
+GENERATE_INFOPLIST_FILE = YES
+INFOPLIST_KEY_NSSupportsLiveActivities = YES
+INFOPLIST_KEY_NSSupportsLiveActivitiesFrequentUpdates = YES
+INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES
+INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES
+INFOPLIST_KEY_UILaunchScreen_Generation = YES
+INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
+INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
+
+IPHONEOS_DEPLOYMENT_TARGET = 18.0
+TARGETED_DEVICE_FAMILY = 1,2
+
+CODE_SIGN_STYLE = Manual
+
+SUPPORTED_PLATFORMS = iphoneos iphonesimulator
+SUPPORTS_MACCATALYST = NO
+
+SWIFT_EMIT_LOC_STRINGS = YES
+SWIFT_VERSION = 5.0
+
+SWIFT_OBJC_BRIDGING_HEADER = Shared/Headers/iOS-Swift-Widget-Bridging-Header.h
+
+ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
+ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.yml b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.yml
new file mode 100644
index 00000000000..225d5cf643e
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets.yml
@@ -0,0 +1,113 @@
+# yaml-language-server: $schema=../../schema/xcodegen.schema.json
+
+name: iOS-SwiftUI-Widgets
+include:
+ - ../Shared/feature-flags.yml
+createIntermediateGroups: true
+generateEmptyDirectories: true
+configs:
+ Debug: debug
+ Release: release
+fileGroups:
+ - ../Shared/Config
+ - iOS-SwiftUI-Widgets.yml
+projectReferences:
+ Sentry:
+ path: ../../Sentry.xcodeproj
+ SentrySampleShared:
+ path: ../SentrySampleShared/SentrySampleShared.xcodeproj
+options:
+ bundleIdPrefix: io.sentry.sentry-cocoa.samples
+targets:
+ iOS-SwiftUI-Widgets:
+ type: application
+ platform: iOS
+ deploymentTarget: "18.0"
+ sources:
+ - iOS-SwiftUI-Widgets
+ - Shared
+ dependencies:
+ - target: Sentry/Sentry
+ - target: SentrySampleShared/SentrySampleShared
+ - target: iOS-SwiftUI-Widgets-WidgetExtension
+ - target: iOS-SwiftUI-Widgets-LiveActivity
+ settings:
+ CODE_SIGN_ENTITLEMENTS: iOS-SwiftUI-Widgets/Resources/Entitlements.entitlements
+ configFiles:
+ Debug: iOS-SwiftUI-Widgets.xcconfig
+ Release: iOS-SwiftUI-Widgets.xcconfig
+ iOS-SwiftUI-Widgets-WidgetExtension:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "18.0"
+ sources:
+ - iOS-SwiftUI-Widgets-WidgetExtension
+ - Shared
+ dependencies:
+ - target: Sentry/Sentry
+ - target: SentrySampleShared/SentrySampleShared
+ settings:
+ CODE_SIGN_ENTITLEMENTS: iOS-SwiftUI-Widgets-WidgetExtension/Resources/Entitlements.entitlements
+ configFiles:
+ Debug: iOS-SwiftUI-Widgets-WidgetExtension.xcconfig
+ Release: iOS-SwiftUI-Widgets-WidgetExtension.xcconfig
+ iOS-SwiftUI-Widgets-LiveActivity:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "18.0"
+ sources:
+ - iOS-SwiftUI-Widgets-LiveActivity
+ - Shared
+ dependencies:
+ - target: Sentry/Sentry
+ - target: SentrySampleShared/SentrySampleShared
+ settings:
+ CODE_SIGN_ENTITLEMENTS: iOS-SwiftUI-Widgets-LiveActivity/Resources/Entitlements.entitlements
+ configFiles:
+ Debug: iOS-SwiftUI-Widgets-LiveActivity.xcconfig
+ Release: iOS-SwiftUI-Widgets-LiveActivity.xcconfig
+schemes:
+ iOS-SwiftUI-Widgets:
+ build:
+ targets:
+ iOS-SwiftUI-Widgets: all
+ iOS-SwiftUI-Widgets-WidgetExtension: all
+ iOS-SwiftUI-Widgets-LiveActivity: all
+ run:
+ config: Debug
+ profile:
+ config: Release
+ archive:
+ config: Release
+ iOS-SwiftUI-Widgets-WidgetExtension:
+ build:
+ targets:
+ iOS-SwiftUI-Widgets: all
+ iOS-SwiftUI-Widgets-WidgetExtension: all
+ run:
+ config: Debug
+ askForAppToLaunch: true
+ environmentVariables:
+ _XCWidgetKind: SampleWidget
+ iOS-SwiftUI-Widgets-WidgetControl:
+ build:
+ targets:
+ iOS-SwiftUI-Widgets: all
+ iOS-SwiftUI-Widgets-WidgetExtension: all
+ run:
+ config: Debug
+ askForAppToLaunch: true
+ environmentVariables:
+ _XCWidgetKind: SampleWidgetControl
+ iOS-SwiftUI-Widgets-LiveActivity:
+ build:
+ targets:
+ iOS-SwiftUI-Widgets: all
+ iOS-SwiftUI-Widgets-WidgetExtension: all
+ iOS-SwiftUI-Widgets-LiveActivity: all
+ run:
+ config: Debug
+ profile:
+ config: Release
+ archive:
+ config: Release
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000000..0afb3cf0eec
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/100.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/100.png
new file mode 100644
index 00000000000..a0f56ef0988
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/100.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/1024.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/1024.png
new file mode 100644
index 00000000000..f8ec579f520
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/1024.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/114.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/114.png
new file mode 100644
index 00000000000..aa0ecf3e8be
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/114.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/120.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/120.png
new file mode 100644
index 00000000000..0528bffcb39
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/120.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/128.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/128.png
new file mode 100644
index 00000000000..fbd4ea41b6d
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/128.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/144.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/144.png
new file mode 100644
index 00000000000..e0ad9d8dea4
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/144.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/152.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/152.png
new file mode 100644
index 00000000000..646e1d8ae12
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/152.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/16.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/16.png
new file mode 100644
index 00000000000..a8a8a1d91fb
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/16.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/167.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/167.png
new file mode 100644
index 00000000000..57c769cc151
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/167.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/180.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/180.png
new file mode 100644
index 00000000000..88026fdcb1c
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/180.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/20.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/20.png
new file mode 100644
index 00000000000..ce759e31089
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/20.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/256.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/256.png
new file mode 100644
index 00000000000..389357dc05f
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/256.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/29.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/29.png
new file mode 100644
index 00000000000..740e807d6da
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/29.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/32.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/32.png
new file mode 100644
index 00000000000..02893dc42b9
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/32.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/40.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/40.png
new file mode 100644
index 00000000000..e3ec4cdb8b3
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/40.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/50.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/50.png
new file mode 100644
index 00000000000..fc053e94835
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/50.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/512.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/512.png
new file mode 100644
index 00000000000..e89fb1f29a3
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/512.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/57.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/57.png
new file mode 100644
index 00000000000..af147e08ee5
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/57.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/58.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/58.png
new file mode 100644
index 00000000000..6bac6975a62
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/58.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/60.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/60.png
new file mode 100644
index 00000000000..fabc2179d85
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/60.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/64.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/64.png
new file mode 100644
index 00000000000..3380f4f1539
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/64.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/72.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/72.png
new file mode 100644
index 00000000000..aa040b9cca1
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/72.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/76.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/76.png
new file mode 100644
index 00000000000..6b322e35e85
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/76.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/80.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/80.png
new file mode 100644
index 00000000000..39aa9562485
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/80.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/87.png b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/87.png
new file mode 100644
index 00000000000..9806fc0eacf
Binary files /dev/null and b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/87.png differ
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000000..1e5e3b49d7a
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,218 @@
+{
+ "images": [
+ {
+ "filename": "40.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "20x20"
+ },
+ {
+ "filename": "60.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "20x20"
+ },
+ {
+ "filename": "29.png",
+ "idiom": "iphone",
+ "scale": "1x",
+ "size": "29x29"
+ },
+ {
+ "filename": "58.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "29x29"
+ },
+ {
+ "filename": "87.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "29x29"
+ },
+ {
+ "filename": "80.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "40x40"
+ },
+ {
+ "filename": "120.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "40x40"
+ },
+ {
+ "filename": "57.png",
+ "idiom": "iphone",
+ "scale": "1x",
+ "size": "57x57"
+ },
+ {
+ "filename": "114.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "57x57"
+ },
+ {
+ "filename": "120.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "60x60"
+ },
+ {
+ "filename": "180.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "60x60"
+ },
+ {
+ "filename": "20.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "20x20"
+ },
+ {
+ "filename": "40.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "20x20"
+ },
+ {
+ "filename": "29.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "29x29"
+ },
+ {
+ "filename": "58.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "29x29"
+ },
+ {
+ "filename": "40.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "40x40"
+ },
+ {
+ "filename": "80.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "40x40"
+ },
+ {
+ "filename": "50.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "50x50"
+ },
+ {
+ "filename": "100.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "50x50"
+ },
+ {
+ "filename": "72.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "72x72"
+ },
+ {
+ "filename": "144.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "72x72"
+ },
+ {
+ "filename": "76.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "76x76"
+ },
+ {
+ "filename": "152.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "76x76"
+ },
+ {
+ "filename": "167.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "83.5x83.5"
+ },
+ {
+ "filename": "1024.png",
+ "idiom": "ios-marketing",
+ "scale": "1x",
+ "size": "1024x1024"
+ },
+ {
+ "filename": "16.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "16x16"
+ },
+ {
+ "filename": "32.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "16x16"
+ },
+ {
+ "filename": "32.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "32x32"
+ },
+ {
+ "filename": "64.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "32x32"
+ },
+ {
+ "filename": "128.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "128x128"
+ },
+ {
+ "filename": "256.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "128x128"
+ },
+ {
+ "filename": "256.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "256x256"
+ },
+ {
+ "filename": "512.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "256x256"
+ },
+ {
+ "filename": "512.png",
+ "idiom": "mac",
+ "scale": "1x",
+ "size": "512x512"
+ },
+ {
+ "filename": "1024.png",
+ "idiom": "mac",
+ "scale": "2x",
+ "size": "512x512"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/Contents.json b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/Contents.json
new file mode 100644
index 00000000000..74d6a722cf3
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Entitlements.entitlements b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Entitlements.entitlements
new file mode 100644
index 00000000000..903def2af53
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Entitlements.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ aps-environment
+ development
+
+
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Info.plist b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Info.plist
new file mode 100644
index 00000000000..456298674e9
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Resources/Info.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ NSSupportsLiveActivities
+
+ NSSupportsLiveActivitiesFrequentUpdates
+
+
+
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/LiveActivityView.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/LiveActivityView.swift
new file mode 100644
index 00000000000..fcce6b4f361
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/LiveActivityView.swift
@@ -0,0 +1,81 @@
+import ActivityKit
+import SwiftUI
+
+struct LiveActivityView: View {
+ @StateObject private var viewModel = LiveActivityViewModel()
+
+ var body: some View {
+ VStack(spacing: 20) {
+ Text("Live Activity Test")
+ .font(.title)
+
+ // Debug info
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Debug Info:")
+ .font(.headline)
+ Text("Activities Enabled: \(ActivityAuthorizationInfo().areActivitiesEnabled ? "Yes" : "No")")
+ .font(.caption)
+ Text("Existing Activities: \(Activity.activities.count)")
+ .font(.caption)
+ }
+ .padding()
+ .background(Color.gray.opacity(0.1))
+ .cornerRadius(8)
+
+ // Simulator note
+ VStack(alignment: .leading, spacing: 4) {
+ Text("ℹ️ Simulator Note:")
+ .font(.headline)
+ .foregroundColor(.blue)
+ Text("Live Activities show on lock screen in simulator. Dynamic Island requires a physical iPhone 14 Pro or later.")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ .padding()
+ .background(Color.blue.opacity(0.1))
+ .cornerRadius(8)
+
+ if let activityViewState = viewModel.activityViewState {
+ VStack(spacing: 10) {
+ Text("✅ Live Activity is running")
+ .font(.headline)
+ .foregroundColor(.green)
+
+ Text(activityViewState.pushToken ?? "No token available")
+ .font(.caption)
+ .foregroundColor(.secondary)
+
+ Text("Lock the simulator to see it on the lock screen")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ .multilineTextAlignment(.center)
+
+ Button("End Activity") {
+ Task {
+ await viewModel.endLiveActivity()
+ }
+ }
+ .buttonStyle(.bordered)
+ }
+ .padding()
+ .background(Color.green.opacity(0.1))
+ .cornerRadius(10)
+ } else {
+ Button("Start Live Activity") {
+ viewModel.startLiveActivity()
+ }
+ .buttonStyle(.borderedProminent)
+ }
+
+ if let errorMessage = viewModel.errorMessage {
+ Text(errorMessage)
+ .foregroundColor(.red)
+ .font(.caption)
+ .padding()
+ .background(Color.red.opacity(0.1))
+ .cornerRadius(8)
+ }
+ }
+ .padding()
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/LiveActivityViewModel.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/LiveActivityViewModel.swift
new file mode 100644
index 00000000000..08ab83b087a
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/LiveActivityViewModel.swift
@@ -0,0 +1,148 @@
+import ActivityKit
+import os.log
+import SwiftUI
+
+@MainActor
+final class LiveActivityViewModel: ObservableObject {
+ struct ActivityViewState: Sendable {
+ var activityState: ActivityState
+ var contentState: LiveActivityAttributes.ContentState
+ var pushToken: String?
+ }
+
+ @Published var activityViewState: ActivityViewState?
+ @Published var errorMessage: String?
+
+ private var currentActivity: Activity?
+
+ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "LiveActivity")
+
+ func startLiveActivity() {
+ logger.info("Starting Live Activity...")
+
+ let authInfo = ActivityAuthorizationInfo()
+ logger.info("Live Activities enabled: \(authInfo.areActivitiesEnabled)")
+
+ guard authInfo.areActivitiesEnabled else {
+ errorMessage = "Live Activities are not enabled. Please enable them in Settings."
+ logger.error("Live Activities are not enabled")
+ return
+ }
+
+ // Check for existing activities
+ let existingActivities = Activity.activities
+ logger.info("Existing activities count: \(existingActivities.count)")
+
+ do {
+ let attributes = LiveActivityAttributes(
+ id: ""
+ )
+ let initialState = LiveActivityAttributes.ContentState(timestamp: Date())
+
+ logger.info("Requesting activity with attributes name: \(attributes.id)")
+
+ let activity = try Activity.request(
+ attributes: attributes,
+ content: .init(state: initialState, staleDate: nil),
+ pushType: .token
+ )
+
+ logger.info("Activity created successfully! ID: \(activity.id), State: \(String(describing: activity.activityState))")
+ logger.info("Activity pushToken initially: \(activity.pushToken?.hexadecimalString ?? "nil")")
+
+ currentActivity = activity
+ self.activityViewState = .init(
+ activityState: activity.activityState,
+ contentState: activity.content.state,
+ pushToken: activity.pushToken?.hexadecimalString
+ )
+ errorMessage = nil
+
+ logger.info("Starting to observe activity updates")
+ observeActivity(activity: activity)
+
+ // Log all current activities for debugging
+ let allActivities = Activity.activities
+ logger.info("Total activities after creation: \(allActivities.count)")
+ for (index, act) in allActivities.enumerated() {
+ logger.info("Activity \(index): ID=\(act.id), State=\(String(describing: act.activityState))")
+ }
+ } catch {
+ let errorDescription = error.localizedDescription
+ logger.error("Failed to start Live Activity: \(errorDescription)")
+ let nsError = error as NSError
+ logger.error("NSError domain: \(nsError.domain), code: \(nsError.code), userInfo: \(nsError.userInfo)")
+ errorMessage = "Failed to start Live Activity: \(errorDescription)"
+ }
+ }
+
+ func observeActivity(activity: Activity) {
+ Task {
+ await withTaskGroup(of: Void.self) { group in
+ group.addTask { @MainActor in
+ for await activityState in activity.activityStateUpdates {
+ self.logger.info("Activity state changed: \(String(describing: activityState))")
+ if activityState == .dismissed {
+ self.logger.info("Activity was dismissed, cleaning up")
+ self.cleanUpDismissedActivity()
+ } else {
+ self.activityViewState?.activityState = activityState
+ self.logger.info("Updated activityViewState.activityState to: \(String(describing: activityState))")
+ }
+ }
+ }
+
+ group.addTask { @MainActor in
+ for await contentState in activity.contentUpdates {
+ self.activityViewState?.contentState = contentState.state
+ }
+ }
+
+ group.addTask { @MainActor in
+ for await pushToken in activity.pushTokenUpdates {
+ let pushTokenString = pushToken.hexadecimalString
+ let frequentUpdateEnabled = ActivityAuthorizationInfo().frequentPushesEnabled
+
+ self.logger.info("Push token received: \(pushTokenString)")
+ self.logger.info("Frequent updates enabled: \(frequentUpdateEnabled)")
+
+ // Update the view state with the push token
+ self.activityViewState?.pushToken = pushTokenString
+ }
+ }
+ }
+ }
+ }
+
+ func cleanUpDismissedActivity() {
+ self.currentActivity = nil
+ self.activityViewState = nil
+ }
+
+ func endLiveActivity() async {
+ guard let activity = currentActivity else {
+ logger.warning("No current activity to end")
+ return
+ }
+
+ logger.info("Ending activity: \(activity.id)")
+
+ let finalState = LiveActivityAttributes.ContentState(timestamp: Date())
+ await activity.end(
+ ActivityContent(state: finalState, staleDate: nil),
+ dismissalPolicy: .immediate
+ )
+
+ cleanUpDismissedActivity()
+
+ logger.info("Activity ended")
+ }
+}
+
+private extension Data {
+ var hexadecimalString: String {
+ self.reduce("") {
+ $0 + String(format: "%02x", $1)
+ }
+ }
+}
diff --git a/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/MainApp.swift b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/MainApp.swift
new file mode 100644
index 00000000000..cc414f48fd8
--- /dev/null
+++ b/Samples/iOS-SwiftUI-Widgets/iOS-SwiftUI-Widgets/Sources/MainApp.swift
@@ -0,0 +1,26 @@
+import Combine
+import Sentry
+import SentrySampleShared
+import SwiftUI
+import UIKit
+import UserNotifications
+
+@main
+struct MainApp: App {
+ init() {
+ // For this sample app we need a specific configuration set, therefore we do not use the shared sample initializer
+ SentrySDK.start { options in
+ options.dsn = SentrySDKWrapper.defaultDSN
+ options.debug = true
+
+ // App Hang Tracking must be enabled, but should not be installed
+ options.enableAppHangTracking = true
+ }
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ LiveActivityView()
+ }
+ }
+}
diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj
index 3d21e31bb4f..c16bc9f225b 100644
--- a/Sentry.xcodeproj/project.pbxproj
+++ b/Sentry.xcodeproj/project.pbxproj
@@ -774,6 +774,8 @@
D452FE6D2DDC873A00AFF56F /* SentryWatchdogTerminationAttributesProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE6C2DDC873900AFF56F /* SentryWatchdogTerminationAttributesProcessorTests.swift */; };
D452FE6F2DDC890A00AFF56F /* TestFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE6E2DDC890A00AFF56F /* TestFileManager.swift */; };
D452FE712DDC8C4400AFF56F /* SentryWatchdogTerminationBreadcrumbProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE702DDC8C4400AFF56F /* SentryWatchdogTerminationBreadcrumbProcessorTests.swift */; };
+ D4563FA92EBA3B73005B33E2 /* SentryExtensionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4563FA72EBA3B73005B33E2 /* SentryExtensionDetector.swift */; };
+ D4563FAA2EBA3B73005B33E2 /* SentryExtensionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4563FA82EBA3B73005B33E2 /* SentryExtensionType.swift */; };
D456B4322D706BDF007068CB /* SentrySpanOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4312D706BDD007068CB /* SentrySpanOperation.h */; };
D456B4362D706BF2007068CB /* SentryTraceOrigin.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4352D706BEE007068CB /* SentryTraceOrigin.h */; };
D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4372D706BFB007068CB /* SentrySpanDataKey.h */; };
@@ -2137,6 +2139,8 @@
D452FE702DDC8C4400AFF56F /* SentryWatchdogTerminationBreadcrumbProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryWatchdogTerminationBreadcrumbProcessorTests.swift; sourceTree = ""; };
D452FE722DDC8DB700AFF56F /* TestSentryWatchdogTerminationAttributesProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryWatchdogTerminationAttributesProcessor.swift; sourceTree = ""; };
D452FE742DDC8DC400AFF56F /* TestSentryWatchdogTerminationBreadcrumbProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryWatchdogTerminationBreadcrumbProcessor.swift; sourceTree = ""; };
+ D4563FA72EBA3B73005B33E2 /* SentryExtensionDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtensionDetector.swift; sourceTree = ""; };
+ D4563FA82EBA3B73005B33E2 /* SentryExtensionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtensionType.swift; sourceTree = ""; };
D456B4312D706BDD007068CB /* SentrySpanOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanOperation.h; path = include/SentrySpanOperation.h; sourceTree = ""; };
D456B4352D706BEE007068CB /* SentryTraceOrigin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTraceOrigin.h; path = include/SentryTraceOrigin.h; sourceTree = ""; };
D456B4372D706BFB007068CB /* SentrySpanDataKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanDataKey.h; path = include/SentrySpanDataKey.h; sourceTree = ""; };
@@ -2675,6 +2679,8 @@
FAEEC04C2E75E55A00E79CA9 /* SentrySerializationSwift.swift */,
FAEEBFE92E74517800E79CA9 /* SentryFileManager.swift */,
FA94E7232E6F32FA00576666 /* SentryEnvelopeItemType.swift */,
+ D4563FA72EBA3B73005B33E2 /* SentryExtensionDetector.swift */,
+ D4563FA82EBA3B73005B33E2 /* SentryExtensionType.swift */,
FA458CBD2E691A6E0061B13D /* SentryProcessInfo.swift */,
F4A930222E65FDAF006DA6EF /* SentryMobileProvisionParser.swift */,
F4FE9DBC2E621F100014FED5 /* SentryRandom.swift */,
@@ -5854,6 +5860,8 @@
15360CED2433A15500112302 /* SentryInstallation.m in Sources */,
FAAB95FF2EA301670030A2DB /* SentryWatchdogTerminationScopeObserver.swift in Sources */,
D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */,
+ D4563FA92EBA3B73005B33E2 /* SentryExtensionDetector.swift in Sources */,
+ D4563FAA2EBA3B73005B33E2 /* SentryExtensionType.swift in Sources */,
63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */,
844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */,
D451ED5F2D92ECDE00C9BEA8 /* SentryReplayFrame.swift in Sources */,
diff --git a/Sentry.xcworkspace/contents.xcworkspacedata b/Sentry.xcworkspace/contents.xcworkspacedata
index df84c70c570..514724c3092 100644
--- a/Sentry.xcworkspace/contents.xcworkspacedata
+++ b/Sentry.xcworkspace/contents.xcworkspacedata
@@ -43,6 +43,9 @@
+
+
diff --git a/SentryTestUtils/Sources/TestInfoPlistWrapper.swift b/SentryTestUtils/Sources/TestInfoPlistWrapper.swift
index 0160f2cef94..6c010b51d32 100644
--- a/SentryTestUtils/Sources/TestInfoPlistWrapper.swift
+++ b/SentryTestUtils/Sources/TestInfoPlistWrapper.swift
@@ -9,6 +9,9 @@ import XCTest
public var getAppValueBooleanInvocations = Invocations<(String, NSErrorPointer)>()
private var mockedGetAppValueBooleanReturnValue: [String: Result] = [:]
+ public var getAppValueDictionaryInvocations = Invocations()
+ private var mockedGetAppValueDictionaryReturnValue: [String: Result<[String: Any], Error>] = [:]
+
public init() {}
public func mockGetAppValueStringReturnValue(forKey key: String, value: String) {
@@ -55,4 +58,26 @@ import XCTest
return false
}
}
+
+ public func mockGetAppValueDictionaryReturnValue(forKey key: String, value: [String: Any]) {
+ mockedGetAppValueDictionaryReturnValue[key] = .success(value)
+ }
+
+ public func mockGetAppValueDictionaryThrowError(forKey key: String, error: Error) {
+ mockedGetAppValueDictionaryReturnValue[key] = .failure(error)
+ }
+
+ public func getAppValueDictionary(for key: String) throws -> [String: Any] {
+ getAppValueDictionaryInvocations.record(key)
+ guard let result = mockedGetAppValueDictionaryReturnValue[key] else {
+ XCTFail("TestInfoPlistWrapper: No mocked return value set for getAppValueDictionary(for:) for key: \(key)")
+ return [:]
+ }
+ switch result {
+ case .success(let value):
+ return value
+ case .failure(let error):
+ throw error
+ }
+ }
}
diff --git a/SentryTestUtilsTests/Sources/TestInfoPlistWrapperTests.swift b/SentryTestUtilsTests/Sources/TestInfoPlistWrapperTests.swift
index de85b628ce5..0e9a9a90d26 100644
--- a/SentryTestUtilsTests/Sources/TestInfoPlistWrapperTests.swift
+++ b/SentryTestUtilsTests/Sources/TestInfoPlistWrapperTests.swift
@@ -1,3 +1,5 @@
+// swiftlint:disable file_length type_body_length
+
@_spi(Private) @testable import Sentry
@_spi(Private) @testable import SentryTestUtils
import XCTest
@@ -279,4 +281,147 @@ class TestInfoPlistWrapperTests: XCTestCase {
XCTAssertFalse(result2, "Should return false for key2")
XCTAssertNil(error2, "Should not set error for key2")
}
+
+ // MARK: - getAppValueDictionary(for:)
+
+ func testGetAppValueDictionary_withoutMockedValue_shouldFail() throws {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ // Don't mock any value for this key
+
+ // -- Act & Assert --
+ XCTExpectFailure("We are expecting a failure when accessing an unmocked key, as it indicates the test setup is incomplete")
+ _ = try sut.getAppValueDictionary(for: "unmockedKey")
+ }
+
+ func testGetAppValueDictionary_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ let expectedDict = ["key1": "value1", "key2": 123] as [String: Any]
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "dictKey", value: expectedDict)
+
+ // -- Act --
+ let result = try sut.getAppValueDictionary(for: "dictKey")
+
+ // -- Assert --
+ XCTAssertEqual(result["key1"] as? String, "value1", "Should return the mocked dictionary")
+ XCTAssertEqual(result["key2"] as? Int, 123, "Should return the mocked dictionary")
+ }
+
+ func testGetAppValueDictionary_withMockedValue_withMultipleInvocations_shouldReturnSameValue() throws {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ let expectedDict = ["test": "value"] as [String: Any]
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key1", value: expectedDict)
+
+ // -- Act --
+ let result1 = try sut.getAppValueDictionary(for: "key1")
+ let result2 = try sut.getAppValueDictionary(for: "key1")
+
+ // -- Assert --
+ XCTAssertEqual(result1["test"] as? String, "value", "First invocation should return mocked value")
+ XCTAssertEqual(result2["test"] as? String, "value", "Second invocation should return same mocked value")
+ }
+
+ func testGetAppValueDictionary_shouldRecordInvocations() throws {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key1", value: ["a": 1])
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key2", value: ["b": 2])
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key3", value: ["c": 3])
+
+ // -- Act --
+ _ = try sut.getAppValueDictionary(for: "key1")
+ _ = try sut.getAppValueDictionary(for: "key2")
+ _ = try sut.getAppValueDictionary(for: "key3")
+
+ // -- Assert --
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.count, 3, "Should record all three invocations")
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 0), "key1", "First invocation should be for key1")
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 1), "key2", "Second invocation should be for key2")
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 2), "key3", "Third invocation should be for key3")
+ }
+
+ func testGetAppValueDictionary_withDifferentKeys_shouldReturnDifferentValues() throws {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key1", value: ["value": "one"])
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key2", value: ["value": "two"])
+
+ // -- Act --
+ let result1 = try sut.getAppValueDictionary(for: "key1")
+ let result2 = try sut.getAppValueDictionary(for: "key2")
+
+ // -- Assert --
+ XCTAssertEqual(result1["value"] as? String, "one", "Should return 'one' for key1")
+ XCTAssertEqual(result2["value"] as? String, "two", "Should return 'two' for key2")
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.count, 2, "Should record both invocations")
+ }
+
+ func testGetAppValueDictionary_withFailureResult_shouldThrowError() {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ sut.mockGetAppValueDictionaryThrowError(forKey: "key", error: SentryInfoPlistError.keyNotFound(key: "testKey"))
+
+ // -- Act & Assert --
+ XCTAssertThrowsError(try sut.getAppValueDictionary(for: "key")) { error in
+ guard case SentryInfoPlistError.keyNotFound(let key) = error else {
+ XCTFail("Expected SentryInfoPlistError.keyNotFound, got \(error)")
+ return
+ }
+ XCTAssertEqual(key, "testKey", "Error should contain the expected key")
+ }
+ }
+
+ func testGetAppValueDictionary_withDifferentErrorTypes_shouldThrowCorrectError() {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+
+ // Test mainInfoPlistNotFound
+ sut.mockGetAppValueDictionaryThrowError(forKey: "key1", error: SentryInfoPlistError.mainInfoPlistNotFound)
+ XCTAssertThrowsError(try sut.getAppValueDictionary(for: "key1")) { error in
+ guard case SentryInfoPlistError.mainInfoPlistNotFound = error else {
+ XCTFail("Expected SentryInfoPlistError.mainInfoPlistNotFound, got \(error)")
+ return
+ }
+ }
+
+ // Test unableToCastValue
+ sut.mockGetAppValueDictionaryThrowError(forKey: "key2", error: SentryInfoPlistError.unableToCastValue(key: "castKey", value: "not a dict", type: [String: Any].self))
+ XCTAssertThrowsError(try sut.getAppValueDictionary(for: "key2")) { error in
+ guard case SentryInfoPlistError.unableToCastValue(let key, let value, let type) = error else {
+ XCTFail("Expected SentryInfoPlistError.unableToCastValue, got \(error)")
+ return
+ }
+ XCTAssertEqual(key, "castKey", "Error should contain the correct key")
+ XCTAssertEqual(value as? String, "not a dict", "Error should contain the correct value")
+ XCTAssertTrue(type == [String: Any].self, "Error should contain the correct type")
+ }
+ }
+
+ func testGetAppValueDictionary_afterThrowingError_shouldRecordInvocation() {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ sut.mockGetAppValueDictionaryThrowError(forKey: "key1", error: SentryInfoPlistError.keyNotFound(key: "testKey"))
+
+ // -- Act --
+ _ = try? sut.getAppValueDictionary(for: "key1")
+
+ // -- Assert --
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.count, 1, "Should record invocation even when throwing error")
+ XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 0), "key1", "Should record the correct key")
+ }
+
+ func testGetAppValueDictionary_withEmptyDictionary_shouldReturnEmptyDictionary() throws {
+ // -- Arrange --
+ let sut = TestInfoPlistWrapper()
+ sut.mockGetAppValueDictionaryReturnValue(forKey: "key", value: [:])
+
+ // -- Act --
+ let result = try sut.getAppValueDictionary(for: "key")
+
+ // -- Assert --
+ XCTAssertTrue(result.isEmpty, "Should return empty dictionary when mocked with empty dictionary")
+ }
}
+// swiftlint:enable file_length type_body_length
diff --git a/Sources/Sentry/SentryANRTrackingIntegration.m b/Sources/Sentry/SentryANRTrackingIntegration.m
index 539546840a3..43a1ad8caaa 100644
--- a/Sources/Sentry/SentryANRTrackingIntegration.m
+++ b/Sources/Sentry/SentryANRTrackingIntegration.m
@@ -42,6 +42,18 @@ - (BOOL)installWithOptions:(SentryOptions *)options
return NO;
}
+ // Disable app hang tracking for Widgets, Live Activities, and certain extensions
+ // where app hang detection might report false positives. These components run
+ // in separate processes or sandboxes with different execution characteristics.
+ SentryExtensionDetector *extensionDetector = SentryDependencies.extensionDetector;
+ if ([extensionDetector shouldDisableAppHangTracking]) {
+ NSString *extensionType = [extensionDetector getExtensionPointIdentifier];
+ SENTRY_LOG_WARN(@"Not enabling app hang tracking for extension: %@", extensionType);
+ [self logWithReason:[NSString stringWithFormat:@"because it's running in an extension (%@)",
+ extensionType]];
+ return NO;
+ }
+
#if SENTRY_HAS_UIKIT
self.tracker =
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval];
diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m
index 40c47c2b59d..5707f7d0738 100644
--- a/Sources/Sentry/SentrySDKInternal.m
+++ b/Sources/Sentry/SentrySDKInternal.m
@@ -750,6 +750,13 @@ + (void)stopProfiler
}
#endif // SENTRY_HAS_UIKIT
+/** Only needed for testing. We can't use `SENTRY_TEST || SENTRY_TEST_CI` because we call this from
+ * the iOS-Swift sample app. */
++ (NSArray *)trimmedInstalledIntegrationNames
+{
+ return [SentrySDKInternal.currentHub trimmedInstalledIntegrationNames];
+}
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h
index beb8ce15d38..db1ec7e56e9 100644
--- a/Sources/Sentry/include/SentryHub+Private.h
+++ b/Sources/Sentry/include/SentryHub+Private.h
@@ -83,6 +83,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)registerSessionListener:(id)listener;
- (void)unregisterSessionListener:(id)listener;
- (nullable id)getInstalledIntegration:(Class)integrationClass;
+- (NSSet *)installedIntegrationNames;
#if SENTRY_TARGET_REPLAY_SUPPORTED
- (NSString *__nullable)getSessionReplayId;
diff --git a/Sources/Swift/Helper/Dependencies.swift b/Sources/Swift/Helper/Dependencies.swift
index 8f1b41eafba..d98c66464f4 100644
--- a/Sources/Swift/Helper/Dependencies.swift
+++ b/Sources/Swift/Helper/Dependencies.swift
@@ -8,6 +8,9 @@
@objc public static let sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentChecker = {
SentrySessionReplayEnvironmentChecker(infoPlistWrapper: Dependencies.infoPlistWrapper)
}()
+ @objc public static var extensionDetector: SentryExtensionDetector = {
+ SentryExtensionDetector(infoPlistWrapper: Dependencies.infoPlistWrapper)
+ }()
@objc public static let dispatchQueueWrapper = SentryDispatchQueueWrapper()
@objc public static let notificationCenterWrapper: SentryNSNotificationCenterWrapper = NotificationCenter.default
@objc public static let crashWrapper = SentryCrashWrapper(processInfoWrapper: Dependencies.processInfoWrapper)
diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift
index 10cf6607acd..f13ca9606ab 100644
--- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift
+++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistKey.swift
@@ -8,8 +8,21 @@ enum SentryInfoPlistKey: String {
///
/// If `NO`, the system uses the UI design of the running OS, with no compatibility mode. Absence of the key, or NO, is the default value for apps linking against the latest SDKs.
///
- /// - Warning: This key is used temporarily while reviewing and refining an app’s UI for the design in the latest SDKs (i.e. Liquid Glass).
+ /// - Warning: This key is used temporarily while reviewing and refining an app's UI for the design in the latest SDKs (i.e. Liquid Glass).
///
/// - SeeAlso: [Apple Documentation](https://developer.apple.com/documentation/BundleResources/Information-Property-List/UIDesignRequiresCompatibility)
case designRequiresCompatibility = "UIDesignRequiresCompatibility"
+
+ /// The extension configuration dictionary for app extensions
+ ///
+ /// - SeeAlso: [Apple Documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsextension)
+ case `extension` = "NSExtension"
+
+ /// Keys within the NSExtension dictionary
+ enum Extension: String {
+ /// The extension point identifier that specifies the type of app extension
+ ///
+ /// - SeeAlso: [Apple Documentation](https://developer.apple.com/documentation/bundleresources/information_property_list/nsextension/nsextensionpointidentifier)
+ case pointIdentifier = "NSExtensionPointIdentifier"
+ }
}
diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift
index 3b0ed9bafd0..675133162fe 100644
--- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift
+++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapper.swift
@@ -28,6 +28,13 @@ final class SentryInfoPlistWrapper: SentryInfoPlistWrapperProvider {
return value
}
+ public func getAppValueDictionary(for key: String) throws -> [String: Any] {
+ guard let value = try getAppValue(for: key, type: [String: Any].self) else {
+ throw SentryInfoPlistError.keyNotFound(key: key)
+ }
+ return value
+ }
+
// MARK: - Swift Implementation
private func getAppValue(for key: String, type: T.Type) throws -> T? {
diff --git a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapperProvider.swift b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapperProvider.swift
index 0ff95929ba9..3c2a70c8541 100644
--- a/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapperProvider.swift
+++ b/Sources/Swift/Helper/InfoPlist/SentryInfoPlistWrapperProvider.swift
@@ -1,27 +1,32 @@
protocol SentryInfoPlistWrapperProvider {
- /**
- * Retrieves a value from the app's `Info.plist` file for the given key and trys to cast it to a ``String``.
- *
- * - Parameter key: The key for which to retrieve the value from the `Info.plist`.
- * - Throws: An error if the value cannot be cast to type ``String`` or ``SentryInfoPlistError.keyNotFound`` if the key was not found or the value is `nil`
- * - Returns: The value associated with the specified key cast to type ``String``
- * - Note: The return value can not be nullable, because a throwing function in Objective-C uses `nil` to indicate an error:
- *
- * Throwing method cannot be a member of an '@objc' protocol because it returns a value of optional type 'String?'; 'nil' indicates failure to Objective-C
- */
+ /// Retrieves a value from the app's `Info.plist` file for the given key and tries to cast it to a `String`.
+ ///
+ /// - Parameter key: The key for which to retrieve the value from the `Info.plist`.
+ /// - Throws: An error if the value cannot be cast to type `String` or `SentryInfoPlistError.keyNotFound` if the key was not found or the value is `nil`
+ /// - Returns: The value associated with the specified key cast to type `String`
+ /// - Note: The return value can not be nullable, because a throwing function in Objective-C uses `nil` to indicate an error:
+ ///
+ /// Throwing method cannot be a member of an '@objc' protocol because it returns a value of optional type 'String?'; 'nil' indicates failure to Objective-C
func getAppValueString(for key: String) throws -> String
- /**
- * Retrieves a value from the app's `Info.plist` file for the given key and trys to cast it to a ``Bool``.
- *
- * - Parameters
- * - key: The key for which to retrieve the value from the `Info.plist`.
- * - error: A pointer to a an `NSError` to return an error value.
- * - Throws: An error if the value cannot be cast to type ``String`` or ``SentryInfoPlistError.keyNotFound`` if the value is `nil`
- * - Returns: The value associated with the specified key cast to type ``String``
- * - Note: This method can not use `throws` because a falsy return value would indicate an error in Objective-C:
- *
- * Throwing method cannot be a member of an '@objc' protocol because it returns a value of type 'Bool'; return 'Void' or a type that bridges to an Objective-C class
- */
+ /// Retrieves a value from the app's `Info.plist` file for the given key and tries to cast it to a `Bool`.
+ ///
+ /// - Parameters:
+ /// - key: The key for which to retrieve the value from the `Info.plist`.
+ /// - errorPtr: A pointer to an `NSError` to return an error value.
+ /// - Returns: The value associated with the specified key cast to type `Bool`
+ /// - Note: This method can not use `throws` because a falsy return value would indicate an error in Objective-C:
+ ///
+ /// Throwing method cannot be a member of an '@objc' protocol because it returns a value of type 'Bool'; return 'Void' or a type that bridges to an Objective-C class
func getAppValueBoolean(for key: String, errorPtr: NSErrorPointer) -> Bool
+
+ /// Retrieves a value from the app's `Info.plist` file for the given key and tries to cast it to a `[String: Any]` dictionary.
+ ///
+ /// - Parameter key: The key for which to retrieve the value from the `Info.plist`.
+ /// - Throws: An error if the value cannot be cast to type `[String: Any]` or `SentryInfoPlistError.keyNotFound` if the key was not found or the value is `nil`
+ /// - Returns: The value associated with the specified key cast to type `[String: Any]`
+ /// - Note: The return value can not be nullable, because a throwing function in Objective-C uses `nil` to indicate an error:
+ ///
+ /// Throwing method cannot be a member of an '@objc' protocol because it returns a value of optional type '[String: Any]?'; 'nil' indicates failure to Objective-C
+ func getAppValueDictionary(for key: String) throws -> [String: Any]
}
diff --git a/Sources/Swift/Helper/SentryExtensionDetector.swift b/Sources/Swift/Helper/SentryExtensionDetector.swift
new file mode 100644
index 00000000000..168c7283bf2
--- /dev/null
+++ b/Sources/Swift/Helper/SentryExtensionDetector.swift
@@ -0,0 +1,54 @@
+import Foundation
+
+@_spi(Private) public final class SentryExtensionDetector: NSObject {
+ /// All extension types where app hang tracking should be disabled
+ private static var disabledAppHangTypes: [SentryExtensionType] {
+ return [
+ .widget,
+ .intent,
+ .action,
+ .share
+ ]
+ }
+
+ private let infoPlistWrapper: SentryInfoPlistWrapperProvider
+
+ init(infoPlistWrapper: SentryInfoPlistWrapperProvider) {
+ self.infoPlistWrapper = infoPlistWrapper
+ super.init()
+ }
+
+ /// Detects if the current process is running in any extension where app hang tracking should be disabled.
+ @objc public func shouldDisableAppHangTracking() -> Bool {
+ guard let extensionPointIdentifier = getExtensionPointIdentifier() else {
+ return false
+ }
+ return Self.disabledAppHangTypes.contains { $0.identifier == extensionPointIdentifier }
+ }
+
+ /// Returns the NSExtensionPointIdentifier from the Bundle's Info.plist, if present.
+ @objc public func getExtensionPointIdentifier() -> String? {
+ do {
+ let extensionDict = try infoPlistWrapper.getAppValueDictionary(
+ for: SentryInfoPlistKey.extension.rawValue
+ )
+ guard let pointIdentifier = extensionDict[SentryInfoPlistKey.Extension.pointIdentifier.rawValue] as? String else {
+ // NSExtensionPointIdentifier not found in NSExtension dictionary
+ return nil
+ }
+ return pointIdentifier
+ } catch SentryInfoPlistError.mainInfoPlistNotFound {
+ // Info.plist not found - not an extension
+ return nil
+ } catch SentryInfoPlistError.keyNotFound {
+ // NSExtension key not found - not an extension
+ return nil
+ } catch SentryInfoPlistError.unableToCastValue(let key, let value, let type) {
+ SentrySDKLog.error("Failed to cast NSExtension value for key '\(key)': \(value) to type \(type)")
+ return nil
+ } catch {
+ SentrySDKLog.error("Unexpected error reading extension info from Info.plist: \(error)")
+ return nil
+ }
+ }
+}
diff --git a/Sources/Swift/Helper/SentryExtensionType.swift b/Sources/Swift/Helper/SentryExtensionType.swift
new file mode 100644
index 00000000000..d61ae50f2bd
--- /dev/null
+++ b/Sources/Swift/Helper/SentryExtensionType.swift
@@ -0,0 +1,26 @@
+/// Extension point identifiers for common iOS extension types
+@_spi(Private) @objc
+public enum SentryExtensionType: Int {
+ /// WidgetKit extensions (includes Widgets and Live Activities)
+ case widget
+ /// Intents extensions
+ case intent
+ /// Action extensions (share, today, etc.)
+ case action
+ /// Share extensions
+ case share
+
+ /// Returns the NSExtensionPointIdentifier string for this extension type
+ public var identifier: String {
+ switch self {
+ case .widget:
+ return "com.apple.widgetkit-extension"
+ case .intent:
+ return "com.apple.intents-service"
+ case .action:
+ return "com.apple.ui-services"
+ case .share:
+ return "com.apple.share-services"
+ }
+ }
+}
diff --git a/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml b/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml
index b95cbb5865f..6da04599440 100644
--- a/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml
+++ b/TestSamples/SwiftUICrashTest/SwiftUICrashTest.yml
@@ -1,3 +1,5 @@
+# yaml-language-server: $schema=../../schema/xcodegen.schema.json
+
name: SwiftUICrashTest
createIntermediateGroups: true
generateEmptyDirectories: true
diff --git a/Tests/SentryTests/Helper/SentryExtensionDetectorTests.swift b/Tests/SentryTests/Helper/SentryExtensionDetectorTests.swift
new file mode 100644
index 00000000000..c1b235b19e8
--- /dev/null
+++ b/Tests/SentryTests/Helper/SentryExtensionDetectorTests.swift
@@ -0,0 +1,250 @@
+@_spi(Private) @testable import Sentry
+@_spi(Private) import SentryTestUtils
+import XCTest
+
+final class SentryExtensionDetectorTests: XCTestCase {
+
+ private var infoPlistWrapper: TestInfoPlistWrapper!
+ private var sut: SentryExtensionDetector!
+
+ override func setUp() {
+ super.setUp()
+ infoPlistWrapper = TestInfoPlistWrapper()
+
+ // Default: NSExtension key not found (not an extension)
+ infoPlistWrapper.mockGetAppValueDictionaryThrowError(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.extension.rawValue)
+ )
+
+ sut = SentryExtensionDetector(infoPlistWrapper: infoPlistWrapper)
+ }
+
+ override func tearDown() {
+ sut = nil
+ infoPlistWrapper = nil
+ super.tearDown()
+ }
+
+ // MARK: - Extension Point Identifier Detection
+
+ func testGetExtensionPointIdentifier_notAnExtension() {
+ // Arrange & Act
+ let extensionPointIdentifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertNil(extensionPointIdentifier, "Non-extension should return nil")
+ }
+
+ func testGetExtensionPointIdentifier_widgetExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.widgetkit-extension"]
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertEqual(identifier, "com.apple.widgetkit-extension")
+ }
+
+ func testGetExtensionPointIdentifier_intentExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.intents-service"]
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertEqual(identifier, "com.apple.intents-service")
+ }
+
+ func testGetExtensionPointIdentifier_actionExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.ui-services"]
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertEqual(identifier, "com.apple.ui-services")
+ }
+
+ func testGetExtensionPointIdentifier_shareExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.share-services"]
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertEqual(identifier, "com.apple.share-services")
+ }
+
+ // MARK: - App Hang Tracking Disable Detection
+
+ func testShouldDisableAppHangTracking_notAnExtension() {
+ // Arrange & Act
+ let shouldDisable = sut.shouldDisableAppHangTracking()
+
+ // Assert
+ XCTAssertFalse(shouldDisable, "Non-extension should not disable app hang tracking")
+ }
+
+ func testShouldDisableAppHangTracking_widgetExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.widgetkit-extension"]
+ )
+
+ // Act
+ let shouldDisable = sut.shouldDisableAppHangTracking()
+
+ // Assert
+ XCTAssertTrue(shouldDisable, "Widget extension should disable app hang tracking")
+ }
+
+ func testShouldDisableAppHangTracking_intentExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.intents-service"]
+ )
+
+ // Act
+ let shouldDisable = sut.shouldDisableAppHangTracking()
+
+ // Assert
+ XCTAssertTrue(shouldDisable, "Intent extension should disable app hang tracking")
+ }
+
+ func testShouldDisableAppHangTracking_actionExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.ui-services"]
+ )
+
+ // Act
+ let shouldDisable = sut.shouldDisableAppHangTracking()
+
+ // Assert
+ XCTAssertTrue(shouldDisable, "Action extension should disable app hang tracking")
+ }
+
+ func testShouldDisableAppHangTracking_shareExtension() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.share-services"]
+ )
+
+ // Act
+ let shouldDisable = sut.shouldDisableAppHangTracking()
+
+ // Assert
+ XCTAssertTrue(shouldDisable, "Share extension should disable app hang tracking")
+ }
+
+ func testShouldDisableAppHangTracking_unknownExtension() {
+ // Arrange - extension with unknown identifier
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.unknown-extension"]
+ )
+
+ // Act
+ let shouldDisable = sut.shouldDisableAppHangTracking()
+
+ // Assert
+ XCTAssertFalse(shouldDisable, "Unknown extension type should not disable app hang tracking")
+ }
+
+ // MARK: - Error Handling Tests
+
+ func testGetExtensionPointIdentifier_withInfoPlistNotFound_returnsNil() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryThrowError(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ error: SentryInfoPlistError.mainInfoPlistNotFound
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertNil(identifier, "Missing Info.plist should return nil")
+ }
+
+ func testGetExtensionPointIdentifier_withKeyNotFound_returnsNil() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryThrowError(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.extension.rawValue)
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertNil(identifier, "Missing NSExtension key should return nil")
+ }
+
+ func testGetExtensionPointIdentifier_withUnableToCast_returnsNil() {
+ // Arrange
+ infoPlistWrapper.mockGetAppValueDictionaryThrowError(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ error: SentryInfoPlistError.unableToCastValue(
+ key: SentryInfoPlistKey.extension.rawValue,
+ value: "not a dictionary",
+ type: [String: Any].self
+ )
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertNil(identifier, "Cast error should return nil")
+ }
+
+ func testGetExtensionPointIdentifier_withMissingPointIdentifier_returnsNil() {
+ // Arrange - NSExtension exists but NSExtensionPointIdentifier is missing
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: [:] // Empty dictionary
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertNil(identifier, "Missing NSExtensionPointIdentifier in dictionary should return nil")
+ }
+
+ func testGetExtensionPointIdentifier_withWrongTypePointIdentifier_returnsNil() {
+ // Arrange - NSExtensionPointIdentifier exists but is wrong type
+ infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": 123] // Integer instead of String
+ )
+
+ // Act
+ let identifier = sut.getExtensionPointIdentifier()
+
+ // Assert
+ XCTAssertNil(identifier, "Wrong type for NSExtensionPointIdentifier should return nil")
+ }
+}
diff --git a/Tests/SentryTests/Helper/SentryExtensionTypeTests.swift b/Tests/SentryTests/Helper/SentryExtensionTypeTests.swift
new file mode 100644
index 00000000000..7f8458fdcc9
--- /dev/null
+++ b/Tests/SentryTests/Helper/SentryExtensionTypeTests.swift
@@ -0,0 +1,20 @@
+@_spi(Private) @testable import Sentry
+import XCTest
+
+final class SentryExtensionTypeTests: XCTestCase {
+ func testWidget_shouldReturnExpectedIdentifier() {
+ XCTAssertEqual(SentryExtensionType.widget.identifier, "com.apple.widgetkit-extension")
+ }
+
+ func testIntent_shouldReturnExpectedIdentifier() {
+ XCTAssertEqual(SentryExtensionType.intent.identifier, "com.apple.intents-service")
+ }
+
+ func testAction_shouldReturnExpectedIdentifier() {
+ XCTAssertEqual(SentryExtensionType.action.identifier, "com.apple.ui-services")
+ }
+
+ func testShare_shouldReturnExpectedIdentifier() {
+ XCTAssertEqual(SentryExtensionType.share.identifier, "com.apple.share-services")
+ }
+}
diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift
index dd9729621e3..408720f1c6b 100644
--- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift
+++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift
@@ -11,6 +11,10 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase {
let currentDate = TestCurrentDateProvider()
let debugImageProvider = TestDebugImageProvider()
+ let infoPlistWrapper = TestInfoPlistWrapper()
+ let dispatchQueueWrapper = TestSentryDispatchQueueWrapper()
+
+ private var originalExtensionDetector: SentryExtensionDetector!
init() {
options = Options()
@@ -20,6 +24,24 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase {
debugImageProvider.debugImages = [TestData.debugImage]
}
+
+ func setUpDI(extensionDetector: SentryExtensionDetector) throws {
+ SentryDependencyContainer.sharedInstance().fileManager = try TestFileManager(
+ options: options,
+ dateProvider: currentDate,
+ dispatchQueueWrapper: dispatchQueueWrapper
+ )
+
+ originalExtensionDetector = Dependencies.extensionDetector
+ Dependencies.extensionDetector = extensionDetector
+ }
+
+ func tearDownDI() throws {
+ SentryDependencyContainer.sharedInstance().fileManager = nil
+ if let extensionDetector = originalExtensionDetector {
+ Dependencies.extensionDetector = extensionDetector
+ }
+ }
}
private var fixture: Fixture!
@@ -33,12 +55,15 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase {
super.setUp()
fixture = Fixture()
- SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = TestSentryDispatchQueueWrapper()
+ SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = fixture.dispatchQueueWrapper
SentryDependencyContainer.sharedInstance().debugImageProvider = fixture.debugImageProvider
}
- override func tearDown() {
+ override func tearDownWithError() throws {
sut?.uninstall()
+
+ try fixture.tearDownDI()
+
clearTestState()
super.tearDown()
}
@@ -721,6 +746,135 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase {
func testEventIsNotANR() {
XCTAssertFalse(Event().isAppHangEvent)
}
+
+ func testInstall_notRunningInExtension_shouldInstall() throws {
+ // Arrange
+ fixture.infoPlistWrapper.mockGetAppValueDictionaryThrowError(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ error: SentryInfoPlistError.keyNotFound(key: SentryInfoPlistKey.extension.rawValue)
+ )
+ try fixture.setUpDI(
+ extensionDetector: SentryExtensionDetector(infoPlistWrapper: fixture.infoPlistWrapper)
+ )
+ crashWrapper.internalIsBeingTraced = false
+
+ sut = SentryANRTrackingIntegration()
+
+ // Act
+ let result = sut.install(with: options)
+
+ // Assert
+ XCTAssertTrue(result, "Should install when not running in an extension")
+ XCTAssertNotNil(Dynamic(sut).tracker.asAnyObject, "Tracker should be initialized")
+ }
+
+ func testInstall_runningInWidgetExtension_shouldNotInstall() throws {
+ // Arrange
+ fixture.infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.widgetkit-extension"]
+ )
+ try fixture.setUpDI(
+ extensionDetector: SentryExtensionDetector(infoPlistWrapper: fixture.infoPlistWrapper)
+ )
+ crashWrapper.internalIsBeingTraced = false
+
+ sut = SentryANRTrackingIntegration()
+
+ // Act
+ let result = sut.install(with: options)
+
+ // Assert
+ XCTAssertFalse(result, "Should not install when running in a Widget extension")
+ XCTAssertNil(Dynamic(sut).tracker.asAnyObject, "Tracker should not be initialized")
+ }
+
+ func testInstall_runningInIntentExtension_shouldNotInstall() throws {
+ // Arrange
+ fixture.infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.intents-service"]
+ )
+ try fixture.setUpDI(
+ extensionDetector: SentryExtensionDetector(infoPlistWrapper: fixture.infoPlistWrapper)
+ )
+ crashWrapper.internalIsBeingTraced = false
+
+ sut = SentryANRTrackingIntegration()
+
+ // Act
+ let result = sut.install(with: options)
+
+ // Assert
+ XCTAssertFalse(result, "Should not install when running in an Intent extension")
+ XCTAssertNil(Dynamic(sut).tracker.asAnyObject, "Tracker should not be initialized")
+ }
+
+ func testInstall_runningInActionExtension_shouldNotInstall() throws {
+ // Arrange
+ fixture.infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.ui-services"]
+ )
+ try fixture.setUpDI(
+ extensionDetector: SentryExtensionDetector(infoPlistWrapper: fixture.infoPlistWrapper)
+ )
+ crashWrapper.internalIsBeingTraced = false
+
+ sut = SentryANRTrackingIntegration()
+
+ // Act
+ let result = sut.install(with: options)
+
+ // Assert
+ XCTAssertFalse(result, "Should not install when running in an Action extension")
+ XCTAssertNil(Dynamic(sut).tracker.asAnyObject, "Tracker should not be initialized")
+ }
+
+ func testInstall_runningInShareExtension_shouldNotInstall() throws {
+ // Arrange
+ fixture.infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.share-services"]
+ )
+ try fixture.setUpDI(
+ extensionDetector: SentryExtensionDetector(infoPlistWrapper: fixture.infoPlistWrapper)
+ )
+ crashWrapper.internalIsBeingTraced = false
+
+ sut = SentryANRTrackingIntegration()
+
+ // Act
+ let result = sut.install(with: options)
+
+ // Assert
+ XCTAssertFalse(result, "Should not install when running in a Share extension")
+ XCTAssertNil(Dynamic(sut).tracker.asAnyObject, "Tracker should not be initialized")
+ }
+
+ func testInstall_runningInUnknownExtension_shouldInstall() throws {
+ // Arrange
+ fixture.infoPlistWrapper.mockGetAppValueDictionaryReturnValue(
+ forKey: SentryInfoPlistKey.extension.rawValue,
+ value: ["NSExtensionPointIdentifier": "com.apple.unknown-extension"]
+ )
+ try fixture.setUpDI(
+ extensionDetector: SentryExtensionDetector(infoPlistWrapper: fixture.infoPlistWrapper)
+ )
+ defer {
+ try? fixture.tearDownDI()
+ }
+ crashWrapper.internalIsBeingTraced = false
+
+ sut = SentryANRTrackingIntegration()
+
+ // Act
+ let result = sut.install(with: options)
+
+ // Assert
+ XCTAssertTrue(result, "Should install when running in an unknown extension type")
+ XCTAssertNotNil(Dynamic(sut).tracker.asAnyObject, "Tracker should be initialized")
+ }
private func givenInitializedTracker(isBeingTraced: Bool = false, crashedLastLaunch: Bool = false) {
givenSdkWithHub()
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 87feaaced8c..901f9041050 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -8,6 +8,8 @@ platform :ios do
ios_swift_infoplist_path = "./Samples/iOS-Swift/iOS-Swift/Info.plist"
ios_swift_clip_infoplist_path = "./Samples/iOS-Swift/iOS-SwiftClip/Info.plist"
ios_swift_share_extension_infoplist_path = "./Samples/iOS-Swift/iOS-Swift-ShareExtension/Info.plist"
+ ios_swift_intent_extension_infoplist_path = "./Samples/iOS-Swift/iOS-Swift-IntentExtension/Info.plist"
+ ios_swift_action_extension_infoplist_path = "./Samples/iOS-Swift/iOS-Swift-ActionExtension/Info.plist"
# Helper method to run tests with common configuration
def run_ui_tests(scheme:, result_bundle_name:, device: nil, address_sanitizer: false)
@@ -73,6 +75,16 @@ platform :ios do
key: "CFBundleShortVersionString",
value: new_version
)
+ set_info_plist_value(
+ path: ios_swift_intent_extension_infoplist_path,
+ key: "CFBundleShortVersionString",
+ value: new_version
+ )
+ set_info_plist_value(
+ path: ios_swift_action_extension_infoplist_path,
+ key: "CFBundleShortVersionString",
+ value: new_version
+ )
sentryInfoPlistPath = "./Sources/Resources/Info.plist"
set_info_plist_value(
@@ -99,7 +111,9 @@ platform :ios do
"io.sentry.iOS-Swift-UITests.xctrunner",
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
- "io.sentry.sample.iOS-Swift.ShareExtension"
+ "io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension"
],
readonly: true
)
@@ -113,7 +127,9 @@ platform :ios do
"io.sentry.iOS-Swift-UITests.xctrunner",
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
- "io.sentry.sample.iOS-Swift.ShareExtension"
+ "io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension"
],
readonly: true
)
@@ -128,7 +144,9 @@ platform :ios do
app_identifier: [
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
- "io.sentry.sample.iOS-Swift.ShareExtension"
+ "io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension"
],
)
@@ -158,7 +176,9 @@ platform :ios do
app_identifier: [
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
- "io.sentry.sample.iOS-Swift.ShareExtension"
+ "io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension"
]
)
@@ -184,6 +204,8 @@ platform :ios do
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
"io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension",
"io.sentry.iOS-Swift-UITests.xctrunner"
],
)
@@ -205,6 +227,8 @@ platform :ios do
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
"io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension",
"io.sentry.iOS-Benchmarking.xctrunner"
],
)
@@ -375,7 +399,9 @@ platform :ios do
"io.sentry.iOS-Swift-UITests.xctrunner",
"io.sentry.sample.iOS-Swift",
"io.sentry.sample.iOS-Swift.Clip",
- "io.sentry.sample.iOS-Swift.ShareExtension"
+ "io.sentry.sample.iOS-Swift.ShareExtension",
+ "io.sentry.sample.iOS-Swift.IntentExtension",
+ "io.sentry.sample.iOS-Swift.ActionExtension"
]
sync_code_signing(
type: "development",
diff --git a/sdk_api.json b/sdk_api.json
index 93b36c1b636..79f901f0517 100644
--- a/sdk_api.json
+++ b/sdk_api.json
@@ -18370,6 +18370,337 @@
"isLet": true,
"hasStorage": true
},
+ {
+ "kind": "TypeDecl",
+ "name": "SentryExtensionType",
+ "printedName": "SentryExtensionType",
+ "children": [
+ {
+ "kind": "Constructor",
+ "name": "init",
+ "printedName": "init(rawValue:)",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "Optional",
+ "printedName": "Sentry.SentryExtensionType?",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ }
+ ],
+ "usr": "s:Sq"
+ },
+ {
+ "kind": "TypeNominal",
+ "name": "Int",
+ "printedName": "Swift.Int",
+ "usr": "s:Si"
+ }
+ ],
+ "declKind": "Constructor",
+ "usr": "s:So19SentryExtensionTypeV8rawValueABSgSi_tcfc",
+ "mangledName": "$sSo19SentryExtensionTypeV8rawValueABSgSi_tcfc",
+ "moduleName": "Sentry",
+ "implicit": true,
+ "init_kind": "Designated"
+ },
+ {
+ "kind": "Var",
+ "name": "rawValue",
+ "printedName": "rawValue",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "Int",
+ "printedName": "Swift.Int",
+ "usr": "s:Si"
+ }
+ ],
+ "declKind": "Var",
+ "usr": "s:So19SentryExtensionTypeV8rawValueSivp",
+ "mangledName": "$sSo19SentryExtensionTypeV8rawValueSivp",
+ "moduleName": "Sentry",
+ "implicit": true,
+ "accessors": [
+ {
+ "kind": "Accessor",
+ "name": "Get",
+ "printedName": "Get()",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "Int",
+ "printedName": "Swift.Int",
+ "usr": "s:Si"
+ }
+ ],
+ "declKind": "Accessor",
+ "usr": "s:So19SentryExtensionTypeV8rawValueSivg",
+ "mangledName": "$sSo19SentryExtensionTypeV8rawValueSivg",
+ "moduleName": "Sentry",
+ "implicit": true,
+ "accessorKind": "get"
+ }
+ ]
+ },
+ {
+ "kind": "TypeAlias",
+ "name": "RawValue",
+ "printedName": "RawValue",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "Int",
+ "printedName": "Swift.Int",
+ "usr": "s:Si"
+ }
+ ],
+ "declKind": "TypeAlias",
+ "usr": "s:So19SentryExtensionTypeV8RawValuea",
+ "mangledName": "$sSo19SentryExtensionTypeV8RawValuea",
+ "moduleName": "Sentry",
+ "implicit": true
+ },
+ {
+ "kind": "Var",
+ "name": "widget",
+ "printedName": "widget",
+ "children": [
+ {
+ "kind": "TypeFunc",
+ "name": "Function",
+ "printedName": "(Sentry.SentryExtensionType.Type) -> Sentry.SentryExtensionType",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ },
+ {
+ "kind": "TypeNominal",
+ "name": "Metatype",
+ "printedName": "Sentry.SentryExtensionType.Type",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "declKind": "EnumElement",
+ "usr": "c:@M@Sentry@E@SentryExtensionType@SentryExtensionTypeWidget",
+ "moduleName": "Sentry",
+ "declAttributes": [
+ "ObjC"
+ ]
+ },
+ {
+ "kind": "Var",
+ "name": "intent",
+ "printedName": "intent",
+ "children": [
+ {
+ "kind": "TypeFunc",
+ "name": "Function",
+ "printedName": "(Sentry.SentryExtensionType.Type) -> Sentry.SentryExtensionType",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ },
+ {
+ "kind": "TypeNominal",
+ "name": "Metatype",
+ "printedName": "Sentry.SentryExtensionType.Type",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "declKind": "EnumElement",
+ "usr": "c:@M@Sentry@E@SentryExtensionType@SentryExtensionTypeIntent",
+ "moduleName": "Sentry",
+ "declAttributes": [
+ "ObjC"
+ ]
+ },
+ {
+ "kind": "Var",
+ "name": "action",
+ "printedName": "action",
+ "children": [
+ {
+ "kind": "TypeFunc",
+ "name": "Function",
+ "printedName": "(Sentry.SentryExtensionType.Type) -> Sentry.SentryExtensionType",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ },
+ {
+ "kind": "TypeNominal",
+ "name": "Metatype",
+ "printedName": "Sentry.SentryExtensionType.Type",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "declKind": "EnumElement",
+ "usr": "c:@M@Sentry@E@SentryExtensionType@SentryExtensionTypeAction",
+ "moduleName": "Sentry",
+ "declAttributes": [
+ "ObjC"
+ ]
+ },
+ {
+ "kind": "Var",
+ "name": "share",
+ "printedName": "share",
+ "children": [
+ {
+ "kind": "TypeFunc",
+ "name": "Function",
+ "printedName": "(Sentry.SentryExtensionType.Type) -> Sentry.SentryExtensionType",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ },
+ {
+ "kind": "TypeNominal",
+ "name": "Metatype",
+ "printedName": "Sentry.SentryExtensionType.Type",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "SentryExtensionType",
+ "printedName": "Sentry.SentryExtensionType",
+ "usr": "c:@M@Sentry@E@SentryExtensionType"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "declKind": "EnumElement",
+ "usr": "c:@M@Sentry@E@SentryExtensionType@SentryExtensionTypeShare",
+ "moduleName": "Sentry",
+ "declAttributes": [
+ "ObjC"
+ ]
+ }
+ ],
+ "declKind": "Enum",
+ "usr": "c:@M@Sentry@E@SentryExtensionType",
+ "moduleName": "Sentry",
+ "objc_name": "SentryExtensionType",
+ "declAttributes": [
+ "SynthesizedProtocol",
+ "ObjC",
+ "SynthesizedProtocol",
+ "Sendable",
+ "Dynamic"
+ ],
+ "enumRawTypeName": "Int",
+ "conformances": [
+ {
+ "kind": "Conformance",
+ "name": "Copyable",
+ "printedName": "Copyable",
+ "usr": "s:s8CopyableP",
+ "mangledName": "$ss8CopyableP"
+ },
+ {
+ "kind": "Conformance",
+ "name": "Escapable",
+ "printedName": "Escapable",
+ "usr": "s:s9EscapableP",
+ "mangledName": "$ss9EscapableP"
+ },
+ {
+ "kind": "Conformance",
+ "name": "RawRepresentable",
+ "printedName": "RawRepresentable",
+ "children": [
+ {
+ "kind": "TypeWitness",
+ "name": "RawValue",
+ "printedName": "RawValue",
+ "children": [
+ {
+ "kind": "TypeNameAlias",
+ "name": "RawValue",
+ "printedName": "Sentry.SentryExtensionType.RawValue",
+ "children": [
+ {
+ "kind": "TypeNominal",
+ "name": "Int",
+ "printedName": "Swift.Int",
+ "usr": "s:Si"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "usr": "s:SY",
+ "mangledName": "$sSY"
+ },
+ {
+ "kind": "Conformance",
+ "name": "Sendable",
+ "printedName": "Sendable",
+ "usr": "s:s8SendableP",
+ "mangledName": "$ss8SendableP"
+ },
+ {
+ "kind": "Conformance",
+ "name": "Equatable",
+ "printedName": "Equatable",
+ "usr": "s:SQ",
+ "mangledName": "$sSQ"
+ },
+ {
+ "kind": "Conformance",
+ "name": "Hashable",
+ "printedName": "Hashable",
+ "usr": "s:SH",
+ "mangledName": "$sSH"
+ }
+ ]
+ },
{
"kind": "TypeDecl",
"name": "SentryFeedbackAPI",