Skip to content

PoC: Jkt/bk/km c/ad attribution dashboard fix#4441

Draft
jonathanKingston wants to merge 94 commits intomainfrom
jkt/bk/kmC/ad-attribution-dashboard-fix
Draft

PoC: Jkt/bk/km c/ad attribution dashboard fix#4441
jonathanKingston wants to merge 94 commits intomainfrom
jkt/bk/kmC/ad-attribution-dashboard-fix

Conversation

@jonathanKingston
Copy link
Copy Markdown
Collaborator

@jonathanKingston jonathanKingston commented Apr 11, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/1199237043598868/task/1214038914184192?focus=true
Tech Design URL:
CC:

Description

Testing Steps

Impact and Risks

What could go wrong?

Quality Considerations

Notes to Reviewer


Internal references:

Definition of Done | Engineering Expectations | Tech Design Template


Note

Medium Risk
Touches tracker classification/reporting paths (content blocking, allowlist/temp-unprotected handling, ad-click attribution) and bumps core dependencies; regressions could impact blocking accuracy and dashboard reporting, though changes are largely additive with updated tests.

Overview
Upgrades content-scope-scripts to 14.0.0 (and updates SwiftPM pins, including swift-crypto and new swift-asn1) and removes the legacy ContentBlockerRulesUserScript/SurrogatesUserScript and their bundled JS resources from BrowserServicesKit.

Introduces a new Content-Scope–driven tracker protection pipeline: adds TrackerProtectionSubfeature message handling plus a native TrackerProtectionEventMapper that re-classifies C-S-S observations with TrackerResolver, including allowlist normalization and temp-unprotected subdomain parity, and adds TrackerProtectionDataSource/JavaScriptTrackerData to provide a trimmed tracker-data payload for JS injection.

Fixes ad-click attribution activity pixel firing to also treat ruleException requests on the active vendor’s own pages as attribution matches, loosens dashboard page association matching, and migrates/rewrites WebKit content-blocking tests to the new proxy-based harness (dropping the old CTL blocking test).

Reviewed by Cursor Bugbot for commit 3ecfff8. Bugbot is set up for automated code reviews on this repo. Configure here.

laghee and others added 30 commits March 18, 2026 16:36
The `currentAdClickAttributionTrackerData` property uses the `TrackerData`
type from TrackerRadarKit, but only ContentBlocking was imported.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The override logic was too broad — it converted ALL trackers found in the
attribution TrackerData to .allowed(reason: .adClickAttribution), but only
trackers whose host is in the attribution allowlist (e.g. convert.ad-company.site)
should be allowed. Other trackers from the same entity (e.g. ad-company.site)
should remain blocked.

Changes:
- Replace attributionTrackerData with allowlistHosts in the override check
- iOS: add attribution rule list to C-S-S data source so attribution trackers
  are visible to the JavaScript tracker resolver
- Add 7 tests covering allowlist matching, subdomain matching, and edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allowlist rules are already regex patterns (e.g. tracker\.com/.*) used
directly in JavaScript's .match(). The escapedForRegex() call was
double-escaping them, breaking matching in C-S-S's tracker resolver.

Also remove unused parameter from injectTrackerProtectionSettings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…1 invariant tests

Restores files deleted by this PR that are required for A1 parity:

Legacy scripts (from main):
- ContentBlockerRulesUserScript.swift + contentblockerrules.js
- SurrogatesUserScript.swift + surrogates.js
- TrackerResolver.swift

Legacy tests (from main):
- TrackerResolverTests, ContentBlockerRulesUserScriptsTests
- SurrogatesUserScriptTests, ContentBlockerReferenceTests
- SurrogatesReferenceTests, TrackerAllowlistReferenceTests
- ClickToLoadBlockingTests, DomainMatchingReportTests
- iOS DomainMatchingReportTests, macOS PopupHandlingUITests

New A1 tests:
- ContentBlockingA1InvariantTests (single-authority gate, CTL parity, dataset contract)
- ContentBlockingDatasetContractTests (extractSurrogates contract, perf structural assertions)

A1 hard gate: legacy processRule path is sole classification authority.
C-S-S trackerProtection is removed from Apple platform support (separate C-S-S commit).
PR #4073 modified this file to remove MockRulesUserScriptDelegate,
MockSurrogatesUserScriptDelegate, TestSchemeContentBlockerUserScriptConfig,
and TestSchemeSurrogatesUserScriptConfig. These are required by the
restored legacy test suites.
SPM does not auto-discover .js resources when the target has an explicit
resources: block. Add contentblockerrules.js and surrogates.js to the
BrowserServicesKit target resources so loadJS can find them via
Bundle.module.
…Scope contexts

TrackerProtectionDataSource.encodedTrackerData now applies
extractSurrogates() before encoding, reducing the payload from
~1.5M chars to ~30-50K chars per the dataset contract.

ContentScopeUserScript.generateSource() nils out trackerData for
non-contentScope contexts (e.g. contentScopeIsolated), preventing
the 1.5M TDS from being duplicated into scripts that don't use it.

Expected perf improvement: contentScope source ~670K (from 2.2M),
contentScopeIsolated source ~973K (from 2.5M), makeUserScripts
back to ~280ms baseline.
The call sites were passing .trackerData (full TDS) directly to
ContentScopeProperties, bypassing the encodedTrackerData filtering.

Added surrogateFilteredTrackerData property to TrackerProtectionDataSource
that returns extractSurrogates(from:) result as a TrackerData object.
Updated iOS and macOS call sites to use it.
…Source

Protocol conformance fix after adding surrogateFilteredTrackerData
to TrackerProtectionDataSource in A2.
TrackerProtectionSubfeature:
- ResourceObservation: raw {url, resourceType, potentiallyBlocked, pageUrl}
- SurrogateInjection: minimal {url, pageUrl, surrogateName} with migration compat
- Handles new resourceObserved + legacy trackerDetected (mapped to ResourceObservation)
- Delegate protocol uses didObserveResource instead of didDetectTracker

TrackerProtectionEventMapper:
- Accepts ResourceObservation, classifies via TrackerResolver with full TDS
- No JS classification authority — native TrackerResolver is sole classifier
- potentiallyBlocked treated as hint, TrackerResolver determines final state
…tion/SurrogateInjection types

Tests updated for:
- ResourceObservation decoding (url, resourceType, potentiallyBlocked, pageUrl)
- SurrogateInjection new minimal schema (url, pageUrl, surrogateName)
- SurrogateInjection legacy schema migration compatibility
- resourceObserved handler registration
- Legacy trackerDetected handler still registered (migration)

Remaining compile errors in:
- TrackerProtectionEventMapperTests.swift (needs TrackerResolver fixtures)
- ContentBlockingTabExtension.swift (macOS delegate conformance)
- TabViewController.swift (iOS delegate conformance)
Tests:
- TrackerProtectionSubfeatureTests: ResourceObservation/SurrogateInjection decoding
- TrackerProtectionEventMapperTests: TrackerResolver-based classification with test TDS
- Added memberwise inits to ResourceObservation and SurrogateInjection for test construction

App layer (shadow mode — legacy path remains dashboard-authoritative):
- macOS ContentBlockingTabExtension: didObserveResource logs shadow observations
- iOS TabViewController: didObserveResource/didInjectSurrogate are shadow-only stubs
- C-S-S surrogateInjectionEnabled=false during shadow: no C-S-S surrogate injection
- Legacy processRule + surrogates.js paths unchanged and authoritative
- Add contentBlockerRulesConfig and surrogatesConfig to protocol and struct
- Remove dangling newTabPageActionsManager assignment (removed in main)
- Restore mergeTrackerDataSets and encodeTrackerData methods needed by buildSurrogatesConfig
The merge from main dropped ContentBlockerRulesUserScript and SurrogatesUserScript
from both iOS and macOS UserScripts. These are the legacy scripts that send processRule
messages to native, feeding the privacy dashboard with blocked/allowed request data.

- macOS: Add contentBlockerRulesScript + surrogatesScript declarations, init, and userScripts array
- iOS: Add protocol requirements, stored properties, build methods, script creation, and userScripts array
macOS:
- ContentBlockingTabExtension: restore to main's version exactly (init params,
  ContentBlockerRulesUserScriptDelegate + SurrogatesUserScriptDelegate conformances,
  delegate sinks via publishers). Removes shadow-only code (tld, dedup struct,
  TrackerProtectionSubfeatureDelegate).
- TabExtensions.swift: pass contentBlockerRulesUserScriptPublisher +
  surrogatesUserScriptPublisher instead of trackerProtectionSubfeaturePublisher.

iOS:
- TabViewController: restore surrogatesScript.delegate and contentBlockerUserScript.delegate
  assignments, restore ContentBlockerRulesUserScriptDelegate + SurrogatesUserScriptDelegate
  conformance extensions from main, restore contentBlockerUserScript accessor, restore
  ad-click attribution wiring to use contentBlockerUserScript. Remove
  TrackerProtectionSubfeatureDelegate conformance (moved to follow-up commit).
…led promise rejections)

The subfeature was registered on contentScopeUserScript but had no delegate after
the legacy restore, causing C-S-S tracker-protection.js to fire resourceObserved
messages that timed out with unhandled promise rejections. These interfered with
the legacy content blocking scripts, causing some tracker detections to be lost.

Removed from both iOS and macOS UserScripts. Will be re-added in a separate shadow
delegate commit with proper delegate wiring.
…sScriptPublisher

The branch's AdClickAttributionTabExtension.init takes trackerProtectionSubfeaturePublisher
(optional, defaults to nil), not contentBlockerRulesScriptPublisher. Pass neither — let the
default nil apply. The legacy ad-click attribution wiring via contentBlockerUserScript is
handled on the iOS side directly in TabViewController.
…line

Both paths now run in parallel. Legacy ContentBlockerRulesUserScriptDelegate +
SurrogatesUserScriptDelegate remain dashboard-authoritative. The new
TrackerProtectionSubfeatureDelegate receives C-S-S resourceObserved/surrogateInjected
signals and logs them (macOS) or silently acknowledges them (iOS) for parity
comparison without affecting the dashboard.

macOS:
- UserScripts: re-add trackerProtectionSubfeature property + registration
- ContentBlockingTabExtension: add trackerProtectionSubfeaturePublisher param +
  sink + TrackerProtectionSubfeatureDelegate conformance (shadow logging)
- TabExtensions: pass trackerProtectionSubfeature publisher at call site

iOS:
- UserScripts: re-add trackerProtectionSubfeature property + registration
- TabViewController: add delegate assignment + TrackerProtectionSubfeatureDelegate
  conformance (shadow, no-op acknowledgement)
…elegates

Legacy remains sole dashboard authority. Shadow delegates now classify via
TrackerProtectionEventMapper and log parity results instead of raw URLs.

macOS ContentBlockingTabExtension:
- Add trackerProtectionMapper property (constructed from TDS + privacyConfig)
- Init takes tld + contentBlockingManager for mapper construction
- didObserveResource: classify via mapper, log blocked/allowed + entity
- didInjectSurrogate: classify via mapper, log surrogate host + entity

macOS TabExtensions:
- Pass tld + contentBlockingManager at call site

iOS TabViewController:
- Add makeMapper() helper using ContentBlocking.shared
- Delegate methods remain no-op (iOS parity instrumentation deferred to Commit B)
…at init time)

currentMainRules is nil during ContentBlockingTabExtension.init because content
blocking assets haven't been compiled yet. Changed from eager let (always nil)
to lazy var that constructs the mapper on first access when rules are available.
- macOS ContentBlockingTabExtension: replace legacy ContentBlockerRulesUserScriptDelegate
  and SurrogatesUserScriptDelegate with authoritative TrackerProtectionSubfeatureDelegate
  using TrackerProtectionEventMapper classification (blocked → .tracker, cross-site
  non-blocked → .thirdPartyRequest, same-site → skip)
- macOS TabExtensions: remove legacy publisher args, wire trackerProtectionSubfeaturePublisher
  into AdClickAttributionTabExtension
- iOS TabViewController: replace legacy delegates with authoritative TrackerProtectionSubfeatureDelegate,
  update AdClickAttributionLogicDelegate to use trackerProtectionSubfeature instead of
  contentBlockerUserScript, use DetectedRequest.entityName for privacy stats
- Both platforms UserScripts: remove contentBlockerRulesScript/contentBlockerUserScript
  and surrogatesScript declarations, initialization, and userScripts array entries
- Both platforms ScriptSourceProviding: remove contentBlockerRulesConfig/surrogatesConfig
  from protocol, struct, init, and related build/merge/encode methods
Fix 1: TrackerProtectionEventMapper now computes potentiallyBlocked from
privacyConfig.isEnabled(.contentBlocking) instead of forwarding the C-S-S
observation.potentiallyBlocked flag. C-S-S sends pre-block signals where
potentiallyBlocked reflects JS heuristics, not WKContentRuleList verdicts.
The native-computed candidate mirrors legacy contentblockerrules.js semantics.

Fix 2: classifyResource/classifySurrogate now accept an optional
adClickAttributionVendor parameter. When provided, a per-call resolver is
built with the vendor so ad attribution requests classify as
.allowed(.adClickAttribution) instead of .allowed(.ruleException).
TrackerResolver remains immutable.
The A1 naming referred to a past migration phase and is misleading.
Move 3 non-duplicate tests (legacy JS source structure assertions and
encoded surrogate TDS round-trip) into the contract tests file.
Drop the duplicate trackerData-absent test already covered there.
Delete ContentBlockingA1InvariantTests.swift.
jonathanKingston and others added 16 commits April 7, 2026 08:19
- Allowlist override: TrackerProtectionEventMapper now checks the native
  allowlist when classifying known trackers, preventing allowlisted
  trackers from being misclassified as blocked.
- Affiliated entity reporting: cross-site non-tracker resources belonging
  to the same entity as the page now emit .ownedByFirstParty instead of
  being silently dropped.
- Temp-unprotected subdomain parity: temp-unprotected domains now cover
  subdomains in the native classification path.
- Same-site known-tracker suppression: same-site requests to known
  tracker domains are now suppressed before reaching consumers.
- CTL Option A (macOS): exclude CTL supplementary TDS from the mapper
  when fbBlockingEnabled is false.
- iOS callsite: pass trackerAllowlist to the mapper constructor.

Made-with: Cursor
…ains

Replaces the old test:// scheme handler with a loopback HTTP proxy
(TestLoopbackProxy) using WKWebsiteDataStore.proxyConfigurations.
Uses real PSL-listed domains for faithful eTLD+1/same-site semantics.

Supports split TDS (supplementaryTrackerData, cssTrackerData) for CTL
test scenarios. Config builder sets both trackerProtection.settings and
features.clickToLoad for pre-built C-S-S bundle compatibility.

Legacy helper types preserved for unmigrated tests.

Made-with: Cursor
Migrated test files:
- ContentBlockerReferenceTests: domain_matching_tests.json scenarios
  via proxy-based harness with .test->.org domain normalization
- TrackerAllowlistReferenceTests: tracker_allowlist_reference.json
  scenarios with blocked/allowlisted dual-layer assertions
- ContentBlockerRulesUserScriptsTests: tests 1-12 migrated; test 13
  (contentBlockingEnabled=false) deferred via XCTSkip

New replacement suites:
- TrackerProtectionBundledSurrogatesSmokeTests: proves real bundled
  surrogates (analytics.js, gpt.js) inject end-to-end
- TrackerProtectionClickToLoadSmokeTests: proves CTL gating/reporting
  semantics for active/inactive/non-CTL scenarios

Focused regression tests added to TrackerProtectionEventMapperTests for
allowlist override, port/query stripping, subdomain matching, and CTL
supplementary TDS validation.

Made-with: Cursor
Temporarily references the kmC/native-content-blocking-tests-059d branch
which includes the raw resource observation pipeline and CTL Option B
(potentiallyBlocked suppression for CTL-disabled block-ctl-* rules).

Made-with: Cursor
Resolved conflicts:
- Package.swift: keep C-S-S branch ref (pr-releases/kmC/native-content-blocking-tests-059d)
- Package.resolved (both): update C-S-S pin to branch SHA, take main for other deps
- macOS UserScripts.swift: keep both trackerProtectionSubfeature and duckAiNativeStorageUserScript

Auto-merged cleanly:
- HistoryTabExtension.swift: incorporated lazy-load-tabs changes (visits(matching:), restoreLocalHistoryIDs)
- TabExtensions.swift: incorporated AutoplayPolicyTabExtension + TabSuspension changes
- TabViewModel.swift: incorporated lazy-load title/suspension refactors
- iOS TabViewController.swift: incorporated voice mode, privacy dashboard anchor, contextual onboarding
- iOS UserScripts.swift: incorporated duckAiNativeStorageUserScript
…iolations

- macOS UserScripts.swift: add 'import PrivacyConfig' for PrivacyFeature type
  used in ContentScopePrivacyConfigurationJSONGenerator excludedFeatures param
- TrackerProtectionClickToLoadSmokeTests: fix statement_position lint (else on same line)
- TrackerProtectionBundledSurrogatesSmokeTests: fix statement_position lint (else on same line)
- Cache macOS TrackerProtectionEventMapper with vendor-based invalidation,
  matching the iOS caching pattern. Avoids per-event reconstruction on
  tracker-heavy pages (bwaresiak feedback on ContentBlockingTabExtension:59)
- Rename ContentScopeProperties.trackerData → surrogateTrackerData to clarify
  it contains surrogate-filtered data, not the full TDS. CodingKey preserved
  as 'trackerData' for C-S-S JS compatibility (bwaresiak feedback on :100)
- Add explanatory comment on AdClickAttributionLogic.onRequestDetected
  ruleException fallback for vendor-page matching (bwaresiak feedback on :242)
- Remove contentblockerrules.js and surrogates.js from Package.swift resources
  and delete the files (no longer used in production — C-S-S trackerProtection
  handles blocking/surrogates via the ContentScopeUserScript pipeline)
- Delete SurrogatesReferenceTests (superseded by TrackerProtectionBundledSurrogatesSmokeTests)
- Delete SurrogatesUserScriptTests (superseded by rewritten proxy-based tests)
- Delete ClickToLoadBlockingTests (superseded by TrackerProtectionClickToLoadSmokeTests)
- Remove legacy JS source structure assertions from ContentBlockingDatasetContractTests
  (the deleted generateSource tests validated the removed JS files)
Production source (no longer referenced by iOS or macOS app code):
- Delete ContentBlockerRulesUserScript.swift (241 lines) — replaced by
  TrackerProtectionSubfeature + C-S-S trackerProtection pipeline
- Delete SurrogatesUserScript.swift (221 lines) — surrogate injection
  now handled by C-S-S with surrogateInjectionEnabled setting

Test infrastructure (no remaining consumers after previous test deletions):
- Remove MockRulesUserScriptDelegate from WebViewTestHelper
- Remove MockSurrogatesUserScriptDelegate from WebViewTestHelper
- Remove TestSchemeContentBlockerUserScriptConfig from WebViewTestHelper
- Remove TestSchemeSurrogatesUserScriptConfig from WebViewTestHelper

Retained (still actively used):
- TrackerResolver.swift — used by TrackerProtectionEventMapper
- MockNavigationDelegate, TestSchemeHandler, MockWebsite — used by
  fingerprinting, GPC, rules manager, and HTTPS upgrade tests
DefaultTrackerProtectionDataSource now computes all three derived values
(trackerData, surrogateFilteredTrackerData, encodedTrackerData) eagerly
at init from a single currentRules snapshot, then stores them as let
properties. This eliminates:

- Per-access TDS merging (main + CTL rule lists)
- Per-access extractSurrogates filtering
- Per-access JSON encoding (~30-50K payload)

Previously these were computed properties that re-derived on every read.
On tracker-heavy pages, encodedTrackerData could be accessed multiple
times per page load via ContentScopeUserScript.generateSource().

The init signature, protocol, and all call sites are unchanged. The
ScriptSourceProvider already constructs the data source once at init
time, so the eager computation simply front-loads the same work.
…Reason

Fix #3 from deep review: isTrackerAllowlisted was calling
NSRegularExpression(pattern:) for every allowlist entry on every blocked
tracker URL — O(blocked_requests × allowlist_entries) compilations.
Now pre-compiled at TrackerProtectionEventMapper.init into a
[String: [(regex: NSRegularExpression, domains: [String])]] lookup.
Matching is pure lookup + firstMatch with zero compilation overhead.

Fix #11 from deep review: TrackerBlockingReason.swift was dead code with
zero references in any production source file. Deleted.
@jonathanKingston jonathanKingston added the Run PR Checks Set this label on a draft pull request to force running CI checks. label Apr 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 11, 2026

Warnings
⚠️ PR has 5662 lines of added code (excluding Xcode projects and assets). Consider splitting into smaller PRs if possible.

Generated by 🚫 dangerJS against 8852d32

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 315e80b. Configure here.

jonathanKingston and others added 11 commits April 12, 2026 00:01
Identical to testTempListSubdomainUnblocksTracker: same mapper construction,
same URL, same page URL, same assertions. The comment claimed to test
'exception-list domains merged into tempList' but the test used tempList
directly without any merging.
- Delete TrackerProtectionMemoryGuardrailTests.swift (159 lines): inlined
  dict merge instead of testing DefaultTrackerProtectionDataSource, giving
  false confidence and CI flake risk without guarding real code paths.

- Fix testWhenTrackerAllowlistPresentThenItIsExcluded: add trackerAllowlist
  to the source fixture so the nil assertion proves real stripping, not
  vacuous absence.

- Tighten testNonTrackerThirdPartyRequestClassification: replace silently-
  passing empty-events branch with XCTSkipIf + XCTAssertNotNil so the test
  either proves the path works or explicitly skips with explanation.
The mapper cache was keyed only on ad-click attribution vendor. Toggling
FB blocking changes supplementary TDS composition (CTL list is included
or excluded) without changing the vendor, so the cached mapper could
return stale supplementary data. Add fbBlockingEnabled to the cache key
so the mapper rebuilds when CTL gating changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Run PR Checks Set this label on a draft pull request to force running CI checks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants