Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Sources/Sentry/SentryNetworkTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -562,4 +562,15 @@ - (SentryLevel)getBreadcrumbLevel:(NSURLSessionTask *)sessionTask
return breadcrumbLevel;
}

- (void)captureResponseDetails:(NSData *)data
response:(NSURLResponse *)response
requestURL:(NSURL *)requestURL
task:(NSURLSessionTask *)task
{
// TODO: Implementation
// 2. Parse response body data
// 3. Store in appropriate location for session replay
Comment thread
43jay marked this conversation as resolved.
// 4. Handle size limits and truncation if needed
}
Comment thread
43jay marked this conversation as resolved.

@end
81 changes: 81 additions & 0 deletions Sources/Sentry/SentrySwizzleWrapperHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,87 @@ + (void)swizzleURLSessionTask:(SentryNetworkTracker *)networkTracker
#pragma clang diagnostic pop
}

/**
* Swizzles NSURLSession data task creation methods that use completion handlers
* to enable response body capture for session replay.
*
* Both dataTaskWithRequest: and dataTaskWithURL: are independent implementations
* (neither calls through to the other), so both need swizzling.
*
* See SentryNSURLSessionTaskSearchTests that verifies these assumptions still hold.
*/
+ (void)swizzleURLSessionDataTasksForResponseCapture:(SentryNetworkTracker *)networkTracker
{
[self swizzleDataTaskWithRequestCompletionHandler:networkTracker];
[self swizzleDataTaskWithURLCompletionHandler:networkTracker];
}

/**
* Swizzles -[NSURLSession dataTaskWithRequest:completionHandler:] to intercept response data.
*/
+ (void)swizzleDataTaskWithRequestCompletionHandler:(SentryNetworkTracker *)networkTracker
Comment thread
43jay marked this conversation as resolved.
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshadow"
SEL selector = @selector(dataTaskWithRequest:completionHandler:);
SentrySwizzleInstanceMethod([NSURLSession class], selector,
SentrySWReturnType(NSURLSessionDataTask *),
SentrySWArguments(NSURLRequest * request,
void (^completionHandler)(NSData *, NSURLResponse *, NSError *)),
SentrySWReplacement({
__block NSURLSessionDataTask *task = nil;
void (^wrappedHandler)(NSData *, NSURLResponse *, NSError *) = nil;
if (completionHandler) {
wrappedHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error && data && task) {
[networkTracker captureResponseDetails:data
response:response
requestURL:request.URL
task:task];
}
completionHandler(data, response, error);
};
}
task = SentrySWCallOriginal(request, wrappedHandler ?: completionHandler);
return task;
Comment thread
43jay marked this conversation as resolved.
}),
SentrySwizzleModeOncePerClassAndSuperclasses, (void *)selector);
#pragma clang diagnostic pop
}

/**
* Swizzles -[NSURLSession dataTaskWithURL:completionHandler:] to intercept response data.
*/
+ (void)swizzleDataTaskWithURLCompletionHandler:(SentryNetworkTracker *)networkTracker
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshadow"
SEL selector = @selector(dataTaskWithURL:completionHandler:);
SentrySwizzleInstanceMethod([NSURLSession class], selector,
SentrySWReturnType(NSURLSessionDataTask *),
SentrySWArguments(
NSURL * url, void (^completionHandler)(NSData *, NSURLResponse *, NSError *)),
SentrySWReplacement({
__block NSURLSessionDataTask *task = nil;
void (^wrappedHandler)(NSData *, NSURLResponse *, NSError *) = nil;
if (completionHandler) {
wrappedHandler = ^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error && data && task) {
[networkTracker captureResponseDetails:data
response:response
requestURL:url
task:task];
}
completionHandler(data, response, error);
};
}
task = SentrySWCallOriginal(url, wrappedHandler ?: completionHandler);
return task;
}),
SentrySwizzleModeOncePerClassAndSuperclasses, (void *)selector);
#pragma clang diagnostic pop
}
Comment thread
cursor[bot] marked this conversation as resolved.

@end

NS_ASSUME_NONNULL_END
5 changes: 5 additions & 0 deletions Sources/Sentry/include/SentryNetworkTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ static NSString *const SENTRY_NETWORK_REQUEST_TRACKER_BREADCRUMB
@property (nonatomic, readonly) BOOL isCaptureFailedRequestsEnabled;
@property (nonatomic, readonly) BOOL isGraphQLOperationTrackingEnabled;

- (void)captureResponseDetails:(NSData *)data
response:(NSURLResponse *)response
requestURL:(nullable NSURL *)requestURL
task:(NSURLSessionTask *)task;

@end

NS_ASSUME_NONNULL_END
4 changes: 4 additions & 0 deletions Sources/Sentry/include/SentrySwizzleWrapperHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ NS_ASSUME_NONNULL_BEGIN

+ (void)swizzleURLSessionTask:(SentryNetworkTracker *)networkTracker;

// Swizzle [NSURLSession dataTaskWithURL:completionHandler:]
// [NSURLSession dataTaskWithRequest:completionHandler:]
+ (void)swizzleURLSessionDataTasksForResponseCapture:(SentryNetworkTracker *)networkTracker;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ final class SentryNetworkTrackingIntegration<Dependencies: NetworkTrackerProvide
super.init()

SentrySwizzleWrapperHelper.swizzleURLSessionTask(networkTracker)

Comment thread
43jay marked this conversation as resolved.
#if (os(iOS) || os(tvOS)) && !SENTRY_NO_UI_FRAMEWORK
if options.sessionReplay.networkDetailHasUrls {
SentrySwizzleWrapperHelper.swizzleURLSessionDataTasks(forResponseCapture: networkTracker)
}
#endif
}

func uninstall() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,80 @@
import XCTest

// We need to know whether Apple changes the NSURLSessionTask implementation.
class SentryNSURLSessionTaskSearchTests: XCTestCase {

// We need to know whether Apple changes the NSURLSessionTask implementation.
func test_URLSessionTask_ByIosVersion() {
func test_URLSessionTask_ByIosVersion() {
let classes = SentryNSURLSessionTaskSearch.urlSessionTaskClassesToTrack()

XCTAssertEqual(classes.count, 1)
XCTAssertTrue(classes.first === URLSessionTask.self)
}

// 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, these tests
// will fail and we'll know to update the swizzling approach.

func test_URLSessionDataTaskWithRequest_ByIosVersion() {
let selector = #selector(URLSession.dataTask(with:completionHandler:)
as (URLSession) -> (URLRequest, @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask)
assertNSURLSessionImplementsDirectly(selector: selector, selectorName: "dataTaskWithRequest:completionHandler:")
}
Comment thread
43jay marked this conversation as resolved.

func test_URLSessionDataTaskWithURL_ByIosVersion() {
let selector = #selector(URLSession.dataTask(with:completionHandler:)
as (URLSession) -> (URL, @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask)
assertNSURLSessionImplementsDirectly(selector: selector, selectorName: "dataTaskWithURL:completionHandler:")
}

// MARK: - Helper

/// Walks the class hierarchy for sessions created with default and ephemeral
/// configurations and asserts that no subclass overrides `selector`.
private func assertNSURLSessionImplementsDirectly(selector: Selector, selectorName: String) {
let baseClass: AnyClass = URLSession.self

// The base class must implement the method.
XCTAssertNotNil(
class_getInstanceMethod(baseClass, selector),
"URLSession should implement \(selectorName)"
)

// Check sessions created with each relevant configuration.
let configs: [URLSessionConfiguration] = [
.default,
.ephemeral
]

for config in configs {
let session = URLSession(configuration: config)
let sessionClass: AnyClass = type(of: session)

defer { session.invalidateAndCancel() }

if sessionClass === baseClass {
continue
}

// If Apple returns a subclass, it must NOT provide its own
// implementation β€” it should inherit from URLSession.
let subMethod = class_getInstanceMethod(sessionClass, selector)
let baseMethod = class_getInstanceMethod(baseClass, selector)

if let subMethod, let baseMethod {
let subIMP = method_getImplementation(subMethod)
let baseIMP = method_getImplementation(baseMethod)
XCTAssertEqual(
subIMP, baseIMP,
"\(NSStringFromClass(sessionClass)) overrides \(selectorName) with an unexpected IMP β€” "
+ "Verify swizzling in SentrySwizzleWrapperHelper is correct for dataTasks."
)
}
}
}
}
7 changes: 7 additions & 0 deletions sdk_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,13 @@
"name": "UIKit",
"printedName": "UIKit"
},
{
"declKind": "Import",
"kind": "Import",
"moduleName": "Sentry",
"name": "UniformTypeIdentifiers",
"printedName": "UniformTypeIdentifiers"
},
{
"declKind": "Import",
"kind": "Import",
Expand Down
Loading