diff --git a/CHANGELOG_V10.md b/CHANGELOG_V10.md index 2f512ec69b..31ade7c374 100644 --- a/CHANGELOG_V10.md +++ b/CHANGELOG_V10.md @@ -10,12 +10,15 @@ - Add query parameter filtering for network spans, breadcrumbs, and failed requests using `options.dataCollection.urlQueryParams` (#8414) - Enable automatic user information for logs, metrics, and IP inference by default; configure it with `options.dataCollection.userInfo` (#8254) - Add HTTP header and cookie filtering for failed requests using `options.dataCollection` (#8460) + - Gate Session Replay request and response body capture with `options.dataCollection.httpBodies`, and scrub sensitive body values (#8547) ### Breaking Changes - Remove Objective-C `@objc` attributes from SentrySDK (#8308) - Remove deprecated `locale` from device context; use `locale` in culture context instead (#8325) - Change `SentryRequest.cookies` from a string to a dictionary of cookie names and values (#8460) +- Change `SentryReplayOptions.networkCaptureBodies` from `Bool` to `SentryReplayOptions.NetworkBodyCapture`. (#8547) + Use `.inherit` to follow `dataCollection.httpBodies`, `.enabled` to capture all Replay network bodies, or `.disabled` to capture none ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index a253ee5cb4..40659abede 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -1787,6 +1787,7 @@ Public/SentryObjCPrivateSDKOnly.h, Public/SentryObjCRedactRegionType.h, Public/SentryObjCReplayApi.h, + Public/SentryObjCReplayNetworkBodyCapture.h, Public/SentryObjCReplayOptions.h, Public/SentryObjCReplayQuality.h, Public/SentryObjCRequest.h, diff --git a/Sources/SentryObjC/Public/SentryObjC.h b/Sources/SentryObjC/Public/SentryObjC.h index a6246cb1a9..6aa99630b0 100644 --- a/Sources/SentryObjC/Public/SentryObjC.h +++ b/Sources/SentryObjC/Public/SentryObjC.h @@ -14,6 +14,7 @@ # import "SentryObjCLastRunStatus.h" # import "SentryObjCLevel.h" # import "SentryObjCLogLevel.h" +# import "SentryObjCReplayNetworkBodyCapture.h" # import "SentryObjCReplayQuality.h" # import "SentryObjCSampleDecision.h" # import "SentryObjCSpanStatus.h" @@ -24,6 +25,7 @@ # import # import # import +# import # import # import # import diff --git a/Sources/SentryObjC/Public/SentryObjCReplayNetworkBodyCapture.h b/Sources/SentryObjC/Public/SentryObjCReplayNetworkBodyCapture.h new file mode 100644 index 0000000000..bb6335d390 --- /dev/null +++ b/Sources/SentryObjC/Public/SentryObjCReplayNetworkBodyCapture.h @@ -0,0 +1,13 @@ +#if SDK_V10 +# import + +/// Controls whether Session Replay captures HTTP request and response bodies. +typedef NS_ENUM(NSInteger, SentryObjCReplayNetworkBodyCapture) { + /// Inherit request and response body capture from data collection options. + SentryObjCReplayNetworkBodyCaptureInherit = 0, + /// Capture both request and response bodies. + SentryObjCReplayNetworkBodyCaptureEnabled, + /// Do not capture request or response bodies. + SentryObjCReplayNetworkBodyCaptureDisabled +}; +#endif // SDK_V10 diff --git a/Sources/SentryObjC/Public/SentryObjCReplayOptions.h b/Sources/SentryObjC/Public/SentryObjCReplayOptions.h index 8bdf26afdb..a0e43cdcc0 100644 --- a/Sources/SentryObjC/Public/SentryObjCReplayOptions.h +++ b/Sources/SentryObjC/Public/SentryObjCReplayOptions.h @@ -1,7 +1,9 @@ #import #if !__has_include() +# import "SentryObjCReplayNetworkBodyCapture.h" # import "SentryObjCReplayQuality.h" #else +# import # import #endif @@ -78,6 +80,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, copy) NSArray *unmaskedViewClasses; +#if SDK_V10 +/** + * Controls request and response body capture for allowed URLs. + * + * The default is @c SentryObjCReplayNetworkBodyCaptureInherit. It uses the global data collection + * body types unless explicitly enabled or disabled for Replay. + */ +@property (nonatomic) SentryObjCReplayNetworkBodyCapture networkCaptureBodies; +#else /** * Whether to capture request and response bodies for allowed URLs. * @@ -86,6 +97,7 @@ NS_ASSUME_NONNULL_BEGIN * When @c NO, only headers and metadata will be captured for allowed URLs. */ @property (nonatomic) BOOL networkCaptureBodies; +#endif /** * Request headers to capture for allowed URLs during session replay. diff --git a/Sources/SentryObjCCompat/SentryObjCReplayNetworkBodyCapture.swift b/Sources/SentryObjCCompat/SentryObjCReplayNetworkBodyCapture.swift new file mode 100644 index 0000000000..4309b2e339 --- /dev/null +++ b/Sources/SentryObjCCompat/SentryObjCReplayNetworkBodyCapture.swift @@ -0,0 +1,36 @@ +#if SDK_V10 +// swiftlint:disable missing_docs +#if SWIFT_PACKAGE +internal import SentrySwift +#else +internal import Sentry +#endif +import Foundation + +@objc public enum SentryObjCReplayNetworkBodyCapture: Int { + case inherit + case enabled + case disabled +} + +extension SentryObjCReplayNetworkBodyCapture { + init(_ underlying: SentryReplayOptions.NetworkBodyCapture) { + switch underlying { + case .inherit: self = .inherit + case .enabled: self = .enabled + case .disabled: self = .disabled + @unknown default: self = .inherit + } + } + + var underlying: SentryReplayOptions.NetworkBodyCapture { + switch self { + case .inherit: return .inherit + case .enabled: return .enabled + case .disabled: return .disabled + } + } +} + +// swiftlint:enable missing_docs +#endif // SDK_V10 diff --git a/Sources/SentryObjCCompat/SentryObjCReplayOptions.swift b/Sources/SentryObjCCompat/SentryObjCReplayOptions.swift index 7ad7a8fba6..2e624deb20 100644 --- a/Sources/SentryObjCCompat/SentryObjCReplayOptions.swift +++ b/Sources/SentryObjCCompat/SentryObjCReplayOptions.swift @@ -62,10 +62,17 @@ import Foundation set { wrapped.unmaskedViewClasses = newValue } } +#if SDK_V10 + @objc public var networkCaptureBodies: SentryObjCReplayNetworkBodyCapture { + get { SentryObjCReplayNetworkBodyCapture(wrapped.networkCaptureBodies) } + set { wrapped.networkCaptureBodies = newValue.underlying } + } +#else @objc public var networkCaptureBodies: Bool { get { wrapped.networkCaptureBodies } set { wrapped.networkCaptureBodies = newValue } } +#endif // SDK_V10 @objc public var networkRequestHeaders: [String] { get { wrapped.networkRequestHeaders } diff --git a/Sources/Swift/DataCollection/SentryDataCollection+KeyValueFilter.swift b/Sources/Swift/DataCollection/SentryDataCollection+KeyValueFilter.swift index 8da87b34dd..d326ec3dde 100644 --- a/Sources/Swift/DataCollection/SentryDataCollection+KeyValueFilter.swift +++ b/Sources/Swift/DataCollection/SentryDataCollection+KeyValueFilter.swift @@ -9,6 +9,14 @@ extension SentryDataCollection { "sso", "saml", "csrf", "xsrf", "credentials", "session", "sid", "identity" ] + static func filterSensitiveValues(_ values: [String: Any]) -> [String: Any] { + values.reduce(into: [:]) { result, pair in + result[pair.key] = matches(key: pair.key, terms: sensitiveTerms) + ? filteredValue + : pair.value + } + } + static func filter( _ values: [String: String], behavior: SentryDataCollection.KeyValueCollectionBehavior diff --git a/Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebOptionsEvent.swift b/Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebOptionsEvent.swift index 2f0451e630..8392fd0c85 100644 --- a/Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebOptionsEvent.swift +++ b/Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebOptionsEvent.swift @@ -43,7 +43,11 @@ final class SentryRRWebOptionsEvent: SentryRRWebCustomEvent { return String(describing: pattern) } } +#if SDK_V10 + payload["networkCaptureBodies"] = options.networkCaptureBodies.serializedValue +#else payload["networkCaptureBodies"] = options.networkCaptureBodies +#endif // SDK_V10 payload["networkRequestHeaders"] = options.networkRequestHeaders payload["networkResponseHeaders"] = options.networkResponseHeaders } diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift index 429058c3dd..502129c79d 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayNetworkDetails.swift @@ -51,6 +51,8 @@ enum NetworkBodyWarning: String { /// Captured request or response body with optional parsing warnings. struct Body { + private static let filteredValue = "[Filtered]" + let content: BodyContent let warnings: [NetworkBodyWarning] @@ -79,8 +81,12 @@ enum NetworkBodyWarning: String { } else if #available(macOS 11, *), let parsed = Body.parseByMimeType(mimeType, data: slice, encoding: encoding, isTruncated: isTruncated, warnings: &warnings) { self = parsed } else { +#if SDK_V10 + self = Body(content: Body.filteredValue) +#else let description = "[Body not captured: contentType=\(contentType ?? "unknown") (\(data.count) bytes)]" self = Body(content: description) +#endif // SDK_V10 } } @@ -137,7 +143,11 @@ enum NetworkBodyWarning: String { } if utType.conforms(to: .text) { if isTruncated { warnings.append(.textTruncated) } +#if SDK_V10 + return Body(content: filteredValue, warnings: warnings) +#else return parseText(data, encoding: encoding, warnings: &warnings) +#endif // SDK_V10 } return nil } @@ -145,10 +155,24 @@ enum NetworkBodyWarning: String { private static func parseJSON(_ data: Data, encoding: String.Encoding = .utf8, warnings: inout [NetworkBodyWarning]) -> Body { do { let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) +#if SDK_V10 + guard let values = json as? [String: Any] else { + return Body(content: filteredValue, warnings: warnings) + } + return Body( + content: SentryDataCollection.KeyValueFilter.filterSensitiveValues(values), + warnings: warnings + ) +#else return Body(content: json, warnings: warnings) +#endif // SDK_V10 } catch { warnings.append(.bodyParseError) +#if SDK_V10 + return Body(content: filteredValue, warnings: warnings) +#else return parseText(data, encoding: encoding, warnings: &warnings) +#endif // SDK_V10 } } @@ -156,7 +180,11 @@ enum NetworkBodyWarning: String { private static func parseFormEncoded(_ data: Data, encoding: String.Encoding, warnings: inout [NetworkBodyWarning]) -> Body { guard let urlEncodedFormData = String(data: data, encoding: encoding) ?? String(data: data, encoding: .utf8) else { warnings.append(.bodyParseError) +#if SDK_V10 + return Body(content: filteredValue, warnings: warnings) +#else return parseText(data, encoding: encoding, warnings: &warnings) +#endif // SDK_V10 } var formData = [String: Any]() @@ -164,7 +192,11 @@ enum NetworkBodyWarning: String { let comps = rawElement.components(separatedBy: "=") if comps.count < 2 { warnings.append(.bodyParseError) +#if SDK_V10 + return Body(content: filteredValue, warnings: warnings) +#else return parseText(data, encoding: encoding, warnings: &warnings) +#endif // SDK_V10 } let key = decodeFormComponent(comps[0]) let value = decodeFormComponent(comps.dropFirst().joined(separator: "=")) @@ -180,7 +212,14 @@ enum NetworkBodyWarning: String { formData[key] = value } } +#if SDK_V10 + return Body( + content: SentryDataCollection.KeyValueFilter.filterSensitiveValues(formData), + warnings: warnings + ) +#else return Body(content: formData, warnings: warnings) +#endif // SDK_V10 } /// Decodes a form-urlencoded component: converts `+` to space and removes percent-encoding. diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift index 681183123f..50b3dfdeea 100644 --- a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -28,7 +28,11 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { // Network capture configuration defaults public static let networkDetailAllowUrls: [SentryUrlMatchable] = [] public static let networkDetailDenyUrls: [SentryUrlMatchable] = [] +#if SDK_V10 + public static let networkCaptureBodies: NetworkBodyCapture = .inherit +#else public static let networkCaptureBodies: Bool = true +#endif // SDK_V10 public static let networkRequestHeaders: [String] = ["Content-Type", "Content-Length", "Accept"] public static let networkResponseHeaders: [String] = ["Content-Type", "Content-Length", "Accept"] @@ -108,6 +112,26 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { } } +#if SDK_V10 + /// Controls whether Session Replay captures HTTP request and response bodies. + public enum NetworkBodyCapture { + /// Inherit request and response body capture from `dataCollection.httpBodies`. + case inherit + /// Capture both request and response bodies. + case enabled + /// Do not capture request or response bodies. + case disabled + + var serializedValue: String { + switch self { + case .inherit: return "inherit" + case .enabled: return "enabled" + case .disabled: return "disabled" + } + } + } +#endif // SDK_V10 + /** * Indicates the percentage in which the replay for the session will be created. * @@ -360,6 +384,19 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { */ public var networkDetailDenyUrls: [SentryUrlMatchable] +#if SDK_V10 + /** + * Controls request and response body capture for allowed URLs. + * + * The default ``NetworkBodyCapture/inherit`` value uses `dataCollection.httpBodies` for each + * body direction. Use ``NetworkBodyCapture/enabled`` or ``NetworkBodyCapture/disabled`` to + * override the global setting for Replay. + * + * - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty. + * - Note: Bodies are automatically truncated to 150KB to prevent excessive memory usage. + */ + @nonobjc public var networkCaptureBodies: NetworkBodyCapture +#else /** * Whether to capture request and response bodies for allowed URLs. * @@ -374,6 +411,7 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { * - Note: Bodies are automatically truncated to 150KB to prevent excessive memory usage. */ public var networkCaptureBodies: Bool +#endif // SDK_V10 /** * Request headers to capture for allowed URLs during session replay. @@ -705,7 +743,11 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { self.includedViewClasses = includedViewClasses ?? DefaultValues.includedViewClasses self.networkDetailAllowUrls = networkDetailAllowUrls ?? DefaultValues.networkDetailAllowUrls self.networkDetailDenyUrls = networkDetailDenyUrls ?? DefaultValues.networkDetailDenyUrls +#if SDK_V10 + self.networkCaptureBodies = networkCaptureBodies.map { $0 ? .enabled : .disabled } ?? .inherit +#else self.networkCaptureBodies = networkCaptureBodies ?? DefaultValues.networkCaptureBodies +#endif // SDK_V10 self._networkRequestHeaders = Self.mergeWithDefaultHeaders(networkRequestHeaders, defaults: DefaultValues.networkRequestHeaders) self._networkResponseHeaders = Self.mergeWithDefaultHeaders(networkResponseHeaders, defaults: DefaultValues.networkResponseHeaders) @@ -745,4 +787,23 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions { } } + +#if SDK_V10 +extension SentryReplayOptions { + @nonobjc + func shouldCaptureNetworkBody( + _ bodyType: SentryDataCollection.HttpBodyType, + dataCollection: SentryDataCollection.Options + ) -> Bool { + switch networkCaptureBodies { + case .inherit: + return dataCollection.httpBodies.contains(bodyType) + case .enabled: + return true + case .disabled: + return false + } + } +} +#endif // swiftlint:enable file_length missing_docs type_body_length diff --git a/Sources/Swift/Networking/SentryNetworkTracker.swift b/Sources/Swift/Networking/SentryNetworkTracker.swift index 380d624a3e..b521b6890e 100644 --- a/Sources/Swift/Networking/SentryNetworkTracker.swift +++ b/Sources/Swift/Networking/SentryNetworkTracker.swift @@ -199,9 +199,17 @@ final class SentryDefaultNetworkTracker Void = { _ in } + ) throws -> [String: Any] { + guard #available(iOS 16.0, tvOS 16.0, *) else { return [:] } + + let url = URL(string: "https://api.example.com/users")! + let options = Options() + options.dsn = "https://key@sentry.io/1234" + options.dataCollection.httpBodies = httpBodies + options.sessionReplay.networkDetailAllowUrls = ["api.example.com"] + configureOptions(options) + + let scope = Scope() + let client = TestClient(options: options) + let hub = TestHub(client: client, andScope: scope) + SentrySDKInternal.setCurrentHub(hub) + SentrySDK.setStart(with: options) + + let tracker = TestNetworkTracker( + options: options, + dependencies: SentryDependencyContainer.sharedInstance() + ) + tracker.enableNetworkTracking() + tracker.enableNetworkBreadcrumbs() + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = Data(#"{"request":"value"}"#.utf8) + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let task = URLSessionDataTaskMock(request: request) + let response = try XCTUnwrap(HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: "1.1", + headerFields: ["Content-Type": "application/json"] + )) + task.setResponse(response) + + tracker.urlSessionTask(task, setState: .running) + tracker.captureResponseDetails( + Data(#"{"response":"value"}"#.utf8), + response: response, + request: url, + task: task + ) + + guard case .valid(let details) = task.networkDetails else { + XCTFail("Expected network details") + return [:] + } + return details.serialize() + } +#endif + /// Regression test for #8388: `captureResponseDetails` must read the response `Content-Type` /// case-insensitively. HTTP/2 and HTTP/3 lowercase field names, so the server sends /// `content-type`. If the tracker reverts to the case-sensitive `allHeaderFields["Content-Type"]` @@ -704,7 +820,11 @@ class SentryNetworkTrackerTests: XCTestCase { let options = Options() options.dsn = "https://key@sentry.io/1234" options.sessionReplay.networkDetailAllowUrls = ["api.example.com"] +#if SDK_V10 + options.sessionReplay.networkCaptureBodies = .enabled +#else options.sessionReplay.networkCaptureBodies = true +#endif let scope = Scope() let client = TestClient(options: options) @@ -728,7 +848,7 @@ class SentryNetworkTrackerTests: XCTestCase { )) task.setResponse(httpResponse) - let jsonBody = Data(#"{"key":"value"}"#.utf8) + let jsonBody = Data(#"{"field":"value"}"#.utf8) // -- Act -- tracker.urlSessionTask(task, setState: .running) @@ -753,7 +873,7 @@ class SentryNetworkTrackerTests: XCTestCase { // unwrap below fails. let bodyDict = try XCTUnwrap(responseDict["body"] as? [String: Any]) let parsedBody = try XCTUnwrap(bodyDict["body"] as? [String: Any]) - XCTAssertEqual(parsedBody["key"] as? String, "value") + XCTAssertEqual(parsedBody["field"] as? String, "value") clearTestState() } diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsNetworkTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsNetworkTests.swift index 317e4eee99..97b1d8bebf 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsNetworkTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsNetworkTests.swift @@ -20,7 +20,11 @@ class SentryReplayOptionsNetworkTests: XCTestCase { // -- Assert -- XCTAssertEqual(options.networkDetailAllowUrls.count, 0, "networkDetailAllowUrls should fallback to empty array on invalid data provided") +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .inherit) +#else XCTAssertTrue(options.networkCaptureBodies, "networkCaptureBodies should fallback to true on invalid data provided") +#endif } func testNetworkDetailUrls_withEmptyStrings_shouldFilterOutEmptyEntries() { diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift index 3675c65db6..d69911cb91 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayOptionsTests.swift @@ -29,11 +29,111 @@ class SentryReplayOptionsTests: XCTestCase { XCTAssertFalse(options.networkDetailHasUrls) XCTAssertEqual(options.networkDetailAllowUrls.count, 0) XCTAssertEqual(options.networkDetailDenyUrls.count, 0) +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .inherit) +#else XCTAssertTrue(options.networkCaptureBodies) +#endif XCTAssertEqual(options.networkRequestHeaders, ["Content-Type", "Content-Length", "Accept"]) XCTAssertEqual(options.networkResponseHeaders, ["Content-Type", "Content-Length", "Accept"]) } + func testShouldCaptureNetworkBody_whenReplayOptionInherits_shouldUseDataCollection() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let options = SentryReplayOptions() + let dataCollection = SentryDataCollection.Options(httpBodies: [.outgoingRequest]) + + // -- Act & Assert -- + XCTAssertTrue(options.shouldCaptureNetworkBody(.outgoingRequest, dataCollection: dataCollection)) + XCTAssertFalse(options.shouldCaptureNetworkBody(.incomingResponse, dataCollection: dataCollection)) +#endif + } + + func testShouldCaptureNetworkBody_whenReplayOptionIsEnabled_shouldOverrideDataCollection() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let options = SentryReplayOptions() + options.networkCaptureBodies = .enabled + let dataCollection = SentryDataCollection.Options(httpBodies: []) + + // -- Act & Assert -- + XCTAssertTrue(options.shouldCaptureNetworkBody(.outgoingRequest, dataCollection: dataCollection)) + XCTAssertTrue(options.shouldCaptureNetworkBody(.incomingResponse, dataCollection: dataCollection)) +#endif + } + + func testShouldCaptureNetworkBody_whenReplayOptionIsDisabled_shouldOverrideDataCollection() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let options = SentryReplayOptions() + options.networkCaptureBodies = .disabled + let dataCollection = SentryDataCollection.Options(httpBodies: .all) + + // -- Act & Assert -- + XCTAssertFalse(options.shouldCaptureNetworkBody(.outgoingRequest, dataCollection: dataCollection)) + XCTAssertFalse(options.shouldCaptureNetworkBody(.incomingResponse, dataCollection: dataCollection)) +#endif + } + + func testShouldCaptureNetworkBody_whenDictionaryContainsTrue_shouldEnable() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let options = SentryReplayOptions(dictionary: ["networkCaptureBodies": true]) + let dataCollection = SentryDataCollection.Options(httpBodies: []) + + // -- Act & Assert -- + XCTAssertEqual(options.networkCaptureBodies, .enabled) + XCTAssertTrue(options.shouldCaptureNetworkBody(.outgoingRequest, dataCollection: dataCollection)) + XCTAssertTrue(options.shouldCaptureNetworkBody(.incomingResponse, dataCollection: dataCollection)) +#endif + } + + func testShouldCaptureNetworkBody_whenDictionaryContainsFalse_shouldDisable() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let options = SentryReplayOptions(dictionary: ["networkCaptureBodies": false]) + let dataCollection = SentryDataCollection.Options(httpBodies: .all) + + // -- Act & Assert -- + XCTAssertEqual(options.networkCaptureBodies, .disabled) + XCTAssertFalse(options.shouldCaptureNetworkBody(.outgoingRequest, dataCollection: dataCollection)) + XCTAssertFalse(options.shouldCaptureNetworkBody(.incomingResponse, dataCollection: dataCollection)) +#endif + } + + func testRRWebOptionsEvent_whenNetworkBodyCaptureConfigured_shouldSerializeValue() { + // -- Arrange -- + let options = SentryReplayOptions() + options.networkDetailAllowUrls = ["example.com"] +#if SDK_V10 + options.networkCaptureBodies = .inherit +#else + options.networkCaptureBodies = false +#endif + + // -- Act -- + let event = SentryRRWebOptionsEvent(timestamp: Date(), options: options) + + // -- Assert -- + let payload = event.data?["payload"] as? [String: Any] +#if SDK_V10 + XCTAssertEqual(payload?["networkCaptureBodies"] as? String, "inherit") +#else + XCTAssertEqual(payload?["networkCaptureBodies"] as? Bool, false) +#endif + } + func testInit_withAllArguments_shouldSetValues() { // -- Act -- // Use the opposite of the default values to check if they are set correctly @@ -49,7 +149,11 @@ class SentryReplayOptionsTests: XCTestCase { // Set network details options after initialization since they're not in the public initializer options.networkDetailAllowUrls = ["https://api.example.com", "https://test.example.org"] options.networkDetailDenyUrls = ["https://sensitive.example.com", "https://private.example.org"] +#if SDK_V10 + options.networkCaptureBodies = .disabled +#else options.networkCaptureBodies = false +#endif options.networkRequestHeaders = ["Authorization", "User-Agent", "X-Custom-Header"] options.networkResponseHeaders = ["Cache-Control", "Set-Cookie", "X-Rate-Limit"] @@ -73,7 +177,11 @@ class SentryReplayOptionsTests: XCTestCase { 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"]) +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .disabled) +#else XCTAssertFalse(options.networkCaptureBodies) +#endif XCTAssertEqual(options.networkRequestHeaders, ["Content-Type", "Content-Length", "Accept", "Authorization", "User-Agent", "X-Custom-Header"]) XCTAssertEqual(options.networkResponseHeaders, ["Content-Type", "Content-Length", "Accept", "Cache-Control", "Set-Cookie", "X-Rate-Limit"]) } @@ -241,7 +349,11 @@ class SentryReplayOptionsTests: XCTestCase { // Network options should use defaults when not specified in dictionary XCTAssertEqual(options.networkDetailAllowUrls.count, 0) XCTAssertEqual(options.networkDetailDenyUrls.count, 0) +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .inherit) +#else XCTAssertTrue(options.networkCaptureBodies) +#endif XCTAssertEqual(options.networkRequestHeaders, ["Content-Type", "Content-Length", "Accept"]) XCTAssertEqual(options.networkResponseHeaders, ["Content-Type", "Content-Length", "Accept"]) } @@ -303,7 +415,11 @@ class SentryReplayOptionsTests: XCTestCase { // Network options XCTAssertEqual(options.networkDetailAllowUrls as? [String], ["https://api.example.com", "https://test.com"]) XCTAssertEqual(options.networkDetailDenyUrls as? [String], ["https://sensitive.com", "https://auth.com"]) +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .disabled) +#else XCTAssertFalse(options.networkCaptureBodies) +#endif XCTAssertEqual(options.networkRequestHeaders, ["Content-Type", "Content-Length", "Accept", "Authorization", "User-Agent"]) XCTAssertEqual(options.networkResponseHeaders, ["Content-Type", "Content-Length", "Accept", "Cache-Control", "Set-Cookie"]) } @@ -796,7 +912,11 @@ class SentryReplayOptionsTests: XCTestCase { // -- Assert -- XCTAssertEqual(options.networkDetailAllowUrls as? [String], ["https://api.example.com", "https://test.com"]) - XCTAssertTrue(options.networkCaptureBodies) // Should remain default +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .inherit) +#else + XCTAssertTrue(options.networkCaptureBodies) +#endif } func testInitFromDict_networkCaptureBodies_whenValidValue_shouldSetValue() { @@ -806,7 +926,11 @@ class SentryReplayOptionsTests: XCTestCase { ]) // -- Assert -- +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .disabled) +#else XCTAssertFalse(options.networkCaptureBodies) +#endif XCTAssertEqual(options.networkDetailAllowUrls.count, 0) // Should remain default } @@ -817,7 +941,11 @@ class SentryReplayOptionsTests: XCTestCase { ]) // -- Assert -- +#if SDK_V10 + XCTAssertEqual(options.networkCaptureBodies, .enabled) +#else XCTAssertTrue(options.networkCaptureBodies) +#endif } func testInitFromDict_networkDetailDenyUrls_whenValidValue_shouldSetValue() { diff --git a/Tests/SentryTests/Networking/SentryReplayNetworkDetailsBodyTests.swift b/Tests/SentryTests/Networking/SentryReplayNetworkDetailsBodyTests.swift index 8a6552a982..7dee177aff 100644 --- a/Tests/SentryTests/Networking/SentryReplayNetworkDetailsBodyTests.swift +++ b/Tests/SentryTests/Networking/SentryReplayNetworkDetailsBodyTests.swift @@ -9,7 +9,7 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { func testInit_withJSONDictionary_shouldParseCorrectly() throws { // -- Arrange -- - let bodyContent: [String: Any] = ["key": "value", "number": 42] + let bodyContent: [String: Any] = ["field": "value", "number": 42] let bodyData = try JSONSerialization.data(withJSONObject: bodyContent) // -- Act -- @@ -18,13 +18,55 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- if case .json(let value) = body?.content { let dict = value as? [String: Any] - XCTAssertEqual(dict?["key"] as? String, "value") + XCTAssertEqual(dict?["field"] as? String, "value") XCTAssertEqual(dict?["number"] as? Int, 42) } else { XCTFail("Expected .json content") } } + func testInit_whenV10JSONHasSensitiveTopLevelKeys_shouldFilterTheirValues() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let bodyData = try JSONSerialization.data(withJSONObject: [ + "authToken": "abc123", + "password": "secret", + "name": "Jane" + ]) + + // -- Act -- + let body = try XCTUnwrap(Body(data: bodyData, contentType: "application/json")) + + // -- Assert -- + let values = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) + XCTAssertEqual(values["authToken"] as? String, "[Filtered]") + XCTAssertEqual(values["password"] as? String, "[Filtered]") + XCTAssertEqual(values["name"] as? String, "Jane") +#endif + } + + func testInit_whenV10JSONHasNestedSensitiveKeys_shouldNotFilterNestedValues() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Arrange -- + let bodyData = try JSONSerialization.data(withJSONObject: [ + "profile": ["token": "nested-value"], + "name": "Jane" + ]) + + // -- Act -- + let body = try XCTUnwrap(Body(data: bodyData, contentType: "application/json")) + + // -- Assert -- + let values = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) + let profile = try XCTUnwrap(values["profile"] as? [String: Any]) + XCTAssertEqual(profile["token"] as? String, "nested-value") +#endif + } + func testInit_withJSONArray_shouldParseCorrectly() throws { // -- Arrange -- let bodyContent = ["item1", "item2", "item3"] @@ -34,6 +76,9 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { let body = Body(data: bodyData, contentType: "application/json") // -- Assert -- +#if SDK_V10 + XCTAssertEqual(body?.serialize()["body"] as? String, "[Filtered]") +#else if case .json(let value) = body?.content { let array = value as? [String] XCTAssertEqual(array?.count, 3) @@ -43,6 +88,7 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { } else { XCTFail("Expected .json content") } +#endif } func testInit_withTextData_shouldStoreAsString() { @@ -56,7 +102,11 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(body) if case .text(let string) = body?.content { +#if SDK_V10 + XCTAssertEqual(string, "[Filtered]") +#else XCTAssertEqual(string, bodyContent) +#endif } else { XCTFail("Expected .text content") } @@ -81,9 +131,13 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(body) if case .text(let string) = body?.content { +#if SDK_V10 + XCTAssertEqual(string, "[Filtered]") +#else XCTAssertEqual(string, invalidJSON) +#endif } else { - XCTFail("Expected .text fallback for invalid JSON") + XCTFail("Expected text content") } XCTAssertTrue(body?.warnings.contains(.bodyParseError) == true) } @@ -123,16 +177,24 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(body) if case .text(let string) = body?.content { +#if SDK_V10 + XCTAssertEqual(string, "[Filtered]") +#else XCTAssertTrue(string.hasPrefix("[Body not captured")) XCTAssertTrue(string.contains("8 bytes")) XCTAssertTrue(string.contains("image/png")) +#endif } else { - XCTFail("Expected .text content with binary description") + XCTFail("Expected text content") } let result = try XCTUnwrap(body?.serialize()) - let bodyString = try XCTUnwrap(result["body"] as? String, "Expected body to be a string with binary data prefix") + let bodyString = try XCTUnwrap(result["body"] as? String) +#if SDK_V10 + XCTAssertEqual(bodyString, "[Filtered]") +#else XCTAssertTrue(bodyString.hasPrefix("[Body not captured")) +#endif } func testInit_withNilContentType_shouldCreatePlaceholder() { @@ -145,11 +207,15 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(body) if case .text(let string) = body?.content { +#if SDK_V10 + XCTAssertEqual(string, "[Filtered]") +#else XCTAssertTrue(string.hasPrefix("[Body not captured")) XCTAssertTrue(string.contains("4 bytes")) XCTAssertTrue(string.contains("unknown")) +#endif } else { - XCTFail("Expected .text content with placeholder description") + XCTFail("Expected text content") } } @@ -163,11 +229,15 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(body) if case .text(let string) = body?.content { +#if SDK_V10 + XCTAssertEqual(string, "[Filtered]") +#else XCTAssertTrue(string.hasPrefix("[Body not captured")) XCTAssertTrue(string.contains("application/x-custom-format")) XCTAssertTrue(string.contains("9 bytes")) +#endif } else { - XCTFail("Expected .text content with placeholder description") + XCTFail("Expected text content") } } @@ -175,7 +245,7 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { func testInit_withFormURLEncoded_shouldParseAsForm() { // -- Arrange -- - let formString = "key1=value1&key2=value2&key3=value%20with%20spaces" + let formString = "field1=value1&field2=value2&field3=value%20with%20spaces" let bodyData = Data(formString.utf8) // -- Act -- @@ -184,14 +254,31 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- if case .json(let value) = body?.content { let dict = value as? [String: String] - XCTAssertEqual(dict?["key1"], "value1") - XCTAssertEqual(dict?["key2"], "value2") - XCTAssertEqual(dict?["key3"], "value with spaces") + XCTAssertEqual(dict?["field1"], "value1") + XCTAssertEqual(dict?["field2"], "value2") + XCTAssertEqual(dict?["field3"], "value with spaces") } else { XCTFail("Expected .json content for form data") } } + func testInit_whenV10FormHasSensitiveKeys_shouldFilterTheirValues() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + // -- Act -- + let body = try XCTUnwrap(Body( + data: Data("sessionId=abc123&name=Jane".utf8), + contentType: "application/x-www-form-urlencoded" + )) + + // -- Assert -- + let values = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) + XCTAssertEqual(values["sessionId"] as? String, "[Filtered]") + XCTAssertEqual(values["name"] as? String, "Jane") +#endif + } + func testInit_withFormURLEncoded_duplicateKeys_shouldPromoteToArray() throws { // -- Act -- let body = try XCTUnwrap(Body( @@ -207,27 +294,31 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { func testInit_withFormURLEncoded_emptyValue_shouldParseAsEmptyString() throws { // -- Act -- let body = try XCTUnwrap(Body( - data: Data("key1=&key2=value2".utf8), + data: Data("field1=&field2=value2".utf8), contentType: "application/x-www-form-urlencoded; charset=utf-8" )) // -- Assert -- let dict = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) - XCTAssertEqual(dict["key1"] as? String, "") - XCTAssertEqual(dict["key2"] as? String, "value2") + XCTAssertEqual(dict["field1"] as? String, "") + XCTAssertEqual(dict["field2"] as? String, "value2") } func testInit_withFormURLEncoded_missingEquals_shouldFallbackToText() throws { // -- Act -- let body = try XCTUnwrap(Body( - data: Data("key1=value1&malformed&key2=value2".utf8), + data: Data("field1=value1&malformed&field2=value2".utf8), contentType: "application/x-www-form-urlencoded; charset=utf-8" )) // -- Assert -- let serialized = body.serialize() let text = try XCTUnwrap(serialized["body"] as? String) - XCTAssertEqual(text, "key1=value1&malformed&key2=value2") +#if SDK_V10 + XCTAssertEqual(text, "[Filtered]") +#else + XCTAssertEqual(text, "field1=value1&malformed&field2=value2") +#endif let warnings = try XCTUnwrap(serialized["warnings"] as? [String]) XCTAssertTrue(warnings.contains("BODY_PARSE_ERROR")) } @@ -235,14 +326,14 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { func testInit_withFormURLEncoded_emptyKeys_shouldBeSkipped() throws { // -- Act -- let body = try XCTUnwrap(Body( - data: Data("=value1&key2=value2".utf8), + data: Data("=value1&field2=value2".utf8), contentType: "application/x-www-form-urlencoded; charset=utf-8" )) // -- Assert -- let dict = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) XCTAssertNil(dict[""]) - XCTAssertEqual(dict["key2"] as? String, "value2") + XCTAssertEqual(dict["field2"] as? String, "value2") } func testInit_withFormURLEncoded_equalsInValue_shouldPreserve() throws { @@ -255,7 +346,11 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- let dict = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) XCTAssertEqual(dict["query"] as? String, "a=1") +#if SDK_V10 + XCTAssertEqual(dict["token"] as? String, "[Filtered]") +#else XCTAssertEqual(dict["token"] as? String, "abc") +#endif } func testInit_withFormURLEncoded_plusAsSpace_shouldDecode() throws { @@ -275,16 +370,40 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // %ZZ is invalid percent-encoding → removingPercentEncoding returns nil. // The fallback should still preserve +-to-space and = in values. let body = try XCTUnwrap(Body( - data: Data("key=a%ZZ=b&greeting=hello+world".utf8), + data: Data("field=a%ZZ=b&greeting=hello+world".utf8), contentType: "application/x-www-form-urlencoded; charset=utf-8" )) let dict = try XCTUnwrap(body.serialize()["body"] as? [String: Any]) // Value preserves the joined "=" and the +-to-space conversion on the valid pair - XCTAssertEqual(dict["key"] as? String, "a%ZZ=b") + XCTAssertEqual(dict["field"] as? String, "a%ZZ=b") XCTAssertEqual(dict["greeting"] as? String, "hello world") } + func testInit_whenV10BodyIsNotKeyValueStructure_shouldFilterEntireBody() throws { +#if !SDK_V10 + throw XCTSkip("Test skipped for SDK_V10") +#else + let bodies: [(Data, String?)] = [ + (Data("{ invalid json }".utf8), "application/json"), + (try JSONSerialization.data(withJSONObject: ["item1", "item2"]), "application/json"), + (Data("42".utf8), "application/json"), + (Data("key=value&malformed".utf8), "application/x-www-form-urlencoded"), + (Data("plain text".utf8), "text/plain"), + (Data("".utf8), "text/html"), + (Data("".utf8), "application/xml"), + (Data([0x89, 0x50, 0x4E, 0x47]), "image/png"), + (Data("unknown".utf8), nil), + (Data("unknown".utf8), "application/x-custom-format") + ] + + for (data, contentType) in bodies { + let body = try XCTUnwrap(Body(data: data, contentType: contentType)) + XCTAssertEqual(body.serialize()["body"] as? String, "[Filtered]") + } +#endif + } + // MARK: - Multi-byte Truncation func testInit_withTruncatedMultiByteUTF8_shouldRecoverValidPrefix() throws { @@ -298,36 +417,60 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { let cjk = Data("你好".utf8) XCTAssertEqual(cjk.count, 6) let body1 = try XCTUnwrap(Body(data: cjk.prefix(5), contentType: "text/plain; charset=utf-8")) +#if SDK_V10 + XCTAssertEqual(body1.serialize()["body"] as? String, "[Filtered]") +#else XCTAssertEqual(body1.serialize()["body"] as? String, "你") +#endif // -- dropLast(2): 3-byte char split after 1 byte -- // prefix(4) cuts second char after 1 of 3 bytes let body2 = try XCTUnwrap(Body(data: cjk.prefix(4), contentType: "text/plain; charset=utf-8")) +#if SDK_V10 + XCTAssertEqual(body2.serialize()["body"] as? String, "[Filtered]") +#else XCTAssertEqual(body2.serialize()["body"] as? String, "你") +#endif // -- dropLast(3): 4-byte emoji split after 1 byte -- // "A😀" = 1 + 4 = 5 bytes; prefix(2) cuts emoji after 1 of 4 bytes let emoji = Data("A😀".utf8) XCTAssertEqual(emoji.count, 5) let body3 = try XCTUnwrap(Body(data: emoji.prefix(2), contentType: "text/plain; charset=utf-8")) +#if SDK_V10 + XCTAssertEqual(body3.serialize()["body"] as? String, "[Filtered]") +#else XCTAssertEqual(body3.serialize()["body"] as? String, "A") +#endif // -- no truncation needed: clean boundary -- // prefix(3) is exactly "你", no bytes to drop let body4 = try XCTUnwrap(Body(data: cjk.prefix(3), contentType: "text/plain; charset=utf-8")) +#if SDK_V10 + XCTAssertEqual(body4.serialize()["body"] as? String, "[Filtered]") +#else XCTAssertEqual(body4.serialize()["body"] as? String, "你") +#endif // -- pure ASCII: never affected -- let ascii = Data("hello".utf8) let body5 = try XCTUnwrap(Body(data: ascii.prefix(3), contentType: "text/plain; charset=utf-8")) +#if SDK_V10 + XCTAssertEqual(body5.serialize()["body"] as? String, "[Filtered]") +#else XCTAssertEqual(body5.serialize()["body"] as? String, "hel") +#endif // -- 2-byte char split after 1 byte -- // "Aé" = 1 + 2 = 3 bytes; prefix(2) cuts "é" after 1 of 2 bytes let accented = Data("Aé".utf8) XCTAssertEqual(accented.count, 3) let body6 = try XCTUnwrap(Body(data: accented.prefix(2), contentType: "text/plain; charset=utf-8")) +#if SDK_V10 + XCTAssertEqual(body6.serialize()["body"] as? String, "[Filtered]") +#else XCTAssertEqual(body6.serialize()["body"] as? String, "A") +#endif } // MARK: - Serialization Tests @@ -343,7 +486,11 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(result) +#if SDK_V10 + XCTAssertEqual(result?["body"] as? String, "[Filtered]") +#else XCTAssertEqual(result?["body"] as? String, bodyContent) +#endif } func testSerialize_withJSONDictionary_shouldReturnDictionary() throws { @@ -371,9 +518,13 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { let result = try XCTUnwrap(body?.serialize()) // -- Assert -- +#if SDK_V10 + XCTAssertEqual(result["body"] as? String, "[Filtered]") +#else let bodyArray = try XCTUnwrap(result["body"] as? NSArray, "Expected body to be NSArray") XCTAssertEqual(bodyArray.count, 3) XCTAssertEqual(bodyArray[0] as? String, "item1") +#endif } func testSerialize_withNoContentType_shouldCreatePlaceholder() { @@ -388,7 +539,11 @@ class SentryReplayNetworkDetailsBodyTests: XCTestCase { // -- Assert -- XCTAssertNotNil(result) let bodyString = result?["body"] as? String +#if SDK_V10 + XCTAssertEqual(bodyString, "[Filtered]") +#else XCTAssertTrue(bodyString?.hasPrefix("[Body not captured") == true) +#endif } // MARK: - parseMimeAndEncoding diff --git a/sdk_api_objc_v10.diff.json b/sdk_api_objc_v10.diff.json index c0679c1bec..2403d549b0 100644 --- a/sdk_api_objc_v10.diff.json +++ b/sdk_api_objc_v10.diff.json @@ -190,6 +190,13 @@ "returnType": "SentryObjCTransactionNameSource", "instance": true }, + { + "kind": "ObjCMethodDecl", + "name": "networkCaptureBodies", + "parent": "SentryObjCReplayOptions", + "returnType": "SentryObjCReplayNetworkBodyCapture", + "instance": true + }, { "kind": "ObjCMethodDecl", "name": "new", @@ -340,6 +347,12 @@ "parent": "SentryObjCTransactionContext", "type": "SentryObjCTransactionNameSource" }, + { + "kind": "ObjCPropertyDecl", + "name": "networkCaptureBodies", + "parent": "SentryObjCReplayOptions", + "type": "SentryObjCReplayNetworkBodyCapture" + }, { "kind": "ObjCPropertyDecl", "name": "onLastRunStatusDetermined", @@ -626,6 +639,13 @@ "returnType": "enum SentryObjCTransactionNameSource", "instance": true }, + { + "kind": "ObjCMethodDecl", + "name": "networkCaptureBodies", + "parent": "SentryObjCReplayOptions", + "returnType": "enum SentryObjCReplayNetworkBodyCapture", + "instance": true + }, { "kind": "ObjCMethodDecl", "name": "new", @@ -790,6 +810,12 @@ "parent": "SentryObjCTransactionContext", "type": "enum SentryObjCTransactionNameSource" }, + { + "kind": "ObjCPropertyDecl", + "name": "networkCaptureBodies", + "parent": "SentryObjCReplayOptions", + "type": "enum SentryObjCReplayNetworkBodyCapture" + }, { "kind": "ObjCPropertyDecl", "name": "onLastRunStatusDetermined", diff --git a/sdk_api_objc_v10.json b/sdk_api_objc_v10.json index b567fbba6c..0a347d8e63 100644 --- a/sdk_api_objc_v10.json +++ b/sdk_api_objc_v10.json @@ -159,6 +159,21 @@ "name": "SentryObjCRedactRegionTypeRedactSwiftUI", "parent": "SentryObjCRedactRegionType" }, + { + "kind": "EnumConstantDecl", + "name": "SentryObjCReplayNetworkBodyCaptureDisabled", + "parent": "SentryObjCReplayNetworkBodyCapture" + }, + { + "kind": "EnumConstantDecl", + "name": "SentryObjCReplayNetworkBodyCaptureEnabled", + "parent": "SentryObjCReplayNetworkBodyCapture" + }, + { + "kind": "EnumConstantDecl", + "name": "SentryObjCReplayNetworkBodyCaptureInherit", + "parent": "SentryObjCReplayNetworkBodyCapture" + }, { "kind": "EnumConstantDecl", "name": "SentryObjCReplayQualityHigh", @@ -356,6 +371,10 @@ "kind": "EnumDecl", "name": "SentryObjCRedactRegionType" }, + { + "kind": "EnumDecl", + "name": "SentryObjCReplayNetworkBodyCapture" + }, { "kind": "EnumDecl", "name": "SentryObjCReplayQuality" @@ -4390,7 +4409,7 @@ "kind": "ObjCMethodDecl", "name": "networkCaptureBodies", "parent": "SentryObjCReplayOptions", - "returnType": "BOOL", + "returnType": "SentryObjCReplayNetworkBodyCapture", "instance": true }, { @@ -10307,7 +10326,7 @@ "kind": "ObjCPropertyDecl", "name": "networkCaptureBodies", "parent": "SentryObjCReplayOptions", - "type": "BOOL" + "type": "SentryObjCReplayNetworkBodyCapture" }, { "kind": "ObjCPropertyDecl", @@ -11501,6 +11520,11 @@ "name": "SentryObjCRedactRegionType", "type": "enum SentryObjCRedactRegionType" }, + { + "kind": "TypedefDecl", + "name": "SentryObjCReplayNetworkBodyCapture", + "type": "enum SentryObjCReplayNetworkBodyCapture" + }, { "kind": "TypedefDecl", "name": "SentryObjCReplayQuality", diff --git a/sdk_api_v10.json b/sdk_api_v10.json index 8d01fa6460..2f51138065 100644 --- a/sdk_api_v10.json +++ b/sdk_api_v10.json @@ -55645,9 +55645,9 @@ "children": [ { "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" } ], "declAttributes": [ @@ -55656,20 +55656,20 @@ "declKind": "Accessor", "implicit": true, "kind": "Accessor", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesSbvgZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesAC011NetworkBodyG0OvgZ", "moduleName": "Sentry", "name": "Get", "printedName": "Get()", "static": true, - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesSbvgZ" + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesAC011NetworkBodyG0OvgZ" } ], "children": [ { "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" } ], "declAttributes": [ @@ -55680,12 +55680,12 @@ "hasStorage": true, "isLet": true, "kind": "Var", - "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesSbvpZ", + "mangledName": "$s6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesAC011NetworkBodyG0OvpZ", "moduleName": "Sentry", "name": "networkCaptureBodies", "printedName": "networkCaptureBodies", "static": true, - "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesSbvpZ" + "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC20networkCaptureBodiesAC011NetworkBodyG0OvpZ" }, { "accessors": [ @@ -56239,6 +56239,250 @@ "printedName": "DefaultValues", "usr": "s:6Sentry0A13ReplayOptionsC13DefaultValuesC" }, + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + }, + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + }, + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "kind": "Function", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO2eeoiySbAE_AEtFZ", + "moduleName": "Sentry", + "name": "==", + "printedName": "==(_:_:)", + "static": true, + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO2eeoiySbAE_AEtFZ" + }, + { + "children": [ + { + "kind": "TypeNominal", + "name": "Hasher", + "paramValueOwnership": "InOut", + "printedName": "Swift.Hasher", + "usr": "s:s6HasherV" + }, + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ], + "declKind": "Func", + "funcSelfKind": "NonMutating", + "kind": "Function", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO4hash4intoys6HasherVz_tF", + "moduleName": "Sentry", + "name": "hash", + "printedName": "hash(into:)", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO4hash4intoys6HasherVz_tF" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture.Type" + }, + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryReplayOptions.NetworkBodyCapture.Type) -> Sentry.SentryReplayOptions.NetworkBodyCapture" + } + ], + "declKind": "EnumElement", + "kind": "Var", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO8disabledyA2EmF", + "moduleName": "Sentry", + "name": "disabled", + "printedName": "disabled", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO8disabledyA2EmF" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture.Type" + }, + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryReplayOptions.NetworkBodyCapture.Type) -> Sentry.SentryReplayOptions.NetworkBodyCapture" + } + ], + "declKind": "EnumElement", + "kind": "Var", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO7enabledyA2EmF", + "moduleName": "Sentry", + "name": "enabled", + "printedName": "enabled", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO7enabledyA2EmF" + }, + { + "accessors": [ + { + "accessorKind": "get", + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Accessor", + "kind": "Accessor", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO9hashValueSivg", + "moduleName": "Sentry", + "name": "Get", + "printedName": "Get()", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO9hashValueSivg" + } + ], + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Var", + "kind": "Var", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO9hashValueSivp", + "moduleName": "Sentry", + "name": "hashValue", + "printedName": "hashValue", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO9hashValueSivp" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture.Type" + }, + { + "kind": "TypeNominal", + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + } + ], + "kind": "TypeFunc", + "name": "Function", + "printedName": "(Sentry.SentryReplayOptions.NetworkBodyCapture.Type) -> Sentry.SentryReplayOptions.NetworkBodyCapture" + } + ], + "declKind": "EnumElement", + "kind": "Var", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO7inherityA2EmF", + "moduleName": "Sentry", + "name": "inherit", + "printedName": "inherit", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO7inherityA2EmF" + } + ], + "conformances": [ + { + "kind": "Conformance", + "mangledName": "$ss8CopyableP", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP" + }, + { + "kind": "Conformance", + "mangledName": "$sSQ", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ" + }, + { + "kind": "Conformance", + "mangledName": "$ss9EscapableP", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP" + }, + { + "kind": "Conformance", + "mangledName": "$sSH", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH" + } + ], + "declKind": "Enum", + "kind": "TypeDecl", + "mangledName": "$s6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO", + "moduleName": "Sentry", + "name": "NetworkBodyCapture", + "printedName": "NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" + }, { "children": [ { @@ -57244,31 +57488,28 @@ "children": [ { "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" } ], - "declAttributes": [ - "ObjC" - ], "declKind": "Accessor", "implicit": true, "kind": "Accessor", - "mangledName": "$s6Sentry0A13ReplayOptionsC20networkCaptureBodiesSbvg", + "mangledName": "$s6Sentry0A13ReplayOptionsC20networkCaptureBodiesAC011NetworkBodyE0Ovg", "moduleName": "Sentry", "name": "Get", "printedName": "Get()", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)networkCaptureBodies" + "usr": "s:6Sentry0A13ReplayOptionsC20networkCaptureBodiesAC011NetworkBodyE0Ovg" }, { "accessorKind": "set", "children": [ { "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" }, { "kind": "TypeNominal", @@ -57276,39 +57517,36 @@ "printedName": "()" } ], - "declAttributes": [ - "ObjC" - ], "declKind": "Accessor", "implicit": true, "kind": "Accessor", - "mangledName": "$s6Sentry0A13ReplayOptionsC20networkCaptureBodiesSbvs", + "mangledName": "$s6Sentry0A13ReplayOptionsC20networkCaptureBodiesAC011NetworkBodyE0Ovs", "moduleName": "Sentry", "name": "Set", "printedName": "Set()", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(im)setNetworkCaptureBodies:" + "usr": "s:6Sentry0A13ReplayOptionsC20networkCaptureBodiesAC011NetworkBodyE0Ovs" } ], "children": [ { "kind": "TypeNominal", - "name": "Bool", - "printedName": "Swift.Bool", - "usr": "s:Sb" + "name": "NetworkBodyCapture", + "printedName": "Sentry.SentryReplayOptions.NetworkBodyCapture", + "usr": "s:6Sentry0A13ReplayOptionsC18NetworkBodyCaptureO" } ], "declAttributes": [ "HasStorage", - "ObjC" + "NonObjC" ], "declKind": "Var", "hasStorage": true, "kind": "Var", - "mangledName": "$s6Sentry0A13ReplayOptionsC20networkCaptureBodiesSbvp", + "mangledName": "$s6Sentry0A13ReplayOptionsC20networkCaptureBodiesAC011NetworkBodyE0Ovp", "moduleName": "Sentry", "name": "networkCaptureBodies", "printedName": "networkCaptureBodies", - "usr": "c:@M@Sentry@objc(cs)SentryReplayOptions(py)networkCaptureBodies" + "usr": "s:6Sentry0A13ReplayOptionsC20networkCaptureBodiesAC011NetworkBodyE0Ovp" }, { "accessors": [