DataDog / dd-sdk-ios

Datadog SDK for iOS - Swift and Objective-C.
Apache License 2.0
222 stars 129 forks source link

What URLSessionDelegate should be used for automatic tracing? #1604

Closed cszatmary closed 11 months ago

cszatmary commented 11 months ago

The thing

I'm setting up the DD SDK on my company's iOS app. I'm following the documentation for enabling tracing here: https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/ios/?tab=cocoapods

It has this code snippet for setting up automatically tracing HTTP requests:

Trace.enable(
    with: Trace.Configuration(
        urlSessionTracking: Trace.Configuration.URLSessionTracking(
            firstPartyHostsTracing: .trace(hosts: ["example.com", "api.yourdomain.com"])
        )
    )
)

URLSessionInstrumentation.enable(
    with: .init(
        delegateClass: SessionDelegate.self,
    )
)

let session = URLSession(
    configuration: .default,
    delegate: SessionDelegate(),
    delegateQueue: nil
)

I'm confused as to what should be used for the delegate. There is no such thing as SessionDelegate so I assume this must be a placeholder? (This was not clear at first). Does the SDK provide a delegate that can be used out of the box or do I have to define my own? I noticed there is the DatadogURLSessionDelegate type however it is deprecated.

If I need to create my own custom delegate implementation is there any documentation on what needs to be implemented in order to make it work with this SDK?

ganeshnj commented 11 months ago

Quoting from https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/ios/?tab=cocoapods

In order for the SDK to automatically trace all network requests made to the given hosts, specify the firstPartyHosts array in the Datadog initialization, enable URLSessionInstrumentation for your delegate type and pass the delegate instance to the URLSession:

This delegate type must come from your application/library code that you need to conform.

cszatmary commented 11 months ago

Is there an example you can share of what this custom delegate type should look like? I'm not clear on what the implementation needs to be in order to make it work with automatic tracing.

ambrusha commented 11 months ago

Case number 1 I assume that you don't use any concrete SessionDelegate for your own purposes. In this case you need to create an empty object like

final class MyCustomSessionDelegate: NSObject, URLSessionDataDelegate { }

then use it in Datadog init

URLSessionInstrumentation.enable(
    with: .init(
        delegateClass: MyCustomSessionDelegate.self,
    )
)

and in each place where you use/create URLSession instance use your delegate like

let session = URLSession(
    configuration: .default,
    delegate: MyCustomSessionDelegate(),
    delegateQueue: nil
)

Case number 2 When you need to handle custom logic for session delegates e.g. socket connection or ssl pinning. I use next

final class MyCustomSessionDelegate: NSObject, URLSessionDataDelegate { 
    private let mySessionDelegate: URLSessionDelegate

    init(with sessionDelegate: URLSessionDelegate) {
        self. sessionDelegate = sessionDelegate
    }
}

extension MyCustomSessionDelegate: URLSessionWebSocketDelegate {
    func urlSession(
        _ session: URLSession,
        webSocketTask: URLSessionWebSocketTask,
        didOpenWithProtocol protocol: String?
    ) {
        if let sessionDelegate = self.sessionDelegate as? URLSessionWebSocketDelegate {
            sessionDelegate.urlSession?(
                session,
                webSocketTask: webSocketTask,
                didOpenWithProtocol: `protocol`
            )
        }
    }

    func urlSession(
        _ session: URLSession,
        webSocketTask: URLSessionWebSocketTask,
        didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
        reason: Data?
    ) {
        if let sessionDelegate = self.sessionDelegate as? URLSessionWebSocketDelegate {
            sessionDelegate.urlSession?(
                session,
                webSocketTask: webSocketTask,
                didCloseWith: closeCode,
                reason: reason
            )
        }
    }
}

where you either conform your class to URLSessionWebSocketDelegate or create separate object of such delegate. and in case of conformance

class MyClass: URLSessionWebSocketDelegate {

let session = URLSession(
    configuration: .default,
    delegate: MyCustomSessionDelegate(with: self),
    delegateQueue: nil
)

}

@cszatmary Hope it will help.

cszatmary commented 11 months ago

Thanks so much @ambrusha, this is exactly what I was looking for! I just tried Case 1 and it worked, I've verified traces are showing up in DD. I wasn't sure if I need to implement any of the delegate methods with any specific logic, but looks like an empty class works just fine.

ganeshnj commented 11 months ago

thanks @ambrusha for exhaustive example, closing it.

dineshiOSDev commented 7 months ago

Hi

I tired creating the sample below. I see only the trace related to span are getting logged. If i comment the span nothing is getting traced in datadog. I have set the custom delegate. What am i missing ? Please help. final class CustomDelegate: NSObject, URLSessionDataDelegate { }

private func setupTrace() {
        Trace.enable(
            with: Trace.Configuration(sampleRate: 100,
                                      service: "ios-app",
                urlSessionTracking: Trace.Configuration.URLSessionTracking(
                    firstPartyHostsTracing: .trace(hosts: ["jsonplaceholder.typicode.com"])
                ),
                networkInfoEnabled: true
            )
        )
        self.setupURLSessionTrace()

    }

    private func setupSessionReplay() {
        SessionReplay.enable(
               with: SessionReplay.Configuration(
                   replaySampleRate: 100
               )
           )
    }

    private func setupURLSessionTrace() {
        URLSessionInstrumentation.enable(
            with: .init(
                delegateClass: CustomDelegate.self
            )
        )
    }
class TodoService {
    let session = URLSession(
        configuration: .default,
        delegate: CustomDelegate(),
        delegateQueue: nil
    )

    private let baseURL = URL(string: "https://jsonplaceholder.typicode.com/todos")!
    func fetchTodos() -> AnyPublisher<[Todo], Error> {
        session.dataTaskPublisher(for: baseURL)
            .map(\.data)
            .decode(type: [Todo].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

struct Todo: Decodable {
    let id: Int
    let title: String
    let completed: Bool
}
final class CustomDelegate: NSObject, URLSessionDataDelegate { }

class TodoViewModel: ObservableObject {
    private var cancellables = Set<AnyCancellable>()
    private let todoService = TodoService()   
    @Published var todos: [Todo] = []    
    func fetchTodos() {
        let tracer = Tracer.shared().startSpan(operationName: "Demo API", tags: ["Sample":"Sample"])
        todoService.fetchTodos()
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in }) { todos in
                 tracer.finish()
                self.todos = todos
            }
            .store(in: &cancellables)
    }
}