Skip to content

Make creative click rewriting a separate feature with its own switch #1234

Description

@aram356

Summary

Click wrapping is not its own feature today. The creative rewrite pass rewrites every <a href> and <area href> in a winning creative to a signed /first-party/click redirect. It does this in the same pass, behind the same switch and the same per-URL check that it uses to proxy assets. An operator can't keep first-party click tracking while leaving assets direct, or proxy assets while leaving landing links alone.

Make click rewriting a separate feature with its own switch.

Related: #1231 adds a host allowlist for asset rewriting and explicitly leaves anchors out of it. Both issues touch to_abs and its call sites in creative.rs, so coordinate if they're worked in parallel.

Spec first

Write a design spec and get it approved before opening an implementation PR.

  • Put it at docs/superpowers/specs/YYYY-MM-DD-1234-<slug>-design.md and follow the existing specs there. Include the issue link, date, status, problem, goals, non-goals, design, test plan and rollout.
  • Land the spec in its own PR. The implementation PR links the approved spec and follows it. If the design changes during implementation, update the spec in the same PR.
  • The spec must settle these open decisions:
    • The switch's name, table and type. An optional bool that follows rewrite_creatives when unset is one way to keep existing configs unchanged. Write down the rollout and rollback order given deny_unknown_fields.
    • Whether the switch also covers click wrapping inside HTML fetched through /first-party/proxy, which ignores rewrite_creatives today.
    • When <base> removal and TSJS injection run once assets and clicks are separate. This issue proposes running both when either switch is on.
    • How the server switch relates to the client-only TsCreativeConfig.clickGuard flag.
    • How the change lands alongside Add a host allowlist for creative asset rewriting #1231 if both are in flight.

Current behavior

Line numbers are at 8048301.

  • The anchor handler in rewrite_creative_html_impl (crates/trusted-server-core/src/creative.rs:1283-1291) calls to_abs, then build_click_url, then sets both href and data-tsclick to the signed click URL.
  • to_abs (creative.rs:54-75) is the per-URL check for assets and anchors alike. It skips relative URLs, non-http schemes such as mailto: and javascript:, and hosts in [rewrite] exclude_domains. An excluded anchor keeps its raw href and gets no data-tsclick.
  • [auction] rewrite_creatives (checked at creative.rs:1031) turns the whole pass on or off, so assets, clicks, <base> removal and TSJS injection all go together. The comment on it in trusted-server.example.toml says as much: "Set false to skip proxy/click-URL conversion and creative TSJS injection."
  • Three entry points share the pass:
    • POST /auction adm, via process_auction_creative (auction/formats.rs:398). Injects TSJS, so the click guard runs in the creative frame.
    • SSAT and page-bids inline creatives, via process_inline_auction_creative (publisher.rs:5835). Emits absolute click URLs and data-tsclick but injects no TSJS, so no click guard runs there.
    • HTML fetched through /first-party/proxy (CreativeHtmlProcessor, proxy.rs:645-680). This path ignores rewrite_creatives and always rewrites, clicks included.
  • Runtime pieces, always routed in every adapter and gated by no setting:
    • /first-party/click (handle_first_party_click, proxy.rs:1545) validates the signature, appends ts-ec to the target, and returns a 302.
    • /first-party/proxy-rebuild (proxy.rs:1812) re-signs a click after creative script added or removed query params.
    • The click guard (crates/trusted-server-js/lib/src/integrations/creative/click.ts) acts only on anchors with a server-set data-tsclick. It defaults to on through TsCreativeConfig.clickGuard, which is a client-only flag that no server setting maps to.

Proposed change

  • Add a click switch, for example [auction] rewrite_clicks, that gates the anchor handler.
  • rewrite_creatives then governs asset rewriting only. Keep the name so existing configs still parse, and update its doc comment and the example TOML.
  • <base> removal and TSJS injection run when either switch is on. <base> removal protects root-relative click URLs as well as proxy URLs, and on the /auction path TSJS injection is what delivers the click guard.
  • Operators with rewrite_creatives = false today also have clicks off, and upgrading must not turn click wrapping on for them. One option is an optional rewrite_clicks that follows rewrite_creatives when unset.
  • exclude_domains keeps applying to clicks. The asset allowlist from Add a host allowlist for creative asset rewriting #1231 does not.
  • Whether the switch also covers click wrapping in proxied HTML is an open decision for the spec.

Constraints

  • Config structs use deny_unknown_fields, so older binaries reject blobs that carry the new key. Skip serializing the default the way rewrite_creatives does (auction_config_types.rs:46-50, tested in config_payload.rs:715). The upgrade order stays binary first, config second.
  • EdgeZero env overlays only replace scalar leaves that already exist in the TOML. Add the new leaf to trusted-server.example.toml so overlays can reach it.

Out of scope

  • The asset host allowlist (Add a host allowlist for creative asset rewriting #1231).
  • Unregistering /first-party/click and /first-party/proxy-rebuild when the switch is off. Creatives that were already served can still hold signed click URLs.
  • Adding a click guard to the inline SSAT path.

Done when

  • The design spec is approved and merged under docs/superpowers/specs/, and the implementation PR links it.
  • With the new setting unset, rewritten output is unchanged for both values of rewrite_creatives.
  • Tests cover all four combinations of the asset and click switches on the /auction and inline paths, including <base> removal and TSJS injection when only one switch is on.
  • With clicks off, anchors keep their raw href and get no data-tsclick.
  • With clicks on and assets off, anchors are wrapped and asset URLs are untouched.
  • exclude_domains still applies to anchors.
  • A config round-trip test shows the default is left out of the serialized blob.
  • docs/guide/configuration.md, docs/guide/creative-processing.md, trusted-server.example.toml and CHANGELOG.md are updated.

Affected area

HTML processing / JS injection

Activity

  1. added theissue type on Oct 4, 2026
  2. changed the issue type fromtoon Oct 4, 2026
  3. added this to the 202610 milestone on Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions