Skip to content

Commit 590a329

Browse files
sojingleclaude
andauthored
feat: make header and body capture feature GA & fix bug for allowlist rule (#396)
* feat: make header and body capture feature GA & fix bug for allowlist rule * chore: remove Package.resolved and fix deprecated init ambiguity Add Package.resolved to .gitignore (library packages should not pin resolved versions). Remove default value from deprecated blocklist parameter to avoid overload ambiguity with the new excludelist init. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5794cf9 commit 590a329

File tree

7 files changed

+85
-24
lines changed

7 files changed

+85
-24
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ DerivedData/
77
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
88
.netrc
99
node_modules/
10+
Package.resolved

Examples/AmplitudeSwiftUIExample/AmplitudeSwiftUIExample/AmplitudeSwiftUIExampleApp.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
// Created by Hao Yu on 11/30/22.
66
//
77

8-
@_spi(NetworkTracking)
98
import AmplitudeSwift
109
import AppTrackingTransparency
1110
import Experiment

Sources/Amplitude/Plugins/NetworkTrackingPlugin.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,17 @@ public struct NetworkTrackingOptions {
8383

8484
public struct CaptureBody: Decodable {
8585
public let allowlist: [String]
86-
public let blocklist: [String]
86+
public let excludelist: [String]
8787

88-
public init(allowlist: [String], blocklist: [String] = []) {
88+
public init(allowlist: [String], excludelist: [String] = []) {
8989
self.allowlist = allowlist
90-
self.blocklist = blocklist
90+
self.excludelist = excludelist
9191
}
9292

93-
enum CodingKeys: String, CodingKey {
94-
case allowlist
95-
case blocklist = "excludelist"
93+
@available(*, deprecated, renamed: "init(allowlist:excludelist:)", message: "Deprecated, use 'excludelist' instead")
94+
public init(allowlist: [String], blocklist: [String]) {
95+
self.allowlist = allowlist
96+
self.excludelist = blocklist
9697
}
9798
}
9899

@@ -155,7 +156,6 @@ public struct NetworkTrackingOptions {
155156
self.responseBody = nil
156157
}
157158

158-
@_spi(NetworkTracking)
159159
public init(urls: [URLPattern],
160160
methods: [String] = ["*"],
161161
statusCodeRange: String = "500-599",
@@ -561,7 +561,7 @@ class CompiledNetworkTrackingOptions {
561561
let objectFilter: ObjectFilter
562562

563563
init(body: NetworkTrackingOptions.CaptureBody) {
564-
self.objectFilter = ObjectFilter(allowList: body.allowlist, blockList: body.blocklist)
564+
self.objectFilter = ObjectFilter(allowList: body.allowlist, blockList: body.excludelist)
565565
}
566566

567567
func filterBody(_ body: Any) -> Any? {

Sources/Amplitude/Utilities/ObjectFilter.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,6 @@ class ObjectFilter {
7474
return isAllowed(path) ? value : nil
7575
}
7676

77-
// Return entire container for exact non-wildcard matches
78-
if allowKeyPaths.contains(where: {
79-
!$0.contains { $0 == "*" || $0 == "**" } && matches(path, $0) && !isBlocked(path)
80-
}) {
81-
return value
82-
}
83-
8477
// Return empty containers that match patterns
8578
if isAllowed(path) {
8679
if let dict = value as? [String: Any], dict.isEmpty { return value }
@@ -115,7 +108,13 @@ class ObjectFilter {
115108
}
116109

117110
private func canMatchDescendants(_ path: KeyPath, _ pattern: KeyPath) -> Bool {
118-
guard pattern.count > path.count else { return pattern.contains("**") }
111+
guard pattern.count > path.count else {
112+
for i in 0..<pattern.count {
113+
if pattern[i] == "**" { return true }
114+
if pattern[i] != path[i] && pattern[i] != "*" { return false }
115+
}
116+
return false
117+
}
119118

120119
for i in 0..<path.count {
121120
if pattern[i] == "**" { return true }

Tests/AmplitudeTests/AutocaptureRemoteConfigTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,11 +524,11 @@ class AutocaptureRemoteConfigTests: XCTestCase {
524524
// Verify body configuration
525525
XCTAssertNotNil(captureRule.requestBody)
526526
XCTAssertEqual(captureRule.requestBody?.allowlist, ["userId", "eventType"])
527-
XCTAssertEqual(captureRule.requestBody?.blocklist, ["password", "token"])
527+
XCTAssertEqual(captureRule.requestBody?.excludelist, ["password", "token"])
528528

529529
XCTAssertNotNil(captureRule.responseBody)
530530
XCTAssertEqual(captureRule.responseBody?.allowlist, ["status", "message"])
531-
XCTAssertEqual(captureRule.responseBody?.blocklist, ["secret"])
531+
XCTAssertEqual(captureRule.responseBody?.excludelist, ["secret"])
532532
}
533533
}
534534

Tests/AmplitudeTests/Plugins/NetworkTrackingPluginTest.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import XCTest
99

10-
@_spi(NetworkTracking)
1110
@testable import AmplitudeSwift
1211

1312
// swiftlint:disable force_cast
@@ -1062,8 +1061,8 @@ final class NetworkTrackingPluginTest: XCTestCase {
10621061
responseHeaders: NetworkTrackingOptions.CaptureHeader(),
10631062
requestBody: nil, // GET requests typically don't have body
10641063
responseBody: NetworkTrackingOptions.CaptureBody(
1065-
allowlist: ["products", "total"],
1066-
blocklist: ["internal_metadata"]
1064+
allowlist: ["products/**", "total"],
1065+
excludelist: ["internal_metadata"]
10671066
)
10681067
)
10691068
]

Tests/AmplitudeTests/Utilities/ObjectFilterTests.swift

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ final class ObjectFilterTests: XCTestCase {
540540

541541
func testFilterdWithArray() throws {
542542
// Test array filtering with index
543-
let filter = ObjectFilter(allowList: ["users/0", "users/1/name"])
543+
let filter = ObjectFilter(allowList: ["users/0/**", "users/1/name"])
544544

545545
let input: [String: Any] = [
546546
"users": [
@@ -566,7 +566,7 @@ final class ObjectFilterTests: XCTestCase {
566566
}
567567

568568
func testFilterdArrayAtRoot() throws {
569-
let filter = ObjectFilter(allowList: ["0/name", "1"])
569+
let filter = ObjectFilter(allowList: ["0/name", "1/**"])
570570

571571
let input: [Any] = [
572572
["name": "John", "email": "john@example.com"],
@@ -707,6 +707,69 @@ final class ObjectFilterTests: XCTestCase {
707707
XCTAssertNil(result3)
708708
}
709709

710+
func testExactMatchOnPrimitiveIsCaptured() throws {
711+
let filter = ObjectFilter(allowList: ["name"])
712+
let input: [String: Any] = ["name": "John", "age": 30]
713+
let result = filter.filterd(input) as? [String: Any]
714+
XCTAssertNotNil(result)
715+
XCTAssertEqual(result?["name"] as? String, "John")
716+
XCTAssertNil(result?["age"])
717+
}
718+
719+
func testExactMatchOnContainerIsNotCaptured() throws {
720+
let filter = ObjectFilter(allowList: ["user"])
721+
let input: [String: Any] = ["user": ["name": "John", "password": "secret"]]
722+
// "user" is a container — exact match without wildcard should not return it
723+
XCTAssertNil(filter.filterd(input))
724+
}
725+
726+
func testExactMatchOnContainerVsWildcard() throws {
727+
let input: [String: Any] = ["user": ["name": "John", "password": "secret"] as [String: Any]]
728+
729+
// "user" alone does not capture the object
730+
let filter1 = ObjectFilter(allowList: ["user"])
731+
XCTAssertNil(filter1.filterd(input))
732+
733+
// "user/**" captures the full object
734+
let filter2 = ObjectFilter(allowList: ["user/**"])
735+
let result2 = filter2.filterd(input) as? [String: Any]
736+
let user2 = result2?["user"] as? [String: Any]
737+
XCTAssertEqual(user2?["name"] as? String, "John")
738+
XCTAssertEqual(user2?["password"] as? String, "secret")
739+
740+
// "user/*" captures direct children
741+
let filter3 = ObjectFilter(allowList: ["user/*"])
742+
let result3 = filter3.filterd(input) as? [String: Any]
743+
let user3 = result3?["user"] as? [String: Any]
744+
XCTAssertEqual(user3?["name"] as? String, "John")
745+
XCTAssertEqual(user3?["password"] as? String, "secret")
746+
}
747+
748+
func testExactMatchWithBlocklistOnDescendants() throws {
749+
let filter = ObjectFilter(allowList: ["user/**"], blockList: ["user/password"])
750+
let input: [String: Any] = ["user": ["name": "John", "password": "secret"] as [String: Any]]
751+
let result = filter.filterd(input) as? [String: Any]
752+
let user = result?["user"] as? [String: Any]
753+
XCTAssertEqual(user?["name"] as? String, "John")
754+
XCTAssertNil(user?["password"])
755+
}
756+
757+
func testExactMatchOnArrayIsNotCaptured() throws {
758+
let filter = ObjectFilter(allowList: ["items"])
759+
let input: [String: Any] = ["items": ["a", "b", "c"]]
760+
XCTAssertNil(filter.filterd(input))
761+
}
762+
763+
func testDoesNotDescendIntoMismatchedPrefixWithDoubleWildcard() throws {
764+
let filter = ObjectFilter(allowList: ["c/**"])
765+
let input: [String: Any] = [
766+
"a": [
767+
"nested": ["value": "ignored"] as [String: Any]
768+
] as [String: Any]
769+
]
770+
XCTAssertNil(filter.filterd(input))
771+
}
772+
710773
func testFilterdNilInput() throws {
711774
let filter = ObjectFilter(allowList: ["user"])
712775

0 commit comments

Comments
 (0)