From 5f821ede2876365599a42ad7a6163e1d5af0a168 Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 11:24:23 +0200 Subject: [PATCH 1/2] Modify DDTraceImplementation to properly nest spans and link them to their parents --- .../ios/Sources/DdTraceImplementation.swift | 57 ++++++++++++++----- packages/core/ios/Tests/DdTraceTests.swift | 28 ++++++--- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/core/ios/Sources/DdTraceImplementation.swift b/packages/core/ios/Sources/DdTraceImplementation.swift index acbc67437..f8073e215 100644 --- a/packages/core/ios/Sources/DdTraceImplementation.swift +++ b/packages/core/ios/Sources/DdTraceImplementation.swift @@ -4,14 +4,20 @@ * Copyright 2016-Present Datadog, Inc. */ -import Foundation import DatadogTrace +import Foundation @objc public class DdTraceImplementation: NSObject { private lazy var tracer: OTTracer = tracerProvider() private let tracerProvider: () -> OTTracer - private(set) var spanDictionary = [NSString: OTSpan]() + + private(set) var spansById: [String: OTSpan] = [:] + private(set) var spanStack: [String] = [] + + private var activeSpan: OTSpan? { + spanStack.last.flatMap { spansById[$0] } + } internal init(_ tracerProvider: @escaping () -> OTTracer) { self.tracerProvider = tracerProvider @@ -23,35 +29,56 @@ public class DdTraceImplementation: NSObject { } @objc - public func startSpan(operation: String, context: NSDictionary, timestampMs: Double, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { - let id = UUID().uuidString as NSString + public func startSpan( + operation: String, context: NSDictionary, timestampMs: Double, + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + + let id = UUID().uuidString let timeIntervalSince1970: TimeInterval = timestampMs / 1_000 let startDate = Date(timeIntervalSince1970: timeIntervalSince1970) - objc_sync_enter(self) let span = tracer.startSpan( operationName: operation, - childOf: nil, + childOf: activeSpan?.context, tags: castAttributesToSwift(context).mergeWithGlobalAttributes(), startTime: startDate ) + span.setActive() - spanDictionary[id] = span - objc_sync_exit(self) + spansById[id] = span + spanStack.append(id) resolve(id) } @objc - public func finishSpan(spanId: NSString, context: NSDictionary, timestampMs: Double, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + public func finishSpan( + spanId: NSString, context: NSDictionary, timestampMs: Double, + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { objc_sync_enter(self) - let optionalSpan = spanDictionary.removeValue(forKey: spanId) - if let span = optionalSpan { - set(tags: castAttributesToSwift(context).mergeWithGlobalAttributes(), to: span) - let timeIntervalSince1970: TimeInterval = timestampMs / 1_000 - span.finish(at: Date(timeIntervalSince1970: timeIntervalSince1970)) + defer { objc_sync_exit(self) } + + guard let span = spansById.removeValue(forKey: spanId as String) else { + resolve(nil) + return + } + + set(tags: castAttributesToSwift(context).mergeWithGlobalAttributes(), to: span) + let timeIntervalSince1970: TimeInterval = timestampMs / 1_000 + span.finish(at: Date(timeIntervalSince1970: timeIntervalSince1970)) + + if let idx = spanStack.lastIndex(of: spanId as String) { + let wasTop = (idx == spanStack.count - 1) + spanStack.remove(at: idx) + + if wasTop, let prev = activeSpan { + prev.setActive() + } } - objc_sync_exit(self) resolve(nil) } diff --git a/packages/core/ios/Tests/DdTraceTests.swift b/packages/core/ios/Tests/DdTraceTests.swift index 5e670d02b..cce1c0726 100644 --- a/packages/core/ios/Tests/DdTraceTests.swift +++ b/packages/core/ios/Tests/DdTraceTests.swift @@ -90,10 +90,11 @@ internal class DdTraceTests: XCTestCase { resolve: mockResolve, reject: mockReject ) - let spanID = lastResolveValue as! NSString + let spanID = lastResolveValue as! String XCTAssertNotNil(spanID) - XCTAssertEqual(Array(tracer.spanDictionary.keys), [spanID]) + XCTAssertEqual(Array(tracer.spansById.keys), [spanID]) + XCTAssertEqual(Array(tracer.spanStack), [spanID]) let startedSpan = try XCTUnwrap(mockNativeTracer.startedSpans.last) XCTAssertEqual(startedSpan.finishTime, MockSpan.unfinished) @@ -101,9 +102,10 @@ internal class DdTraceTests: XCTestCase { let spanDurationMs = spanDuration * 1_000 let finishTimestampMs = timestampMs + spanDurationMs let finishingContext = NSDictionary(dictionary: ["last_key": "last_value"]) - tracer.finishSpan(spanId: spanID, context: finishingContext, timestampMs: finishTimestampMs, resolve: mockResolve, reject: mockReject) + tracer.finishSpan(spanId: spanID as NSString, context: finishingContext, timestampMs: finishTimestampMs, resolve: mockResolve, reject: mockReject) - XCTAssertEqual(Array(tracer.spanDictionary.keys), []) + XCTAssertEqual(Array(tracer.spansById.keys), []) + XCTAssertEqual(Array(tracer.spanStack), []) XCTAssertEqual( startedSpan.finishTime!.timeIntervalSince1970, // swiftlint:disable:this force_unwrapping (startDate + spanDuration).timeIntervalSince1970, @@ -124,7 +126,8 @@ internal class DdTraceTests: XCTestCase { reject: mockReject ) - XCTAssertEqual(tracer.spanDictionary.count, 1) + XCTAssertEqual(tracer.spansById.count, 1) + XCTAssertEqual(tracer.spanStack.count, 1) XCTAssertNoThrow( tracer.finishSpan( @@ -136,7 +139,8 @@ internal class DdTraceTests: XCTestCase { ) ) - XCTAssertEqual(tracer.spanDictionary.count, 1) + XCTAssertEqual(tracer.spansById.count, 1) + XCTAssertEqual(tracer.spanStack.count, 1) } func testTracingConcurrently() { @@ -158,7 +162,8 @@ internal class DdTraceTests: XCTestCase { } XCTAssertEqual(mockNativeTracer.startedSpans.count, iterationCount, "\(mockNativeTracer.startedSpans)") - XCTAssertEqual(tracer.spanDictionary.count, 0) + XCTAssertEqual(tracer.spansById.count, 0) + XCTAssertEqual(tracer.spanStack.count, 0) } } @@ -205,8 +210,15 @@ private class MockSpan: OTSpan { self.finishTime = time } + private final class Ctx: OTSpanContext { + func forEachBaggageItem(callback: (String, String) -> Bool) { + // No baggage in tests → do nothing + } + } + // swiftlint:disable unavailable_function - var context: OTSpanContext { fatalError("Should not be called") } + var context: OTSpanContext = Ctx() + func tracer() -> OTTracer { fatalError("Should not be called") } From 539de5dc7740e3baabdb30a0cd2cdef077681f4d Mon Sep 17 00:00:00 2001 From: Sergio Barrio Date: Fri, 12 Sep 2025 11:25:08 +0200 Subject: [PATCH 2/2] Update example app to add a Trace testing section --- example/src/WixApp.tsx | 14 ++++++++++- example/src/screens/TraceScreen.tsx | 37 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 example/src/screens/TraceScreen.tsx diff --git a/example/src/WixApp.tsx b/example/src/WixApp.tsx index 285982295..55e93f6e4 100644 --- a/example/src/WixApp.tsx +++ b/example/src/WixApp.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { View, Text, Button } from 'react-native'; import MainScreen from './screens/MainScreen'; import ErrorScreen from './screens/ErrorScreen'; @@ -11,6 +11,8 @@ import { } from '@datadog/mobile-react-native-navigation'; import styles from './screens/styles'; +import { DdTrace } from '@datadog/mobile-react-native'; +import TraceScreen from './screens/TraceScreen'; const viewPredicate: ViewNamePredicate = ( _event: ComponentDidAppearEvent, @@ -37,6 +39,7 @@ function registerScreens() { Navigation.registerComponent('Home', () => HomeScreen); Navigation.registerComponent('Main', () => MainScreen); Navigation.registerComponent('Error', () => ErrorScreen); + Navigation.registerComponent('Trace', () => TraceScreen); Navigation.registerComponent('About', () => AboutScreen); } @@ -64,6 +67,15 @@ const HomeScreen = props => { }} /> +