Skip to content

feat(network-details): Introduce new swizzling to capture response bodies for session replay#7584

Open
43jay wants to merge 3 commits intomobile-935/data-classesfrom
mobile-935/new-swizzling
Open

feat(network-details): Introduce new swizzling to capture response bodies for session replay#7584
43jay wants to merge 3 commits intomobile-935/data-classesfrom
mobile-935/new-swizzling

Conversation

@43jay
Copy link
Copy Markdown
Collaborator

@43jay 43jay commented Mar 3, 2026

📜 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: SentryNSURLSessionTaskSearchTests

    • test_URLSessionDataTaskWithRequest_ByIosVersion
    • test_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

xcodebuild test -workspace Sentry.xcworkspace -scheme Sentry -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:Tests/SentryTests/Integrations/Performance/Network/SentryNSURLSessionTaskSearchTests.swift | xcbeautify

test_URLSessionTask_ByIosVersion ✅
test_URLSessionDataTaskWithRequest_ByIosVersion ✅
test_URLSessionDataTaskWithURL_ByIosVersion ✅

SKIP_SWIFTLINT=1 xcodebuild test -workspace Sentry.xcworkspace -scheme Sentry -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:SentryTests/SentrySwizzleWrapperHelperTests

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled. gated by networkDetailAllowUrls
  • I updated the docs if needed. future pr
  • I updated the wizard if needed. N/A
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog. #skip-changelog future PR
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs. 🤔

@linear
Copy link
Copy Markdown

linear bot commented Mar 3, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 3, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


This PR will not appear in the changelog.


🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 3, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 75d4331

@43jay 43jay marked this pull request as ready for review March 3, 2026 20:10
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 3, 2026

Codecov Report

❌ Patch coverage is 1.96078% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.798%. Comparing base (41f8885) to head (75d4331).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
Sources/Sentry/SentrySwizzleWrapperHelper.m 0.000% 49 Missing ⚠️
...nce/Network/SentryNetworkTrackingIntegration.swift 50.000% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                      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     
Files with missing lines Coverage Δ
Sources/Sentry/SentryNetworkTracker.m 99.651% <ø> (ø)
...nce/Network/SentryNetworkTrackingIntegration.swift 97.142% <50.000%> (-2.858%) ⬇️
Sources/Sentry/SentrySwizzleWrapperHelper.m 52.427% <0.000%> (-47.573%) ⬇️

... and 8 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 41f8885...75d4331. Read the comment docs.

Copy link
Copy Markdown
Member

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

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

Thanks for tackling this. I have one major high-level question before reviewing this more closely.

@43jay 43jay force-pushed the mobile-935/data-classes branch from 71fd40b to 78d1d60 Compare March 4, 2026 21:03
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 9473efb to 5bc5830 Compare March 4, 2026 21:03
@43jay 43jay force-pushed the mobile-935/data-classes branch from 78d1d60 to 9a4f8b3 Compare March 4, 2026 22:00
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 5bc5830 to 7faf4ef Compare March 4, 2026 22:00
@43jay 43jay force-pushed the mobile-935/data-classes branch from 9a4f8b3 to c62d15c Compare March 6, 2026 16:33
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 7faf4ef to 6e19c0a Compare March 6, 2026 16:33
@43jay 43jay force-pushed the mobile-935/data-classes branch from c62d15c to e717c1a Compare March 6, 2026 19:25
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 6e19c0a to 9a8d48d Compare March 6, 2026 19:25
@43jay 43jay marked this pull request as draft March 9, 2026 15:12
@43jay 43jay force-pushed the mobile-935/new-swizzling branch 2 times, most recently from 8584657 to c3afd6d Compare March 9, 2026 19:02
@43jay 43jay force-pushed the mobile-935/data-classes branch 2 times, most recently from 2d31c8d to 72a427a Compare March 9, 2026 19:48
@43jay 43jay force-pushed the mobile-935/new-swizzling branch 2 times, most recently from 6e873ec to a691160 Compare March 9, 2026 20:51
@43jay 43jay force-pushed the mobile-935/data-classes branch from 72a427a to 56c7a59 Compare March 9, 2026 20:51
SentrySWArguments(NSURLRequest * request,
void (^completionHandler)(NSData *, NSURLResponse *, NSError *)),
SentrySWReplacement({
NSMutableArray<NSURLSessionDataTask *> *taskHolder =
Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 9, 2026

Choose a reason for hiding this comment

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

@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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 10, 2026

Choose a reason for hiding this comment

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

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);

@43jay 43jay marked this pull request as ready for review March 9, 2026 21:00
SentrySWArguments(NSURLRequest * request,
void (^completionHandler)(NSData *, NSURLResponse *, NSError *)),
SentrySWReplacement({
NSMutableArray<NSURLSessionDataTask *> *taskHolder =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +13 to +27
// 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:")
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 10, 2026

Choose a reason for hiding this comment

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

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].

Copy link
Copy Markdown
Collaborator Author

@43jay 43jay Mar 10, 2026

Choose a reason for hiding this comment

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

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.

@43jay 43jay force-pushed the mobile-935/data-classes branch from 56c7a59 to a41d68f Compare March 10, 2026 21:06
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from a691160 to 7db5474 Compare March 10, 2026 21:06
@philipphofmann
Copy link
Copy Markdown
Member

philipphofmann commented Mar 11, 2026

Review notes — treat with care

Disclaimer: I'm on parental leave starting Monday, so I only gave this a quick glance today. Most of the investigation below came from prompting Claude Code — I walked it through the code step by step, asked it to trace the task usage across the PR stack, and challenged its initial assessment when things felt off. The findings are plausible and worth considering, but I haven't verified everything myself. Treat this as "leads worth investigating" rather than definitive conclusions. If needed, I can take another look tomorrow or the day after, but it might be better if @itaybre or @philprime takes this over fully.


TL;DR — suggested restructure

The taskHolder pattern (mutable array to break a chicken-and-egg dependency between the completion handler and the task) feels fishy. The completion handler doesn't actually need the task if you restructure the flow:

  1. Don't use the task as a bridge between request-time and response-time data
  2. Do all replay network detail processing in the completion handler itself — it already has request (captured in swizzle scope), response, and data
  3. Don't piggyback on the setState:/breadcrumb path — the completion handler fires after setState:, so the timing doesn't work anyway

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 taskHolder, no objc_getAssociatedObject lookup in the completion handler, no timing dependencies.


Detailed findings from Claude Code investigation

1. The taskHolder chicken-and-egg pattern

The wrapped completion handler needs a reference to the NSURLSessionDataTask, but the task doesn't exist yet when the handler is constructed. The current solution:

taskHolder = NSMutableArray (empty)
task = SentrySWCallOriginal(request, wrappedHandler)  // wrappedHandler captures taskHolder
[taskHolder addObject:task]
return task

The assumption is: the completion handler will never fire before [taskHolder addObject:task] because the task starts in NSURLSessionTaskStateSuspended and must be resumed first.

Why this is concerning: This relies on an undocumented implementation detail. Apple doesn't guarantee that:

  • A cached response won't trigger the completion handler synchronously during creation
  • An invalid/malformed request won't deliver an error inline before returning
  • An invalidated session won't call back immediately
  • This behavior won't change in a future OS version

If any of these edge cases fire the completion handler during SentrySWCallOriginal, taskHolder.firstObject is nil, and capture silently fails. No crash, just silent data loss — which is worse because you'd never notice.

2. The setState: vs completion handler timing

I asked Claude to trace the execution order. The setState: swizzle fires before the completion handler:

1. Our setState: swizzle fires (task.state is still .running)
2. Original setState: executes (state becomes .completed)
3. Completion handler fires

This means the approach in #7590 (reading SentryReplayNetworkDetails from the task in addBreadcrumbForSessionTask:) would read the associated object before the completion handler has populated the response data. This is a second potential race condition in the stack.

3. Class cluster concern

The swizzle targets [NSURLSession class], but NSURLSession is a class cluster — actual instances are private subclasses (e.g., __NSURLSessionLocal). If those subclasses override dataTaskWithRequest:completionHandler:, the swizzle on the base class won't intercept the calls. Compare with swizzleURLSessionTask: in the same file, which uses SentryNSURLSessionTaskSearch to discover the actual runtime classes. The response capture swizzle skips this step.

4. The existing SentryNetworkTracker already captures almost everything

The existing resume/setState: flow in SentryNetworkTracker.m is rock solid. It already captures URL, method, status code, request/response headers, body sizes — all directly from task properties. The only thing it can't get is the response body bytes (NSData), which is only available in the completion handler.

So the completion handler swizzle is necessary, but it should be minimal: just capture the data and process it right there, rather than building an elaborate bridge back to the setState: path via associated objects.

5. How I got here (prompting approach)

I asked Claude to:

  1. Read swizzleURLSessionDataTasksForResponseCapture and explain it
  2. Give its honest opinion on the approach — it initially said the taskHolder pattern was "clever and correct"
  3. I pushed back on the race condition concern — Claude reconsidered and acknowledged the undocumented timing assumptions
  4. Read all 4 stacked PRs (7582, 7585, 7588, 7590) to trace how the task flows through the stack
  5. Read the full SentryNetworkTracker.m to understand the existing infrastructure
  6. Propose how to merge the new functionality with the existing bulletproof flow

Again: these are Claude-assisted findings. The reasoning is sound to me on a quick read, but please verify independently before making architectural changes based on this.

@43jay
Copy link
Copy Markdown
Collaborator Author

43jay commented Mar 11, 2026

Review notes — treat with care

Disclaimer: I'm on parental leave starting Monday, so I only gave this a quick glance today. Most of the investigation below came from prompting Claude Code — I walked it through the code step by step, asked it to trace the task usage across the PR stack, and challenged its initial assessment when things felt off. The findings are plausible and worth considering, but I haven't verified everything myself. Treat this as "leads worth investigating" rather than definitive conclusions. If needed, I can take another look tomorrow or the day after, but it might be better if @itaybre or @philprime takes this over fully.

TL;DR — suggested restructure

The taskHolder pattern (mutable array to break a chicken-and-egg dependency between the completion handler and the task) feels fishy. The completion handler doesn't actually need the task if you restructure the flow:

  1. Don't use the task as a bridge between request-time and response-time data
  2. Do all replay network detail processing in the completion handler itself — it already has request (captured in swizzle scope), response, and data
  3. Don't piggyback on the setState:/breadcrumb path — the completion handler fires after setState:, so the timing doesn't work anyway

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 taskHolder, no objc_getAssociatedObject lookup in the completion handler, no timing dependencies.

Detailed findings from Claude Code investigation

1. The taskHolder chicken-and-egg pattern

The wrapped completion handler needs a reference to the NSURLSessionDataTask, but the task doesn't exist yet when the handler is constructed. The current solution:

taskHolder = NSMutableArray (empty)
task = SentrySWCallOriginal(request, wrappedHandler)  // wrappedHandler captures taskHolder
[taskHolder addObject:task]
return task

The assumption is: the completion handler will never fire before [taskHolder addObject:task] because the task starts in NSURLSessionTaskStateSuspended and must be resumed first.

Why this is concerning: This relies on an undocumented implementation detail. Apple doesn't guarantee that:

  • A cached response won't trigger the completion handler synchronously during creation
  • An invalid/malformed request won't deliver an error inline before returning
  • An invalidated session won't call back immediately
  • This behavior won't change in a future OS version

If any of these edge cases fire the completion handler during SentrySWCallOriginal, taskHolder.firstObject is nil, and capture silently fails. No crash, just silent data loss — which is worse because you'd never notice.

2. The setState: vs completion handler timing

I asked Claude to trace the execution order. The setState: swizzle fires before the completion handler:

1. Our setState: swizzle fires (task.state is still .running)
2. Original setState: executes (state becomes .completed)
3. Completion handler fires

This means the approach in #7590 (reading SentryReplayNetworkDetails from the task in addBreadcrumbForSessionTask:) would read the associated object before the completion handler has populated the response data. This is a second potential race condition in the stack.

3. Class cluster concern

The swizzle targets [NSURLSession class], but NSURLSession is a class cluster — actual instances are private subclasses (e.g., __NSURLSessionLocal). If those subclasses override dataTaskWithRequest:completionHandler:, the swizzle on the base class won't intercept the calls. Compare with swizzleURLSessionTask: in the same file, which uses SentryNSURLSessionTaskSearch to discover the actual runtime classes. The response capture swizzle skips this step.

4. The existing SentryNetworkTracker already captures almost everything

The existing resume/setState: flow in SentryNetworkTracker.m is rock solid. It already captures URL, method, status code, request/response headers, body sizes — all directly from task properties. The only thing it can't get is the response body bytes (NSData), which is only available in the completion handler.

So the completion handler swizzle is necessary, but it should be minimal: just capture the data and process it right there, rather than building an elaborate bridge back to the setState: path via associated objects.

5. How I got here (prompting approach)

I asked Claude to:

  1. Read swizzleURLSessionDataTasksForResponseCapture and explain it
  2. Give its honest opinion on the approach — it initially said the taskHolder pattern was "clever and correct"
  3. I pushed back on the race condition concern — Claude reconsidered and acknowledged the undocumented timing assumptions
  4. Read all 4 stacked PRs (7582, 7585, 7588, 7590) to trace how the task flows through the stack
  5. Read the full SentryNetworkTracker.m to understand the existing infrastructure
  6. Propose how to merge the new functionality with the existing bulletproof flow

Again: these are Claude-assisted findings. The reasoning is sound to me on a quick read, but please verify independently before making architectural changes based on this.

appreciate the input, will work through it fully. At a glance some is valid and some I already worked through.
edit: worked through it. Responses below

The main issue :

  1. The taskHolder chicken-and-egg pattern

Don't use the task as a bridge between request-time and response-time data
Do all replay network detail processing in the completion handler itself — it already has request (captured in swizzle scope), response, and data

The issue I hit was that NSURLRequest is not enough to differentiate, for example, multiple simultaneous requests with the same parameters - afaict NSURLRequest is basically a URL and an HTTP verb.

The others don't seem relevant or seem inaccurate:

2. The setState: vs completion handler timing

Don't piggyback on the setState:/breadcrumb path — the completion handler fires after setState:, so the timing doesn't work anyway

  1. Our setState: swizzle fires (task.state is still .running)
  2. Original setState: executes (state becomes .completed)
  3. Completion handler fires

This isn't correct based on my local testing. The completionHandler always fired before setState(.completed) runs. This logically makes sense as the app needs to 'handle completion' before the task can be considered 'completed' (edit: Found a counterexample that invalidates this)

Regardless the code is written so that if completionHandler fires before ANY call to setState, then captureResponseDetails call won't find a SentryReplayNetworkDetails and bails

if (!details) {
SENTRY_LOG_DEBUG(@"[NetworkCapture] No SentryReplayNetworkDetails found for %@ - "
@"skipping response capture",
url);
return;
}

3. Class cluster concern

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 SentryNSURLSessionTaskSearchTests.swift are meant to uncover this by initializing NSURLSession with different configs to assert that the class returned at run-time has exactly the imp from the base [NSURLSession]

4. The existing SentryNetworkTracker already captures almost everything

This is point 1. The taskHolder chicken-and-egg pattern
Or rather the issue is the same - SentryNetworkTracker (aiui) doesn't have a way to associate a specific NSURLRequest with the completionHandler NSURLResponse

@43jay 43jay force-pushed the mobile-935/new-swizzling branch from 7db5474 to a75e679 Compare March 16, 2026 20:31
@43jay 43jay force-pushed the mobile-935/data-classes branch from a41d68f to 41f8885 Compare March 16, 2026 20:31
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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.originalRequest and the URL-based swizzle no longer fabricates an eager URL-only request.

Create PR

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.

43jay added 3 commits March 18, 2026 10:30
…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
@43jay 43jay force-pushed the mobile-935/new-swizzling branch from a75e679 to 75d4331 Compare March 18, 2026 19:25
@github-actions
Copy link
Copy Markdown
Contributor

🚨 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:

  • Sources/Sentry/SentryNetworkTracker.m

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

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.

4 participants