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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
### Features

- Make feature Metrics generally available, moving experimental options to top-level options (#7843)
- Add Session Replay network details capture, including request/response headers and bodies extraction (#7580, #7582, #7584, #7585, #7588, #7590, #7854)

### Fixes

Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryNetworkTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ - (BOOL)isNetworkDetailCaptureEnabledFor:(NSString *)urlString options:(SentryOp
return NO;
}

if (!options.experimental.enableReplayNetworkDetailsCapturing) {
return NO;
}

if (!urlString) {
return NO;
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import Foundation
if options.experimental.enableUnhandledCPPExceptionsV2 {
features.append("unhandledCPPExceptionsV2")
}
if options.experimental.enableReplayNetworkDetailsCapturing {
features.append("replayNetworkDetails")
}
if options.enableMetrics {
features.append("metrics")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ final class SentryNetworkTrackingIntegration<Dependencies: NetworkTrackerProvide
SentrySwizzleWrapperHelper.swizzleURLSessionTask(networkTracker)

#if (os(iOS) || os(tvOS)) && !SENTRY_NO_UI_FRAMEWORK
if options.sessionReplay.networkDetailHasUrls {
if options.experimental.enableReplayNetworkDetailsCapturing && options.sessionReplay.networkDetailHasUrls {
SentrySwizzleWrapperHelper.swizzleURLSessionDataTasks(forResponseCapture: networkTracker)
}
#endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
* ```
*
* - Note: Request and response bodies are truncated to 150KB maximum.
* - Note: Requires `options.experimental.enableReplayNetworkDetailsCapturing` to be `true`.
* - Note: See ``SentryReplayOptions.DefaultValues.networkDetailAllowUrls`` for the default value.
*/
public var networkDetailAllowUrls: [SentryUrlMatchable]
Expand All @@ -354,6 +355,8 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
* - String patterns: "/auth/", "/payment/", "password", ".internal."
* - NSRegularExpression patterns: Use try NSRegularExpression(pattern:) to create regex objects
* - Mixed arrays are supported with both types
*
* - Note: Requires `options.experimental.enableReplayNetworkDetailsCapturing` to be `true`.
*/
public var networkDetailDenyUrls: [SentryUrlMatchable]

Expand All @@ -369,6 +372,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
*
* - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty.
* - Note: Bodies are automatically truncated to 150KB to prevent excessive memory usage.
* - Note: Requires `options.experimental.enableReplayNetworkDetailsCapturing` to be `true`.
*/
public var networkCaptureBodies: Bool

Expand All @@ -391,6 +395,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
*
* - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty.
* - Note: Header names preserve the case seen on the request, not the case specified here.
* - Note: Requires `options.experimental.enableReplayNetworkDetailsCapturing` to be `true`.
*/
public var networkRequestHeaders: [String] {
get { _networkRequestHeaders }
Expand All @@ -417,6 +422,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
*
* - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty.
* - Note: Header names preserve the case seen on the response, not the case specified here.
* - Note: Requires `options.experimental.enableReplayNetworkDetailsCapturing` to be `true`.
*/
public var networkResponseHeaders: [String] {
get { _networkResponseHeaders }
Expand Down Expand Up @@ -488,8 +494,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
*
* - Returns: `true` if `networkDetailAllowUrls` is non-empty, `false` otherwise.
*/
@objc
public var networkDetailHasUrls: Bool {
var networkDetailHasUrls: Bool {
!networkDetailAllowUrls.isEmpty
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/Swift/SentryExperimentalOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ public final class SentryExperimentalOptions: NSObject {
/// When enabled, the SDK uses a more efficient mechanism for detecting watchdog terminations.
public var enableWatchdogTerminationsV2 = false

/**
* Enables network detail capture for Session Replay.
*
* When enabled, the SDK can capture request and response headers and bodies for network
* requests during session replay. You must also configure
* `options.sessionReplay.networkDetailAllowUrls` with URL patterns to specify which
* requests should be captured.
*/
public var enableReplayNetworkDetailsCapturing = false

// swiftlint:disable:next missing_docs
@_spi(Private) public func validateOptions(_ options: [String: Any]?) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,68 @@ import XCTest
///
/// Uses postman-echo.com so no local test server is required.
class SentryNetworkDetailSwizzlingTests: XCTestCase {

private let echoURL = URL(string: "https://postman-echo.com/get")!

override func tearDown() {
super.tearDown()
clearTestState()
}

override func setUp() {
super.setUp()

// MARK: - Tests
func testDataNotCapturedIfExperimentalFlasNotEnabled() throws {
let options = Options()
options.dsn = TestConstants.dsnAsString(username: "SentryNetworkDetailSwizzlingTests")
options.tracesSampleRate = 1.0
options.enableNetworkBreadcrumbs = true
options.sessionReplay.networkDetailAllowUrls = ["postman-echo.com"]
options.sessionReplay.networkCaptureBodies = true
options.experimental.enableReplayNetworkDetailsCapturing = false
SentrySDK.start(options: options)
}

let transaction = SentrySDK.startTransaction(
name: "Test", operation: "test", bindToScope: true
)

override func tearDown() {
super.tearDown()
clearTestState()
}
let expect = expectation(description: "Request completed")
expect.assertForOverFulfill = false

// MARK: - Tests
let session = URLSession(configuration: .default)
let request = URLRequest(url: echoURL)

var receivedData: Data?
var receivedResponse: URLResponse?
var receivedError: Error?

let task = session.dataTask(with: request) { data, response, error in
receivedData = data
receivedResponse = response
receivedError = error
expect.fulfill()
}
defer { task.cancel() }

task.resume()
wait(for: [expect], timeout: 5)

transaction.finish()

// Original completion handler received valid data
XCTAssertNil(receivedError, "Request should succeed")
XCTAssertNotNil(receivedData, "Should receive response data")
let httpResponse = try XCTUnwrap(receivedResponse as? HTTPURLResponse)
XCTAssertEqual(httpResponse.statusCode, 200)

// Network details were captured via the swizzled completion handler
let breadcrumb = try lastHTTPBreadcrumb(for: echoURL)
XCTAssertNil(breadcrumb.data?[SentryReplayNetworkDetails.replayNetworkDetailsKey], "Breadcrumbs should not contain any network details")
}

/// Verifies the swizzle of `-[NSURLSession dataTaskWithRequest:completionHandler:]`
/// captures response details into the breadcrumb.
func testDataTaskWithRequest_completionHandler_capturesNetworkDetails() throws {
startSDK()

let transaction = SentrySDK.startTransaction(
name: "Test", operation: "test", bindToScope: true
)
Expand Down Expand Up @@ -86,6 +123,8 @@ class SentryNetworkDetailSwizzlingTests: XCTestCase {
/// Verifies the swizzle of `-[NSURLSession dataTaskWithURL:completionHandler:]`
/// captures response details into the breadcrumb.
func testDataTaskWithURL_completionHandler_capturesNetworkDetails() throws {
startSDK()

let transaction = SentrySDK.startTransaction(
name: "Test", operation: "test", bindToScope: true
)
Expand Down Expand Up @@ -130,6 +169,17 @@ class SentryNetworkDetailSwizzlingTests: XCTestCase {
}

// MARK: - Helpers

private func startSDK() {
let options = Options()
options.dsn = TestConstants.dsnAsString(username: "SentryNetworkDetailSwizzlingTests")
options.tracesSampleRate = 1.0
options.enableNetworkBreadcrumbs = true
options.sessionReplay.networkDetailAllowUrls = ["postman-echo.com"]
options.sessionReplay.networkCaptureBodies = true
options.experimental.enableReplayNetworkDetailsCapturing = true
SentrySDK.start(options: options)
}

/// Finds the most recent HTTP breadcrumb whose URL matches the given URL.
private func lastHTTPBreadcrumb(for url: URL) throws -> Breadcrumb {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ class SentryNetworkTrackerTests: XCTestCase {
XCTAssertEqual(payloadData["fragment"] as? String, "fragment")
}

#if canImport(UIKit) && SENTRY_TARGET_REPLAY_SUPPORTED
#if os(iOS) || os(tvOS)
/// Simple case - when network details are enabled, `addBreadcrumbForSessionTask` will include
/// serialized network details in the breadcrumb data.
func testAddBreadcrumb_withNetworkDetails_shouldIncludeSerializedDetailsInBreadcrumbData() throws {
Expand All @@ -511,6 +511,7 @@ class SentryNetworkTrackerTests: XCTestCase {
options.sessionReplay.networkDetailAllowUrls = ["api.example.com"]
options.sessionReplay.networkResponseHeaders = ["Cache-Control"]
options.sessionReplay.networkCaptureBodies = false
options.experimental.enableReplayNetworkDetailsCapturing = true

let scope = Scope()
let client = TestClient(options: options)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ - (void)testInit_withoutArguments_shouldUseDefaults

// Currently there is not Obj-C SDK support for network details
// => assert it's turned off.
XCTAssertFalse(options.networkDetailHasUrls);
}

- (void)testInit_withAllArguments_shouldSetAllValues
Expand Down Expand Up @@ -72,7 +71,6 @@ - (void)testInit_withAllArguments_shouldSetAllValues

// Currently there is not Obj-C SDK support for network details
// => assert it's turned off.
XCTAssertFalse(options.networkDetailHasUrls);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SentryReplayOptionsTests: XCTestCase {
XCTAssertEqual(options.sessionSegmentDuration, 5)
XCTAssertEqual(options.maximumDuration, 60 * 60)

XCTAssertFalse(options.networkDetailHasUrls)
XCTAssertEqual(options.networkDetailAllowUrls.count, 0)
XCTAssertEqual(options.networkDetailDenyUrls.count, 0)
XCTAssertTrue(options.networkCaptureBodies)
Expand Down Expand Up @@ -69,6 +70,7 @@ class SentryReplayOptionsTests: XCTestCase {
XCTAssertEqual(options.maximumDuration, 60 * 60)

// Network details assertions
XCTAssertTrue(options.networkDetailHasUrls)
XCTAssertEqual(options.networkDetailAllowUrls as? [String], ["https://api.example.com", "https://test.example.org"])
XCTAssertEqual(options.networkDetailDenyUrls as? [String], ["https://sensitive.example.com", "https://private.example.org"])
XCTAssertFalse(options.networkCaptureBodies)
Expand Down
119 changes: 76 additions & 43 deletions sdk_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -36731,6 +36731,82 @@
"printedName": "init()",
"usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)init"
},
{
"accessors": [
{
"accessorKind": "get",
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
}
],
"declAttributes": [
"Final",
"ObjC"
],
"declKind": "Accessor",
"implicit": true,
"kind": "Accessor",
"mangledName": "$s6Sentry0A19ExperimentalOptionsC35enableReplayNetworkDetailsCapturingSbvg",
"moduleName": "Sentry",
"name": "Get",
"printedName": "Get()",
"usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)enableReplayNetworkDetailsCapturing"
},
{
"accessorKind": "set",
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
},
{
"kind": "TypeNominal",
"name": "Void",
"printedName": "()"
}
],
"declAttributes": [
"Final",
"ObjC"
],
"declKind": "Accessor",
"implicit": true,
"kind": "Accessor",
"mangledName": "$s6Sentry0A19ExperimentalOptionsC35enableReplayNetworkDetailsCapturingSbvs",
"moduleName": "Sentry",
"name": "Set",
"printedName": "Set()",
"usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(im)setEnableReplayNetworkDetailsCapturing:"
}
],
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
}
],
"declAttributes": [
"Final",
"HasStorage",
"ObjC"
],
"declKind": "Var",
"hasStorage": true,
"kind": "Var",
"mangledName": "$s6Sentry0A19ExperimentalOptionsC35enableReplayNetworkDetailsCapturingSbvp",
"moduleName": "Sentry",
"name": "enableReplayNetworkDetailsCapturing",
"printedName": "enableReplayNetworkDetailsCapturing",
"usr": "c:@M@Sentry@objc(cs)SentryExperimentalOptions(py)enableReplayNetworkDetailsCapturing"
},
{
"accessors": [
{
Expand Down Expand Up @@ -50338,49 +50414,6 @@
"printedName": "networkDetailDenyUrls",
"usr": "s:6Sentry0A13ReplayOptionsC21networkDetailDenyUrlsSayAA0A12UrlMatchable_pGvp"
},
{
"accessors": [
{
"accessorKind": "get",
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
}
],
"declAttributes": [
"ObjC"
],
"declKind": "Accessor",
"kind": "Accessor",
"mangledName": "$s6Sentry0A13ReplayOptionsC20networkDetailHasUrlsSbvg",
"moduleName": "Sentry",
"name": "Get",
"printedName": "Get()",
"usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)networkDetailHasUrls"
}
],
"children": [
{
"kind": "TypeNominal",
"name": "Bool",
"printedName": "Swift.Bool",
"usr": "s:Sb"
}
],
"declAttributes": [
"ObjC"
],
"declKind": "Var",
"kind": "Var",
"mangledName": "$s6Sentry0A13ReplayOptionsC20networkDetailHasUrlsSbvp",
"moduleName": "Sentry",
"name": "networkDetailHasUrls",
"printedName": "networkDetailHasUrls",
"usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)networkDetailHasUrls"
},
{
"accessors": [
{
Expand Down
Loading