Skip to content

Commit 1d0cc82

Browse files
committed
feat(network-details): Add type-safe SentryUrlMatchable protocol for URL filtering
Replace [Any] with [SentryUrlMatchable] for networkDetailAllowUrls/DenyUrls. Provides compile-time type safety in Swift while maintaining Objective-C compatibility through bridge properties. Follows the API pattern of SentryAttributeValue/Content.
1 parent 4b9d859 commit 1d0cc82

4 files changed

Lines changed: 141 additions & 124 deletions

File tree

Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift

Lines changed: 49 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
2626
public static let includedViewClasses: Set<String> = []
2727

2828
// Network capture configuration defaults
29-
public static let networkDetailAllowUrls: [Any] = []
30-
public static let networkDetailDenyUrls: [Any] = []
29+
public static let networkDetailAllowUrls: [SentryUrlMatchable] = []
30+
public static let networkDetailDenyUrls: [SentryUrlMatchable] = []
3131
public static let networkCaptureBodies: Bool = true
3232
public static let networkRequestHeaders: [String] = ["Content-Type", "Content-Length", "Accept"]
3333
public static let networkResponseHeaders: [String] = ["Content-Type", "Content-Length", "Accept"]
@@ -301,14 +301,14 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
301301

302302
/**
303303
* A list of URL patterns to capture request and response details for during session replay.
304-
*
304+
*
305305
* When non-empty, network requests with URLs matching any of these patterns will have their
306306
* 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)):
307+
*
308+
* Supports both String and NSRegularExpression patterns:
309309
* - String: Uses substring contains
310310
* - NSRegularExpression: Uses full regex matching
311-
*
311+
*
312312
* Default: empty array (network detail capture disabled)
313313
*
314314
* Example:
@@ -335,27 +335,41 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
335335
* - Note: Request and response bodies are truncated to 150KB maximum.
336336
* - Note: See ``SentryReplayOptions.DefaultValues.networkDetailAllowUrls`` for the default value.
337337
*/
338-
public var networkDetailAllowUrls: [Any]
338+
public var networkDetailAllowUrls: [SentryUrlMatchable]
339+
340+
/// Objective-C bridge for networkDetailAllowUrls.
341+
/// - Warning: This property exists for Objective-C compatibility only. Swift code should use
342+
/// `networkDetailAllowUrls` directly. This is not part of the public API.
343+
@objc(networkDetailAllowUrls) public var networkDetailAllowUrlsForObjC: NSArray {
344+
return networkDetailAllowUrls as NSArray
345+
}
339346

340347
/**
341348
* A list of URL patterns to exclude from network detail capture during session replay.
342-
*
349+
*
343350
* 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
351+
* even if they match patterns in `networkDetailAllowUrls`. This provides fine-grained
345352
* 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()`)
353+
*
354+
* Supports both String and NSRegularExpression patterns:
355+
* - String: Uses substring match
349356
* - NSRegularExpression: Uses full regex matching
350-
*
357+
*
351358
* Default: empty array (no URLs explicitly denied)
352359
*
353360
* Examples:
354361
* - String patterns: "/auth/", "/payment/", "password", ".internal."
355362
* - NSRegularExpression patterns: Use try NSRegularExpression(pattern:) to create regex objects
356363
* - Mixed arrays are supported with both types
357364
*/
358-
public var networkDetailDenyUrls: [Any]
365+
public var networkDetailDenyUrls: [SentryUrlMatchable]
366+
367+
/// Objective-C bridge for networkDetailDenyUrls.
368+
/// - Warning: This property exists for Objective-C compatibility only. Swift code should use
369+
/// `networkDetailDenyUrls` directly. This is not part of the public API.
370+
@objc(networkDetailDenyUrls) public var networkDetailDenyUrlsForObjC: NSArray {
371+
return networkDetailDenyUrls as NSArray
372+
}
359373

360374
/**
361375
* Whether to capture request and response bodies for allowed URLs.
@@ -496,45 +510,35 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
496510
return false
497511
}
498512

499-
if matchesAnyPattern(urlString, patterns: networkDetailDenyUrls) {
513+
if matches(url: urlString, against: networkDetailDenyUrls) {
500514
return false
501515
}
502516

503-
return matchesAnyPattern(urlString, patterns: networkDetailAllowUrls)
517+
return matches(url: urlString, against: networkDetailAllowUrls)
504518
}
505519

506520
/**
507521
* Helper method to check if a URL string matches any pattern in a list.
508522
*
509523
* Supports both String and NSRegularExpression patterns:
510-
* - String: Uses substring containment check (like JavaScript's includes())
524+
* - String: Uses substring match
511525
* - NSRegularExpression: Uses full regex matching
512526
*
513527
* - Parameters:
514-
* - urlString: The URL string to test
515-
* - patterns: Array of String or NSRegularExpression patterns
528+
* - url: The URL string to test
529+
* - matchers: Array of SentryUrlMatchable patterns
516530
* - Returns: `true` if the URL matches any pattern, `false` otherwise
517531
*/
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-
}
532+
private func matches(url: String, against matchers: [SentryUrlMatchable]) -> Bool {
533+
matchers.contains { matcher in
534+
switch matcher.asSentryUrlMatcher {
535+
case .string(let pattern):
536+
return url.contains(pattern)
537+
case .regex(let regex):
538+
let range = NSRange(url.startIndex..., in: url)
539+
return regex.firstMatch(in: url, range: range) != nil
535540
}
536541
}
537-
return false
538542
}
539543

540544
/**
@@ -601,11 +605,11 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
601605
maximumDuration: (dictionary["maximumDuration"] as? NSNumber)?.doubleValue,
602606
excludedViewClasses: (dictionary["excludedViewClasses"] as? [String]).map { Set($0) },
603607
includedViewClasses: (dictionary["includedViewClasses"] as? [String]).map { Set($0) },
604-
networkDetailAllowUrls: Self.validateNetworkDetailUrlPatterns(from: dictionary["networkDetailAllowUrls"]),
605-
networkDetailDenyUrls: Self.validateNetworkDetailUrlPatterns(from: dictionary["networkDetailDenyUrls"]),
608+
networkDetailAllowUrls: SentryUrlMatcher.convertFromAny(dictionary["networkDetailAllowUrls"]),
609+
networkDetailDenyUrls: SentryUrlMatcher.convertFromAny(dictionary["networkDetailDenyUrls"]),
606610
networkCaptureBodies: (dictionary["networkCaptureBodies"] as? NSNumber)?.boolValue,
607-
networkRequestHeaders: Self.parseStringArray(from: dictionary["networkRequestHeaders"]),
608-
networkResponseHeaders: Self.parseStringArray(from: dictionary["networkResponseHeaders"])
611+
networkRequestHeaders: (dictionary["networkRequestHeaders"] as? [Any])?.compactMap { $0 as? String },
612+
networkResponseHeaders: (dictionary["networkResponseHeaders"] as? [Any])?.compactMap { $0 as? String }
609613
)
610614
}
611615

@@ -661,72 +665,6 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
661665
)
662666
}
663667

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(trimmed)
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-
730668
// swiftlint:disable:next function_parameter_count cyclomatic_complexity
731669
private init(
732670
sessionSampleRate: Float?,
@@ -745,8 +683,8 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
745683
maximumDuration: TimeInterval?,
746684
excludedViewClasses: Set<String>? = nil,
747685
includedViewClasses: Set<String>? = nil,
748-
networkDetailAllowUrls: [Any]? = nil,
749-
networkDetailDenyUrls: [Any]? = nil,
686+
networkDetailAllowUrls: [SentryUrlMatchable]? = nil,
687+
networkDetailDenyUrls: [SentryUrlMatchable]? = nil,
750688
networkCaptureBodies: Bool? = nil,
751689
networkRequestHeaders: [String]? = nil,
752690
networkResponseHeaders: [String]? = nil
@@ -767,8 +705,8 @@ public class SentryReplayOptions: NSObject, SentryRedactOptions {
767705
self.maximumDuration = maximumDuration ?? DefaultValues.maximumDuration
768706
self.excludedViewClasses = excludedViewClasses ?? DefaultValues.excludedViewClasses
769707
self.includedViewClasses = includedViewClasses ?? DefaultValues.includedViewClasses
770-
self.networkDetailAllowUrls = Self.validateNetworkDetailUrlPatterns(from: networkDetailAllowUrls) ?? DefaultValues.networkDetailAllowUrls
771-
self.networkDetailDenyUrls = Self.validateNetworkDetailUrlPatterns(from: networkDetailDenyUrls) ?? DefaultValues.networkDetailDenyUrls
708+
self.networkDetailAllowUrls = networkDetailAllowUrls ?? DefaultValues.networkDetailAllowUrls
709+
self.networkDetailDenyUrls = networkDetailDenyUrls ?? DefaultValues.networkDetailDenyUrls
772710
self.networkCaptureBodies = networkCaptureBodies ?? DefaultValues.networkCaptureBodies
773711
self._networkRequestHeaders = Self.mergeWithDefaultHeaders(networkRequestHeaders, defaults: DefaultValues.networkRequestHeaders)
774712
self._networkResponseHeaders = Self.mergeWithDefaultHeaders(networkResponseHeaders, defaults: DefaultValues.networkResponseHeaders)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
3+
/// A protocol that represents values that can be used for URL pattern matching.
4+
///
5+
/// Currently used by the network details API for session replay URL filtering.
6+
/// May be reused by other SDK features requiring URL pattern matching in the future.
7+
///
8+
/// This protocol provides type safety for URL pattern arrays, preventing runtime errors
9+
/// by enforcing valid types at compile time.
10+
///
11+
/// ```swift
12+
/// options.networkDetailAllowUrls = [
13+
/// "api.example.com", // String ✅
14+
/// try! NSRegularExpression(pattern: ".*\\.sentry\\.io.*") // NSRegularExpression ✅
15+
/// ]
16+
/// options.networkDetailAllowUrls = [42] // ❌ compile error — Int doesn't conform
17+
/// ```
18+
///
19+
/// Conforming types: String (substring matching), NSRegularExpression (regex matching).
20+
public protocol SentryUrlMatchable {
21+
/// Converts the conforming value to a `SentryUrlMatcher` enum representation.
22+
/// Internal SDK use only.
23+
var asSentryUrlMatcher: SentryUrlMatcher { get }
24+
}
25+
26+
extension String: SentryUrlMatchable {
27+
/// Converts the string to a `SentryUrlMatcher.string` value for substring matching.
28+
public var asSentryUrlMatcher: SentryUrlMatcher {
29+
return .string(self)
30+
}
31+
}
32+
33+
extension NSRegularExpression: SentryUrlMatchable {
34+
/// Converts the NSRegularExpression to a `SentryUrlMatcher.regex` value for full regex matching.
35+
public var asSentryUrlMatcher: SentryUrlMatcher {
36+
return .regex(self)
37+
}
38+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
3+
/// A type-safe representation of URL pattern values used by network detail filtering.
4+
///
5+
/// `SentryUrlMatcher` provides a strongly-typed enum for representing URL pattern types
6+
/// including strings and regular expressions.
7+
///
8+
/// - Note: This type should not be used directly. Use `String` or `NSRegularExpression`
9+
/// when configuring URL patterns.
10+
public enum SentryUrlMatcher {
11+
/// String pattern for substring matching.
12+
case string(String)
13+
/// NSRegularExpression pattern for regex matching.
14+
case regex(NSRegularExpression)
15+
16+
/// Converts an array of Any values to an array of SentryUrlMatchable, filtering out invalid types.
17+
///
18+
/// Validates and filters entries: trim whitespace from strings, discard empty strings,
19+
/// and preserve only valid types (String and NSRegularExpression).
20+
///
21+
/// - Parameter value: Array from dictionary that may contain mixed types
22+
/// - Returns: Array of valid SentryUrlMatchable values, or nil if input is not an array
23+
static func convertFromAny(_ value: Any?) -> [SentryUrlMatchable]? {
24+
guard let array = value as? [Any] else { return nil }
25+
return array.compactMap { element in
26+
if let string = element as? String {
27+
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
28+
return trimmed.isEmpty ? nil : trimmed
29+
}
30+
if let regex = element as? NSRegularExpression { return regex }
31+
return nil
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)