diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index 10abab1dd1d..b86702ee409 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -1,8 +1,10 @@ #import "SentryProfiler.h" #if SENTRY_TARGET_PROFILING_SUPPORTED +# import "NSDate+SentryExtras.h" # import "SentryBacktrace.hpp" # import "SentryClient+Private.h" +# import "SentryCurrentDate.h" # import "SentryDebugImageProvider.h" # import "SentryDebugMeta.h" # import "SentryDefines.h" @@ -356,9 +358,9 @@ - (void)start const auto queueMetadata = [NSMutableDictionary dictionary]; sampledProfile[@"thread_metadata"] = threadMetadata; sampledProfile[@"queue_metadata"] = queueMetadata; - _profile[@"sampled_profile"] = sampledProfile; + _profile[@"profile"] = sampledProfile; _startTimestamp = getAbsoluteTime(); - _startDate = [NSDate date]; + _startDate = [SentryCurrentDate date]; SENTRY_LOG_DEBUG(@"Starting profiler %@ at system time %llu.", self, _startTimestamp); @@ -378,9 +380,6 @@ - (void)start NSMutableDictionary *metadata = threadMetadata[threadID]; if (metadata == nil) { metadata = [NSMutableDictionary dictionary]; - if (backtrace.threadMetadata.threadID == mainThreadID) { - metadata[@"is_main_thread"] = @YES; - } threadMetadata[threadID] = metadata; } if (!backtrace.threadMetadata.name.empty() && metadata[@"name"] == nil) { @@ -427,7 +426,7 @@ - (void)start } const auto sample = [NSMutableDictionary dictionary]; - sample[@"relative_timestamp_ns"] = + sample[@"elapsed_since_start_ns"] = [@(getDurationNs(strongSelf->_startTimestamp, backtrace.absoluteTimestamp)) stringValue]; sample[@"thread_id"] = threadID; @@ -475,7 +474,7 @@ - (void)stop _profiler->stopSampling(); _endTimestamp = getAbsoluteTime(); - _endDate = [NSDate date]; + _endDate = [SentryCurrentDate date]; SENTRY_LOG_DEBUG(@"Stopped profiler %@ at system time: %llu.", self, _endTimestamp); } } @@ -486,6 +485,7 @@ - (void)captureEnvelope @synchronized(self) { profile = [_profile mutableCopy]; } + profile[@"version"] = @"1"; const auto debugImages = [NSMutableArray *> new]; const auto debugMeta = [_debugImageProvider getDebugImages]; for (SentryDebugMeta *debugImage in debugMeta) { @@ -522,6 +522,9 @@ - (void)captureEnvelope const auto profileDuration = getDurationNs(_startTimestamp, _endTimestamp); profile[@"duration_ns"] = [@(profileDuration) stringValue]; profile[@"truncation_reason"] = profilerTruncationReasonName(_truncationReason); + profile[@"platform"] = _transactions.firstObject.platform; + profile[@"environment"] = _hub.scope.environmentString ?: _hub.getClient.options.environment ?: kSentryDefaultEnvironment; + profile[@"timestamp"] = [[SentryCurrentDate date] sentry_toIso8601String]; const auto bundle = NSBundle.mainBundle; profile[@"release"] = @@ -569,8 +572,8 @@ - (void)captureEnvelope # endif // SENTRY_HAS_UIKIT // populate info from all transactions that occurred while profiler was running - profile[@"platform"] = _transactions.firstObject.platform; auto transactionsInfo = [NSMutableArray array]; + NSString *mainThreadID = [profile[@"profile"][@"samples"] firstObject][@"thread_id"]; for (SentryTransaction *transaction in _transactions) { const auto relativeStart = [NSString stringWithFormat:@"%llu", @@ -585,12 +588,15 @@ - (void)captureEnvelope : (unsigned long long)( [transaction.timestamp timeIntervalSinceDate:_startDate] * 1e9)]; [transactionsInfo addObject:@{ - @"environment" : _hub.scope.environmentString ?: _hub.getClient.options.environment ?: kSentryDefaultEnvironment, @"id" : transaction.eventId.sentryIdString, @"trace_id" : transaction.trace.context.traceId.sentryIdString, @"name" : transaction.transaction, @"relative_start_ns" : relativeStart, - @"relative_end_ns" : relativeEnd + @"relative_end_ns" : relativeEnd, + @"active_thread_id" : + mainThreadID // TODO: we are just using the main thread ID for all transactions to + // fix a backend validation error, but this needs to be gathered from + // transaction starts in their contexts and carried forward to here }]; } profile[@"transactions"] = transactionsInfo; diff --git a/Sources/Sentry/include/NSDate+SentryExtras.h b/Sources/Sentry/include/NSDate+SentryExtras.h index 7993ae072ee..c6257c8c1ec 100644 --- a/Sources/Sentry/include/NSDate+SentryExtras.h +++ b/Sources/Sentry/include/NSDate+SentryExtras.h @@ -5,7 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NSDate (SentryExtras) -+ (NSDate *)sentry_fromIso8601String:(NSString *)string; ++ (NSDate *)sentry_fromIso8601String:(NSString *)string NS_SWIFT_NAME(sentry_from(iso8601String:)); - (NSString *)sentry_toIso8601String; diff --git a/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift b/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift index 8c463522a92..188abdcecba 100644 --- a/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift +++ b/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift @@ -240,6 +240,14 @@ private extension SentryProfilerSwiftTests { func assertValidProfileData(data: Data, transactionEnvironment: String = kSentryDefaultEnvironment, numberOfTransactions: Int = 1, shouldTimeout: Bool = false) { let profile = try! JSONSerialization.jsonObject(with: data) as! [String: Any] + + XCTAssertNotNil(profile["version"]) + if let timestampString = profile["timestamp"] as? String { + XCTAssertNotNil(NSDate.sentry_from(iso8601String: timestampString)) + } else { + XCTFail("Expected a top-level timestamp") + } + let device = profile["device"] as? [String: Any?] XCTAssertNotNil(device) XCTAssertEqual("Apple", device!["manufacturer"] as! String) @@ -259,6 +267,8 @@ private extension SentryProfilerSwiftTests { XCTAssertEqual("cocoa", profile["platform"] as! String) + XCTAssertEqual(transactionEnvironment, profile["environment"] as! String) + let version = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) ?? "(null)" let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") ?? "(null)" let releaseString = "\(version) (\(build))" @@ -275,12 +285,11 @@ private extension SentryProfilerSwiftTests { XCTAssertGreaterThan((firstImage["image_size"] as! Int), 0) XCTAssertEqual(firstImage["type"] as! String, "macho") - let sampledProfile = profile["sampled_profile"] as! [String: Any] + let sampledProfile = profile["profile"] as! [String: Any] let threadMetadata = sampledProfile["thread_metadata"] as! [String: [String: Any]] let queueMetadata = sampledProfile["queue_metadata"] as! [String: Any] XCTAssertFalse(threadMetadata.isEmpty) XCTAssertFalse(threadMetadata.values.compactMap { $0["priority"] }.filter { ($0 as! Int) > 0 }.isEmpty) - XCTAssertFalse(threadMetadata.values.filter { $0["is_main_thread"] as? Bool == true }.isEmpty) XCTAssertFalse(queueMetadata.isEmpty) XCTAssertFalse(((queueMetadata.first?.value as! [String: Any])["label"] as! String).isEmpty) @@ -316,13 +325,15 @@ private extension SentryProfilerSwiftTests { if let traceIDString = transaction["trace_id"] { XCTAssertNotEqual(SentryId.empty, SentryId(uuidString: traceIDString)) } - XCTAssertEqual(transactionEnvironment, transaction["environment"]) XCTAssertNotNil(transaction["trace_id"]) XCTAssertNotNil(transaction["relative_start_ns"]) XCTAssertNotNil(transaction["relative_end_ns"]) + XCTAssertNotNil(transaction["active_thread_id"]) } for sample in samples { + XCTAssertNotNil(sample["elapsed_since_start_ns"] as! String) + XCTAssertNotNil(sample["thread_id"]) XCTAssertNotNil(stacks[sample["stack_id"] as! Int]) }