Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUM-2690 fix: URLSession* swizzling issues on iOS 13 and 12 #1637

Conversation

ncreated
Copy link
Member

@ncreated ncreated commented Jan 18, 2024

What and why?

🧰 Fixing broken network instrumentation on iOS 13.x and 12.x by remedying the URLSessionTask.resume() swizzling and making sure DatadogInternalTests pass on these OSes.

How?

When swizzling URLSessionTask.resume() selector, the interception callback was not called on iOS 13 and 12. Being inspired with these hints, this PR changes the swizzled class to private __NSCFLocalSessionTask.

Observation: __NSCFLocalSessionTask seems to be uniformly used across existing major versions of the OS, making it a good candidate to swizzle. We can't anticipate if this will continue to work in future versions of the OS. I did several attempts on adding observability to this assumption (including this), unfortunately failed due to lack of consistency in some of our swizzlings. This is something I want to follow up on in short-term.

Here, task dumps from specific versions:

# iOS 12.4
- LocalDataTask <UUID>.<1> #0
  - super: __NSCFLocalSessionTask
    - super: __NSCFURLSessionTask
      - super: NSURLSessionTask
        - super: NSObject

# iOS 13.7
- LocalDataTask <UUID>.<1> #0
  - super: __NSCFLocalSessionTask
    - super: __NSCFURLSessionTask
      - super: NSURLSessionTask
        - super: NSObject

# iOS 14.5
- LocalDataTask <UUID>.<1> #0
  - super: __NSCFLocalSessionTask
    - super: NSURLSessionTask
      - super: NSObject

# iOS 15.5
- LocalDataTask <UUID>.<1> #0
  - super: __NSCFLocalSessionTask
    - super: NSURLSessionTask
      - super: NSObject

# iOS 16.2:
- LocalDataTask <UUID>.<1> #0
  - super: __NSCFLocalSessionTask
    - super: NSURLSessionTask
      - super: NSObject

# iOS 17.0
- LocalDataTask <UUID>.<1> #0
  - super: __NSCFLocalSessionTask
    - super: NSURLSessionTask
      - super: NSObject

Review checklist

  • Feature or bugfix MUST have appropriate tests (unit, integration)
  • Make sure each commit and the PR mention the Issue number or JIRA reference
  • Add CHANGELOG entry for user facing changes

Custom CI job configuration (optional)

  • Run unit tests for Core, RUM, Trace, Logs, CR and WVT
  • Run unit tests for Session Replay
  • Run integration tests
  • Run smoke tests
  • Run tests for tools/

@datadog-datadog-prod-us1
Copy link

datadog-datadog-prod-us1 bot commented Jan 18, 2024

Datadog Report

Branch report: ncreated/RUM-2690/quick-fix-iOS-13-and-12-fail-to-intercept-URLSessionTask-resume
Commit report: 786d506
Test service: dd-sdk-ios

✅ 0 Failed, 2790 Passed, 0 Skipped, 29m 29.64s Wall Time
🔻 Test Sessions change in coverage: 13 decreased, 2 increased

🔻 Code Coverage Decreases vs Default Branch (13)

This report shows up to 5 code coverage decreases.

  • test DatadogRUMTests iOS 79.24% (Δ-0.17%) - Details
  • test DatadogCrashReportingTests tvOS 28.89% (Δ-0.16%) - Details
  • test DatadogLogsTests tvOS 44.80% (Δ-0.15%) - Details
  • test DatadogLogsTests iOS 44.74% (Δ-0.15%) - Details
  • test DatadogWebViewTrackingTests iOS 22.93% (Δ-0.15%) - Details

Comment on lines +51 to +53
guard let klass = NSClassFromString(className) else {
throw InternalError(description: "Failed to swizzle `URLSessionTask`: `\(className)` class not found.")
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we should dump the runtime description of URLSessionTask in this telemetry, so we can see the list of subclasses to what is broken in assumed swizzling, like:

var stack = ""
dump(task, to: &stack)

print(stack)

// ```
// - LocalDataTask <...>.<1>
//   - super: __NSCFLocalSessionTask
//     - super: NSURLSessionTask
//       - super: NSObject
// ```

However, this can only be done at runtime as we need a task object. I was unable to get this information from any intercepted task (e.g. in session.dataTask(...) swizzling) as these interceptions are extensively called multiple times if more than one session is instrumented. Ultimately it was leading to many false-positives.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we should start investigating as soon as we get this telemetry error. It will most likely be a change on a new OS version so we should be able to dump the object while debugging.
Maybe we should start having monitors of specific error!

Comment on lines +28 to +34
if #available(iOS 13.0, *) {
// Prior to iOS 13.0 the `URLSession.dataTask(with:url, completionHandler:handler)` makes an internal
// call to `URLSession.dataTask(with:request, completionHandler:handler)`. To avoid duplicated call
// to the callback, we don't apply below swizzling prior to iOS 13.
dataTaskURLCompletionHandler = try DataTaskURLCompletionHandler.build()
dataTaskURLCompletionHandler?.swizzle(interceptCompletion: interceptCompletionHandler)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is porting the configuration from previous version of URLSession instrumentation, this:

if #available(iOS 13.0, *) {
// Prior to iOS 13.0 we do not apply following swizzlings, as those methods call
// the `URLSession.dataTask(with:completionHandler:)` internally which is managed
// by the `DataTaskWithURLRequestAndCompletion` swizzling.
dataTaskWithURLAndCompletion = try DataTaskWithURLAndCompletion.build()
dataTaskWithURL = try DataTaskWithURL.build()
}

This is required to make unit tests pass on iOS 12.4. Without this change, the expectation in this test was fulfilled 4 times instead of 2.

Rationale: The promise of URLSessionSwizzler is to intercept completion handler by abstracting runtime details (swizzling). In no circumstances this block should be called more than once. If iOS 12.4 details is different, the difference should be erased in this abstraction, delivering the right promise to higher level.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this little selector to RUM debugging flow, so we can send instrumented request. Required for debugging in iOS 12.4 where the other flow like this is not available (as it is using SwiftUI).

Screenshot 2024-01-19 at 10 05 17

@ncreated ncreated marked this pull request as ready for review January 19, 2024 09:22
@ncreated ncreated requested review from a team as code owners January 19, 2024 09:22
Copy link
Member

@maxep maxep left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On point 🎯

Good catch of the dataTaskURL issue as well!

@ncreated ncreated force-pushed the ncreated/RUM-2690/quick-fix-iOS-13-and-12-fail-to-intercept-URLSessionTask-resume branch from 658db76 to ad26b44 Compare January 19, 2024 15:20
@ncreated ncreated force-pushed the ncreated/RUM-2690/quick-fix-iOS-13-and-12-fail-to-intercept-URLSessionTask-resume branch from ad26b44 to 786d506 Compare January 22, 2024 15:20
@ncreated ncreated merged commit 2ec4a69 into develop Jan 22, 2024
10 checks passed
@ncreated ncreated deleted the ncreated/RUM-2690/quick-fix-iOS-13-and-12-fail-to-intercept-URLSessionTask-resume branch January 22, 2024 17:07
@ncreated ncreated mentioned this pull request Jan 25, 2024
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants