Repository navigation
Add a module that writes Google's tags on the page from the settings - #1371
Open
jwrosewell wants to merge 56 commits into
Open
jwrosewell wants to merge 56 commits into
jwrosewell wants to merge 56 commits into
Conversation
`MiddlewareAction` gains `body_open_inserts`, markup written once per document immediately after the opening `<body>` tag, in the order given, beside the places it had for the head and for straight after the script bundle. A vendor's two-part installation may put its second part there and nowhere else, as Tag Manager's noscript frame is, and until now a middleware had no way to reach that place. A document with no body gets none, and a second body tag in a template fragment gets nothing.
A publisher whose page templates forbid third-party scripts cannot put a Google tag on the page, so it has to come from the proxy, and nothing did that: the Tag Manager module re-serves a tag the page already carries, which needs a page that already loads Google. `crates/tag/google-tag`, selected by `[tag] modules` with `[tag.google-tag]`, writes Google's own snippets. At the start of head, the consent mode defaults, then Tag Manager's container script, then the gtag.js snippet, then the AdSense loader. Immediately after the opening body tag, Tag Manager's noscript iframe. Each is copied from Google's documentation rather than paraphrased and carries the page it came from, so a reader can compare the two. Consent defaults come first because Google's consent guide requires the default command before anything that sends measurement data, and all four types default to denied because no Google identifier may be stored before a permission signal has been processed. Where each loader points is settled at startup, because head and body insertions never reach the attribute rewriters. When the Tag Manager module serves the same tag or container, the loader names the first-party path, otherwise Google's own address, and the Tag Manager crate offers the two paths and the two id shapes it accepts so the two modules cannot disagree about an id. The noscript iframe always names Google, because `ns.html` has no first-party route behind it. A page also loading gtag.js for the same tag has that duplicate loader removed, which is why the module registers before Tag Manager in the stock list, since a removal short-circuits the rewriter chain. An inline configuration call in the publisher's own page is not removed and still sends a second page view, which the guide says plainly, because removing it would mean buffering every response.
# Conflicts: # CHANGELOG.md
The docs said the head snippets land at the start of head, where a serve middleware writes them immediately before the script bundle's tag. The registration id is google_tag and private. The consent types are named as the four Google requires for its own services. The validate doc in the Tag Manager crate is back on validate, and unused links, repeated reasoning and the builders order comment are cut.
# Conflicts: # docs/guide/configuration.md # trusted-server.example.toml
# Conflicts: # docs/guide/configuration.md
…as the workspace config does
A page that carries no GPT loader of its own got the bootstrap, the shim and the slot handoff from this module and nothing that loads GPT, so its slots never filled. `[ad-tag.google] write_loader = true` writes the loader tag into the head, last among the module's inserts, after the shim and the bootstrap it relies on. The tag asks for the loader on the module's own route, `/integrations/gpt/script`, so the whole chain stays first party. It is off by default, because a page that already carries the loader would otherwise load it twice. The guide's two tables and the GPT page name the setting, the example settings file shows it, and the changelog says what it adds. Three tests cover the tag written when asked, written last beside the slim-Prebid address, and absent by default.
rustfmt breaks the two assertions over several lines, and prettier pads the middleware table's row to the table's width.
This was referenced Oct 15, 2026
…vice The Google tag module wrote Tag Manager's container script, the gtag.js snippet, the AdSense loader and Tag Manager's noscript iframe on every reader's copy, whatever the reader's permissions said, so a reader who had not agreed, or who sent an opt-out such as Global Privacy Control, still got them. The middleware now declares storage on the device, the permission each of them needs because each stores and reads Google's identifiers there, and reads the reader's permissions in its preparation, which core hands every serve preparation. It writes the loaders and the iframe only where that permission is set, and none where no permission state reached it. The consent defaults load nothing and are still written for every reader, and the page's own loader for the configured tag is still removed for every reader, so a reader without the permission ends with none. New tests plan the middleware through core's serve preparation for a reader with every permission but storage, a reader with storage alone, a preparation handed no permission state and a document nothing was prepared for, and run a page for a reader in Germany who has given no signal and a reader in California who has opted out, under the policy core ships. The page tests now prepare both phases as core does for a page it does not store. The changelog, the configuration guide and the example settings say who gets the loaders.
With consent_defaults.region set, the Google tag module wrote one default command carrying the regions and none without a region, so a visitor outside the listed regions got no default from it, and Google's guide says that by default no consent mode value is set. The guide also says a default command with no region sets the default for every visitor no regional command covers, and gives it after the regional one. The module now writes that command after the regional one, with all four types denied and the same wait, because a grant in the settings holds only in the regions they list. With no region set it writes the one command as before. A new test sets a grant and a wait for one region and checks that two commands are written, the regional one carrying the grant and the second naming no region and denying all four with the same wait. The changelog and the configuration guide say what every other visitor gets.
Every page test of the Google tag module selected it alone, so the case where the Tag Manager module's fetch middleware has already pointed the page's own loader at the first-party path was simulated by a page that carried that path from the start. The new test selects both modules, names the Tag Manager middleware in a [[fetch]] entry and this module's in a [[serve]] entry, and runs a page that loads the configured tag from Google's host. The fetch phase rewrites the page's loader, the serve phase removes it and writes its own, and the page ends with one loader, on the first-party path, and none on Google's host.
This was referenced Oct 17, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes
crates/tag/google-tagcrates/ad-tag/googlecrates/trusted-server-core/srccrates/tag/google-tag-managerdocs/guidetrusted-server.example.tomlCargo.lockcrates/testing/seam-probeAGENTS.md.cargo/config.tomlcrates/trusted-server-cli/srccrates/trusted-server-modules/srcscripts/clippy-wasm-clockCHANGELOG.mdCargo.tomlclippy.tomlcrates/trusted-server-modules/Cargo.tomlCloses
Closes #1288
Spec
Spec: google-tag-module
Test plan
CI on this pull request at d8a8f15 passed all 20 checks.
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code (clippy deniesunwrap_usedin CI)logmacros as AGENTS.md asks (notprintln!)Stacks on sizes by viewport width (#1370).
What changed after the 51Degrees PR review
Agreed at the review on 23 October, point 1. No configuration or permission policy is compiled into the WASM, and a WASM without a policy does not start.
PermissionMaps::standard(), the policy core compiled in, is gone since Add the permission model with the Privacy Taxonomy vocabulary #1345.Kept current
mainat 7aa5c99 and the changes made lower in the stack.What this pull request does
A module,
crates/tag/google-tag, that writes Google's own snippets from the settings. In the head, the consent mode defaults, then Tag Manager's container script, then the gtag.js snippet, then the AdSense loader. Immediately after the opening body tag, Tag Manager's noscript iframe. Each snippet is copied from Google's documentation rather than paraphrased and carries the page it came from, so a reader can compare the two.The head snippets do not land at the start of head. A serve middleware's head inserts are written immediately before the script bundle's tag, after the permission state script, the ad-slots script and every fetch-phase head insert. In the stock layout
cmp.inmobiis a fetch middleware, so the consent defaults land after its stubs and loader. Google asks for the snippets as high in the head as possible, and within this module's own inserts the consent defaults come first.The consent snippet sets the four consent types Google requires for its own services,
ad_storage,ad_user_data,ad_personalizationandanalytics_storage, and each defaults to denied. It writes nowait_for_updateand noregion, so with a CMP in the deployment the tag fires with denied defaults the moment it loads rather than waiting for the CMP's update.Core gains one place, where a middleware may write markup immediately after the opening body tag,
body_open_insertson its action, once per document, beside the places it had for the head and for straight after the script bundle. A deployment whose middleware write nothing there registers no handler for it.How it relates to
tag.google-tag-managerTag Manager is a fetch middleware that re-serves a Google tag the page already carries from the publisher's own origin. This module puts the tag on a page that carries none. When both are selected and Tag Manager serves the same tag or container, the loader this module writes names Tag Manager's first-party path, otherwise Google's own address. Tag Manager's crate offers the two paths and the two id shapes it accepts, so the two modules cannot disagree about an id. The noscript iframe always names Google, because
ns.htmlhas no first-party route behind it.A page also loading gtag.js for the configured tag has that duplicate loader removed. An inline configuration call in the publisher's own page is not removed and still sends a second page view, which the guide says plainly, because removing it would mean buffering every response.
There is no guide page under
docs/guide/integrations/for this module. Its whole contract is the four settings and what it writes, which the configuration guide's[tag.google-tag]section states.Why
A publisher whose page templates forbid third-party scripts cannot put a Google tag on the page, so it has to come from the proxy. Nothing did that, because the Tag Manager module re-serves a tag the page already carries, which needs a page that already loads Google. And a page that loads Google's tags itself sends measurement data before any consent default is set.
The seam probe's middleware writes a marker immediately after the opening body tag, and its page test reads it back once, directly after
<body>, through the fetch processor.How it was verified
Google's own handling of the snippets is not exercised. The tests hold the snippets to the text Google publishes and prove where each lands on the page.
References #1288.