feat(network-details): Introduce new swizzling to capture response bodies for session replay#7584
feat(network-details): Introduce new swizzling to capture response bodies for session replay#758443jay wants to merge 3 commits intomobile-935/data-classesfrom
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. This PR will not appear in the changelog. 🤖 This preview updates automatically when you update the PR. |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## mobile-935/data-classes #7584 +/- ##
=============================================================
- Coverage 84.985% 84.798% -0.188%
=============================================================
Files 486 486
Lines 28906 28958 +52
Branches 12577 12564 -13
=============================================================
- Hits 24566 24556 -10
- Misses 4293 4353 +60
- Partials 47 49 +2
... and 8 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
|
82a2dd5 to
9473efb
Compare
philipphofmann
left a comment
There was a problem hiding this comment.
Thanks for tackling this. I have one major high-level question before reviewing this more closely.
71fd40b to
78d1d60
Compare
9473efb to
5bc5830
Compare
78d1d60 to
9a4f8b3
Compare
5bc5830 to
7faf4ef
Compare
9a4f8b3 to
c62d15c
Compare
7faf4ef to
6e19c0a
Compare
c62d15c to
e717c1a
Compare
6e19c0a to
9a8d48d
Compare
8584657 to
c3afd6d
Compare
2d31c8d to
72a427a
Compare
6e873ec to
a691160
Compare
72a427a to
56c7a59
Compare
| SentrySWArguments(NSURLRequest * request, | ||
| void (^completionHandler)(NSData *, NSURLResponse *, NSError *)), | ||
| SentrySWReplacement({ | ||
| NSMutableArray<NSURLSessionDataTask *> *taskHolder = |
There was a problem hiding this comment.
@philipphofmann this NSMutableArray replaced the __block variable that was being used previously.
The issue is pulling out the code (de-duplication) into SentryWrapCompletionHandler makes the usage of the __block invalid - it moves out of scope & creates dangling pointer once execution passes to SentryWrapCompletionHandler (quickly crashes).
(claude's) solution -> use NSMutableArray instead of __block, b/c NSMutableArray is heap-allocated unlike __block.
There was a problem hiding this comment.
What's stopping you from using the same approach as we use in swizzleURLSessionTask? We basically just call the network tracker there and let the network tracker take care of everything. We know that this approach works, so I would stick to it.
There was a problem hiding this comment.
unless I'm missing something swizzleURLSessionTask is necessarily different; it's swizzling out the function for another function we want to call as well (1:1):
SentrySwizzleInstanceMethod(classToSwizzle, resumeSelector, SentrySWReturnType(void),
SentrySWArguments(),
SentrySWReplacement({
// Replacement function - just passthrough to our impl on networkTracker
[networkTracker urlSessionTaskResume:self];
// and invoke SentrySWCallOriginal 👍
SentrySWCallOriginal();
}),
SentrySwizzleModeOncePerClassAndSuperclasses, (void *)resumeSelector);whereas the new swizzling is swizzling out the function
[NSURLSession dataTaskWith[URL|Request]:completionHandler:]
to intercept dataTask creation in order to wrap the completionHandler
// really we just want to swizzle the completionHandler, but can only access it at dataTask creation time.
The completionHandler callback has the data (NSURLResponse) we need to pass to the network tracker:
SentrySwizzleInstanceMethod([NSURLSession class], dataTaskWithRequestSelector, SentrySWReturnType(NSURLSessionDataTask *),
SentrySWArguments(NSURLRequest * request,
void (^completionHandler)(NSData *, NSURLResponse *, NSError *)),
SentrySWReplacement({
// Replacement function -
// Chicken-and-egg: wrappedHandler needs the task, which exists at callback time
// but here does not exist until `SentrySWCallOriginal` runs
// Soln: create a holder for the to-be-initialised `NSURLSessionDataTask`
__block NSURLSessionDataTask *task = nil;
// Provide the wrapped completionHandler implementation.
void (^wrappedHandler)(NSData *, NSURLResponse *, NSError *) = nil;
if (completionHandler) {
wrappedHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error && data && task) {
[networkTracker captureResponseDetails:data
response:response
request:request
task:task];
}
completionHandler(data, response, error);
};
}
// Pass the wrappedHandler to `SentrySWCallOriginal` (apple's impl of `[NSURLSession dataTaskWithRequest:completionHandler:]`
// Assign to `__block task` b/c networkTracker needs the task to know whose response this is.
task = SentrySWCallOriginal(request, wrappedHandler ?: completionHandler);
// Give the caller a reference to the newly initialised task 👍
return task;
}),
SentrySwizzleModeOncePerClassAndSuperclasses, (void *)dataTaskWithRequestSelector);
Tests/SentryTests/Integrations/Performance/Network/SentryNSURLSessionTaskSearchTests.swift
Outdated
Show resolved
Hide resolved
| SentrySWArguments(NSURLRequest * request, | ||
| void (^completionHandler)(NSData *, NSURLResponse *, NSError *)), | ||
| SentrySWReplacement({ | ||
| NSMutableArray<NSURLSessionDataTask *> *taskHolder = |
There was a problem hiding this comment.
What's stopping you from using the same approach as we use in swizzleURLSessionTask? We basically just call the network tracker there and let the network tracker take care of everything. We know that this approach works, so I would stick to it.
Tests/SentryTests/Integrations/Performance/Network/SentryNSURLSessionTaskSearchTests.swift
Outdated
Show resolved
Hide resolved
| // MARK: - NSURLSession class hierarchy validation tests | ||
| // | ||
| // Based on testing, NSURLSession implements dataTaskWithRequest:completionHandler: | ||
| // and dataTaskWithURL:completionHandler: directly on the base class. | ||
| // | ||
| // The swizzling code relies on this by swizzling [NSURLSession class] directly | ||
| // rather than doing runtime discovery. These tests verify that assumption | ||
| // still holds — if Apple ever moves these methods to a subclass, these tests | ||
| // will fail and we'll know to update the swizzling approach. | ||
|
|
||
| func test_URLSession_isNotClassCluster_dataTaskWithRequest() { | ||
| let selector = #selector(URLSession.dataTask(with:completionHandler:) | ||
| as (URLSession) -> (URLRequest, @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask) | ||
| assertNSURLSessionImplementsDirectly(selector: selector, selectorName: "dataTaskWithRequest:completionHandler:") | ||
| } |
There was a problem hiding this comment.
m:I’m not sure we actually need tests at this level. Instead of testing this internal behavior, we should probably test the system at a slightly higher level. For example, we could verify that when we use the concrete implementation and interact with the API (i.e., performing web requests), we receive the expected results such as the correct response bodies.
The current test feels very close to implementation details. While we could keep it, it may not provide much real value. If our higher-level tests already verify that we correctly retrieve and process data from NSURLSession, then changes in the internal implementation shouldn’t matter.
In other words, this test could fail even though the functionality still works correctly, which would make it brittle. Conversely, it could pass while the actual behavior is broken. I would prefer tests that focus on observable behavior and validate the real functionality, without depending too heavily on implementation details.
There was a problem hiding this comment.
these tests are trying to keep parity with the existing test_URLSessionTask_ByIosVersion() for the new swizzling.
I.e.:
// To know whether Apple changes NSURLSessionTask
so test_URLSessionTask_ByIosVersion does
-> initialise a localDataTask (NSURLSessionDataTask); walk its superclasses to discover alternative impls ("class cluster")
-> asserts that this list only ever has the 1 expected impl.
-> asserts that the expected impl is on the expected class
Then,
// To know whether Apple changes NSURLSession dataTask factory initializers.
test_URLSession_isNotClassCluster_dataTaskWithURL
test_URLSession_isNotClassCluster_dataTaskWithRequest will
-> initialise a URLSession (.ephemeral and .default configs)
-> asserts that regardless the returned URLSession, it has the expected impl from [NSURLSession].
There was a problem hiding this comment.
There's 2 additional tests that i removed (Dont think they are that valuable):
test_dataTaskWithURL_doesNotCallThrough_dataTaskWithRequest
test_dataTaskWithRequest_doesNotCallThrough_dataTaskWithURL
these are testing whether the two functions we added swizzling for interact with each other. This came up in this bot comment which ended up being hallucination - but i had written the tests to verify so included them.
Based on our other convo they can introduce flakiness in tests.
56c7a59 to
a41d68f
Compare
a691160 to
7db5474
Compare
Review notes — treat with care
TL;DR — suggested restructureThe
The completion handler swizzle would become a simple, self-contained block with no tricky patterns: wrapped = ^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error && data) {
[networkTracker captureNetworkDetailsForReplayWithRequest:request
response:response
responseData:data];
}
completionHandler(data, response, error);
};No Detailed findings from Claude Code investigation1. The
|
appreciate the input, will work through it fully. At a glance some is valid and some I already worked through. The main issue :
The issue I hit was that The others don't seem relevant or seem inaccurate:
This isn't correct based on my local testing. The completionHandler always fired before Regardless the code is written so that if completionHandler fires before ANY call to setState, then sentry-cocoa/Sources/Sentry/SentryNetworkTracker.m Lines 605 to 610 in fea8910
This is our other convo - basically NSURLSession dataTask initializers are NOT currently class clusters (claude misspoke/inaccurate above). Regardless, the impl might change. The tests added to
This is point 1. The taskHolder chicken-and-egg pattern |
7db5474 to
a75e679
Compare
a41d68f to
41f8885
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Fabricated request creates inconsistency between swizzled methods
- Both swizzles now pass
task.currentRequest ?: task.originalRequestand the URL-based swizzle no longer fabricates an eager URL-only request.
- Both swizzles now pass
Or push these changes by commenting:
@cursor push 089f8d3da3
Preview (089f8d3da3)
diff --git a/Sources/Sentry/SentrySwizzleWrapperHelper.m b/Sources/Sentry/SentrySwizzleWrapperHelper.m
--- a/Sources/Sentry/SentrySwizzleWrapperHelper.m
+++ b/Sources/Sentry/SentrySwizzleWrapperHelper.m
@@ -129,11 +129,12 @@
void (^wrappedHandler)(NSData *, NSURLResponse *, NSError *) = nil;
if (completionHandler) {
wrappedHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
- if (!error && data && task) {
+ NSURLRequest *capturedRequest = task.currentRequest ?: task.originalRequest;
+ if (!error && data && task && capturedRequest) {
[networkTracker captureResponseDetails:data
- response:response
- request:request
- task:task];
+ response:response
+ request:capturedRequest
+ task:task];
}
completionHandler(data, response, error);
};
@@ -158,16 +159,16 @@
SentrySWArguments(
NSURL * url, void (^completionHandler)(NSData *, NSURLResponse *, NSError *)),
SentrySWReplacement({
- NSURLRequest *request = [NSURLRequest requestWithURL:url];
__block NSURLSessionDataTask *task = nil;
void (^wrappedHandler)(NSData *, NSURLResponse *, NSError *) = nil;
if (completionHandler) {
wrappedHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
- if (!error && data && task) {
+ NSURLRequest *capturedRequest = task.currentRequest ?: task.originalRequest;
+ if (!error && data && task && capturedRequest) {
[networkTracker captureResponseDetails:data
- response:response
- request:request
- task:task];
+ response:response
+ request:capturedRequest
+ task:task];
}
completionHandler(data, response, error);
};This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
…onse capture Add (no-op) callback into SentryNetworkTracker Remove run-time discovery for swizzle targets; directly swizzle [NSURLSession class] Add unit test to do run-time discovery and report if assumptions invalid
#7584 (comment) test_dataTaskWithURL_doesNotCallThrough_dataTaskWithRequest test_dataTaskWithRequest_doesNotCallThrough_dataTaskWithURL were asserting that one impl does not rely on the other internally. There's no reason to believe that they do - the test was probably checking for something that would not happen. Additionally it was relying on method_setImplementation which could cause random failures in CI if not cleaned up properly => remove
…etworkDetailHasUrls
a75e679 to
75d4331
Compare
🚨 Detected changes in high risk code 🚨High-risk code can easily blow up and is hard to test. We had severe bugs in the past. Be extra careful when changing these files, and have an extra careful look at these:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


📜 Description
Newly swizzled methods:
[NSURLSession dataTaskWithURL:completionHandler:][NSURLSession dataTaskWithRequest:completionHandler:]This swizzling is skipped when network detail capture is not enabled (SentryReplayOptions#networkDetailAllowUrls is empty)
Added testing to validate for future iOS SDKs that the
IMPs we are swizzling are implemented directly on NSURLSession: SentryNSURLSessionTaskSearchTeststest_URLSessionDataTaskWithRequest_ByIosVersiontest_URLSessionDataTaskWithURL_ByIosVersion💡 Motivation and Context
With this change sentry-cocoa can inspect response bodies of NSURLSession
dataTask's that use completionHandlers.Swizzling is added to inspect the NSURLResponse before delegating to the original completionHandler.
The request body will be captured via existing swizzling into SentryNetworkTracker (
setState,resume).See swizzling discussion on #7582 for more context.
💚 How did you test it?
Unit tests
📝 Checklist
You have to check all boxes before merging:
sendDefaultPIIis enabled. gated by networkDetailAllowUrls