Skip to content

Make creative click rewriting a separate switch - #1252

Open
dhruv8sh wants to merge 20 commits into
mainfrom
feat/1234-creative-click-rewrite-switch
Open

dhruv8sh wants to merge 20 commits into
mainfrom
feat/1234-creative-click-rewrite-switch

Conversation

@dhruv8sh

@dhruv8sh dhruv8sh commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Makes creative click wrapping its own feature. [auction] rewrite_clicks controls <a href> and <area href> wrapping into signed /first-party/click redirects, and rewrite_creatives now controls asset URLs only. Operators can keep first-party click tracking with assets left direct, or proxy assets with landing links left alone.
  • rewrite_clicks is an Option<bool>, and unset is the default:
    • Auction creatives follow rewrite_creatives, so existing configs keep their asset and link behavior.
    • HTML fetched through /first-party/proxy keeps wrapping links as it does today.
    • An explicit value applies to every path.
    • <base> handling and TSJS injection (on POST /auction only) run when either switch is on.
  • Behavior change for every config with rewriting on, including the default: a bidder <base target> is now kept with its href dropped, instead of the whole element being removed, so raw links keep opening where the bidder intended.
  • Introduces the normalizer and host-policy split that Add a host allowlist for creative asset rewriting #1231 builds on:
    • to_abs is replaced by normalize_creative_url plus Rewrite::should_proxy_asset and Rewrite::should_wrap_click.
    • exclude_domains matching is now case-insensitive and trimmed.

Implements the approved design in #1249: docs/superpowers/specs/2026-10-07-1234-creative-click-rewrite-switch-design.md.

#1249 was closed without merging, so this PR targets main and carries the spec itself. Use git diff -w when reading creative.rs, because gating the handlers re-indents a large block.

Changes

File Change
crates/trusted-server-core/src/auction_config_types.rs rewrite_clicks: Option<bool> (left out of the blob when unset), rewrites_auction_clicks(), rewrites_proxied_clicks(), tests
crates/trusted-server-core/src/creative.rs normalize_creative_url, asset_target/click_target, CreativeFeatures. Asset and anchor handlers are registered only when their switch is on. Caller wiring, module docs, the four-combination matrix tests, and a byte-for-byte pin against main's output
crates/trusted-server-core/src/settings.rs Rewrite::should_proxy_asset, should_wrap_click and normalize (trim, lowercase, drop inert entries); removes is_excluded; tests
crates/trusted-server-core/src/proxy.rs /first-party/sign uses the normalizer and policy; proxied-HTML click tests
crates/trusted-server-core/src/config_payload.rs Blob round-trip and legacy-blob tests
crates/trusted-server-core/src/auction/formats.rs, publisher.rs End-to-end /auction and inline tests; doc and log updates
crates/trusted-server-core/src/auction/orchestrator.rs New field in the full AuctionConfig literal
crates/trusted-server-core/src/auction/endpoints.rs, auction/README.md Doc updates
crates/trusted-server-cli/tests/config_env_overlay.rs Pins the TRUSTED_SERVER__AUCTION__REWRITE_CLICKS override behavior, with and without the TOML leaf
trusted-server.example.toml rewrite_creatives comment narrowed to assets; commented-out rewrite_clicks line with rollout notes
docs/guide/configuration.md, creative-processing.md, auction-orchestration.md, api-reference.md Operator docs: switch matrix, proxied-HTML rule, clickGuard vs rewrite_clicks, rollout and rollback
CHANGELOG.md Added rewrite_clicks; Fixed case-insensitive exclude_domains and the related edge cases

Closes

Closes #1234

Test plan

  • cargo test-fastly && cargo test-axum. Also cargo test-fastly-reuse, cargo test-cloudflare, cargo test-spin, ./scripts/test-cli.sh and the parity tests.
  • cargo clippy-fastly && cargo clippy-axum. All 8 clippy aliases pass, and every intermediate commit passes clippy-fastly.
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run (no JS changes)
  • JS format: cd crates/trusted-server-js/lib && npm run format (no JS changes)
  • Docs format: cd docs && npm run format, plus the markdown prettier check outside docs/
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: every new behavior test was seen failing before it was implemented. A table mapping each spec completion criterion to its test is in the plan.

Rollout: deploy the binary first; while rewrite_clicks is unset, asset and link behavior is unchanged (apart from the <base target> change above). Then push a config that sets it. Rollback: remove any explicit rewrite_clicks and push first, then roll back the binary.

Checklist

  • Changes follow AGENTS.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses tracing macros (not println!)
  • New code has tests
  • No secrets or credentials committed

Follow-ups

Spec and implementation plan for #1234: an [auction] rewrite_clicks switch that follows rewrite_creatives when unset, so click wrapping can be controlled independently of asset rewriting.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Describe the review fixes: the extended shared-step CHANGELOG entry and the sign status change, rollback in both directions, the reworded Auction Rewrite Control docs and renderGuard note, the fuller body-less matrix fixture, the exact default-output pin, and the stronger proxied-HTML test.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Replace to_abs with normalize_creative_url and move exclusion policy to Rewrite::should_proxy_asset and Rewrite::should_wrap_click, sharing the case-insensitive proxy host matcher. Normalize exclude_domains at load. This is the shared step for #1231 and #1234.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…when unset

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The rewrite pass registers asset handlers only when rewrite_creatives is on and the anchor handler only when click rewriting resolves on. Base removal and TSJS injection run whenever either is on.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…rty proxy

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…avior

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
@dhruv8sh dhruv8sh self-assigned this Oct 7, 2026
@dhruv8sh dhruv8sh linked an issue Oct 7, 2026 that may be closed by this pull request
8 tasks
@aram356
aram356 marked this pull request as draft October 8, 2026 15:50
Correct the renderGuard caveat for the sandboxed /auction renderer, list the
operator docs that become false, keep a bidder <base target> while dropping
its href, and warn on the public rewrite wrappers.

Also fix citations, the shared CHANGELOG Fixed wording, red-step ordering,
the Step 5 grep, rollout and staged rollback notes, and other precision items.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…-creative-click-rewrite-switch

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Restore the example2.com.fake.com style case dropped in the port of
test_rewrite_is_excluded, and point the should_proxy_asset docs at
is_host_allowed in code text so cargo doc stops warning about a link to a
private item.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
rewrite_creative_html and rewrite_inline_creative_html rewrite assets and
clicks unconditionally. Document that production code must use the
auction processing entry points, which honor both switches.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The rewrite pass removed every <base>, which also dropped target="_blank"
so raw links opened inside the creative iframe. Drop only the href, which
is what can rebase first-party URLs, and remove a <base> left without a
target as before, so default output is unchanged.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Name rewrite_clicks in the orchestration example, environment overrides
and rollback note, fix the rewrite_creatives = false summary, correct the
renderGuard caveat for the sandboxed renderer, and document base target
handling and the inert click guard.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
List the CSS re-quoting, https: prefixing and sign error text changes in
the Fixed entry, and note that a bidder base target is now kept.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
@dhruv8sh

Copy link
Copy Markdown
Collaborator Author

@prk-Jr Your review comments on the spec PRs (#1248 and #1249) are addressed in the spec branches. Those changes are merged here, along with the matching code changes: the include_domains host round-trip check, the warning for exclude_domains entries that can never match, the <link> let-chain, keeping <base target>, the doc warning on the public rewrite functions, and the operator doc and CHANGELOG corrections. One thing differs from what we discussed: in clicks-only mode <base> still loses its href, because /auction click URLs are root-relative and would otherwise resolve against the bidder's base. The spec explains the tradeoff.

@dhruv8sh
dhruv8sh marked this pull request as ready for review October 12, 2026 13:02
@aram356 aram356 added this to the 202610 milestone Oct 12, 2026
aram356 added a commit that referenced this pull request Oct 12, 2026
Brings #1252 at 7c5eca0, including the design spec and plan from its
spec/1234-creative-click-rewrite-switch base.

Integration decisions:
- Settings normalization runs #1252's rewrite.normalize() and keeps
  main's fallible image_optimizer.normalize()? from #1221.
- CHANGELOG.md keeps the RC's entries and replaces the older
  rewrite_creatives entry with #1252's version, which defers click
  wrapping to rewrite_clicks.
aram356 added a commit that referenced this pull request Oct 12, 2026
Brings #1253 at e210f62, including the design spec and plan from its
spec/1231-creative-asset-host-allowlist base.

#1253 carries its own copy of #1252's rewrite_clicks commits plus the
include_domains allowlist, so its conflicts with #1252 are two versions
of the same change.

Integration decisions:
- proxy.rs, creative.rs and settings.rs take a clean three-way merge of
  #1253 against the RC before #1252. #1253's versions of the shared
  rewrite_clicks code supersede #1252's, and they also carry #1252's
  suffix-spoofing test case and doc-link fix. Every other file #1253
  touches matches that re-merge exactly, so nothing is duplicated.
- Settings normalization keeps main's fallible image_optimizer
  normalize()? beside rewrite.normalize().
- CHANGELOG.md keeps the RC's entries and takes #1253's exclude_domains,
  include_domains, rewrite_clicks and rewrite_creatives entries.
@dhruv8sh
dhruv8sh changed the base branch from spec/1234-creative-click-rewrite-switch to main October 13, 2026 08:57

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Splits click wrapping into [auction] rewrite_clicks and narrows rewrite_creatives to assets, with a shared URL normalizer and host policy. The core logic holds up. For every parseable URL in a 40-input edge corpus, the new normalizer produces signed proxy and click URLs byte-identical to the old to_abs path. 26 of 29 mutants against the new logic are killed, and the tree merged with current main passes clippy-fastly, test-fastly, test-axum and the CLI overlay tests. What blocks is the merge conflict with main, operator docs left contradicting the switch, and release notes that miss a default-config <base target> change.

6 of the inline comments below carry a one-click GitHub suggestion. Use Commit suggestion (or Add suggestion to batch for several at once) to apply them. Each one was applied in a scratch tree and checked with the pinned docs prettier (3.8.1), or with cargo fmt --check, cargo clippy-fastly and the targeted test for the Rust one. The remaining comments describe the fix in prose because it touches lines outside the diff.

Blocking

🔧 wrench

  • Conflicts with main: see the cross-cutting section below
  • Operator docs left contradicting the switch: see inline at docs/guide/auction-orchestration.md:641
  • Default-config <base target> change described as no change: see inline at CHANGELOG.md:44
  • /first-party/sign error-string claim, missing one-time push diff: see inline at CHANGELOG.md:36
  • <base> removal and TSJS wording: see inline at trusted-server.example.toml:267
  • Click-guard sentence needs the rewrite_creatives qualifier: see inline at docs/guide/creative-processing.md:121
  • Spec status and rollout are stale: see inline at docs/superpowers/specs/2026-10-07-1234-creative-click-rewrite-switch-design.md:9
  • No test for non-http(s) schemes that carry a host: see inline at crates/trusted-server-core/src/creative.rs:2063
  • Stale prose outside the diff and smaller precision items: see the cross-cutting section below

Non-blocking

🤔 thinking

  • The /auction click guard ignores the kept <base target>: see inline at crates/trusted-server-core/src/creative.rs:1288

🌱 seedling

  • exclude_domains still misses trailing-dot hosts and IDN entries: see inline at crates/trusted-server-core/src/settings.rs:681

Cross-cutting / body-level findings

  • 🔧 Conflicts with main. GitHub reports the branch as conflicting with main (d2f0d8c), and both conflicts are mechanical. In CHANGELOG.md, both sides add ### Fixed bullets, so keep all of them. In Settings::normalize_deserialized (settings.rs), #1221 made the function return Result and call self.image_optimizer.normalize()?;. Keep that and add this PR's call above it:

            self.proxy.normalize();
            self.rewrite.normalize();
            self.image_optimizer.normalize()?;

    With that resolution, a scratch merge of main into this head passes:

    • cargo fmt --all -- --check;
    • cargo clippy-fastly;
    • cargo test-fastly (core lib 2949 passed, all 18 new core tests included);
    • cargo test-axum;
    • the CLI config_env_overlay tests (10 passed, both new rewrite_clicks tests included).

    None of the 13 main commits interact with this change semantically:

    • #1239's redirect changes still route proxied HTML through rewrite_proxied_html.
    • #1210's template fingerprint changes once because of the core source edit, which is intended.
    • rewrite_clicks: None is not serialized, so it does not perturb the settings hash.
  • 🔧 Stale prose outside the diff. These predate this PR, but they now sit next to the new text and disagree with it:

    • docs/guide/creative-processing.md:704-705 says *.cdn.example.com does not match cdn.example.com ("no subdomain"). is_host_allowed matches the apex (proxy.rs:1246-1250), this PR's rewrite_policy_matches_exclude_patterns_case_insensitively asserts it, and configuration.md:1025 already says ✅. While there, note that matching is case-insensitive.
    • docs/guide/creative-processing.md:968-970 troubleshooting says "Ensure origin response includes proper <base> tag". The rewriter drops <base href> whenever it runs.
    • crates/trusted-server-core/src/settings.rs:640-641: the exclude_domains doc says matching hosts "will not be proxied through first-party endpoints". should_wrap_click uses the same list, so they are not click-wrapped either.
    • crates/trusted-server-core/src/proxy.rs:1621-1624: the # Errors section of handle_first_party_proxy_sign does not list rejection of hosts matched by rewrite.exclude_domains.
    • crates/trusted-server-js/lib/src/shared/origin.ts:56-57: says the server-side rewriter "strips <base> from creative markup whenever rewriting is enabled". It now drops the href (keeping target) when asset or click rewriting is on.
  • 🔧 Smaller precision items in operator docs:

    • docs/guide/configuration.md:1950-1951:
      • "so existing configs keep their current behavior" is followed directly by the <base target> change, which applies to existing configs.
      • The <base> sentence does not mention that sanitize_creatives = true removes <base> entirely before the rewrite pass (creative.rs:898).
      • The same sanitizer caveat applies to the <base> column of the Assets and clicks table (creative-processing.md:107-112) and to creative-processing.md:129-130.
    • docs/guide/configuration.md:2185: "links still follow rewrite_clicks" reads as if links stay wrapped. With it unset they turn off too. Suggest "links are wrapped only when rewrite_clicks = true".
    • docs/guide/auction-orchestration.md:861-862: "rolling back also changes link wrapping when rewrite_clicks was set" is only true when it differed from rewrite_creatives on the auction paths, or was false, which also changes proxied HTML.

CI Status

  • CLAUDE.md symlink guard: PASS
  • browser integration tests: PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo fmt: PASS (required)
  • cargo test: PASS (required)
  • cargo test (axum native): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • format-docs: PASS (required)
  • format-typescript: PASS (required)
  • integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS

Comment thread docs/guide/auction-orchestration.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Comment thread trusted-server.example.toml Outdated
Comment thread docs/guide/creative-processing.md Outdated
Comment thread docs/superpowers/specs/2026-10-07-1234-creative-click-rewrite-switch-design.md Outdated
Comment thread crates/trusted-server-core/src/creative.rs
Comment thread crates/trusted-server-core/src/creative.rs
Comment thread crates/trusted-server-core/src/settings.rs

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please update the remaining processing tables and prose to reflect the separate rewrite_clicks switch. They currently give contradictory expectations for direct links, click-guard attributes, TSJS injection, and proxied HTML. Distinguish the inline path and document the explicit proxied-HTML override so operators can configure tracking correctly; no runtime change is needed.

Non-blocking: reuse the new push helper in the two existing auction-overlay tests to avoid maintaining duplicate push and decoding flows.

Comment thread docs/guide/auction-orchestration.md Outdated
Comment thread crates/trusted-server-cli/tests/config_env_overlay.rs
…ick-rewrite-switch

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>

# Conflicts:
#	CHANGELOG.md
#	crates/trusted-server-core/src/settings.rs
Describe rewrite_clicks separately from asset rewriting across the operator guides, state the default-config <base target> change in the CHANGELOG, refresh the spec status and rollout, and correct stale exclude_domains and <base> prose.

Add non-http(s) host-bearing schemes to the URL normalizer test and reuse the push helper in the existing auction overlay CLI tests.

Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. Separating rewrite_clicks from asset rewriting gives operators independent control, with unset behavior preserving existing click-rewriting defaults.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make creative click rewriting a separate feature with its own switch

4 participants