From 2e3ef928163b68489b6c68780a474d0675158059 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:37:31 +0200 Subject: [PATCH] fix(replay): network breadcrumbs with tracing disabled were missing start timestamp (#4091) Session replay should be independent of the tracing (performance). --- CHANGELOG.md | 1 + Sources/Sentry/SentryNetworkTracker.m | 10 +- .../Network/SentryNetworkTrackerTests.swift | 355 ++++++++++-------- 3 files changed, 199 insertions(+), 167 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7797d82dedf..fa9e7bcb3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `storeEnvelope` ends session for unhandled errors (#4073) - Deprecate `SentryUser.segment`(#4092). Please remove usages of this property. We will remove it in the next major. +- Sentry Replay Network details should be available without Tracing (#4091) ## 8.29.1 diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index 15016d616fb..a781d83958e 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -163,6 +163,12 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask return; } + // Register request start date in the sessionTask to use for breadcrumb + if (self.isNetworkBreadcrumbEnabled) { + objc_setAssociatedObject(sessionTask, &SENTRY_NETWORK_REQUEST_START_DATE, [NSDate date], + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + @synchronized(self) { if (!self.isNetworkTrackingEnabled) { [self addTraceWithoutTransactionToTask:sessionTask]; @@ -220,10 +226,6 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask @"SentryNetworkTracker automatically started HTTP span for sessionTask: %@", netSpan.description); - // Register request start date in the sessionTask to use for breadcrumb - objc_setAssociatedObject(sessionTask, &SENTRY_NETWORK_REQUEST_START_DATE, [NSDate date], - OBJC_ASSOCIATION_RETAIN_NONATOMIC); - objc_setAssociatedObject(sessionTask, &SENTRY_NETWORK_REQUEST_TRACKER_SPAN, netSpan, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index 17a248f5033..f0a7119cbb8 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -5,7 +5,7 @@ import XCTest // swiftlint:disable file_length class SentryNetworkTrackerTests: XCTestCase { - + private static let dsnAsString = TestConstants.dsnAsString(username: "SentrySessionTrackerTests") private static let testUrl = "https://www.domain.com/api" private static let fullUrl = URL(string: "https://www.domain.com/api?query=value&query2=value2#fragment")! @@ -35,7 +35,7 @@ class SentryNetworkTrackerTests: XCTestCase { "X-CSRFTOKEN": "value", "X-XSRF-TOKEN": "value", "VALID_HEADER": "value" ] - + init() { options = Options() options.dsn = SentryNetworkTrackerTests.dsnAsString @@ -44,7 +44,7 @@ class SentryNetworkTrackerTests: XCTestCase { client = TestClient(options: options) hub = TestHub(client: client, andScope: scope) } - + func getSut() -> SentryNetworkTracker { let result = SentryNetworkTracker.sharedInstance result.enableNetworkTracking() @@ -56,82 +56,82 @@ class SentryNetworkTrackerTests: XCTestCase { } private var fixture: Fixture! - + override func setUp() { super.setUp() fixture = Fixture() - + SentrySDK.setCurrentHub(fixture.hub) SentrySDK.setStart(fixture.options) SentryDependencyContainer.sharedInstance().dateProvider = fixture.dateProvider } - + override func tearDown() { super.tearDown() clearTestState() } - + func testCaptureCompletion() { let task = createDataTask() let span = spanForTask(task: task)! - + assertCompletedSpan(task, span) } - + func test_CallResumeTwice_OneSpan() { let task = createDataTask() - + let sut = fixture.getSut() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) sut.urlSessionTaskResume(task) - + let spans = Dynamic(transaction).children as [Span]? - + XCTAssertEqual(spans?.count, 1) } - + func test_noURL() { let task = URLSessionDataTaskMock() let span = spanForTask(task: task) XCTAssertNil(span) } - + func test_NoTransaction() { let task = createDataTask() - + let sut = fixture.getSut() sut.urlSessionTaskResume(task) let span = objc_getAssociatedObject(task, SENTRY_NETWORK_REQUEST_TRACKER_SPAN) - + XCTAssertNil(span) } - + func testCaptureDownloadTask() { let task = createDownloadTask() let span = spanForTask(task: task) - + XCTAssertNotNil(span) setTaskState(task, state: .completed) XCTAssertTrue(span!.isFinished) } - + func testCaptureUploadTask() { let task = createUploadTask() let span = spanForTask(task: task) - + XCTAssertNotNil(span) setTaskState(task, state: .completed) XCTAssertTrue(span!.isFinished) } - + func testIgnoreStreamTask() { let task = createStreamTask() let span = spanForTask(task: task) //Ignored during resume XCTAssertNil(span) - + fixture.getSut().urlSessionTask(task, setState: .completed) //ignored during state change let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? @@ -142,29 +142,29 @@ class SentryNetworkTrackerTests: XCTestCase { func testIgnoreSentryApi() { let task = fixture.sentryTask let span = spanForTask(task: task) - + XCTAssertNil(span) XCTAssertNil(task.observationInfo) } - + func testSDKOptionsNil() { SentrySDK.setCurrentHub(nil) - + let task = fixture.sentryTask let span = spanForTask(task: task) - + XCTAssertNil(span) } - + func testDisabledTracker() { let sut = fixture.getSut() sut.disable() let task = createUploadTask() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) let spans = Dynamic(transaction).children as [Span]? - + XCTAssertEqual(spans!.count, 0) } @@ -193,90 +193,90 @@ class SentryNetworkTrackerTests: XCTestCase { operation: SentryNetworkTrackerTests.transactionOperation), hub: nil, configuration: SentryTracerConfiguration(block: { $0.waitForChildren = true })) fixture.scope.span = tracer - + sut.urlSessionTaskResume(task) tracer.finish() - + let spans = Dynamic(tracer).children as [Span]? let span = spans!.first! - + advanceTime(bySeconds: 5) - + XCTAssertFalse(span.isFinished) setTaskState(task, state: .completed) XCTAssertTrue(span.isFinished) - + assertSpanDuration(span: span, expectedDuration: 5) assertSpanDuration(span: tracer, expectedDuration: 5) } - + func testCaptureCancelledRequest() { assertStatus(status: .cancelled, state: .canceling, response: URLResponse()) } - + func testCaptureSuspendedRequest() { assertStatus(status: .aborted, state: .suspended, response: URLResponse()) } - + func testCaptureRequestWithError() { let task = createDataTask() let span = spanForTask(task: task)! - + task.setError(NSError(domain: "Some Error", code: 1, userInfo: nil)) setTaskState(task, state: .completed) - + XCTAssertEqual(span.status, .unknownError) } - + func testSpanDescriptionNameWithGet() { let task = createDataTask() let span = spanForTask(task: task)! - + XCTAssertEqual(span.spanDescription, "GET \(SentryNetworkTrackerTests.testUrl)") XCTAssertEqual(SentryNetworkTrackerTests.origin, span.origin) } - + func testSpanDescriptionNameWithPost() { let task = createDataTask(method: "POST") let span = spanForTask(task: task)! - + XCTAssertEqual(span.spanDescription, "POST \(SentryNetworkTrackerTests.testUrl)") XCTAssertEqual(SentryNetworkTrackerTests.origin, span.origin) } - + func testStatusForTaskRunning() { let sut = fixture.getSut() let task = createDataTask() let status = Dynamic(sut).statusForSessionTask(task, state: URLSessionTask.State.running) as SentrySpanStatus? XCTAssertEqual(status, .undefined) } - + func testSpanRemovedFromAssociatedObject() { let sut = fixture.getSut() let task = createDataTask() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) let spans = Dynamic(transaction).children as [Span]? - + objc_removeAssociatedObjects(task) - + XCTAssertFalse(spans!.first!.isFinished) - + setTaskState(task, state: .completed) XCTAssertFalse(spans!.first!.isFinished) } - + func testTaskStateChangedForRunning() { let sut = fixture.getSut() let task = createDataTask() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) let spans = Dynamic(transaction).children as [Span]? task.state = .running XCTAssertFalse(spans!.first!.isFinished) - + setTaskState(task, state: .completed) XCTAssertTrue(spans!.first!.isFinished) } @@ -294,18 +294,18 @@ class SentryNetworkTrackerTests: XCTestCase { let sut = fixture.getSut() let task = createDataTask() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) let spans = Dynamic(transaction).children as [Span]? - + task.setError(NSError(domain: "TEST_ERROR", code: -1, userInfo: nil)) sut.urlSessionTask(task, setState: .running) XCTAssertFalse(spans!.first!.isFinished) - + setTaskState(task, state: .completed) XCTAssertTrue(spans!.first!.isFinished) } - + func testCaptureResponses() { assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) assertStatus(status: .undefined, state: .completed, response: createResponse(code: 300)) @@ -321,13 +321,13 @@ class SentryNetworkTrackerTests: XCTestCase { assertStatus(status: .deadlineExceeded, state: .completed, response: createResponse(code: 504)) assertStatus(status: .undefined, state: .completed, response: URLResponse()) } - + func testBreadcrumb() { assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? let breadcrumb = breadcrumbs!.first - + XCTAssertEqual(breadcrumb!.category, "http") XCTAssertEqual(breadcrumb!.level, .info) XCTAssertEqual(breadcrumb!.type, "http") @@ -344,24 +344,53 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertTrue(breadcrumb!.data!["request_start"] is Date) XCTAssertNil(breadcrumb!.data!["graphql_operation_name"]) } - + func testNetworkBreadcrumbForSessionReplay() throws { assertStatus(status: .ok, state: .completed, response: createResponse(code: 200)) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? - + let sut = SentrySRDefaultBreadcrumbConverter() - guard let crumb = breadcrumbs?.first else { - XCTFail("No touch breadcrumb") - return - } - + let crumb = try XCTUnwrap(breadcrumbs?.first, "No breadcrumbs") + let result = try XCTUnwrap(sut.convert(from: crumb) as? SentryRRWebSpanEvent) let crumbData = try XCTUnwrap(result.data) let payload = try XCTUnwrap(crumbData["payload"] as? [String: Any]) let payloadData = try XCTUnwrap(payload["data"] as? [String: Any]) let start = try XCTUnwrap(crumb.data?["request_start"] as? Date) - + + XCTAssertEqual(result.timestamp, start) + XCTAssertEqual(crumbData["tag"] as? String, "performanceSpan") + XCTAssertEqual(payload["description"] as? String, "https://www.domain.com/api") + XCTAssertEqual(payload["op"] as? String, "resource.http") + XCTAssertEqual(payload["startTimestamp"] as? Double, start.timeIntervalSince1970) + XCTAssertEqual(payload["endTimestamp"] as? Double, crumb.timestamp?.timeIntervalSince1970) + XCTAssertEqual(payloadData["statusCode"] as? Int, 200) + XCTAssertEqual(payloadData["query"] as? String, "query=value&query2=value2") + XCTAssertEqual(payloadData["fragment"] as? String, "fragment") + } + + func testNetworkBreadcrumbForSessionReplay_WithoutNetworkTracing() throws { + let tracer = fixture.getSut() + tracer.disable() + tracer.enableNetworkBreadcrumbs() + let task = createDataTask() + tracer.urlSessionTaskResume(task) + task.setResponse(createResponse(code: 200)) + tracer.urlSessionTask(task, setState: .completed) + + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? + + let sut = SentrySRDefaultBreadcrumbConverter() + let crumb = try XCTUnwrap(breadcrumbs?.first, "No breadcrumbs") + + let result = try XCTUnwrap(sut.convert(from: crumb) as? SentryRRWebSpanEvent) + + let crumbData = try XCTUnwrap(result.data) + let payload = try XCTUnwrap(crumbData["payload"] as? [String: Any]) + let payloadData = try XCTUnwrap(payload["data"] as? [String: Any]) + let start = try XCTUnwrap(crumb.data?["request_start"] as? Date) + XCTAssertEqual(result.timestamp, start) XCTAssertEqual(crumbData["tag"] as? String, "performanceSpan") XCTAssertEqual(payload["description"] as? String, "https://www.domain.com/api") @@ -410,23 +439,23 @@ class SentryNetworkTrackerTests: XCTestCase { $0.disable() $0.enableNetworkTracking() } - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? XCTAssertEqual(breadcrumbs?.count, 0) } - + func testBreadcrumb_DisablingNetworkTracking() { let sut = fixture.getSut() let task = createDataTask() - + sut.urlSessionTaskResume(task) task.setResponse(createResponse(code: 200)) - + sut.urlSessionTask(task, setState: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? XCTAssertEqual(breadcrumbs?.count, 1) - + let breadcrumb = breadcrumbs!.first XCTAssertEqual(breadcrumb!.category, "http") XCTAssertEqual(breadcrumb!.level, .info) @@ -434,18 +463,18 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(breadcrumb!.data!["url"] as! String, SentryNetworkTrackerTests.testUrl) XCTAssertEqual(breadcrumb!.data!["method"] as! String, "GET") } - + func testBreadcrumbWithoutSpan() { let task = createDataTask() let _ = spanForTask(task: task)! - + objc_removeAssociatedObjects(task) - + setTaskState(task, state: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? let breadcrumb = breadcrumbs!.first - + XCTAssertEqual(breadcrumb!.category, "http") XCTAssertEqual(breadcrumb!.level, .info) XCTAssertEqual(breadcrumb!.type, "http") @@ -469,43 +498,43 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(amount, 1) } - + func testWhenNoSpan_RemoveObserver() { let task = createDataTask() let _ = spanForTask(task: task)! - + objc_removeAssociatedObjects(task) - + setTaskState(task, state: .completed) setTaskState(task, state: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? XCTAssertEqual(1, breadcrumbs?.count) } - + func testBreadcrumbNotFound() { assertStatus(status: .notFound, state: .completed, response: createResponse(code: 404)) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? let breadcrumb = breadcrumbs!.first - + XCTAssertEqual(breadcrumb!.data!["status_code"] as! NSNumber, NSNumber(value: 404)) XCTAssertEqual(breadcrumb!.data!["reason"] as! String, HTTPURLResponse.localizedString(forStatusCode: 404)) } - + func testBreadcrumbWithError_AndPerformanceTrackingNotEnabled() { fixture.options.enableAutoPerformanceTracing = false - + let task = createDataTask() let _ = spanForTask(task: task)! - + task.setError(NSError(domain: "Some Error", code: 1, userInfo: nil)) - + setTaskState(task, state: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? let breadcrumb = breadcrumbs!.first - + XCTAssertEqual(breadcrumb!.category, "http") XCTAssertEqual(breadcrumb!.level, .error) XCTAssertEqual(breadcrumb!.type, "http") @@ -515,73 +544,73 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertNil(breadcrumb!.data!["status_code"]) XCTAssertNil(breadcrumb!.data!["reason"]) } - + func testBreadcrumbPost() { let task = createDataTask(method: "POST") let _ = spanForTask(task: task)! - + setTaskState(task, state: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? let breadcrumb = breadcrumbs!.first XCTAssertEqual(breadcrumb!.data!["method"] as! String, "POST") } - + func test_NoBreadcrumb_forSentryAPI() { let sut = fixture.getSut() let task = fixture.sentryTask - + setTaskState(task, state: .running) sut.urlSessionTask(task, setState: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? XCTAssertEqual(breadcrumbs?.count, 0) } - + func test_NoBreadcrumb_WithoutURL() { let sut = fixture.getSut() let task = URLSessionDataTaskMock() - + setTaskState(task, state: .running) sut.urlSessionTask(task, setState: .completed) - + let breadcrumbs = Dynamic(fixture.scope).breadcrumbArray as [Breadcrumb]? XCTAssertEqual(breadcrumbs?.count, 0) } - + func testResumeAfterCompleted_OnlyOneSpanCreated() { let task = createDataTask() let sut = fixture.getSut() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) setTaskState(task, state: .completed) sut.urlSessionTaskResume(task) assertOneSpanCreated(transaction) } - + func testResumeAfterCancelled_OnlyOneSpanCreated() { let task = createDataTask() let sut = fixture.getSut() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) setTaskState(task, state: .canceling) sut.urlSessionTaskResume(task) assertOneSpanCreated(transaction) } - + func testResumeCalledMultipleTimesConcurrent_OneSpanCreated() { let task = createDataTask() let sut = fixture.getSut() let transaction = startTransaction() - + let queue = DispatchQueue(label: "SentryNetworkTrackerTests", qos: .userInteractive, attributes: [.concurrent, .initiallyInactive]) let group = DispatchGroup() - + for _ in 0...100_000 { group.enter() queue.async { @@ -590,22 +619,22 @@ class SentryNetworkTrackerTests: XCTestCase { group.leave() } } - + queue.activate() group.waitWithTimeout(timeout: 100) - + assertOneSpanCreated(transaction) } - + func testChangeStateMultipleTimesConcurrent_OneSpanFinished() { let task = createDataTask() let sut = fixture.getSut() let transaction = startTransaction() sut.urlSessionTaskResume(task) - + let queue = DispatchQueue(label: "SentryNetworkTrackerTests", qos: .userInteractive, attributes: [.concurrent, .initiallyInactive]) let group = DispatchGroup() - + for _ in 0...100_000 { group.enter() queue.async { @@ -613,14 +642,14 @@ class SentryNetworkTrackerTests: XCTestCase { group.leave() } } - + queue.activate() group.waitWithTimeout(timeout: 100) - + let spans = Dynamic(transaction).children as [Span]? XCTAssertEqual(1, spans?.count) let span = spans!.first! - + XCTAssertTrue(span.isFinished) //Test if it has observers. Nil means no observers XCTAssertNil(task.observationInfo) @@ -635,7 +664,7 @@ class SentryNetworkTrackerTests: XCTestCase { let expectedBaggageHeader = transaction.traceContext.toBaggage().toHTTPHeader(withOriginalBaggage: nil) XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["baggage"] ?? "", expectedBaggageHeader) } - + func testDontOverrideBaggageHeader() { let sut = fixture.getSut() let task = createDataTask { @@ -659,7 +688,7 @@ class SentryNetworkTrackerTests: XCTestCase { let expectedTraceHeader = networkSpan.toTraceHeader().value() XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", expectedTraceHeader) } - + func testDontOverrideTraceHeader() { let sut = fixture.getSut() let task = createDataTask { @@ -744,27 +773,27 @@ class SentryNetworkTrackerTests: XCTestCase { // String and integer (which isn't valid, make sure it doesn't crash) XCTAssertTrue(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: ["localhost", 123])) } - + func testCaptureHTTPClientErrorRequest() { let sut = fixture.getSut() - + let url = URL(string: "https://www.domain.com/api?query=myQuery#myFragment")! var request = URLRequest(url: url) request.httpMethod = "GET" let headers = ["test": "test", "Cookie": "myCookie", "Set-Cookie": "myCookie"] request.allHTTPHeaderFields = headers - + let task = URLSessionDataTaskMock(request: request) task.setResponse(createResponse(code: 500)) - + sut.urlSessionTask(task, setState: .completed) - + guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { XCTFail("Expected to capture 1 event") return } let sentryRequest = envelope.event.request! - + XCTAssertEqual(sentryRequest.url, "https://www.domain.com/api") XCTAssertEqual(sentryRequest.method, "GET") XCTAssertEqual(sentryRequest.bodySize, 652) @@ -794,7 +823,7 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(sentryRequest.url, "https://[Filtered]:[Filtered]@www.domain.com/api") XCTAssertEqual(sentryRequest.headers, ["VALID_HEADER": "value"]) } - + func testCaptureHTTPClientErrorResponse() { let sut = fixture.getSut() let task = createDataTask() @@ -806,9 +835,9 @@ class SentryNetworkTrackerTests: XCTestCase { httpVersion: "1.1", headerFields: headers)! task.setResponse(response) - + sut.urlSessionTask(task, setState: .completed) - + guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { XCTFail("Expected to capture 1 event") return @@ -842,14 +871,14 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(sentryResponse?["headers"] as? [String: String], ["VALID_HEADER": "value"]) } - + func testCaptureHTTPClientErrorException() { let sut = fixture.getSut() let task = createDataTask() task.setResponse(createResponse(code: 500)) - + sut.urlSessionTask(task, setState: .completed) - + guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { XCTFail("Expected to capture 1 event") return @@ -859,12 +888,12 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(exception.type, "HTTPClientError") XCTAssertEqual(exception.value, "HTTP Client Error with status code: 500") - + let stackTrace = exception.stacktrace! XCTAssertTrue(stackTrace.snapshot!.boolValue) XCTAssertNotNil(stackTrace.frames) } - + func testDoesNotCaptureHTTPClientErrorIfDisabled() { let sut = fixture.getSut() sut.disable() @@ -873,64 +902,64 @@ class SentryNetworkTrackerTests: XCTestCase { let task = createDataTask() task.setResponse(createResponse(code: 500)) - + sut.urlSessionTask(task, setState: .completed) XCTAssertNil(fixture.hub.capturedEventsWithScopes.first) } - + func testDoesNotCaptureHTTPClientErrorIfNotStatusCodeRange() { let sut = fixture.getSut() let task = createDataTask() task.setResponse(createResponse(code: 200)) - + sut.urlSessionTask(task, setState: .completed) XCTAssertNil(fixture.hub.capturedEventsWithScopes.first) } - + func testDoesNotCaptureHTTPClientErrorIfNotTarget() { fixture.options.failedRequestTargets = ["www.example.com"] let sut = fixture.getSut() let task = createDataTask() task.setResponse(createResponse(code: 500)) - + sut.urlSessionTask(task, setState: .completed) XCTAssertNil(fixture.hub.capturedEventsWithScopes.first) } - + func setTaskState(_ task: URLSessionTaskMock, state: URLSessionTask.State) { fixture.getSut().urlSessionTask(task as! URLSessionTask, setState: state) task.state = state } - + func assertStatus(status: SentrySpanStatus, state: URLSessionTask.State, response: URLResponse, configSut: ((SentryNetworkTracker) -> Void)? = nil) { let sut = fixture.getSut() configSut?(sut) - + let task = createDataTask() - + let transaction = startTransaction() - + sut.urlSessionTaskResume(task) - + let spans = Dynamic(transaction).children as [Span]? let span = spans!.first! - + task.setResponse(response) - + sut.urlSessionTask(task, setState: state) - + let httpStatusCode = span.data["http.response.status_code"] as? String - + if let httpResponse = response as? HTTPURLResponse { XCTAssertEqual("\(httpResponse.statusCode)", httpStatusCode!) } else { XCTAssertNil(httpStatusCode) } - + let path = span.data["url"] as? String let method = span.data["http.request.method"] as? String let requestType = span.data["type"] as? String @@ -948,7 +977,7 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(span.status, status) XCTAssertNil(task.observationInfo) } - + private func assertCompletedSpan(_ task: URLSessionDataTaskMock, _ span: Span) { XCTAssertNotNil(span) XCTAssertFalse(span.isFinished) @@ -959,39 +988,39 @@ class SentryNetworkTrackerTests: XCTestCase { //Test if it has observers. Nil means no observers XCTAssertNil(task.observationInfo) } - + private func assertOneSpanCreated(_ transaction: Span) { let spans = Dynamic(transaction).children as [Span]? XCTAssertEqual(1, spans?.count) } - + private func spanForTask(task: URLSessionTask) -> Span? { let sut = fixture.getSut() let transaction = startTransaction() - + sut.urlSessionTaskResume(task) - + let spans = Dynamic(transaction).children as [Span]? return spans?.first } - + private func startTransaction() -> Span { return SentrySDK.startTransaction(name: SentryNetworkTrackerTests.transactionName, operation: SentryNetworkTrackerTests.transactionOperation, bindToScope: true) } - + private func createResponse(code: Int) -> URLResponse { return HTTPURLResponse(url: SentryNetworkTrackerTests.fullUrl, statusCode: code, httpVersion: "1.1", headerFields: nil)! } - + private func advanceTime(bySeconds: TimeInterval) { fixture.dateProvider.setDate(date: fixture.dateProvider.date().addingTimeInterval(bySeconds)) } - + private func assertSpanDuration(span: Span, expectedDuration: TimeInterval) { let duration = span.timestamp!.timeIntervalSince(span.startTimestamp!) XCTAssertEqual(duration, expectedDuration) } - + func createDataTask(method: String = "GET", modifyRequest: ((URLRequest) -> (URLRequest))? = nil) -> URLSessionDataTaskMock { var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl) request.httpMethod = method @@ -1001,23 +1030,23 @@ class SentryNetworkTrackerTests: XCTestCase { } if let modifyRequest = modifyRequest { - request = modifyRequest(request) + request = modifyRequest(request) } return URLSessionDataTaskMock(request: request) } - + func createDownloadTask(method: String = "GET") -> URLSessionDownloadTaskMock { var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl) request.httpMethod = method return URLSessionDownloadTaskMock(request: request) } - + func createUploadTask(method: String = "GET") -> URLSessionUploadTaskMock { var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl) request.httpMethod = method return URLSessionUploadTaskMock(request: request) } - + func createStreamTask(method: String = "GET") -> URLSessionStreamTaskMock { var request = URLRequest(url: SentryNetworkTrackerTests.fullUrl) request.httpMethod = method