Skip to content

Commit d68023b

Browse files
committed
chore: Disable SDK for widget, intent and live activities
1 parent a7a6331 commit d68023b

21 files changed

+2405
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
If you need a precompiled XCFramework built with Xcode 15, continue using Sentry SDK 8.x.x.
4040
- Set `SentryException.type` to `nil` when `NSException` has no `reason` (#6653). The backend then can provide a proper message when there is no reason.
4141
- Rename `SentryLog.Level` and `SentryLog.Attribute` for ObjC (#6666)
42+
- App hang tracking is now automatically disabled for Widgets, Live Activities, Intent Extensions, and Action Extensions (#6670).
43+
These components run in separate processes or sandboxes with different execution characteristics, which can cause false positive app hang reports.
4244

4345
### Features
4446

Samples/iOS-Swift/iOS-Swift.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# yaml-language-server: $schema=../../schema/xcodegen.schema.json
2+
13
name: iOS-Swift
24
include:
35
- ../Shared/feature-flags.yml
@@ -32,6 +34,7 @@ targets:
3234
- target: iOS-SwiftClip
3335
- target: SentrySampleShared/SentrySampleShared
3436
- target: iOS-Swift-ShareExtension
37+
- target: iOS-Swift-WidgetExtension
3538
- target: Sentry/SentrySwiftLog
3639
configFiles:
3740
Debug: iOS-Swift.xcconfig
@@ -107,6 +110,19 @@ targets:
107110
Release: iOS-Swift-ShareExtension.xcconfig
108111
Test: iOS-Swift-ShareExtension.xcconfig
109112
TestCI: iOS-Swift-ShareExtension.xcconfig
113+
iOS-Swift-WidgetExtension:
114+
type: app-extension
115+
platform: auto
116+
sources:
117+
- iOS-Swift-Widget
118+
dependencies:
119+
- target: Sentry/Sentry
120+
- target: SentrySampleShared/SentrySampleShared
121+
configFiles:
122+
Debug: iOS-Swift-WidgetExtension.xcconfig
123+
Release: iOS-Swift-WidgetExtension.xcconfig
124+
Test: iOS-Swift-WidgetExtension.xcconfig
125+
TestCI: iOS-Swift-WidgetExtension.xcconfig
110126
schemes:
111127
iOS-Swift:
112128
templates:
@@ -127,3 +143,22 @@ schemes:
127143
config: Debug
128144
testPlans:
129145
- path: ../../Plans/iOS-Benchmarking_Base.xctestplan
146+
iOS-Swift-WidgetExtension:
147+
build:
148+
targets:
149+
iOS-Swift: all
150+
iOS-Swift-WidgetExtension: all
151+
run:
152+
config: Debug
153+
askForAppToLaunch: true
154+
launchAutomaticallySubstyle: 2
155+
executable: iOS-Swift
156+
debugEnabled: false
157+
environmentVariables:
158+
_XCWidgetKind: io.sentry.sample.iOS-Swift.iOS-Swift-Widget
159+
_XCWidgetDefaultView: timeline
160+
_XCWidgetFamily: systemMedium
161+
profile:
162+
config: Release
163+
askForAppToLaunch: true
164+
executable: iOS-Swift

Samples/iOS-Swift/iOS-Swift/Tools/SentryExposure.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN
2323

2424
- (nullable SentryClientInternal *)getClient;
2525

26+
- (NSArray<NSString *> *)trimmedInstalledIntegrationNames;
27+
2628
@end
2729

2830
@interface SentrySDKInternal : NSObject
@@ -31,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN
3133

3234
+ (SentryHubInternal *)currentHub;
3335

36+
+ (NSArray<NSString *> *)trimmedInstalledIntegrationNames;
37+
3438
@end
3539

3640
NS_ASSUME_NONNULL_END

Sentry.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,8 @@
786786
D452FE6D2DDC873A00AFF56F /* SentryWatchdogTerminationAttributesProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE6C2DDC873900AFF56F /* SentryWatchdogTerminationAttributesProcessorTests.swift */; };
787787
D452FE6F2DDC890A00AFF56F /* TestFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE6E2DDC890A00AFF56F /* TestFileManager.swift */; };
788788
D452FE712DDC8C4400AFF56F /* SentryWatchdogTerminationBreadcrumbProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D452FE702DDC8C4400AFF56F /* SentryWatchdogTerminationBreadcrumbProcessorTests.swift */; };
789+
D4563FA92EBA3B73005B33E2 /* SentryExtensionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4563FA72EBA3B73005B33E2 /* SentryExtensionDetector.swift */; };
790+
D4563FAA2EBA3B73005B33E2 /* SentryExtensionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4563FA82EBA3B73005B33E2 /* SentryExtensionType.swift */; };
789791
D456B4322D706BDF007068CB /* SentrySpanOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4312D706BDD007068CB /* SentrySpanOperation.h */; };
790792
D456B4362D706BF2007068CB /* SentryTraceOrigin.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4352D706BEE007068CB /* SentryTraceOrigin.h */; };
791793
D456B4382D706BFE007068CB /* SentrySpanDataKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D456B4372D706BFB007068CB /* SentrySpanDataKey.h */; };
@@ -2194,6 +2196,8 @@
21942196
D452FE702DDC8C4400AFF56F /* SentryWatchdogTerminationBreadcrumbProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryWatchdogTerminationBreadcrumbProcessorTests.swift; sourceTree = "<group>"; };
21952197
D452FE722DDC8DB700AFF56F /* TestSentryWatchdogTerminationAttributesProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryWatchdogTerminationAttributesProcessor.swift; sourceTree = "<group>"; };
21962198
D452FE742DDC8DC400AFF56F /* TestSentryWatchdogTerminationBreadcrumbProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryWatchdogTerminationBreadcrumbProcessor.swift; sourceTree = "<group>"; };
2199+
D4563FA72EBA3B73005B33E2 /* SentryExtensionDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtensionDetector.swift; sourceTree = "<group>"; };
2200+
D4563FA82EBA3B73005B33E2 /* SentryExtensionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExtensionType.swift; sourceTree = "<group>"; };
21972201
D456B4312D706BDD007068CB /* SentrySpanOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanOperation.h; path = include/SentrySpanOperation.h; sourceTree = "<group>"; };
21982202
D456B4352D706BEE007068CB /* SentryTraceOrigin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTraceOrigin.h; path = include/SentryTraceOrigin.h; sourceTree = "<group>"; };
21992203
D456B4372D706BFB007068CB /* SentrySpanDataKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySpanDataKey.h; path = include/SentrySpanDataKey.h; sourceTree = "<group>"; };
@@ -2748,6 +2752,8 @@
27482752
FAEEC04C2E75E55A00E79CA9 /* SentrySerializationSwift.swift */,
27492753
FAEEBFE92E74517800E79CA9 /* SentryFileManager.swift */,
27502754
FA94E7232E6F32FA00576666 /* SentryEnvelopeItemType.swift */,
2755+
D4563FA72EBA3B73005B33E2 /* SentryExtensionDetector.swift */,
2756+
D4563FA82EBA3B73005B33E2 /* SentryExtensionType.swift */,
27512757
FA458CBD2E691A6E0061B13D /* SentryProcessInfo.swift */,
27522758
F4A930222E65FDAF006DA6EF /* SentryMobileProvisionParser.swift */,
27532759
F4FE9DBC2E621F100014FED5 /* SentryRandom.swift */,
@@ -6028,6 +6034,8 @@
60286034
15360CED2433A15500112302 /* SentryInstallation.m in Sources */,
60296035
FAAB95FF2EA301670030A2DB /* SentryWatchdogTerminationScopeObserver.swift in Sources */,
60306036
D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */,
6037+
D4563FA92EBA3B73005B33E2 /* SentryExtensionDetector.swift in Sources */,
6038+
D4563FAA2EBA3B73005B33E2 /* SentryExtensionType.swift in Sources */,
60316039
63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */,
60326040
844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */,
60336041
D451ED5F2D92ECDE00C9BEA8 /* SentryReplayFrame.swift in Sources */,

SentryTestUtils/Sources/TestInfoPlistWrapper.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import XCTest
99
public var getAppValueBooleanInvocations = Invocations<(String, NSErrorPointer)>()
1010
private var mockedGetAppValueBooleanReturnValue: [String: Result<Bool, NSError>] = [:]
1111

12+
public var getAppValueDictionaryInvocations = Invocations<String>()
13+
private var mockedGetAppValueDictionaryReturnValue: [String: Result<[String: Any], Error>] = [:]
14+
1215
public init() {}
1316

1417
public func mockGetAppValueStringReturnValue(forKey key: String, value: String) {
@@ -55,4 +58,26 @@ import XCTest
5558
return false
5659
}
5760
}
61+
62+
public func mockGetAppValueDictionaryReturnValue(forKey key: String, value: [String: Any]) {
63+
mockedGetAppValueDictionaryReturnValue[key] = .success(value)
64+
}
65+
66+
public func mockGetAppValueDictionaryThrowError(forKey key: String, error: Error) {
67+
mockedGetAppValueDictionaryReturnValue[key] = .failure(error)
68+
}
69+
70+
public func getAppValueDictionary(for key: String) throws -> [String: Any] {
71+
getAppValueDictionaryInvocations.record(key)
72+
guard let result = mockedGetAppValueDictionaryReturnValue[key] else {
73+
XCTFail("TestInfoPlistWrapper: No mocked return value set for getAppValueDictionary(for:) for key: \(key)")
74+
return [:]
75+
}
76+
switch result {
77+
case .success(let value):
78+
return value
79+
case .failure(let error):
80+
throw error
81+
}
82+
}
5883
}

SentryTestUtilsTests/Sources/TestInfoPlistWrapperTests.swift

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// swiftlint:disable file_length type_body_length
2+
13
@_spi(Private) @testable import Sentry
24
@_spi(Private) @testable import SentryTestUtils
35
import XCTest
@@ -279,4 +281,147 @@ class TestInfoPlistWrapperTests: XCTestCase {
279281
XCTAssertFalse(result2, "Should return false for key2")
280282
XCTAssertNil(error2, "Should not set error for key2")
281283
}
284+
285+
// MARK: - getAppValueDictionary(for:)
286+
287+
func testGetAppValueDictionary_withoutMockedValue_shouldFail() throws {
288+
// -- Arrange --
289+
let sut = TestInfoPlistWrapper()
290+
// Don't mock any value for this key
291+
292+
// -- Act & Assert --
293+
XCTExpectFailure("We are expecting a failure when accessing an unmocked key, as it indicates the test setup is incomplete")
294+
_ = try sut.getAppValueDictionary(for: "unmockedKey")
295+
}
296+
297+
func testGetAppValueDictionary_withMockedValue_withSingleInvocations_shouldReturnMockedValue() throws {
298+
// -- Arrange --
299+
let sut = TestInfoPlistWrapper()
300+
let expectedDict = ["key1": "value1", "key2": 123] as [String: Any]
301+
sut.mockGetAppValueDictionaryReturnValue(forKey: "dictKey", value: expectedDict)
302+
303+
// -- Act --
304+
let result = try sut.getAppValueDictionary(for: "dictKey")
305+
306+
// -- Assert --
307+
XCTAssertEqual(result["key1"] as? String, "value1", "Should return the mocked dictionary")
308+
XCTAssertEqual(result["key2"] as? Int, 123, "Should return the mocked dictionary")
309+
}
310+
311+
func testGetAppValueDictionary_withMockedValue_withMultipleInvocations_shouldReturnSameValue() throws {
312+
// -- Arrange --
313+
let sut = TestInfoPlistWrapper()
314+
let expectedDict = ["test": "value"] as [String: Any]
315+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key1", value: expectedDict)
316+
317+
// -- Act --
318+
let result1 = try sut.getAppValueDictionary(for: "key1")
319+
let result2 = try sut.getAppValueDictionary(for: "key1")
320+
321+
// -- Assert --
322+
XCTAssertEqual(result1["test"] as? String, "value", "First invocation should return mocked value")
323+
XCTAssertEqual(result2["test"] as? String, "value", "Second invocation should return same mocked value")
324+
}
325+
326+
func testGetAppValueDictionary_shouldRecordInvocations() throws {
327+
// -- Arrange --
328+
let sut = TestInfoPlistWrapper()
329+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key1", value: ["a": 1])
330+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key2", value: ["b": 2])
331+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key3", value: ["c": 3])
332+
333+
// -- Act --
334+
_ = try sut.getAppValueDictionary(for: "key1")
335+
_ = try sut.getAppValueDictionary(for: "key2")
336+
_ = try sut.getAppValueDictionary(for: "key3")
337+
338+
// -- Assert --
339+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.count, 3, "Should record all three invocations")
340+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 0), "key1", "First invocation should be for key1")
341+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 1), "key2", "Second invocation should be for key2")
342+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 2), "key3", "Third invocation should be for key3")
343+
}
344+
345+
func testGetAppValueDictionary_withDifferentKeys_shouldReturnDifferentValues() throws {
346+
// -- Arrange --
347+
let sut = TestInfoPlistWrapper()
348+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key1", value: ["value": "one"])
349+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key2", value: ["value": "two"])
350+
351+
// -- Act --
352+
let result1 = try sut.getAppValueDictionary(for: "key1")
353+
let result2 = try sut.getAppValueDictionary(for: "key2")
354+
355+
// -- Assert --
356+
XCTAssertEqual(result1["value"] as? String, "one", "Should return 'one' for key1")
357+
XCTAssertEqual(result2["value"] as? String, "two", "Should return 'two' for key2")
358+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.count, 2, "Should record both invocations")
359+
}
360+
361+
func testGetAppValueDictionary_withFailureResult_shouldThrowError() {
362+
// -- Arrange --
363+
let sut = TestInfoPlistWrapper()
364+
sut.mockGetAppValueDictionaryThrowError(forKey: "key", error: SentryInfoPlistError.keyNotFound(key: "testKey"))
365+
366+
// -- Act & Assert --
367+
XCTAssertThrowsError(try sut.getAppValueDictionary(for: "key")) { error in
368+
guard case SentryInfoPlistError.keyNotFound(let key) = error else {
369+
XCTFail("Expected SentryInfoPlistError.keyNotFound, got \(error)")
370+
return
371+
}
372+
XCTAssertEqual(key, "testKey", "Error should contain the expected key")
373+
}
374+
}
375+
376+
func testGetAppValueDictionary_withDifferentErrorTypes_shouldThrowCorrectError() {
377+
// -- Arrange --
378+
let sut = TestInfoPlistWrapper()
379+
380+
// Test mainInfoPlistNotFound
381+
sut.mockGetAppValueDictionaryThrowError(forKey: "key1", error: SentryInfoPlistError.mainInfoPlistNotFound)
382+
XCTAssertThrowsError(try sut.getAppValueDictionary(for: "key1")) { error in
383+
guard case SentryInfoPlistError.mainInfoPlistNotFound = error else {
384+
XCTFail("Expected SentryInfoPlistError.mainInfoPlistNotFound, got \(error)")
385+
return
386+
}
387+
}
388+
389+
// Test unableToCastValue
390+
sut.mockGetAppValueDictionaryThrowError(forKey: "key2", error: SentryInfoPlistError.unableToCastValue(key: "castKey", value: "not a dict", type: [String: Any].self))
391+
XCTAssertThrowsError(try sut.getAppValueDictionary(for: "key2")) { error in
392+
guard case SentryInfoPlistError.unableToCastValue(let key, let value, let type) = error else {
393+
XCTFail("Expected SentryInfoPlistError.unableToCastValue, got \(error)")
394+
return
395+
}
396+
XCTAssertEqual(key, "castKey", "Error should contain the correct key")
397+
XCTAssertEqual(value as? String, "not a dict", "Error should contain the correct value")
398+
XCTAssertTrue(type == [String: Any].self, "Error should contain the correct type")
399+
}
400+
}
401+
402+
func testGetAppValueDictionary_afterThrowingError_shouldRecordInvocation() {
403+
// -- Arrange --
404+
let sut = TestInfoPlistWrapper()
405+
sut.mockGetAppValueDictionaryThrowError(forKey: "key1", error: SentryInfoPlistError.keyNotFound(key: "testKey"))
406+
407+
// -- Act --
408+
_ = try? sut.getAppValueDictionary(for: "key1")
409+
410+
// -- Assert --
411+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.count, 1, "Should record invocation even when throwing error")
412+
XCTAssertEqual(sut.getAppValueDictionaryInvocations.invocations.element(at: 0), "key1", "Should record the correct key")
413+
}
414+
415+
func testGetAppValueDictionary_withEmptyDictionary_shouldReturnEmptyDictionary() throws {
416+
// -- Arrange --
417+
let sut = TestInfoPlistWrapper()
418+
sut.mockGetAppValueDictionaryReturnValue(forKey: "key", value: [:])
419+
420+
// -- Act --
421+
let result = try sut.getAppValueDictionary(for: "key")
422+
423+
// -- Assert --
424+
XCTAssertTrue(result.isEmpty, "Should return empty dictionary when mocked with empty dictionary")
425+
}
282426
}
427+
// swiftlint:enable file_length type_body_length

Sources/Sentry/SentryANRTrackingIntegration.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ - (BOOL)installWithOptions:(SentryOptions *)options
4343
return NO;
4444
}
4545

46+
// Disable app hang tracking for Widgets, Live Activities, and certain extensions
47+
// where app hang detection might report false positives. These components run
48+
// in separate processes or sandboxes with different execution characteristics.
49+
SentryExtensionDetector *extensionDetector = SentryDependencies.extensionDetector;
50+
if ([extensionDetector shouldDisableAppHangTracking]) {
51+
NSString *extensionType = [extensionDetector getExtensionPointIdentifier];
52+
SENTRY_LOG_WARN(@"Not enabling app hang tracking for extension: %@", extensionType);
53+
[self logWithReason:[NSString stringWithFormat:@"because it's running in an extension (%@)",
54+
extensionType]];
55+
return NO;
56+
}
57+
4658
#if SENTRY_HAS_UIKIT
4759
self.tracker =
4860
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval];

Sources/Sentry/SentrySDKInternal.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,13 @@ + (void)stopProfiler
778778
}
779779
#endif // SENTRY_HAS_UIKIT
780780

781+
/** Only needed for testing. We can't use `SENTRY_TEST || SENTRY_TEST_CI` because we call this from
782+
* the iOS-Swift sample app. */
783+
+ (NSArray<NSString *> *)trimmedInstalledIntegrationNames
784+
{
785+
return [SentrySDKInternal.currentHub trimmedInstalledIntegrationNames];
786+
}
787+
781788
@end
782789

783790
NS_ASSUME_NONNULL_END

Sources/Sentry/include/SentryHub+Private.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ NS_ASSUME_NONNULL_BEGIN
8181
- (void)registerSessionListener:(id<SentrySessionListener>)listener;
8282
- (void)unregisterSessionListener:(id<SentrySessionListener>)listener;
8383
- (nullable id<SentryIntegrationProtocol>)getInstalledIntegration:(Class)integrationClass;
84+
- (NSSet<NSString *> *)installedIntegrationNames;
8485

8586
#if SENTRY_TARGET_REPLAY_SUPPORTED
8687
- (NSString *__nullable)getSessionReplayId;

Sources/Swift/Helper/Dependencies.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
@objc public static let sessionReplayEnvironmentChecker: SentrySessionReplayEnvironmentChecker = {
99
SentrySessionReplayEnvironmentChecker(infoPlistWrapper: Dependencies.infoPlistWrapper)
1010
}()
11+
@objc public static let extensionDetector: SentryExtensionDetector = {
12+
SentryExtensionDetector(infoPlistWrapper: Dependencies.infoPlistWrapper)
13+
}()
1114
@objc public static let dispatchQueueWrapper = SentryDispatchQueueWrapper()
1215
@objc public static let notificationCenterWrapper: SentryNSNotificationCenterWrapper = NotificationCenter.default
1316
@objc public static let crashWrapper = SentryCrashWrapper(processInfoWrapper: Dependencies.processInfoWrapper)

0 commit comments

Comments
 (0)