1- // swiftlint:disable file_length missing_docs
1+ // swiftlint:disable file_length missing_docs type_body_length
22import Foundation
33
44@objcMembers
@@ -25,6 +25,13 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
2525 public static let excludedViewClasses : Set < String > = [ ]
2626 public static let includedViewClasses : Set < String > = [ ]
2727
28+ // Network capture configuration defaults
29+ public static let networkDetailAllowUrls : [ Any ] = [ ]
30+ public static let networkDetailDenyUrls : [ Any ] = [ ]
31+ public static let networkCaptureBodies : Bool = true
32+ public static let networkRequestHeaders : [ String ] = [ " Content-Type " , " Content-Length " , " Accept " ]
33+ public static let networkResponseHeaders : [ String ] = [ " Content-Type " , " Content-Length " , " Accept " ]
34+
2835 // The following properties are defaults which are not configurable by the user.
2936
3037 fileprivate static let sdkInfo : [ String : Any ] ? = nil
@@ -292,6 +299,131 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
292299 */
293300 public var enableFastViewRendering : Bool
294301
302+ /**
303+ * A list of URL patterns to capture request and response details for during session replay.
304+ *
305+ * When non-empty, network requests with URLs matching any of these patterns will have their
306+ * headers and bodies captured for session replay.
307+ *
308+ * Supports both String and NSRegularExpression patterns (See [JavaScript SDK](https://github.com/getsentry/sentry-javascript/blob/6fb1ee139a92a6055b52b0bbf5136fa0e5a9353f/packages/core/src/utils/string.ts#L114-L119)):
309+ * - String: Uses substring contains
310+ * - NSRegularExpression: Uses full regex matching
311+ *
312+ * Default: empty array (network detail capture disabled)
313+ *
314+ * Example:
315+ * ```swift
316+ * // String patterns (substring matching)
317+ * options.sessionReplay.networkDetailAllowUrls = [
318+ * "api.example.com", // Matches any URL containing this string
319+ * "/api/v1/", // Matches any URL containing this path
320+ * "https://analytics.myapp.com" // Matches any URL containing this prefix
321+ * ]
322+ *
323+ * // NSRegularExpression patterns (full regex matching)
324+ * let apiRegex = try? NSRegularExpression(pattern: "^https://api\\.example\\.com/v[0-9]+/.*")
325+ * let imageRegex = try? NSRegularExpression(pattern: ".*\\.(jpg|jpeg|png|gif)$")
326+ *
327+ * // Mixed array of both types
328+ * options.sessionReplay.networkDetailAllowUrls = [
329+ * "api.example.com", // String: substring match
330+ * apiRegex!, // Regex: versioned API endpoints
331+ * imageRegex! // Regex: image files
332+ * ]
333+ * ```
334+ *
335+ * - Note: Request and response bodies are truncated to 150KB maximum.
336+ * - Note: See ``SentryReplayOptions.DefaultValues.networkDetailAllowUrls`` for the default value.
337+ */
338+ public var networkDetailAllowUrls : [ Any ]
339+
340+ /**
341+ * A list of URL patterns to exclude from network detail capture during session replay.
342+ *
343+ * URLs matching any pattern in this array will NOT have their headers and bodies captured,
344+ * even if they match patterns in `networkDetailAllowUrls`. This provides fine-grained
345+ * control for excluding sensitive endpoints from capture.
346+ *
347+ * Supports both String and NSRegularExpression patterns (mirroring JavaScript SDK):
348+ * - String: Uses substring containment check (like JavaScript's `includes()`)
349+ * - NSRegularExpression: Uses full regex matching
350+ *
351+ * Default: empty array (no URLs explicitly denied)
352+ *
353+ * Examples:
354+ * - String patterns: "/auth/", "/payment/", "password", ".internal."
355+ * - NSRegularExpression patterns: Use try NSRegularExpression(pattern:) to create regex objects
356+ * - Mixed arrays are supported with both types
357+ */
358+ public var networkDetailDenyUrls : [ Any ]
359+
360+ /**
361+ * Whether to capture request and response bodies for allowed URLs.
362+ *
363+ * When `true` (default), bodies will be captured and parsed (JSON bodies are
364+ * parsed for structured display in the Sentry UI).
365+ *
366+ * When `false`, only headers and metadata will be captured for allowed URLs.
367+ *
368+ * Default: `true`
369+ *
370+ * - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty.
371+ * - Note: Bodies are automatically truncated to 150KB to prevent excessive memory usage.
372+ */
373+ public var networkCaptureBodies : Bool
374+
375+ /**
376+ * Request headers to capture for allowed URLs during session replay.
377+ *
378+ * Specifies which HTTP request headers should be captured and included in session replay
379+ * network details. Header matching is case-insensitive (e.g., "content-type", "Content-Type",
380+ * and "CoNtEnT-tYpE" are all equivalent).
381+ *
382+ * Default (always included): `["Content-Type", "Content-Length", "Accept"]`
383+ *
384+ * Example:
385+ * ```
386+ * options.sessionReplay.networkRequestHeaders = [
387+ * "Authorization",
388+ * "User-Agent"
389+ * ]
390+ * ```
391+ *
392+ * - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty.
393+ * - Note: Header names preserve the case seen on the request, not the case specified here.
394+ */
395+ public var networkRequestHeaders : [ String ] {
396+ get { _networkRequestHeaders }
397+ set { _networkRequestHeaders = Self . mergeWithDefaultHeaders ( newValue, defaults: DefaultValues . networkRequestHeaders) }
398+ }
399+ private var _networkRequestHeaders : [ String ]
400+
401+ /**
402+ * Response headers to capture for allowed URLs during session replay.
403+ *
404+ * Specifies which HTTP response headers should be captured and included in session replay
405+ * network details. Header matching is case-insensitive (e.g., "content-type", "Content-Type",
406+ * and "CoNtEnT-tYpE" are all equivalent).
407+ *
408+ * Default (always included): `["Content-Type", "Content-Length", "Accept"]`
409+ *
410+ * Example:
411+ * ```
412+ * options.sessionReplay.networkResponseHeaders = [
413+ * "Cache-Control", // Custom header
414+ * "Set-Cookie" // Custom header
415+ * ]
416+ * ```
417+ *
418+ * - Note: This setting only applies when ``networkDetailAllowUrls`` is non-empty.
419+ * - Note: Header names preserve the case seen on the response, not the case specified here.
420+ */
421+ public var networkResponseHeaders : [ String ] {
422+ get { _networkResponseHeaders }
423+ set { _networkResponseHeaders = Self . mergeWithDefaultHeaders ( newValue, defaults: DefaultValues . networkResponseHeaders) }
424+ }
425+ private var _networkResponseHeaders : [ String ]
426+
295427 /**
296428 * Defines the quality of the session replay.
297429 *
@@ -351,6 +483,60 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
351483 */
352484 var sdkInfo : [ String : Any ] ?
353485
486+ /**
487+ * Determines if network detail capture is enabled for a given URL.
488+ *
489+ * - Parameter urlString: The URL string to check
490+ * - Returns: `true` if network details should be captured for this URL, `false` otherwise
491+ */
492+ @objc
493+ public func isNetworkDetailCaptureEnabled( for urlString: String ) -> Bool {
494+ // If allow list is empty, network detail capture is disabled
495+ guard !networkDetailAllowUrls. isEmpty else {
496+ return false
497+ }
498+
499+ if matchesAnyPattern ( urlString, patterns: networkDetailDenyUrls) {
500+ return false
501+ }
502+
503+ return matchesAnyPattern ( urlString, patterns: networkDetailAllowUrls)
504+ }
505+
506+ /**
507+ * Helper method to check if a URL string matches any pattern in a list.
508+ *
509+ * Supports both String and NSRegularExpression patterns:
510+ * - String: Uses substring containment check (like JavaScript's includes())
511+ * - NSRegularExpression: Uses full regex matching
512+ *
513+ * - Parameters:
514+ * - urlString: The URL string to test
515+ * - patterns: Array of String or NSRegularExpression patterns
516+ * - Returns: `true` if the URL matches any pattern, `false` otherwise
517+ */
518+ private func matchesAnyPattern( _ urlString: String , patterns: [ Any ] ) -> Bool {
519+ for pattern in patterns {
520+ if let stringPattern = pattern as? String {
521+ // String provided: substring match
522+ // Filter out empty strings and whitespace-only strings
523+ let trimmed = stringPattern. trimmingCharacters ( in: . whitespacesAndNewlines)
524+ guard !trimmed. isEmpty else { continue }
525+
526+ if urlString. contains ( stringPattern) {
527+ return true
528+ }
529+ } else if let regexPattern = pattern as? NSRegularExpression {
530+ // NSRegularExpression: use regex matching
531+ let range = NSRange ( location: 0 , length: urlString. utf16. count)
532+ if regexPattern. firstMatch ( in: urlString, options: [ ] , range: range) != nil {
533+ return true
534+ }
535+ }
536+ }
537+ return false
538+ }
539+
354540 /**
355541 * Initialize session replay options disabled
356542 *
@@ -374,7 +560,12 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
374560 frameRate: nil ,
375561 errorReplayDuration: nil ,
376562 sessionSegmentDuration: nil ,
377- maximumDuration: nil
563+ maximumDuration: nil ,
564+ networkDetailAllowUrls: nil ,
565+ networkDetailDenyUrls: nil ,
566+ networkCaptureBodies: nil ,
567+ networkRequestHeaders: nil ,
568+ networkResponseHeaders: nil
378569 )
379570 }
380571
@@ -409,7 +600,12 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
409600 sessionSegmentDuration: ( dictionary [ " sessionSegmentDuration " ] as? NSNumber ) ? . doubleValue,
410601 maximumDuration: ( dictionary [ " maximumDuration " ] as? NSNumber ) ? . doubleValue,
411602 excludedViewClasses: ( dictionary [ " excludedViewClasses " ] as? [ String ] ) . map { Set ( $0) } ,
412- includedViewClasses: ( dictionary [ " includedViewClasses " ] as? [ String ] ) . map { Set ( $0) }
603+ includedViewClasses: ( dictionary [ " includedViewClasses " ] as? [ String ] ) . map { Set ( $0) } ,
604+ networkDetailAllowUrls: Self . validateNetworkDetailUrlPatterns ( from: dictionary [ " networkDetailAllowUrls " ] ) ,
605+ networkDetailDenyUrls: Self . validateNetworkDetailUrlPatterns ( from: dictionary [ " networkDetailDenyUrls " ] ) ,
606+ networkCaptureBodies: ( dictionary [ " networkCaptureBodies " ] as? NSNumber ) ? . boolValue,
607+ networkRequestHeaders: Self . parseStringArray ( from: dictionary [ " networkRequestHeaders " ] ) ,
608+ networkResponseHeaders: Self . parseStringArray ( from: dictionary [ " networkResponseHeaders " ] )
413609 )
414610 }
415611
@@ -456,10 +652,81 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
456652 sessionSegmentDuration: nil ,
457653 maximumDuration: nil ,
458654 excludedViewClasses: nil ,
459- includedViewClasses: nil
655+ includedViewClasses: nil ,
656+ networkDetailAllowUrls: nil ,
657+ networkDetailDenyUrls: nil ,
658+ networkCaptureBodies: nil ,
659+ networkRequestHeaders: nil ,
660+ networkResponseHeaders: nil
460661 )
461662 }
462663
664+ /**
665+ * Helper method to parse and filter string arrays from dictionary configuration.
666+ *
667+ * Filters out non-string entries from mixed arrays while preserving valid strings.
668+ * Returns nil when the input is not an array type, allowing callers to fall back to defaults.
669+ *
670+ * - Parameter value: The value from the dictionary to parse
671+ * - Returns: Filtered array of strings, or nil if input is not an array
672+ */
673+ private static func parseStringArray( from value: Any ? ) -> [ String ] ? {
674+ guard let array = value as? [ Any ] else {
675+ return nil
676+ }
677+ return array. compactMap { $0 as? String }
678+ }
679+
680+ /**
681+ * Validates developer-provided NetworkDetail URL patterns and returns a subset of only valid entries.
682+ *
683+ * Accepts both String and NSRegularExpression objects.
684+ * Filters out invalid entries and preserves valid patterns.
685+ * Filters out empty strings and whitespace-only strings.
686+ *
687+ * - Parameter value: The value from the dictionary to parse
688+ * - Returns: Filtered array of String and NSRegularExpression patterns, or nil if input is not an array
689+ */
690+ private static func validateNetworkDetailUrlPatterns( from value: Any ? ) -> [ Any ] ? {
691+ guard let array = value as? [ Any ] else {
692+ if let nonNilValue = value {
693+ SentrySDKLog . log ( message: " Invalid networkDetail URL pattern configuration: expected array, got \( type ( of: nonNilValue) ) " ,
694+ andLevel: . warning)
695+ }
696+ return nil
697+ }
698+
699+ var validPatterns : [ Any ] = [ ]
700+ var invalidCount = 0
701+
702+ for (index, element) in array. enumerated ( ) {
703+ if let stringElement = element as? String {
704+ // Filter out empty strings and whitespace-only strings
705+ let trimmed = stringElement. trimmingCharacters ( in: . whitespacesAndNewlines)
706+ if trimmed. isEmpty {
707+ SentrySDKLog . log ( message: " Invalid networkDetail URL pattern at index \( index) : empty or whitespace-only string discarded " ,
708+ andLevel: . warning)
709+ invalidCount += 1
710+ } else {
711+ validPatterns. append ( stringElement)
712+ }
713+ } else if let regexElement = element as? NSRegularExpression {
714+ validPatterns. append ( regexElement)
715+ } else {
716+ SentrySDKLog . log ( message: " Invalid networkDetail URL pattern at index \( index) : expected String or NSRegularExpression, got \( type ( of: element) ) " ,
717+ andLevel: . warning)
718+ invalidCount += 1
719+ }
720+ }
721+
722+ if invalidCount > 0 {
723+ SentrySDKLog . log ( message: " NetworkDetail URL patterns: \( invalidCount) invalid entries discarded, \( validPatterns. count) valid patterns retained " ,
724+ andLevel: . info)
725+ }
726+
727+ return validPatterns
728+ }
729+
463730 // swiftlint:disable:next function_parameter_count cyclomatic_complexity
464731 private init (
465732 sessionSampleRate: Float ? ,
@@ -477,7 +744,12 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
477744 sessionSegmentDuration: TimeInterval ? ,
478745 maximumDuration: TimeInterval ? ,
479746 excludedViewClasses: Set < String > ? = nil ,
480- includedViewClasses: Set < String > ? = nil
747+ includedViewClasses: Set < String > ? = nil ,
748+ networkDetailAllowUrls: [ Any ] ? = nil ,
749+ networkDetailDenyUrls: [ Any ] ? = nil ,
750+ networkCaptureBodies: Bool ? = nil ,
751+ networkRequestHeaders: [ String ] ? = nil ,
752+ networkResponseHeaders: [ String ] ? = nil
481753 ) {
482754 self . sessionSampleRate = sessionSampleRate ?? DefaultValues . sessionSampleRate
483755 self . onErrorSampleRate = onErrorSampleRate ?? DefaultValues . onErrorSampleRate
@@ -495,8 +767,49 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
495767 self . maximumDuration = maximumDuration ?? DefaultValues . maximumDuration
496768 self . excludedViewClasses = excludedViewClasses ?? DefaultValues . excludedViewClasses
497769 self . includedViewClasses = includedViewClasses ?? DefaultValues . includedViewClasses
770+ self . networkDetailAllowUrls = Self . validateNetworkDetailUrlPatterns ( from: networkDetailAllowUrls) ?? DefaultValues . networkDetailAllowUrls
771+ self . networkDetailDenyUrls = Self . validateNetworkDetailUrlPatterns ( from: networkDetailDenyUrls) ?? DefaultValues . networkDetailDenyUrls
772+ self . networkCaptureBodies = networkCaptureBodies ?? DefaultValues . networkCaptureBodies
773+ self . _networkRequestHeaders = Self . mergeWithDefaultHeaders ( networkRequestHeaders, defaults: DefaultValues . networkRequestHeaders)
774+ self . _networkResponseHeaders = Self . mergeWithDefaultHeaders ( networkResponseHeaders, defaults: DefaultValues . networkResponseHeaders)
498775
499776 super. init ( )
500777 }
778+
779+ /**
780+ * Merges user-provided headers with default headers, ensuring defaults are always included.
781+ *
782+ * - Parameter userHeaders: Headers specified by the user (can be nil)
783+ * - Parameter defaults: Default headers that must always be included
784+ * - Returns: Array containing both user headers and default headers (with duplicates removed)
785+ */
786+ private static func mergeWithDefaultHeaders( _ userHeaders: [ String ] ? , defaults: [ String ] ) -> [ String ] {
787+ let providedHeaders = userHeaders ?? [ ]
788+
789+ // Use Set to remove duplicates, then convert back to Array
790+ // Case-insensitive comparison to avoid duplicate headers with different casing
791+ var seenHeaders = Set < String > ( )
792+ var result : [ String ] = [ ]
793+
794+ // Add default headers first
795+ for header in defaults {
796+ let lowercased = header. lowercased ( )
797+ if !seenHeaders. contains ( lowercased) {
798+ seenHeaders. insert ( lowercased)
799+ result. append ( header)
800+ }
801+ }
802+
803+ // Add user-provided headers
804+ for header in providedHeaders {
805+ let lowercased = header. lowercased ( )
806+ if !seenHeaders. contains ( lowercased) {
807+ seenHeaders. insert ( lowercased)
808+ result. append ( header)
809+ }
810+ }
811+
812+ return result
813+ }
501814}
502- // swiftlint:enable file_length missing_docs
815+ // swiftlint:enable file_length missing_docs type_body_length
0 commit comments