Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG_V10.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions Sources/SentryObjC/Public/SentryObjC.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -24,6 +25,7 @@
# import <SentryObjC/SentryObjCLastRunStatus.h>
# import <SentryObjC/SentryObjCLevel.h>
# import <SentryObjC/SentryObjCLogLevel.h>
# import <SentryObjC/SentryObjCReplayNetworkBodyCapture.h>
# import <SentryObjC/SentryObjCReplayQuality.h>
# import <SentryObjC/SentryObjCSampleDecision.h>
# import <SentryObjC/SentryObjCSpanStatus.h>
Expand Down
13 changes: 13 additions & 0 deletions Sources/SentryObjC/Public/SentryObjCReplayNetworkBodyCapture.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#if SDK_V10
# import <Foundation/Foundation.h>

/// 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
12 changes: 12 additions & 0 deletions Sources/SentryObjC/Public/SentryObjCReplayOptions.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#import <Foundation/Foundation.h>
#if !__has_include(<SentryObjC/SentryObjCDefines.h>)
# import "SentryObjCReplayNetworkBodyCapture.h"
# import "SentryObjCReplayQuality.h"
#else
# import <SentryObjC/SentryObjCReplayNetworkBodyCapture.h>
# import <SentryObjC/SentryObjCReplayQuality.h>
#endif

Expand Down Expand Up @@ -78,6 +80,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, copy) NSArray<Class> *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.
*
Expand All @@ -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.
Expand Down
36 changes: 36 additions & 0 deletions Sources/SentryObjCCompat/SentryObjCReplayNetworkBodyCapture.swift
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions Sources/SentryObjCCompat/SentryObjCReplayOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -137,34 +143,60 @@ 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
}

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
}
}

/// Parses `application/x-www-form-urlencoded` data into a dictionary.
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]()
for rawElement in urlEncodedFormData.components(separatedBy: "&") where !rawElement.isEmpty {
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: "="))
Expand All @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Loading
Loading