diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6219bee20..95fb3c4d277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- feat: Core data operation in the main thread (#2879) + ### Fixes - Crash when serializing invalid objects (#2858) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 239124673ca..663f4c22920 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -119,18 +119,9 @@ - (instancetype)initWithOptions:(SentryOptions *)options transportAdapter:(SentryTransportAdapter *)transportAdapter { - SentryInAppLogic *inAppLogic = - [[SentryInAppLogic alloc] initWithInAppIncludes:options.inAppIncludes - inAppExcludes:options.inAppExcludes]; - SentryCrashStackEntryMapper *crashStackEntryMapper = - [[SentryCrashStackEntryMapper alloc] initWithInAppLogic:inAppLogic]; - SentryStacktraceBuilder *stacktraceBuilder = - [[SentryStacktraceBuilder alloc] initWithCrashStackEntryMapper:crashStackEntryMapper]; - id machineContextWrapper = - [[SentryCrashDefaultMachineContextWrapper alloc] init]; SentryThreadInspector *threadInspector = - [[SentryThreadInspector alloc] initWithStacktraceBuilder:stacktraceBuilder - andMachineContextWrapper:machineContextWrapper]; + [[SentryThreadInspector alloc] initWithOptions:options]; + SentryExtraContextProvider *extraContextProvider = [SentryExtraContextProvider sharedInstance]; return [self initWithOptions:options diff --git a/Sources/Sentry/SentryCoreDataTracker.m b/Sources/Sentry/SentryCoreDataTracker.m index 98567e4a56d..0d3ee19f749 100644 --- a/Sources/Sentry/SentryCoreDataTracker.m +++ b/Sources/Sentry/SentryCoreDataTracker.m @@ -1,20 +1,30 @@ #import "SentryCoreDataTracker.h" +#import "SentryFrame.h" #import "SentryHub+Private.h" +#import "SentryInternalDefines.h" #import "SentryLog.h" +#import "SentryNSProcessInfoWrapper.h" #import "SentryPredicateDescriptor.h" #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentrySpanProtocol.h" +#import "SentryStacktrace.h" +#import "SentryThreadInspector.h" @implementation SentryCoreDataTracker { SentryPredicateDescriptor *predicateDescriptor; + SentryThreadInspector *_threadInspector; + SentryNSProcessInfoWrapper *_processInfoWrapper; } -- (instancetype)init +- (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector + processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper; { if (self = [super init]) { predicateDescriptor = [[SentryPredicateDescriptor alloc] init]; + _threadInspector = threadInspector; + _processInfoWrapper = processInfoWrapper; } return self; } @@ -47,8 +57,9 @@ - (NSArray *)managedObjectContext:(NSManagedObjectContext *)context NSArray *result = original(request, error); if (fetchSpan) { - [fetchSpan setDataValue:[NSNumber numberWithInteger:result.count] forKey:@"read_count"]; + [self mainThreadExtraInfo:fetchSpan]; + [fetchSpan setDataValue:[NSNumber numberWithInteger:result.count] forKey:@"read_count"]; [fetchSpan finishWithStatus:result == nil ? kSentrySpanStatusInternalError : kSentrySpanStatusOk]; @@ -91,6 +102,7 @@ - (BOOL)managedObjectContext:(NSManagedObjectContext *)context BOOL result = original(error); if (fetchSpan) { + [self mainThreadExtraInfo:fetchSpan]; [fetchSpan finishWithStatus:result ? kSentrySpanStatusOk : kSentrySpanStatusInternalError]; SENTRY_LOG_DEBUG(@"SentryCoreDataTracker automatically finished span with status: %@", @@ -100,6 +112,20 @@ - (BOOL)managedObjectContext:(NSManagedObjectContext *)context return result; } +- (void)mainThreadExtraInfo:(SentrySpan *)span +{ + BOOL isMainThread = [NSThread isMainThread]; + + [span setDataValue:@(isMainThread) forKey:BLOCKED_MAIN_THREAD]; + + if (!isMainThread) { + return; + } + + SentryStacktrace *stackTrace = [_threadInspector stacktraceForCurrentThreadAsyncUnsafe]; + [span setFrames:stackTrace.frames]; +} + - (NSString *)descriptionForOperations: (NSDictionary *> *)operations inContext:(NSManagedObjectContext *)context diff --git a/Sources/Sentry/SentryCoreDataTrackingIntegration.m b/Sources/Sentry/SentryCoreDataTrackingIntegration.m index 5691c672229..937e2d6bb39 100644 --- a/Sources/Sentry/SentryCoreDataTrackingIntegration.m +++ b/Sources/Sentry/SentryCoreDataTrackingIntegration.m @@ -1,9 +1,12 @@ #import "SentryCoreDataTrackingIntegration.h" #import "SentryCoreDataSwizzling.h" #import "SentryCoreDataTracker.h" +#import "SentryDependencyContainer.h" #import "SentryLog.h" #import "SentryNSDataSwizzling.h" +#import "SentryNSProcessInfoWrapper.h" #import "SentryOptions.h" +#import "SentryThreadInspector.h" @interface SentryCoreDataTrackingIntegration () @@ -20,7 +23,9 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } - self.tracker = [[SentryCoreDataTracker alloc] init]; + self.tracker = [[SentryCoreDataTracker alloc] + initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] + processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [SentryCoreDataSwizzling.sharedInstance startWithMiddleware:self.tracker]; return YES; diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 77c0f6f051a..1aa3991f5ba 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -1,6 +1,7 @@ #import "SentryANRTracker.h" #import "SentryDefaultCurrentDateProvider.h" #import "SentryDispatchQueueWrapper.h" +#import "SentryNSProcessInfoWrapper.h" #import "SentryUIApplication.h" #import #import @@ -229,6 +230,18 @@ - (SentryMXManager *)metricKitManager return _metricKitManager; } +- (SentryNSProcessInfoWrapper *)processInfoWrapper +{ + if (_processInfoWrapper == nil) { + @synchronized(sentryDependencyContainerLock) { + if (_processInfoWrapper == nil) { + _processInfoWrapper = [[SentryNSProcessInfoWrapper alloc] init]; + } + } + } + return _processInfoWrapper; +} + #endif @end diff --git a/Sources/Sentry/SentryExtraContextProvider.m b/Sources/Sentry/SentryExtraContextProvider.m index ef34501854f..1ed102cf6dd 100644 --- a/Sources/Sentry/SentryExtraContextProvider.m +++ b/Sources/Sentry/SentryExtraContextProvider.m @@ -1,6 +1,7 @@ #import "SentryExtraContextProvider.h" #import "SentryCrashIntegration.h" #import "SentryCrashWrapper.h" +#import "SentryDependencyContainer.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryUIDeviceWrapper.h" @@ -25,9 +26,10 @@ + (instancetype)sharedInstance - (instancetype)init { - return [self initWithCrashWrapper:[SentryCrashWrapper sharedInstance] - deviceWrapper:[[SentryUIDeviceWrapper alloc] init] - processInfoWrapper:[[SentryNSProcessInfoWrapper alloc] init]]; + return + [self initWithCrashWrapper:[SentryCrashWrapper sharedInstance] + deviceWrapper:[[SentryUIDeviceWrapper alloc] init] + processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; } - (instancetype)initWithCrashWrapper:(id)crashWrapper diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index a54d6459364..c61c4fd7993 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -2,6 +2,7 @@ #import "SentryCrashDefaultMachineContextWrapper.h" #import "SentryCrashMachineContextWrapper.h" #import "SentryCrashStackEntryMapper.h" +#import "SentryDependencyContainer.h" #import "SentryInAppLogic.h" #import "SentryNSDataTracker.h" #import "SentryNSProcessInfoWrapper.h" @@ -32,8 +33,8 @@ + (SentryNSDataSwizzling *)shared - (void)startWithOptions:(SentryOptions *)options { self.dataTracker = [[SentryNSDataTracker alloc] - initWithThreadInspector:[self buildThreadInspectorForOptions:options] - processInfoWrapper:[[SentryNSProcessInfoWrapper alloc] init]]; + initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] + processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [self.dataTracker enable]; [SentryNSDataSwizzling swizzleNSData]; } @@ -43,21 +44,6 @@ - (void)stop [self.dataTracker disable]; } -- (SentryThreadInspector *)buildThreadInspectorForOptions:(SentryOptions *)options -{ - SentryInAppLogic *inAppLogic = - [[SentryInAppLogic alloc] initWithInAppIncludes:options.inAppIncludes - inAppExcludes:options.inAppExcludes]; - SentryCrashStackEntryMapper *crashStackEntryMapper = - [[SentryCrashStackEntryMapper alloc] initWithInAppLogic:inAppLogic]; - SentryStacktraceBuilder *stacktraceBuilder = - [[SentryStacktraceBuilder alloc] initWithCrashStackEntryMapper:crashStackEntryMapper]; - id machineContextWrapper = - [[SentryCrashDefaultMachineContextWrapper alloc] init]; - return [[SentryThreadInspector alloc] initWithStacktraceBuilder:stacktraceBuilder - andMachineContextWrapper:machineContextWrapper]; -} - // SentrySwizzleInstanceMethod declaration shadows a local variable. The swizzling is working // fine and we accept this warning. #pragma clang diagnostic push diff --git a/Sources/Sentry/SentryNSDataTracker.m b/Sources/Sentry/SentryNSDataTracker.m index 100179076ba..0880e159e5f 100644 --- a/Sources/Sentry/SentryNSDataTracker.m +++ b/Sources/Sentry/SentryNSDataTracker.m @@ -5,6 +5,7 @@ #import "SentryFileManager.h" #import "SentryFrame.h" #import "SentryHub+Private.h" +#import "SentryInternalDefines.h" #import "SentryLog.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryOptions.h" @@ -187,7 +188,7 @@ - (void)mainThreadExtraInfo:(id)span { BOOL isMainThread = [NSThread isMainThread]; - [span setDataValue:@(isMainThread) forKey:@"blocked_main_thread"]; + [span setDataValue:@(isMainThread) forKey:BLOCKED_MAIN_THREAD]; if (!isMainThread) { return; @@ -207,7 +208,7 @@ - (void)mainThreadExtraInfo:(id)span // and only the 'main' frame remains in the stack // therefore, there is nothing to do about it // and we should not report it as an issue. - [span setDataValue:@(NO) forKey:@"blocked_main_thread"]; + [span setDataValue:@(NO) forKey:BLOCKED_MAIN_THREAD]; } else { [((SentrySpan *)span) setFrames:frames]; } diff --git a/Sources/Sentry/SentryNSProcessInfoWrapper.mm b/Sources/Sentry/SentryNSProcessInfoWrapper.mm index 5d672f6c7e3..d92bc3bace4 100644 --- a/Sources/Sentry/SentryNSProcessInfoWrapper.mm +++ b/Sources/Sentry/SentryNSProcessInfoWrapper.mm @@ -2,6 +2,14 @@ @implementation SentryNSProcessInfoWrapper ++ (SentryNSProcessInfoWrapper *)shared +{ + static SentryNSProcessInfoWrapper *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); + return instance; +} + - (NSString *)processDirectoryPath { return NSBundle.mainBundle.bundlePath; diff --git a/Sources/Sentry/SentryPerformanceTrackingIntegration.m b/Sources/Sentry/SentryPerformanceTrackingIntegration.m index 632fdb4a527..d525f2a60cb 100644 --- a/Sources/Sentry/SentryPerformanceTrackingIntegration.m +++ b/Sources/Sentry/SentryPerformanceTrackingIntegration.m @@ -1,5 +1,6 @@ #import "SentryPerformanceTrackingIntegration.h" #import "SentryDefaultObjCRuntimeWrapper.h" +#import "SentryDependencyContainer.h" #import "SentryDispatchQueueWrapper.h" #import "SentryLog.h" #import "SentryNSProcessInfoWrapper.h" @@ -40,7 +41,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options dispatchQueue:dispatchQueue objcRuntimeWrapper:[SentryDefaultObjCRuntimeWrapper sharedInstance] subClassFinder:subClassFinder - processInfoWrapper:[[SentryNSProcessInfoWrapper alloc] init]]; + processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [self.swizzling start]; SentryUIViewControllerPerformanceTracker.shared.enableWaitForFullDisplay diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index 32bcf20df94..0a9301761ec 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -543,7 +543,7 @@ - (void)startMetricProfiler _gCurrentSystemWrapper = [[SentrySystemWrapper alloc] init]; } if (_gCurrentProcessInfoWrapper == nil) { - _gCurrentProcessInfoWrapper = [[SentryNSProcessInfoWrapper alloc] init]; + _gCurrentProcessInfoWrapper = [SentryDependencyContainer.sharedInstance processInfoWrapper]; } if (_gCurrentTimerWrapper == nil) { _gCurrentTimerWrapper = [[SentryNSTimerWrapper alloc] init]; diff --git a/Sources/Sentry/SentryThreadInspector.m b/Sources/Sentry/SentryThreadInspector.m index e6829a24673..8e1b66dc235 100644 --- a/Sources/Sentry/SentryThreadInspector.m +++ b/Sources/Sentry/SentryThreadInspector.m @@ -1,8 +1,12 @@ #import "SentryThreadInspector.h" +#import "SentryCrashDefaultMachineContextWrapper.h" #import "SentryCrashStackCursor.h" #include "SentryCrashStackCursor_MachineContext.h" +#import "SentryCrashStackEntryMapper.h" #include "SentryCrashSymbolicator.h" #import "SentryFrame.h" +#import "SentryInAppLogic.h" +#import "SentryOptions.h" #import "SentryStacktrace.h" #import "SentryStacktraceBuilder.h" #import "SentryThread.h" @@ -59,6 +63,21 @@ - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder return self; } +- (instancetype)initWithOptions:(SentryOptions *)options +{ + SentryInAppLogic *inAppLogic = + [[SentryInAppLogic alloc] initWithInAppIncludes:options.inAppIncludes + inAppExcludes:options.inAppExcludes]; + SentryCrashStackEntryMapper *crashStackEntryMapper = + [[SentryCrashStackEntryMapper alloc] initWithInAppLogic:inAppLogic]; + SentryStacktraceBuilder *stacktraceBuilder = + [[SentryStacktraceBuilder alloc] initWithCrashStackEntryMapper:crashStackEntryMapper]; + id machineContextWrapper = + [[SentryCrashDefaultMachineContextWrapper alloc] init]; + return [self initWithStacktraceBuilder:stacktraceBuilder + andMachineContextWrapper:machineContextWrapper]; +} + - (SentryStacktrace *)stacktraceForCurrentThreadAsyncUnsafe { return [self.stacktraceBuilder buildStacktraceForCurrentThreadAsyncUnsafe]; diff --git a/Sources/Sentry/include/SentryCoreDataTracker.h b/Sources/Sentry/include/SentryCoreDataTracker.h index a6967e1ae96..773e56aeffb 100644 --- a/Sources/Sentry/include/SentryCoreDataTracker.h +++ b/Sources/Sentry/include/SentryCoreDataTracker.h @@ -7,7 +7,13 @@ NS_ASSUME_NONNULL_BEGIN static NSString *const SENTRY_COREDATA_FETCH_OPERATION = @"db.sql.query"; static NSString *const SENTRY_COREDATA_SAVE_OPERATION = @"db.sql.transaction"; +@class SentryThreadInspector, SentryNSProcessInfoWrapper; + @interface SentryCoreDataTracker : NSObject +SENTRY_NO_INIT + +- (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector + processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper; @end diff --git a/Sources/Sentry/include/SentryDependencyContainer.h b/Sources/Sentry/include/SentryDependencyContainer.h index ad5905fd98a..49d34bd8f9a 100644 --- a/Sources/Sentry/include/SentryDependencyContainer.h +++ b/Sources/Sentry/include/SentryDependencyContainer.h @@ -4,7 +4,7 @@ @class SentryAppStateManager, SentryCrashWrapper, SentryThreadWrapper, SentrySwizzleWrapper, SentryDispatchQueueWrapper, SentryDebugImageProvider, SentryANRTracker, - SentryNSNotificationCenterWrapper, SentryMXManager; + SentryNSNotificationCenterWrapper, SentryMXManager, SentryNSProcessInfoWrapper; #if SENTRY_HAS_UIKIT @class SentryScreenshot, SentryUIApplication, SentryViewHierarchy; @@ -32,6 +32,7 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentryNSNotificationCenterWrapper *notificationCenterWrapper; @property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider; @property (nonatomic, strong) SentryANRTracker *anrTracker; +@property (nonatomic, strong) SentryNSProcessInfoWrapper *processInfoWrapper; #if SENTRY_HAS_UIKIT @property (nonatomic, strong) SentryScreenshot *screenshot; diff --git a/Sources/Sentry/include/SentryInternalDefines.h b/Sources/Sentry/include/SentryInternalDefines.h index f18f232a38f..5b8ae123250 100644 --- a/Sources/Sentry/include/SentryInternalDefines.h +++ b/Sources/Sentry/include/SentryInternalDefines.h @@ -31,3 +31,5 @@ static NSString *const SentryDebugImageType = @"macho"; } \ (__cond_result); \ }) + +#define BLOCKED_MAIN_THREAD @"blocked_main_thread" diff --git a/Sources/Sentry/include/SentryThreadInspector.h b/Sources/Sentry/include/SentryThreadInspector.h index 77da2bf208f..216aac1ba46 100644 --- a/Sources/Sentry/include/SentryThreadInspector.h +++ b/Sources/Sentry/include/SentryThreadInspector.h @@ -2,7 +2,7 @@ #import "SentryDefines.h" #import -@class SentryThread, SentryStacktraceBuilder, SentryStacktrace; +@class SentryThread, SentryStacktraceBuilder, SentryStacktrace, SentryOptions; NS_ASSUME_NONNULL_BEGIN @@ -12,6 +12,8 @@ SENTRY_NO_INIT - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder andMachineContextWrapper:(id)machineContextWrapper; +- (instancetype)initWithOptions:(SentryOptions *)options; + - (nullable SentryStacktrace *)stacktraceForCurrentThreadAsyncUnsafe; /** diff --git a/Tests/SentryTests/Integrations/Performance/CoreData/SentryCoreDataTrackerTest.swift b/Tests/SentryTests/Integrations/Performance/CoreData/SentryCoreDataTrackerTest.swift index f63ac81b84c..2236b2d310d 100644 --- a/Tests/SentryTests/Integrations/Performance/CoreData/SentryCoreDataTrackerTest.swift +++ b/Tests/SentryTests/Integrations/Performance/CoreData/SentryCoreDataTrackerTest.swift @@ -6,9 +6,18 @@ class SentryCoreDataTrackerTests: XCTestCase { private class Fixture { let context = TestNSManagedObjectContext() + let threadInspector = TestThreadInspector.instance + let imageProvider = TestDebugImageProvider() func getSut() -> SentryCoreDataTracker { - return SentryCoreDataTracker() + imageProvider.debugImages = [TestData.debugImage] + SentryDependencyContainer.sharedInstance().debugImageProvider = imageProvider + + threadInspector.allThreads = [TestData.thread2] + let processInfoWrapper = TestSentryNSProcessInfoWrapper() + processInfoWrapper.overrides.processDirectoryPath = "sentrytest" + + return SentryCoreDataTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) } func testEntity() -> TestEntity { @@ -46,6 +55,17 @@ class SentryCoreDataTrackerTests: XCTestCase { let fetch = NSFetchRequest(entityName: "TestEntity") assertRequest(fetch, expectedDescription: "SELECT 'TestEntity'") } + + func testFetchRequestBackgroundThread() { + let expect = expectation(description: "Operation in background thread") + DispatchQueue.global(qos: .default).async { + let fetch = NSFetchRequest(entityName: "TestEntity") + self.assertRequest(fetch, expectedDescription: "SELECT 'TestEntity'", mainThread: false) + expect.fulfill() + } + + wait(for: [expect], timeout: 0.1) + } func test_FetchRequest_WithPredicate() { let fetch = NSFetchRequest(entityName: "TestEntity") @@ -82,6 +102,17 @@ class SentryCoreDataTrackerTests: XCTestCase { fixture.context.inserted = [fixture.testEntity()] assertSave("INSERTED 1 'TestEntity'") } + + func testSaveBackgroundThread() { + let expect = expectation(description: "Operation in background thread") + DispatchQueue.global(qos: .default).async { + self.fixture.context.inserted = [self.fixture.testEntity()] + self.assertSave("INSERTED 1 'TestEntity'", mainThread: false) + expect.fulfill() + } + + wait(for: [expect], timeout: 0.1) + } func test_Save_2Insert_1Entity() { fixture.context.inserted = [fixture.testEntity(), fixture.testEntity()] @@ -229,7 +260,7 @@ class SentryCoreDataTrackerTests: XCTestCase { XCTAssertEqual(transaction.children.count, 0) } - func assertSave(_ expectedDescription: String) { + func assertSave(_ expectedDescription: String, mainThread: Bool = true) { let sut = fixture.getSut() let transaction = startTransaction() @@ -237,13 +268,27 @@ class SentryCoreDataTrackerTests: XCTestCase { try? sut.saveManagedObjectContext(fixture.context) { _ in return true } - - XCTAssertEqual(transaction.children.count, 1) - XCTAssertEqual(transaction.children[0].operation, SENTRY_COREDATA_SAVE_OPERATION) - XCTAssertEqual(transaction.children[0].spanDescription, expectedDescription) + + guard let dbSpan = try? XCTUnwrap(transaction.children.first) else { + XCTFail("Span for DB operation don't exist.") + return + } + + XCTAssertEqual(dbSpan.operation, SENTRY_COREDATA_SAVE_OPERATION) + XCTAssertEqual(dbSpan.spanDescription, expectedDescription) + XCTAssertEqual(dbSpan.data["blocked_main_thread"] as? Bool ?? false, mainThread) + + if mainThread { + guard let frames = (dbSpan as? SentrySpan)?.frames else { + XCTFail("File IO Span in the main thread has no frames") + return + } + XCTAssertEqual(frames.first, TestData.mainFrame) + XCTAssertEqual(frames.last, TestData.testFrame) + } } - func assertRequest(_ fetch: NSFetchRequest, expectedDescription: String) { + func assertRequest(_ fetch: NSFetchRequest, expectedDescription: String, mainThread: Bool = true) { let transaction = startTransaction() let sut = fixture.getSut() @@ -254,12 +299,28 @@ class SentryCoreDataTrackerTests: XCTestCase { let result = try? sut.fetchManagedObjectContext(context, request: fetch) { _, _ in return [someEntity] } - + + guard let dbSpan = try? XCTUnwrap(transaction.children.first) else { + XCTFail("Span for DB operation don't exist.") + return + } + XCTAssertEqual(result?.count, 1) - XCTAssertEqual(transaction.children.count, 1) - XCTAssertEqual(transaction.children[0].operation, SENTRY_COREDATA_FETCH_OPERATION) - XCTAssertEqual(transaction.children[0].spanDescription, expectedDescription) - XCTAssertEqual(transaction.children[0].data["read_count"] as? Int, 1) + XCTAssertEqual(dbSpan.operation, SENTRY_COREDATA_FETCH_OPERATION) + XCTAssertEqual(dbSpan.spanDescription, expectedDescription) + XCTAssertEqual(dbSpan.data["read_count"] as? Int, 1) + XCTAssertEqual(dbSpan.data["blocked_main_thread"] as? Bool ?? false, mainThread) + + if mainThread { + guard let frames = (dbSpan as? SentrySpan)?.frames else { + XCTFail("File IO Span in the main thread has no frames") + return + } + XCTAssertEqual(frames.first, TestData.mainFrame) + XCTAssertEqual(frames.last, TestData.testFrame) + } else { + XCTAssertNil((dbSpan as? SentrySpan)?.frames) + } } private func startTransaction() -> SentryTracer {