diff --git a/docs/superpowers/specs/2026-07-15-gam-ts-cohort-attribution-design.md b/docs/superpowers/specs/2026-07-15-gam-ts-cohort-attribution-design.md new file mode 100644 index 000000000..525c1e874 --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-gam-ts-cohort-attribution-design.md @@ -0,0 +1,886 @@ +# GAM `ts=true` attribution for Trusted Server A/B traffic + +Date: 2026-07-15 + +Status: Design + +## Problem + +A publisher will route a small, cookie-sticky A/B cohort through Trusted Server +while the control cohort continues through the existing production path. The +publisher wants Google Ad Manager (GAM) reporting to identify impressions and +clicks generated on pages served through Trusted Server and compare them with +the unmodified production cohort. + +Trusted Server currently adds the slot-level key-value `ts_initial=1` while it +prepares initial GPT slots. That key has a different lifecycle and meaning from +the experiment marker: + +- it identifies the initial slot request prepared by Trusted Server; +- it is cleared before later client-side refresh auctions; and +- it is set on matched slots rather than every GPT request on the page. + +The experiment needs a document-delivery marker. Every request issued by the +document-local GPT PubAds service after marker installation in an HTML document +successfully rewritten by Trusted Server must contain `ts=true`, including +initial requests, publisher-owned slots, lazy slots, and refreshes. Production +documents cannot be modified, so the control cohort remains unmarked. + +## Goals + +1. Add `ts=true` to every in-scope GPT PubAds request made during the lifetime + of an HTML document successfully rewritten by Trusted Server. +2. Leave production/control pages unchanged. +3. Preserve the existing `ts_initial=1` slot-ownership and refresh lifecycle. +4. Support GAM reports that count treatment impressions and clicks and derive + the control counts within the same experiment scope. +5. Avoid changing auction eligibility, ad delivery, consent behavior, or page + performance. +6. Define the data-quality checks needed when an unmarked request is used as the + control baseline. + +## Non-goals + +- Implement or change the cookie-based A/B router. The experiment infrastructure + owns sticky cohort assignment and routes only the treatment cohort through + Trusted Server. +- Prove that a Trusted Server server-side bid won the GAM auction. `ts=true` + means that the page was delivered through Trusted Server, regardless of + whether the winning demand was a server-side bid, a direct GAM line item, Ad + Exchange, or backfill. +- Mark GAM traffic outside a successfully rewritten document's local GPT PubAds + service. IMA/video SDK requests, direct tags, and server-side GAM requests + require separate instrumentation and are outside this design. A nested + document's GPT instance is in scope only when that nested HTML response is + independently routed through Trusted Server and satisfies the same rewrite, + ordering, CSP, and audit prerequisites. +- Replace `ts_initial`, `hb_*`, line-item, bidder, or creative reporting. +- Add a client-side analytics beacon or a Trusted Server telemetry event. +- Make GAM click tracking more complete. The marker only segments clicks that + GAM already records. +- Provide billing-grade or causal experiment analysis from GAM alone. + +## Assumptions and prerequisites + +- The GPT integration is enabled on every page routed into the treatment cohort. + A page served through Trusted Server without the GPT integration does not + receive the GPT head bootstrap and cannot satisfy this design. +- The response enters and successfully completes Trusted Server HTML rewriting, + contains a literal `` element, and reaches that element before any + publisher script issues a GAM request. Pass-through or buffered-unmodified + responses, rewrite failures, and origin markup that omits `` cannot + satisfy the marker guarantee. +- The publisher Content Security Policy allows Trusted Server's bare inline + scripts to execute. Initial `adSlots`, the GPT enable flag, the GPT bootstrap, + and the `bids`/`adInit` invocation are all nonce-less inline scripts; Trusted + Server does not currently propagate a publisher nonce or update CSP hashes. A + policy that blocks those scripts makes the initial TS ad stack inert and is + ineligible at launch even if it allows the synchronous first-party TSJS + bundle. +- Each in-scope HTML document uses one document-local GPT PubAds service. Nested + documents are not implicitly covered by a marked parent: each nested HTML + response must be independently routed through Trusted Server and rewritten to + receive the marker. IMA/video, direct-tag, server-side GAM, and any nested GPT + inventory whose document is not independently rewritten must be excluded from + the experiment and paired reports. +- Treatment and control traffic use the same GAM network and comparable + inventory. Report filters can isolate the pages and time window eligible for + the experiment. +- The experiment owner can obtain the expected treatment allocation from the + cookie router, even though Trusted Server does not read or emit that cookie. +- Publisher code and Trusted Server creative-opportunity slot configuration do + not reuse `ts` for another meaning, set a slot-level `ts` value, or clear + page-level targeting after Trusted Server targeting runs. The deployment audit + must inspect `trusted-server.toml` targeting maps and search publisher code + for `setTargeting`, `setConfig`, and `clearTargeting` uses that could + overwrite or remove the reserved key. If such behavior exists, it must be + resolved before launch; silently filtering operator targeting or wrapping + publisher GPT APIs is out of scope. + +## Existing behavior + +The GPT integration has two related pieces: + +1. `crates/trusted-server-core/src/integrations/gpt_bootstrap.js` is injected at + the start of ``. It creates the GPT command queue early and installs + the minimal `window.tsjs.adInit` implementation used before the richer bundle + is available. +2. `crates/trusted-server-js/lib/src/integrations/gpt/index.ts` installs the + richer GPT integration and applies slot-level auction targeting. + +Both initial-render paths set `ts_initial=1` on slots handled by `adInit`. The +Prebid refresh integration includes `ts_initial` in its list of stale +slot-targeting keys and clears it before subsequent client-side refresh +auctions. SPA cleanup also clears stale `ts_initial` targeting before applying +new route state. + +This behavior is correct for `ts_initial` and must not change. It is not +sufficient for a page-level treatment marker because it does not cover all GPT +slots and intentionally does not persist across refreshes. + +## Decision + +Add a separate page-level GPT key-value: + +```text +ts=true +``` + +The early GPT bootstrap will enqueue page-level targeting before publisher GPT +commands execute. The enqueue must occur after `window.tsjs` is initialized but +before the existing `if (ts.adInit) return;` guard: + +```text +(function () { + if (typeof window === "undefined") return; + var ts = (window.tsjs = window.tsjs || {}); + var tag = (window.googletag = window.googletag || { cmd: [] }); + tag.cmd = tag.cmd || []; + tag.cmd.push(function () { + try { + if (typeof googletag.setConfig === "function") { + googletag.setConfig({ targeting: { ts: "true" } }); + } + } catch (_) { + // Attribution must not interrupt the existing bootstrap. + } + }); + + if (ts.adInit) return; + // Existing initial-load detector and adInit stub follow. +})(); +``` + +The exact implementation must follow the repository's JavaScript formatting and +defensive checks. The important contract is that the page-level targeting +command is queued by the head bootstrap before the origin page can queue its GPT +setup or request ads. Page attribution is independent of whether the bootstrap +needs to install `ts.adInit`, so the existing guard may skip only the ad-init +stub and detector setup, never the marker enqueue. An unavailable targeting API +may skip only the marker callback; it must not prevent later queued publisher or +Trusted Server callbacks from running. + +The existing initial-load detector immediately below the new marker must reuse +the initialized `tag.cmd` reference rather than repeat its current +`(window.googletag = window.googletag || { cmd: [] }).cmd` expression. This +keeps queue initialization and both bootstrap callbacks on one consistent path. + +Moving queue initialization above the `ts.adInit` guard intentionally creates a +standard `window.googletag` command-queue stub on every rewritten, GPT-enabled +page, including a page where `ts.adInit` already exists and GPT never loads. The +stub is inert by itself and preserves the marker-before-guard guarantee; it is +not an accidental behavior to remove during implementation review. + +The TypeScript GPT bundle will defensively enqueue the same page-level targeting +at module initialization after the existing flag-gated shim block and before +`installTsAdInit()`, only when the existing publisher-page bundle tag carries a +non-executable GPT activation attribute. The HTML pipeline adds that attribute +when the GPT integration is enabled. A pre-existing `ts.adInit` is already +covered by placing the bootstrap marker before the guard and is not a reason for +the fallback. + +The fallback exists to preserve delivery-path attribution if the inline +bootstrap unexpectedly stops executing while the synchronous first-party bundle +still runs before publisher GPT. It does not recover `adSlots`, bids, the +`adInit` invocation, or the initial TS auction: those are also nonce-less inline +scripts. A fallback-only marker therefore still truthfully means "page delivered +through Trusted Server," but it also indicates a deployment state that was +ineligible at launch. Synthetic validation must treat that state as a +measurement incident, pause interpretation, and exclude the affected time window +from both paired reports if the incident contaminates collected results. GAM +cannot distinguish fallback-only pages from normally executing treatment pages +because both intentionally use the same marker. + +Neither targeting path can cover a response that was not HTML-rewritten, markup +without ``, a policy that blocks both injected paths, or a publisher GPT +request issued before the injected head content runs. Those are deployment +eligibility and coverage-validation concerns, not runtime conditions the +targeting code can repair. + +The implementation uses GPT's current page-level `googletag.setConfig` API +rather than the deprecated `pubads().setTargeting()` API. See +[GPT configuration API migration](https://developers.google.com/publisher-tag/guides/config-migration). +Page-level targeting is the right scope because GPT applies it to all slots +associated with the `pubads` service. Once installed, it remains effective for +initial, lazy, and refreshed requests for the life of the page. Existing slot +targeting may add or override other keys without requiring Trusted Server to +discover every publisher slot. + +GPT merges page-level targeting per key across `setConfig` calls. Enqueuing +`ts=true` from both Trusted Server paths is therefore idempotent, and a +publisher call that sets an unrelated targeting key preserves `ts`. The explicit +clear operations are a per-key `null`, a whole-targeting `null`, or the +equivalent legacy `pubads().clearTargeting()` calls. See +[GPT key-value targeting](https://developers.google.com/publisher-tag/guides/key-value-targeting). + +`ts` is intentionally not added to the slot-targeting cleanup arrays. Those +arrays manage per-auction state. Clearing page-level `ts` during refresh or SPA +navigation would incorrectly move a treatment page into the unmarked control +cohort. + +## Attribution contract + +### Treatment + +An in-scope GAM request is in the treatment cohort when it contains: + +```text +ts=true +``` + +The marker means: + +> The containing page was delivered through Trusted Server. + +It does not mean: + +- a Trusted Server bidder returned a bid; +- a Trusted Server bid won; +- Trusted Server rendered the winning creative; or +- the request was the first impression for the slot. + +### Control + +The production path cannot be changed. Within the exact experiment inventory, +time window, and publisher scope, an unmarked GAM request is treated as control. + +This is an inference rather than an explicit `ts=false` assertion. A treatment +request that loses its marker would be misclassified as control. The rollout +therefore requires coverage checks that compare the observed GAM treatment share +with the A/B router's expected cookie cohort share. + +### Relationship to `ts_initial` + +| Key | Scope | Lifetime | Meaning | +| -------------- | ---------- | ---------------------------- | ----------------------------------------- | +| `ts=true` | Page-level | Entire browser page lifetime | Page was delivered through Trusted Server | +| `ts_initial=1` | Slot-level | Initial TS-managed request | Initial slot request was prepared by TS | + +The two keys answer different questions and coexist. No code or report should +infer that one is an alias for the other. The value contract is exactly +`ts=true`: do not emit, accept, or report any other value (for example `ts=1`), +and do not dual-write an alternative key name such as `trusted_server`. + +## Request lifecycle + +```text +Sticky A/B cookie + -> control: browser receives production page + -> publisher GPT runs without the `ts` key + -> treatment: request is routed through Trusted Server + -> GPT head bootstrap queues page-level `ts=true` + -> GPT bundle defensively queues the same marker + -> GPT library drains the command queue + -> publisher and TS define/display/refresh slots + -> every in-scope PubAds request carries `ts=true` +``` + +The marker covers: + +- Trusted Server-defined initial slots; +- publisher-defined slots reused by Trusted Server; +- publisher slots that are not part of a Trusted Server creative opportunity; +- slots created lazily after initial page load; +- publisher-initiated refreshes; +- Prebid-managed refreshes; and +- SPA route changes within the same browser document. + +All bullets refer to slots using the same document-local GPT PubAds service. +Requests from IMA/video SDKs, direct tags, or server-side GAM integrations are +not covered merely because the containing document is marked. A nested GPT +instance is marked only when Trusted Server separately rewrites that nested +document and injects the attribution paths into its own ``. + +A full browser navigation creates a new page and repeats cookie-based routing. +The new page receives the marker only when that navigation is routed through +Trusted Server. + +## Component changes + +### Early GPT bootstrap + +`crates/trusted-server-core/src/integrations/gpt_bootstrap.js` owns the +behavior. It will set the page-level key in its earliest GPT command callback, +before the `ts.adInit` early-return guard. The targeting code stays inside the +existing raw bootstrap script returned by `head_inserts`; it must not add a +third head insert. + +The operation must be idempotent. Calling +`googletag.setConfig({ targeting: { ts: 'true' } })` more than once with the +same value is harmless, but the bootstrap should avoid adding a new global state +machine solely for deduplication. + +The bootstrap already binds the local variable `ts` to the `window.tsjs` +namespace, so the targeting key `ts` and that variable are unrelated names that +sit only a few lines apart. Add a clarifying comment at the targeting call so a +maintainer does not read the key as the namespace. The value is the string +`'true'`, never the boolean `true`: GPT targeting values must be strings. + +### TypeScript GPT bundle fallback + +`crates/trusted-server-js/lib/src/integrations/gpt/index.ts` will add a small +`installTrustedServerPageTargeting()` helper and call it during GPT module +initialization after the existing flag-gated `installGptShim()` block and before +`installTsAdInit()` when the publisher-page bundle's activation attribute is +present. The helper creates or reuses the standard GPT command queue, enqueues +the same defensive `setConfig({ targeting: { ts: 'true' } })` call, and does not +read the experiment cookie or wait for an auction. Extend the local `GoogleTag` +interface with optional `setConfig?(config: Record): void` so +the fallback remains defensive when the API is unavailable. + +The bootstrap remains the primary path because it is injected first. The bundle +call is a redundant fallback and must not delay module initialization, create a +request, or add slot-level targeting. A plain GPT module import without the +activation attribute must preserve the existing runtime-gating contract and must +not create `window.googletag`. + +### Non-executable bundle activation + +The publisher HTML pipeline in +`crates/trusted-server-core/src/html_processor.rs`, using a separate, +publisher-page-only tag helper in `crates/trusted-server-core/src/tsjs.rs`, will +add a `data-ts-gpt-enabled="true"` attribute to the existing synchronous +`#trustedserver-js` bundle tag when GPT is enabled. The attribute is data, not +an inline executable, so CSP can block the inline GPT head inserts while still +allowing the external bundle to detect that it owns page attribution. + +At module initialization, the GPT bundle captures `document.currentScript` and +requires that executing synchronous script to carry `data-ts-gpt-enabled="true"` +before the fallback may create a GPT stub. It must fail closed when the +executing script cannot be identified. Do not authorize activation through a +global `#trustedserver-js` lookup: the generic unified tag uses the same ID in +creative and test contexts, and duplicate IDs could select the wrong element. +Binding the signal to the executing tag keeps the activation decision explicit +and testable without relying on an inline global flag. + +The existing `window.__tsjs_gpt_enabled` flag continues to activate +`installGptShim()` when inline scripts run. It cannot activate the CSP fallback +because the server sets it from an inline head insert—the execution path CSP may +block. Migrating shim activation to the data attribute is out of scope; module +initialization preserves the current flag-gated shim installation, then runs the +attribute-gated page-targeting helper, then installs `ts.adInit` and the +remaining GPT bundle hooks. + +This signal must be limited to the publisher-page bundle generated from the +enabled integration registry. Do not infer activation merely because the GPT +module exists in an all-modules bundle: creative and test tooling can load that +bundle outside the publisher GPT integration. Do not add a new script tag or +change the integration's existing head-insert count. + +Extend the `tsjs.rs` and `html_processor.rs` tests to prove that the existing +publisher-page bundle tag gains the activation attribute only when GPT is in the +enabled immediate module set, remains a single external tag, and omits the +attribute for non-GPT publisher bundles and generic all-modules tags. Bundle +tests must also prove that an unrelated or duplicate element with +`id="trustedserver-js"` cannot activate the fallback. + +### GPT Rust integration tests + +`crates/trusted-server-core/src/integrations/gpt.rs` already tests the embedded +bootstrap returned by `head_inserts`. Extend those tests to prove that: + +- the bootstrap contains page-level `ts=true` targeting; +- the marker enqueue appears before the `if (ts.adInit) return;` guard; +- the targeting setup is queued before `ts.adInit` can issue `display` or + `refresh`; +- the existing `ts_initial` marker remains present; and +- the enabled integration without `slim_prebid_url` still emits exactly the + existing two head inserts, proving the marker was added to the bootstrap + instead of a new tag. + +### Bootstrap execution tests + +Add `crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts` +as a Vitest/jsdom behavioral test for the raw bootstrap. The test reads +`crates/trusted-server-core/src/integrations/gpt_bootstrap.js` with Node's +`readFileSync`, resolves the source path relative to `import.meta.url` rather +than the process working directory, and creates an isolated +`new JSDOM(html, { runScripts: 'outside-only' })` realm. Before evaluating the +exact source with that realm's `window.eval`, the test must assert that +`globalThis === window` and `typeof global === 'undefined'` inside the realm. +This is required because Vitest's default jsdom `window.eval` runs in Node's +realm under the current configuration. The harness supplies a minimal mocked GPT +command queue and `pubads` service. It must not evaluate the source in Node's +global context, copy the bootstrap into a test fixture, or add a JavaScript +runtime dependency to Rust. + +The harness must prove that: + +- the attribution callback is queued before a publisher callback added after the + injected bootstrap; +- draining the queue calls `googletag.setConfig` with page-level `ts=true` + before the publisher callback runs; +- a pre-existing `ts.adInit` does not prevent the attribution callback from + being queued or executed; +- a publisher callback queued after the bootstrap still runs when + `googletag.setConfig` throws; +- an unavailable or throwing `googletag.setConfig` does not prevent the existing + `disableInitialLoad` wrapper from being installed; +- `ts.adInit` remains installed when attribution setup is unavailable or throws; + and +- calling the wrapped `disableInitialLoad` still records + `ts.gptInitialLoadDisabled`. + +### Bundle fallback tests + +Extend `crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts` using +its existing dynamic-import and `vi.resetModules()` pattern. Prove that module +initialization with the bundle activation attribute queues page-level `ts=true` +after any existing flag-gated shim installation and before installing +`ts.adInit`, that it reuses an existing GPT command queue, and that unavailable +or throwing `setConfig` does not stop the remaining GPT module installers. +Retain the existing assertion that a plain module import without an activation +signal does not create `window.googletag`. A duplicate call after the bootstrap +must remain safe and must not create another script or network request. + +### Slot cleanup constraints + +No refresh-lifecycle change is required. In particular: + +- do not add `ts` to `TS_REFRESH_TARGETING_KEYS`; +- do not add `ts` to `TS_BASE_TARGETING_KEYS`; +- do not rename or remove `TS_INITIAL_TARGETING_KEY`; and +- do not copy `ts` onto individual slots. + +Leaving these components unchanged is part of the design: slot cleanup cannot +remove a page-level key set through `googletag.setConfig`. + +### Documentation + +Document the distinction between page-level `ts=true` and slot-level +`ts_initial=1` in `docs/guide/integrations/gpt.md`, near the existing command +queue documentation. Include the GAM setup and reporting preconditions below; do +not add this current integration to a planned-future GAM document. + +## GAM configuration + +GAM configuration is a deployment prerequisite and must be completed before the +experiment starts because key-value reporting is not retroactive. + +The request contract is `ts=true`: key name `ts`, predefined value `true`. The +key name is provisional pending a GAM preflight (see issue #1027). The GPT/GAM +`CustomTargetingKey.name` (the code sent in the ad request) is documented as +limited to 10 characters in the SOAP/REST API, which would rule out a fuller +name such as `trusted_server` (14); other Help Center material implies 20, and +these describe different provisioning surfaces, so the enforced limit must be +confirmed in the target network before the contract is fixed. If the confirmed +limit permits a longer name, a more descriptive, less collision-prone key is +preferred over `ts`. Because `ts` is short, it carries a real collision risk with +common publisher timestamp or cache-buster keys, which makes the cross-system +collision audit a hard launch gate, not a formality. If any publisher, Trusted +Server configuration, or GAM object already uses `ts`, the experiment must stop +until the collision is removed or the contract is explicitly revised everywhere +before treatment traffic begins. + +1. In **Inventory > Key-values**, create or verify a key whose request name is + the finalized marker key (`ts` pending the preflight in issue #1027). When the + key is created, confirm the enforced key-name and value length limits on the + provisioning surface actually used: the SOAP/REST + [`CustomTargetingKey`](https://developers.google.com/ad-manager/api/reference/v202511/CustomTargetingService.CustomTargetingKey) + documents a 10-character key-`name` limit and a 40-character value limit, but + the UI surface may differ, so verify against the target network rather than + assuming. +2. Use a predefined value named `true`. +3. Enable `ts` as a dedicated reportable Enhanced key-value dimension. If the + network does not support Enhanced key-value dimensions, use a report filtered + to the single legacy key-value `ts=true`; never sum unfiltered legacy + **Key-values** dimension rows. +4. Reserve `ts` for Trusted Server page attribution. +5. Audit existing publisher GPT code and every GAM object that consumes custom + targeting for an existing `ts` key before deployment. This includes line + items, proposal line items, rules, protections, yield configuration, and any + network-specific custom-targeting surface. +6. Audit every `CreativeOpportunitySlot.targeting` map from all effective + `trusted-server.toml` configuration sources. The arbitrary operator-supplied + map is copied to GPT slots, where a slot-level `ts` value would override the + page-level marker. Any occurrence is a launch blocker; do not silently + discard it because that could change established operator targeting. +7. Audit publisher code for every operation that can remove or supersede the + marker after initial GPT setup. Search for `setConfig({ targeting: null })`, + a `ts: null` or different `ts` value, `pubads().clearTargeting()` with no key + or with `ts`, and slot-level `ts` targeting. Account for equivalent calls + assembled dynamically. + +The audit is a hard precondition. If `ts` already has another meaning, or any +GAM object targets or acts on `ts=true`, the experiment owner must resolve the +collision before deployment. The measurement marker is not intended to change ad +eligibility, pricing, protection, or routing. A pre-existing targeting consumer +for `ts=true` would make the A/B test measure a traffic or demand change at the +same time as Trusted Server delivery. + +Undefined values do not appear in standard key-value reports even when the key +is reportable, so value `true` must exist before treatment traffic begins. See +[Add key-values](https://support.google.com/admanager/answer/9796369) and +[Report on targeting keys](https://support.google.com/admanager/answer/14528835). + +## Reporting and comparison + +### Report scope + +Every comparison must apply identical filters for: + +- publisher/network; +- experiment start and end time; +- sites or inventory included in the cookie experiment; +- ad units and formats; +- geography and device categories, when used; and +- any consent or traffic-quality exclusions. + +Do not compare the TS cohort with all unmarked network traffic unless all that +traffic is eligible for the same experiment. Likewise, exclude smoke tests, +direct hits, operations traffic, and any other TS-served page outside the cookie +experiment. The marker identifies the delivery path, not the router's cohort +assignment, so all such requests also carry `ts=true` when the GPT integration +runs. + +The route owner must use router or access logs to prove that non-experiment TS +traffic is absent from the eligible inventory during the measurement window. If +such traffic cannot be prevented and has no independent inventory or reportable +dimension, GAM cannot remove it from Report B because its marker is identical to +the cohort marker; the experiment must not launch. Record the owner, query, +expected zero threshold, and response procedure in the experiment runbook. + +### Cohort calculations + +Use two reports with identical date boundaries, time zone, inventory filters, +traffic-quality filters, and metric definitions: + +1. **Report A — experiment total.** Do not include **Placement**, legacy + **Key-values**, **Targeting**, **Yield group**, or another dimension that can + represent one event more than once. This report provides one non-duplicated + total for every metric in the eligible experiment scope. +2. **Report B — TS treatment.** Use the dedicated Enhanced `ts` dimension + filtered to `ts=true`. If Enhanced key-value dimensions are unavailable, use + the legacy **Key-values** dimension filtered to exactly `ts=true` and do not + sum any other key-value rows. Do not add **Placement**, **Targeting**, + **Yield group**, or any unrelated dimension that can represent the filtered + treatment event more than once. + +The legacy **Key-values** dimension can emit the same impression or click on +multiple rows when a request contains multiple key-values. It therefore cannot +provide Report A or a summable totals row. See +[Avoid double counting report totals](https://support.google.com/admanager/answer/7642799). + +For this paired report scope, define: + +```text +total_impressions = Report A impressions +ts_impressions = Report B impressions +prod_impressions = total_impressions - ts_impressions + +total_clicks = Report A GAM-recorded clicks +ts_clicks = Report B GAM-recorded clicks +prod_clicks = total_clicks - ts_clicks +``` + +If the selected GAM report exposes an explicit unassigned or `(not set)` row, +that row may be used only as a cross-check. The paired Report A minus Report B +calculation remains the control definition because production cannot send an +explicit value. The experiment owner must retain both report definitions with +the results so later analysis can verify that their filters and metrics match. +Export both reports after the same GAM reporting-latency and invalid-traffic +adjustment window. If GAM restates one report, rerun the pair before applying +the subtraction. + +Use total metrics when the goal includes all GAM demand sources. GAM's +`Ad server impressions` and `Ad server clicks` metrics exclude Ad Exchange and +AdSense, so those narrower metrics should only be used when that exclusion is +intentional. GAM counts impressions and clicks according to its own tracking +rules; adding `ts=true` does not create new impression or click trackers. See +[Counting impressions and clicks](https://support.google.com/admanager/answer/2521337). + +Both reports must use the same non-targeted impression and click metric names. +Do not use targeted-impression or targeted-click metrics for this attribution: +`ts` is intentionally forbidden from line-item targeting, so metrics limited to +keys used for targeting do not represent the requested delivery-path cohort. +Record the exact selected GAM metric names with the saved report definitions +before launch. + +### Unequal cohort sizes + +The treatment cohort is intentionally small, so raw TS and production totals are +not directly comparable. Reports should show the raw counts, but experiment +conclusions should compare normalized measures where compatible metrics are +available: + +- impressions per GAM ad request; +- fill rate; +- clicks per impression (CTR); and +- revenue per thousand impressions or requests. + +Impressions per routed pageview or per assigned visitor require a denominator +from the A/B router or site analytics. GAM alone cannot identify unmarked +production pageviews that made no ad request. Any cross-system experiment +analysis is outside the implementation but should use the same time and +eligibility filters. + +### Data-quality checks + +During the experiment, monitor: + +1. observed `ts=true` ad-request or impression share versus the router's + expected treatment allocation; +2. scheduled synthetic marker presence on initial, lazy, and refreshed treatment + requests; +3. scheduled synthetic marker absence on production requests; +4. non-experiment traffic served through Trusted Server; +5. unexpected `ts` values or line-item targeting; +6. report freshness and GAM invalid-traffic adjustments. + +A gap between expected and observed treatment share is a measurement incident, +not evidence of production performance, until missing-marker and request-volume +differences are ruled out. Because router assignment and GAM delivery normally +use page or visitor counts versus ad-request or impression counts, this share +comparison is a diagnostic rather than direct proof of marker coverage. It can +measure coverage directly only when the router or site analytics supplies a +matched request- or page-level denominator. + +Checks 2–3 use a scheduled synthetic browser crawl of representative experiment +URLs. The crawler supplies known treatment and control cookies, captures GAM +network requests, and triggers initial, lazy, and refreshed slots. A failed +marker assertion is an operational measurement incident. This is external +validation rather than a site beacon or Trusted Server telemetry event; if the +experiment owner cannot operate the crawl, checks 2–3 become documented manual +samples and must not be represented as continuous production metrics. + +On treatment URLs with matched creative opportunities, the crawler must also +detect the fallback-only CSP state: capture CSP violations and verify that the +injected `adSlots`, `bids`, and initial `adInit` handoff executed. A page that +has `ts=true` only because the external bundle ran, while those inline scripts +were blocked, remains correctly marked as TS-delivered but raises a measurement +incident. Since GAM cannot separate those requests afterward, the incident owner +must pause interpretation and exclude the affected time range from both reports +when clean boundaries can be established; otherwise the experiment result is +invalid. + +## Failure handling + +The marker is best-effort instrumentation and must never block ads or page +delivery. + +- If GPT never loads, there is no GAM request to classify. +- If a response is not successfully HTML-rewritten, has no literal ``, or + issues an in-scope GPT request before the injected head content runs, neither + targeting path can mark that request. Such traffic is ineligible for the + experiment and must be detected before launch or excluded from analysis. +- If CSP blocks Trusted Server's nonce-less inline scripts, the initial TS ad + stack is inert and the page is ineligible even when the first-party bundle + queues the attribution marker. The fallback prevents a TS-delivered page from + leaking into the inferred control cohort; it does not make the deployment + healthy. If CSP blocks both inline scripts and the bundle, attribution also + fails. +- If `googletag.setConfig` is unavailable when a queued command runs, the + targeting step is a defensive no-op and must not throw. Supported treatment + deployments must use a GPT version with the configuration API; browser/GAM + validation detects an unsupported or missing API before experiment launch. +- If publisher code or a Trusted Server creative-opportunity targeting map + applies slot-level `ts`, GPT gives the slot-level value precedence. The + deployment audit prevents this collision; runtime filtering or interception is + out of scope because it could silently alter established targeting behavior. +- If publisher code calls `setConfig({ targeting: null })`, sets `ts: null` or a + different value, calls legacy `pubads().clearTargeting()` for all keys or for + `ts`, or applies slot-level `ts`, the effective marker can be removed or + superseded. The publisher-code audit and refresh validation are required + because this design deliberately does not intercept those APIs. +- If the marker is absent on a treatment request, GAM classifies it with the + unmarked baseline. Coverage monitoring is the mitigation. +- GAM configuration or reporting failures do not affect ad serving. + +No retry, beacon, cookie read, backend request, or persistent client state is +added by this feature. + +## Privacy and consent + +`ts=true` contains no unique user identifier, cookie value, page URL, or auction +data. It describes only the delivery path of the current document. Because only +the cookie-sticky treatment cohort is routed through Trusted Server for this +experiment, the value also reveals treatment-path membership for that GAM +request. It is therefore cohort information even though it does not expose the +assignment cookie or identify a person by itself. + +The implementation does not read the experiment cookie. Routing happens before +Trusted Server handles the request. Existing consent gates continue to decide +whether GAM requests or auctions occur. The marker does not create an ad request +that would otherwise be suppressed. Before enabling the key, the experiment +owner must complete the publisher's privacy/data-governance review for sending +this treatment-path attribute to GAM and confirm that existing consent and +data-use terms cover it. + +## Testing strategy + +### Automated tests + +1. Extend Rust GPT head-insert tests to assert page-level `ts=true` targeting is + in the existing raw bootstrap, occurs before the `ts.adInit` guard and any + bootstrap `display()` or `refresh()` call, and does not change the expected + head-insert count. +2. Extend the TSJS tag and HTML processor tests to prove the non-executable GPT + activation attribute appears only on the enabled publisher-page bundle and + adds no script tag. +3. Add the Vitest/jsdom raw-bootstrap harness described above. Exercise the + bootstrap with `googletag.setConfig` available, unavailable, and throwing, + and prove a later publisher callback still runs in every case. Set + `window.tsjs.adInit` before evaluation and prove the marker still runs. +4. Extend the existing GPT bundle tests to prove module initialization queues + the fallback marker and remains non-blocking when `setConfig` is unavailable + or throws. +5. Retain assertions for `ts_initial=1` to prevent accidental replacement. +6. Retain refresh tests proving stale `ts_initial` and `hb_*` slot targeting is + cleared. Add an explicit assertion or source-level invariant that page-level + `ts` is not included in slot cleanup lists. +7. Extend creative-opportunity configuration tests to demonstrate that an + operator targeting map is forwarded verbatim, documenting why the deployment + audit must reject a configured `ts` key rather than assuming the client + overwrites or filters it. +8. Run the project-required target-matched Rust and JavaScript checks for the + touched files. + +### Browser/GAM validation + +Before experiment launch: + +1. Load a treatment page using a known treatment cookie. +2. Confirm the initial in-scope GAM request contains `ts=true` using GPT + Publisher Console, Delivery Inspector, or the browser network panel. +3. Trigger a lazy slot and a refresh; confirm both requests still contain + `ts=true`. +4. Load the equivalent production page with a control cookie and confirm the key + is absent. +5. Confirm `ts_initial=1` remains limited to its existing initial-slot + lifecycle. +6. Validate the deployed CSP by proving `adSlots`, the GPT bootstrap, `bids`, + and the initial `adInit` handoff execute on a representative page with + matched slots. A page that runs only the external bundle is ineligible even + if the fallback marker appears. +7. Set an unrelated page-level targeting key after `ts=true` and confirm both + keys remain on a later request. Treat an explicit page-level or per-key clear + as a failed publisher-code audit, not supported behavior. +8. Validate that IMA/video, direct-tag, server-side GAM, and nested GPT + inventory without an independently TS-rewritten document is absent from the + experiment and paired report scope. Directly validate any independently + rewritten nested documents that are intentionally included. +9. Run a short GAM report and verify treatment totals appear under `ts=true` + while overall totals remain unchanged apart from normal reporting latency. + +## Rollout + +1. Audit response eligibility, including HTML rewriting, `` ordering, CSP, + and publisher GPT calls that could precede or remove the marker. +2. Audit the `ts` key across publisher GPT code, effective `trusted-server.toml` + creative-opportunity targeting maps, and every GAM custom-targeting consumer. +3. Prove through router or access logs that non-experiment traffic is excluded + from the TS route or independently separable in both paired GAM reports. +4. Exclude IMA/video, direct-tag, server-side GAM, and nested GPT inventory + whose document is not independently rewritten. Inventory in intentionally + rewritten nested documents must pass the same request and report validation + as the top-level document. +5. Create and enable the reportable GAM key and predefined value. +6. Deploy the Trusted Server marker before assigning experiment traffic. +7. Provision the scheduled synthetic crawl, assign an incident owner, and obtain + one successful treatment/control run covering initial, lazy, refreshed, and + CSP execution checks. +8. Validate treatment and control requests manually. +9. Start the small cookie-sticky cohort. +10. Compare observed GAM treatment share with the router allocation before using + the results for performance decisions. +11. Monitor normalized metrics over a sufficiently large window; do not infer a + treatment effect from unequal raw totals. + +Rollback stops adding the page-level key to newly loaded documents. Already-open +documents—including long-lived SPA sessions—retain page-level targeting and may +continue issuing marked lazy or refreshed requests after the code rollback. +Record the rollback timestamp, end the experiment reports at the last clean +pre-rollback boundary, and exclude the post-rollback drain interval from both +cohorts. The drain ends only after router/access logs and GAM show no remaining +`ts=true` traffic for one complete agreed reporting interval and a fresh +synthetic navigation confirms that new documents are unmarked. If marked traffic +persists, the interval remains excluded rather than being inferred as control. +Historical GAM rows recorded while the key was active remain valid, and the GAM +key may stay defined and reportable for historical analysis. + +## Alternatives considered + +### Reuse `ts_initial=1` + +Rejected because the key is slot-level, covers only TS-managed initial slots, +and is deliberately cleared on refresh. Changing its lifecycle would also break +its existing ownership semantics. + +### Add slot-level `ts=true` in `adInit` + +Rejected because it would miss publisher-owned or lazy slots that do not pass +through `adInit`, and existing refresh cleanup could remove it. It would measure +auction participation rather than page delivery. + +### Set `ts=true` only in the bootstrap + +Rejected as the sole path. Placing the enqueue before the existing `ts.adInit` +guard correctly handles a pre-installed ad-init implementation. The bundle is +not needed for that case. It remains useful when the inline script unexpectedly +stops executing but the synchronous first-party bundle still runs: without the +fallback, a TS-delivered treatment page would be silently inferred as control. +The fallback does not rescue the simultaneously blocked TS ad-stack scripts, so +that state is an incident rather than an eligible deployment mode. + +### Use a longer descriptive key name such as `trusted_server` + +A descriptive name would lower the collision risk that the short `ts` key +carries. Rejected because GAM limits a custom-targeting key name to 10 +characters, so `trusted_server` (14) cannot be created as a reportable key. The +short `ts` name is therefore mandatory, and the cross-system collision audit is +the compensating control. Do not dual-write `ts` alongside any longer alias: two +names for one cohort would increase GAM setup and audit surface and permit +silent drift between reports. Only `ts=true` is valid. + +### Configure `ts=true` in creative-opportunity slot targeting + +Rejected because creative-opportunity targeting applies only to matched slots. +The experiment requirement covers every request from each successfully rewritten +document's local GPT PubAds service. + +### Rewrite `cust_params` on GAM network requests + +Rejected because it depends on GPT's internal request construction and encoding, +adds interception risk, and duplicates a supported GPT targeting API. + +### Mark production explicitly with `ts=false` + +Preferred in a fully controlled experiment, but unavailable because the +production path cannot be changed. The design documents the resulting unmarked +baseline limitation and requires coverage checks. + +## Acceptance criteria + +1. For a deployment that satisfies the documented GPT-integration, HTML-rewrite, + `` ordering, CSP, reserved-key, request-scope, and targeting-cleanup + prerequisites—and in which a Trusted Server marker callback runs before the + first request—every request from that rewritten document's local GPT PubAds + service carries `ts=true`, including initial, lazy, refreshed, + publisher-owned, and SPA-route requests. A nested document is covered only + when its own HTML response independently satisfies the same prerequisites. +2. Production/control pages remain unmodified and do not carry `ts` from this + feature. +3. `ts_initial=1` retains its current slot-level initial-request lifecycle. +4. The new key is not cleared by Prebid refresh or SPA slot cleanup. +5. No publisher code, effective Trusted Server creative-opportunity targeting + map, or GAM custom-targeting consumer changes ad eligibility, pricing, + protection, routing, or the marker value because of the measurement key. +6. The marker adds no unique identifier, cookie value, network request, or + blocking work. Its disclosure of treatment-path membership to GAM has passed + the publisher's privacy/data-governance review. +7. GAM can report treatment impressions and clicks under `ts=true`, and the + control counts can be derived within the same experiment scope using + identical non-targeted metrics. +8. Known treatment requests are validated directly for marker presence, and the + observed GAM treatment share is compared diagnostically with the router's + expected cohort allocation before experiment results are interpreted. +9. Non-experiment TS traffic is absent from the route during the measurement + window or excluded from both paired GAM reports with identical filters. +10. Only `ts=true` is emitted and reported; no other value (for example `ts=1`) + and no alternative key name (for example `trusted_server`) alias or + dual-write compatibility path exists. +11. CSP-compatible inline execution is proven before launch. A fallback-only + page remains attributed to treatment but raises an incident and cannot be + treated as a healthy experiment page. +12. Rollback reporting excludes already-open documents until observed marked + traffic has drained according to the documented boundary rule.