Skip to content

Commit d975745

Browse files
fix: Crash in Client when reading integrations (#2398)
Fix reading from the integrations list while it's being modified on another thread on the client. Fixes GH-2397
1 parent 89c8109 commit d975745

File tree

10 files changed

+185
-21
lines changed

10 files changed

+185
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Crash in Client when reading integrations (#2398)
78
- Don't update session for dropped events (#2374)
89

910
## 7.31.1

Sentry.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
0A1B497328E597DD00D7BFA3 /* TestLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1B497228E597DD00D7BFA3 /* TestLogOutput.swift */; };
4141
0A1C3592287D7107007D01E3 /* SentryMetaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1C3591287D7107007D01E3 /* SentryMetaTests.swift */; };
4242
0A2690B72885C2E000E4432D /* TestSentryPermissionsObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AABE2EF2885C2120057ED69 /* TestSentryPermissionsObserver.swift */; };
43-
0A2D7BBA29152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */; };
4443
0A283E79291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A283E78291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift */; };
44+
0A2D7BBA29152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */; };
4545
0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D8D5A289815C0008720F6 /* SentryBaseIntegration.m */; };
4646
0A2D8D5D289815EB008720F6 /* SentryBaseIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */; };
4747
0A2D8D8728992260008720F6 /* SentryBaseIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */; };
@@ -466,6 +466,7 @@
466466
7BB654FB253DC14A00887E87 /* SentryUserFeedback.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BB654FA253DC14A00887E87 /* SentryUserFeedback.h */; settings = {ATTRIBUTES = (Public, ); }; };
467467
7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB65500253DC1B500887E87 /* SentryUserFeedback.m */; };
468468
7BB6550D253EEB3900887E87 /* SentryUserFeedbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB6550C253EEB3900887E87 /* SentryUserFeedbackTests.swift */; };
469+
7BB7E7C729267A28004BF96B /* EmptyIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB7E7C629267A28004BF96B /* EmptyIntegration.swift */; };
469470
7BBC826D25DFCFDE005F1ED8 /* SentryInAppLogic.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BBC826C25DFCFDE005F1ED8 /* SentryInAppLogic.h */; };
470471
7BBC827125DFD039005F1ED8 /* SentryInAppLogic.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BBC827025DFD039005F1ED8 /* SentryInAppLogic.m */; };
471472
7BBC827925DFD7D7005F1ED8 /* SentryInAppLogicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBC827825DFD7D7005F1ED8 /* SentryInAppLogicTests.swift */; };
@@ -773,8 +774,8 @@
773774
03F9D37B2819A65C00602916 /* SentryProfilerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryProfilerTests.mm; sourceTree = "<group>"; };
774775
0A1B497228E597DD00D7BFA3 /* TestLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLogOutput.swift; sourceTree = "<group>"; };
775776
0A1C3591287D7107007D01E3 /* SentryMetaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMetaTests.swift; sourceTree = "<group>"; };
776-
0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOutOfMemoryScopeObserverTests.swift; sourceTree = "<group>"; };
777777
0A283E78291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIDeviceWrapperTests.swift; sourceTree = "<group>"; };
778+
0A2D7BB929152CBF008727AF /* SentryOutOfMemoryScopeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOutOfMemoryScopeObserverTests.swift; sourceTree = "<group>"; };
778779
0A2D8D5A289815C0008720F6 /* SentryBaseIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBaseIntegration.m; sourceTree = "<group>"; };
779780
0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryBaseIntegration.h; path = include/SentryBaseIntegration.h; sourceTree = "<group>"; };
780781
0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaseIntegrationTests.swift; sourceTree = "<group>"; };
@@ -1225,6 +1226,7 @@
12251226
7BB654FA253DC14A00887E87 /* SentryUserFeedback.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryUserFeedback.h; path = Public/SentryUserFeedback.h; sourceTree = "<group>"; };
12261227
7BB65500253DC1B500887E87 /* SentryUserFeedback.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryUserFeedback.m; sourceTree = "<group>"; };
12271228
7BB6550C253EEB3900887E87 /* SentryUserFeedbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUserFeedbackTests.swift; sourceTree = "<group>"; };
1229+
7BB7E7C629267A28004BF96B /* EmptyIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyIntegration.swift; sourceTree = "<group>"; };
12281230
7BBC826C25DFCFDE005F1ED8 /* SentryInAppLogic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryInAppLogic.h; path = include/SentryInAppLogic.h; sourceTree = "<group>"; };
12291231
7BBC827025DFD039005F1ED8 /* SentryInAppLogic.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryInAppLogic.m; sourceTree = "<group>"; };
12301232
7BBC827825DFD7D7005F1ED8 /* SentryInAppLogicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInAppLogicTests.swift; sourceTree = "<group>"; };
@@ -2736,6 +2738,7 @@
27362738
7BF1F6AC282A4FC6006BD6AB /* SentryTestObserver.h */,
27372739
7BF1F6AD282A4FE2006BD6AB /* SentryTestObserver.m */,
27382740
7B72D23928D074BC0014798A /* TestExtensions.swift */,
2741+
7BB7E7C629267A28004BF96B /* EmptyIntegration.swift */,
27392742
);
27402743
path = TestUtils;
27412744
sourceTree = "<group>";
@@ -3755,6 +3758,7 @@
37553758
8E70B10125CB8695002B3155 /* SentrySpanIdTests.swift in Sources */,
37563759
7BFE7A0A27A1B6B000D2B66E /* SentryOutOfMemoryIntegrationTests.swift in Sources */,
37573760
7BA61CAF247BBF3C00C130A8 /* SentryDebugImageProviderTests.swift in Sources */,
3761+
7BB7E7C729267A28004BF96B /* EmptyIntegration.swift in Sources */,
37583762
7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */,
37593763
7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */,
37603764
035E73CA27D57398005EEB11 /* SentryThreadHandleTests.mm in Sources */,

Sources/Sentry/SentryClient.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ - (void)setSdk:(SentryEvent *)event
652652
id integrations = event.extra[@"__sentry_sdk_integrations"];
653653
if (!integrations) {
654654
integrations = [NSMutableArray new];
655+
655656
for (NSString *integration in SentrySDK.currentHub.installedIntegrationNames) {
656657
// Every integration starts with "Sentry" and ends with "Integration". To keep the
657658
// payload of the event small we remove both.

Sources/Sentry/SentryHub.m

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@
3131
@property (nonatomic, strong) SentryTracesSampler *tracesSampler;
3232
@property (nonatomic, strong) SentryProfilesSampler *profilesSampler;
3333
@property (nonatomic, strong) id<SentryCurrentDateProvider> currentDateProvider;
34-
@property (nonatomic, strong)
35-
NSMutableArray<NSObject<SentryIntegrationProtocol> *> *installedIntegrations;
36-
@property (nonatomic, strong) NSMutableArray<NSString *> *installedIntegrationNames;
34+
@property (nonatomic, strong) NSMutableArray<id<SentryIntegrationProtocol>> *installedIntegrations;
35+
@property (nonatomic, strong) NSMutableSet<NSString *> *installedIntegrationNames;
3736

3837
@end
3938

4039
@implementation SentryHub {
4140
NSObject *_sessionLock;
41+
NSObject *_integrationsLock;
4242
}
4343

4444
- (instancetype)initWithClient:(nullable SentryClient *)client
@@ -48,8 +48,9 @@ - (instancetype)initWithClient:(nullable SentryClient *)client
4848
_client = client;
4949
_scope = scope;
5050
_sessionLock = [[NSObject alloc] init];
51+
_integrationsLock = [[NSObject alloc] init];
5152
_installedIntegrations = [[NSMutableArray alloc] init];
52-
_installedIntegrationNames = [[NSMutableArray alloc] init];
53+
_installedIntegrationNames = [[NSMutableSet alloc] init];
5354
_crashWrapper = [SentryCrashWrapper sharedInstance];
5455
_tracesSampler = [[SentryTracesSampler alloc] initWithOptions:client.options];
5556
#if SENTRY_TARGET_PROFILING_SUPPORTED
@@ -539,17 +540,53 @@ - (void)configureScope:(void (^)(SentryScope *scope))callback
539540
*/
540541
- (BOOL)isIntegrationInstalled:(Class)integrationClass
541542
{
542-
for (id<SentryIntegrationProtocol> item in self.installedIntegrations) {
543-
if ([item isKindOfClass:integrationClass]) {
544-
return YES;
543+
@synchronized(_integrationsLock) {
544+
for (id<SentryIntegrationProtocol> item in _installedIntegrations) {
545+
if ([item isKindOfClass:integrationClass]) {
546+
return YES;
547+
}
545548
}
549+
return NO;
546550
}
547-
return NO;
548551
}
549552

550553
- (BOOL)hasIntegration:(NSString *)integrationName
551554
{
552-
return [self.installedIntegrationNames containsObject:integrationName];
555+
// installedIntegrations and installedIntegrationNames share the same lock.
556+
// Instead of creating an extra lock object, we use _installedIntegrations.
557+
@synchronized(_integrationsLock) {
558+
return [_installedIntegrationNames containsObject:integrationName];
559+
}
560+
}
561+
562+
- (void)addInstalledIntegration:(id<SentryIntegrationProtocol>)integration name:(NSString *)name
563+
{
564+
@synchronized(_integrationsLock) {
565+
[_installedIntegrations addObject:integration];
566+
[_installedIntegrationNames addObject:name];
567+
}
568+
}
569+
570+
- (void)removeAllIntegrations
571+
{
572+
@synchronized(_integrationsLock) {
573+
[_installedIntegrations removeAllObjects];
574+
[_installedIntegrationNames removeAllObjects];
575+
}
576+
}
577+
578+
- (NSArray<id<SentryIntegrationProtocol>> *)installedIntegrations
579+
{
580+
@synchronized(_integrationsLock) {
581+
return _installedIntegrations.copy;
582+
}
583+
}
584+
585+
- (NSSet<NSString *> *)installedIntegrationNames
586+
{
587+
@synchronized(_integrationsLock) {
588+
return _installedIntegrationNames.copy;
589+
}
553590
}
554591

555592
- (void)setUser:(nullable SentryUser *)user

Sources/Sentry/SentrySDK.m

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,10 @@ + (void)installIntegrations
387387
}
388388
id<SentryIntegrationProtocol> integrationInstance = [[integrationClass alloc] init];
389389
BOOL shouldInstall = [integrationInstance installWithOptions:options];
390+
390391
if (shouldInstall) {
391392
SENTRY_LOG_DEBUG(@"Integration installed: %@", integrationName);
392-
[SentrySDK.currentHub.installedIntegrations addObject:integrationInstance];
393-
[SentrySDK.currentHub.installedIntegrationNames addObject:integrationName];
393+
[SentrySDK.currentHub addInstalledIntegration:integrationInstance name:integrationName];
394394
}
395395
}
396396
}
@@ -407,21 +407,22 @@ + (void)close
407407
{
408408
// pop the hub and unset
409409
SentryHub *hub = SentrySDK.currentHub;
410-
[SentrySDK setCurrentHub:nil];
411410

412411
// uninstall all the integrations
413412
for (NSObject<SentryIntegrationProtocol> *integration in hub.installedIntegrations) {
414413
if ([integration respondsToSelector:@selector(uninstall)]) {
415414
[integration uninstall];
416415
}
417416
}
418-
[hub.installedIntegrations removeAllObjects];
417+
[hub removeAllIntegrations];
419418

420419
// close the client
421420
SentryClient *client = [hub getClient];
422421
client.options.enabled = NO;
423422
[hub bindClient:nil];
424423

424+
[SentrySDK setCurrentHub:nil];
425+
425426
[SentryDependencyContainer reset];
426427

427428
SENTRY_LOG_DEBUG(@"SDK closed!");

Sources/Sentry/include/SentryHub+Private.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ NS_ASSUME_NONNULL_BEGIN
88
@interface
99
SentryHub (Private)
1010

11-
@property (nonatomic, strong)
12-
NSMutableArray<NSObject<SentryIntegrationProtocol> *> *installedIntegrations;
13-
@property (nonatomic, strong) NSMutableArray<NSString *> *installedIntegrationNames;
11+
@property (nonatomic, strong) NSArray<id<SentryIntegrationProtocol>> *installedIntegrations;
12+
@property (nonatomic, strong) NSSet<NSString *> *installedIntegrationNames;
13+
14+
- (void)addInstalledIntegration:(id<SentryIntegrationProtocol>)integration name:(NSString *)name;
15+
- (void)removeAllIntegrations;
1416

1517
- (SentryClient *_Nullable)client;
1618

Tests/SentryTests/SentryClientTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class SentryClientTest: XCTestCase {
3232
let deviceWrapper = TestSentryUIDeviceWrapper()
3333
let locale = Locale(identifier: "en_US")
3434
let timezone = TimeZone(identifier: "Europe/Vienna")!
35+
let queue = DispatchQueue(label: "SentryHubTests", qos: .utility, attributes: [.concurrent])
3536

3637
init() {
3738
session = SentrySession(releaseName: "release")
@@ -1291,6 +1292,41 @@ class SentryClientTest: XCTestCase {
12911292
XCTAssertEqual(item, fixture.transportAdapter.sendEventWithTraceStateInvocations.first?.additionalEnvelopeItems.first)
12921293
}
12931294

1295+
@available(iOS 10.0, tvOS 10.0, OSX 10.12, *)
1296+
func testConcurrentlyAddingInstalledIntegrations_WhileSendingEvents() {
1297+
let sut = fixture.getSut()
1298+
1299+
let hub = SentryHub(client: sut, andScope: nil)
1300+
SentrySDK.setCurrentHub(hub)
1301+
1302+
func addIntegrations(amount: Int) {
1303+
let emptyIntegration = EmptyIntegration()
1304+
for i in 0..<amount {
1305+
hub.addInstalledIntegration(emptyIntegration, name: "Integration\(i)")
1306+
}
1307+
}
1308+
1309+
// So that the loop in Client.setSDK overlaps with addingIntegrations
1310+
addIntegrations(amount: 1_000)
1311+
1312+
let queue = fixture.queue
1313+
let group = DispatchGroup()
1314+
1315+
// Run this in a loop to ensure that add while iterating over the integrations
1316+
// Running it once doesn't guaranty failure
1317+
for _ in 0..<10 {
1318+
group.enter()
1319+
queue.async {
1320+
addIntegrations(amount: 1_000)
1321+
group.leave()
1322+
}
1323+
1324+
sut.capture(event: Event())
1325+
group.waitWithTimeout()
1326+
hub.removeAllIntegrations()
1327+
}
1328+
}
1329+
12941330
private func givenEventWithDebugMeta() -> Event {
12951331
let event = Event(level: SentryLevel.fatal)
12961332
let debugMeta = DebugMeta()

Tests/SentryTests/SentryHubTests.swift

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class SentryHubTests: XCTestCase {
2222
let transactionName = "Some Transaction"
2323
let transactionOperation = "Some Operation"
2424
let random = TestRandom(value: 0.5)
25+
let queue = DispatchQueue(label: "SentryHubTests", qos: .utility, attributes: [.concurrent])
2526

2627
init() {
2728
options = Options()
@@ -673,8 +674,7 @@ class SentryHubTests: XCTestCase {
673674
let sut = fixture.getSut()
674675
sut.startSession()
675676

676-
let queue = DispatchQueue(label: "SentryHubTests", qos: .utility, attributes: [.concurrent])
677-
677+
let queue = fixture.queue
678678
let group = DispatchGroup()
679679
for _ in 0..<count {
680680
group.enter()
@@ -687,6 +687,69 @@ class SentryHubTests: XCTestCase {
687687
group.waitWithTimeout()
688688
}
689689

690+
@available(iOS 10.0, tvOS 10.0, OSX 10.12, *)
691+
func testModifyIntegrationsConcurrently() {
692+
693+
let sut = fixture.getSut()
694+
695+
let outerLoopAmount = 10
696+
let innerLoopAmount = 100
697+
698+
let queue = fixture.queue
699+
let group = DispatchGroup()
700+
701+
for i in 0..<outerLoopAmount {
702+
group.enter()
703+
queue.async {
704+
for j in 0..<innerLoopAmount {
705+
let integrationName = "Integration\(i)\(j)"
706+
sut.addInstalledIntegration(EmptyIntegration(), name: integrationName)
707+
XCTAssertTrue(sut.hasIntegration(integrationName))
708+
}
709+
group.leave()
710+
}
711+
}
712+
713+
group.waitWithTimeout()
714+
715+
XCTAssertEqual(innerLoopAmount * outerLoopAmount, sut.installedIntegrations.count)
716+
XCTAssertEqual(innerLoopAmount * outerLoopAmount, sut.installedIntegrationNames.count)
717+
718+
}
719+
720+
/**
721+
* This test only ensures concurrent modifications don't crash.
722+
*/
723+
@available(iOS 10.0, tvOS 10.0, OSX 10.12, *)
724+
func testModifyIntegrationsConcurrently_NoCrash() {
725+
let sut = fixture.getSut()
726+
727+
let queue = fixture.queue
728+
let group = DispatchGroup()
729+
730+
for i in 0..<1_000 {
731+
group.enter()
732+
queue.async {
733+
for j in 0..<10 {
734+
let integrationName = "Integration\(i)\(j)"
735+
sut.addInstalledIntegration(EmptyIntegration(), name: integrationName)
736+
sut.hasIntegration(integrationName)
737+
sut.isIntegrationInstalled(EmptyIntegration.self)
738+
}
739+
XCTAssertLessThanOrEqual(0, sut.installedIntegrations.count)
740+
sut.installedIntegrations.forEach { XCTAssertNotNil($0) }
741+
742+
XCTAssertLessThanOrEqual(0, sut.installedIntegrationNames.count)
743+
sut.installedIntegrationNames.forEach { XCTAssertNotNil($0) }
744+
sut.removeAllIntegrations()
745+
746+
group.leave()
747+
}
748+
}
749+
750+
group.wait()
751+
}
752+
690753
private func captureEventEnvelope(level: SentryLevel) {
691754
let event = TestData.event
692755
event.level = level

Tests/SentryTests/SentrySDKTests.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ class SentrySDKTests: XCTestCase {
353353
SentrySDK.start(options: options)
354354

355355
assertIntegrationsInstalled(integrations: ["SentryTestIntegration"])
356-
let integration = SentrySDK.currentHub().installedIntegrations.firstObject
356+
let integration = SentrySDK.currentHub().installedIntegrations.first
357357
XCTAssertTrue(integration is SentryTestIntegration)
358358
if let testIntegration = integration as? SentryTestIntegration {
359359
XCTAssertEqual(options.dsn, testIntegration.options.dsn)
@@ -482,6 +482,17 @@ class SentrySDKTests: XCTestCase {
482482
XCTAssertNotEqual(first, second)
483483
}
484484

485+
func testClose_ClearsIntegrations() {
486+
SentrySDK.start { options in
487+
options.dsn = SentrySDKTests.dsnAsString
488+
}
489+
490+
let hub = SentrySDK.currentHub()
491+
SentrySDK.close()
492+
XCTAssertEqual(0, hub.installedIntegrations.count)
493+
assertIntegrationsInstalled(integrations: [])
494+
}
495+
485496
func testFlush_CallsFlushCorrectlyOnTransport() {
486497
SentrySDK.start { options in
487498
options.dsn = SentrySDKTests.dsnAsString
@@ -564,6 +575,7 @@ class SentrySDKTests: XCTestCase {
564575
}
565576

566577
private func assertIntegrationsInstalled(integrations: [String]) {
578+
XCTAssertEqual(integrations.count, SentrySDK.currentHub().installedIntegrations.count)
567579
integrations.forEach { integration in
568580
if let integrationClass = NSClassFromString(integration) {
569581
XCTAssertTrue(SentrySDK.currentHub().isIntegrationInstalled(integrationClass), "\(integration) not installed")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Foundation
2+
3+
class EmptyIntegration: NSObject, SentryIntegrationProtocol {
4+
func install(with options: Options) -> Bool {
5+
return true
6+
}
7+
}

0 commit comments

Comments
 (0)