Skip to content

Commit 1a09b9d

Browse files
committed
feat(network-details): Add request details capture
1 parent 41cf944 commit 1a09b9d

3 files changed

Lines changed: 97 additions & 13 deletions

File tree

Sources/Sentry/SentryNetworkTracker.m

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas
236236
return;
237237
}
238238

239+
SentryOptions *options = SentrySDK.startOption;
240+
NSString *urlString = sessionTask.currentRequest.URL.absoluteString;
241+
if ([self isNetworkDetailCaptureEnabledFor:urlString options:options]) {
242+
[self captureRequestDetails:sessionTask
243+
networkCaptureBodies:options.sessionReplay.networkCaptureBodies
244+
networkRequestHeaders:options.sessionReplay.networkRequestHeaders];
245+
}
246+
239247
if (![self isTaskSupported:sessionTask]) {
240248
return;
241249
}
@@ -562,6 +570,26 @@ - (SentryLevel)getBreadcrumbLevel:(NSURLSessionTask *)sessionTask
562570
return breadcrumbLevel;
563571
}
564572

573+
// Store extracted network details data for session replay.
574+
static const void *SentryNetworkDetailsKey = &SentryNetworkDetailsKey;
575+
576+
- (BOOL)isNetworkDetailCaptureEnabledFor:(NSString *)urlString options:(SentryOptions *)options
577+
{
578+
if (!options) {
579+
return NO;
580+
}
581+
582+
if (!urlString) {
583+
return NO;
584+
}
585+
586+
if (!options.sessionReplay) {
587+
return NO;
588+
}
589+
590+
return [options.sessionReplay isNetworkDetailCaptureEnabledFor:urlString];
591+
}
592+
565593
- (void)captureResponseDetails:(NSData *)data
566594
response:(NSURLResponse *)response
567595
request:(NSURLRequest *)request
@@ -573,4 +601,45 @@ - (void)captureResponseDetails:(NSData *)data
573601
// 4. Handle size limits and truncation if needed
574602
}
575603

604+
- (void)captureRequestDetails:(NSURLSessionTask *)sessionTask
605+
networkCaptureBodies:(BOOL)networkCaptureBodies
606+
networkRequestHeaders:(NSArray<NSString *> *)networkRequestHeaders
607+
{
608+
if (!sessionTask || !sessionTask.currentRequest) {
609+
return;
610+
}
611+
612+
SentryReplayNetworkDetails *existingDetails
613+
= objc_getAssociatedObject(sessionTask, &SentryNetworkDetailsKey);
614+
if (existingDetails) {
615+
return;
616+
}
617+
618+
NSURLRequest *request = sessionTask.currentRequest;
619+
620+
SentryReplayNetworkDetails *details =
621+
[[SentryReplayNetworkDetails alloc] initWithMethod:request.HTTPMethod ?: @"GET"];
622+
623+
// Note: currentRequest.HTTPBody is often nil even when body exists, so we check originalRequest
624+
NSData *bodyData
625+
= networkCaptureBodies ? (request.HTTPBody ?: sessionTask.originalRequest.HTTPBody) : nil;
626+
627+
NSNumber *requestSize = nil;
628+
// Set request size - prefer actual bytes sent, fallback to local body data length
629+
if (sessionTask.countOfBytesSent > 0) {
630+
requestSize = [NSNumber numberWithLongLong:sessionTask.countOfBytesSent];
631+
} else if (bodyData) {
632+
requestSize = [NSNumber numberWithUnsignedInteger:bodyData.length];
633+
}
634+
635+
[details setRequestWithSize:requestSize
636+
bodyData:bodyData
637+
contentType:request.allHTTPHeaderFields[@"Content-Type"]
638+
allHeaders:request.allHTTPHeaderFields
639+
configuredHeaders:networkRequestHeaders];
640+
641+
objc_setAssociatedObject(
642+
sessionTask, &SentryNetworkDetailsKey, details, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
643+
}
644+
576645
@end

Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,36 +187,44 @@ enum NetworkBodyWarning: String {
187187

188188
// MARK: - ObjC Setters
189189

190-
/// Sets request details from raw components.
190+
/// Sets request details from raw body data.
191+
///
192+
/// Parses the body data based on content type (JSON, form-urlencoded, text)
193+
/// and applies size limits and truncation warnings automatically.
191194
///
192195
/// - Parameters:
193196
/// - size: Request body size in bytes, or nil if unknown.
194-
/// - body: Pre-parsed body content (dictionary, array, or string), or nil if not captured.
197+
/// - bodyData: Raw body bytes, or nil if body capture is disabled or unavailable.
198+
/// - contentType: MIME content type for body parsing (e.g. "application/json").
195199
/// - allHeaders: All headers from the request (e.g. from `NSURLRequest.allHTTPHeaderFields`).
196200
/// - configuredHeaders: Header names to extract, matched case-insensitively.
197201
@objc
198-
public func setRequest(size: NSNumber?, body: Any?, allHeaders: [String: Any]?, configuredHeaders: [String]?) {
202+
public func setRequest(size: NSNumber?, bodyData: Data?, contentType: String?, allHeaders: [String: Any]?, configuredHeaders: [String]?) {
199203
self.request = Detail(
200204
size: size,
201-
body: body.map { Body(content: $0) },
205+
body: bodyData.flatMap { Body(data: $0, contentType: contentType) },
202206
headers: SentryReplayNetworkDetails.extractHeaders(from: allHeaders, matching: configuredHeaders)
203207
)
204208
}
205209

206-
/// Sets response details from raw components.
210+
/// Sets response details from raw body data.
211+
///
212+
/// Parses the body data based on content type (JSON, form-urlencoded, text)
213+
/// and applies size limits and truncation warnings automatically.
207214
///
208215
/// - Parameters:
209216
/// - statusCode: HTTP status code.
210217
/// - size: Response body size in bytes, or nil if unknown.
211-
/// - body: Pre-parsed body content (dictionary, array, or string), or nil if not captured.
218+
/// - bodyData: Raw body bytes, or nil if body capture is disabled or unavailable.
219+
/// - contentType: MIME content type for body parsing (e.g. "application/json").
212220
/// - allHeaders: All headers from the response (e.g. from `NSHTTPURLResponse.allHeaderFields`).
213221
/// - configuredHeaders: Header names to extract, matched case-insensitively.
214222
@objc
215-
public func setResponse(statusCode: Int, size: NSNumber?, body: Any?, allHeaders: [String: Any]?, configuredHeaders: [String]?) {
223+
public func setResponse(statusCode: Int, size: NSNumber?, bodyData: Data?, contentType: String?, allHeaders: [String: Any]?, configuredHeaders: [String]?) {
216224
self.statusCode = NSNumber(value: statusCode)
217225
self.response = Detail(
218226
size: size,
219-
body: body.map { Body(content: $0) },
227+
body: bodyData.flatMap { Body(data: $0, contentType: contentType) },
220228
headers: SentryReplayNetworkDetails.extractHeaders(from: allHeaders, matching: configuredHeaders)
221229
)
222230
}

Tests/SentryTests/Networking/SentryReplayNetworkDetailsIntegrationTests.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,25 @@ class SentryReplayNetworkDetailsIntegrationTests: XCTestCase {
2020

2121
// MARK: - Serialization Tests
2222

23-
func testSerialize_withFullData_shouldReturnCompleteDictionary() {
23+
func testSerialize_withFullData_shouldReturnCompleteDictionary() throws {
2424
// -- Arrange --
2525
let details = SentryReplayNetworkDetails(method: "PUT")
2626

27+
let requestBodyData = try JSONSerialization.data(withJSONObject: ["name": "test"])
2728
details.setRequest(
2829
size: 100,
29-
body: ["name": "test"],
30+
bodyData: requestBodyData,
31+
contentType: "application/json",
3032
allHeaders: ["Content-Type": "application/json", "Authorization": "Bearer token", "Accept": "*/*"],
3133
configuredHeaders: ["Content-Type", "Authorization"]
3234
)
35+
36+
let responseBodyData = try JSONSerialization.data(withJSONObject: ["id": 123, "name": "test"])
3337
details.setResponse(
3438
statusCode: 201,
3539
size: 150,
36-
body: ["id": 123, "name": "test"],
40+
bodyData: responseBodyData,
41+
contentType: "application/json",
3742
allHeaders: ["Content-Type": "application/json", "Cache-Control": "no-cache", "Set-Cookie": "session=123"],
3843
configuredHeaders: ["Content-Type", "Cache-Control"]
3944
)
@@ -85,7 +90,8 @@ class SentryReplayNetworkDetailsIntegrationTests: XCTestCase {
8590
details.setResponse(
8691
statusCode: 404,
8792
size: nil,
88-
body: nil,
93+
bodyData: nil,
94+
contentType: nil,
8995
allHeaders: ["Cache-Control": "no-cache", "Content-Type": "text/plain", "X-Custom": "value"],
9096
configuredHeaders: ["Cache-Control", "Content-Type"]
9197
)
@@ -115,7 +121,8 @@ class SentryReplayNetworkDetailsIntegrationTests: XCTestCase {
115121
let details = SentryReplayNetworkDetails(method: "GET")
116122
details.setRequest(
117123
size: nil,
118-
body: nil,
124+
bodyData: nil,
125+
contentType: nil,
119126
allHeaders: [
120127
"Content-Type": "application/json",
121128
"Authorization": "Bearer secret",

0 commit comments

Comments
 (0)