Repository navigation
Conversation
`[cmp] module = "inmobi"` selects it, with the account's Choice loader in `[cmp.inmobi] script_url` and the IAB CMP id in `cmp_id`, which is 10 unless written. Where a `[[fetch]]` entry names `cmp.inmobi`, the module's middleware writes three things into the head, in this order: the IAB TCF v2 stub, which queues every `__tcfapi` call made before the prompt arrives and answers `ping` as a stub; InMobi's GPP stub, which answers the generic `__gpp` commands at once with the CMP id and queues the rest; and the Choice loader, deferred, so a slow consent vendor never holds up the page and the loader never runs ahead of a stub the publisher's own head carries. Each stub defines nothing where the page already has its interface. The module serves no browser module, because the stubs and the Choice script are the whole client side. The prompt writes the reader's answer to the cookie the TCF permission signal reads, so the answer reaches the permission model on the next request the way every signal does. The script URL becomes a `<script src>` on every page, and a settings document reaches the service from a config store rather than from a person reading it, so it is refused when it is empty, is not a URL, is not `https`, is on a host other than `cmp.inmobi.com` or `cdn.inmobi.com`, or carries a user name or password. A host that merely contains an allowed name is refused by what it is rather than by what it looks like, and the URL is escaped again when it is written, because a URL on the right host can still carry a quote in its query. A document that selects the prompt and names it in no entry is refused, because the prompt would never be written, and the table refuses a field it does not read. The two stubs are tested in a stand-in for the browser with Node's test runner, `node --test "crates/cmp/inmobi/tests/*.test.mjs"`, which runs the scripts as the page carries them. The module's tests cover each refusal, the tag's parts and their order, that a vendor's tag named after the prompt is written after it, and the page a reader receives, recorded in `src/fixtures/page-change.recorded.html`. The stock list offers the module after Didomi and lists its middleware in the fetch phase, the example configuration names it in its fetch entry and shows its table, and the cargo aliases build and test it with the other modules. 51Degrees maintain the crate until InMobi adopt it, which its manifest states.
A module that writes a script into pages keeps that script's tests beside it, as `tests/*.test.mjs`, run with Node's test runner and no packages. The vitest job runs every such file after the browser bundle's tests, from the repository root, so a stub that changes is tested the way a page runs it. The InMobi Choice module's two stubs are the first.
A guide page for the module, with its settings, where to place it, what is checked, and what a consent prompt module keeps to, in the sidebar beside the other consent management platforms. The configuration guide's section table, its list of the modules this repository ships, its placing table and its integration sections carry the module, the integrations overview lists it, the crate list in AGENTS.md names it, and the changelog records it.
An entry may cover any media type core streams as text: `text/html`, another `text/` type other than `text/x-component`, whose rows are length prefixed, `application/javascript`, `application/json` or `image/svg+xml`, written as a bare `type/subtype` in lower case. A response is matched by the type and subtype of its content type, without the parameters. A middleware says which media types it works on with `handles_media_type`, HTML alone unless it says otherwise, so every module keeps its behavior with no change to its code. An entry naming a middleware under a media type it does not handle is refused when the registry is built, naming the entry and the middleware. A chain carries its media type, and on one other than HTML its plan refuses an action that decides about HTML, being head markup or an element or text handler, by the middleware's name, because those are decisions about HTML and a stream processor is the one thing that applies to any body. The publisher runs the covering entries' stream processors on a body of such a media type after it has moved the origin's addresses in it: the fetch entry's on what is stored, and the serve entry's on each reader's copy, whether that copy is assembled from the store or fetched for the request, as on a page. Core's test fixture gains a middleware that works on `text/plain` and adds a line at the end of a body, and the tests cover the entries accepted and refused, the media type of a content type, a chain refusing an HTML decision on text and taking a stream, the registry refusing a middleware under a media type it does not handle in either direction, and a text body through the publisher with and without a covering entry.
The configuration guide says which media types an entry may cover, how a response is matched, what runs on a body that is not HTML and what is refused, with an entry for a text file as the example. The integration guide shows `handles_media_type` on a middleware that works on `text/plain`. The changelog records the change.
`[ads-txt] modules = ["sellers"]` selects it, with the lines to add in `[ads-txt.sellers] lines`. The module is a middleware for `text/plain`, named in a `[[fetch]]` entry on the file's path, which passes the origin's file through and writes the lines after its last one, on a line of their own, so a seller the deployment introduces is declared beside the publisher's own. The publisher's file stays authoritative, and a publisher whose origin has no file gets the origin's answer, because the module adds to the publisher's file and never invents one. Every buyer's crawler reads the file and a typo there costs the publisher revenue silently, so each line is checked as the settings load against the file's grammar: a data record is a domain, an account id, DIRECT or RESELLER and an optional certification authority id, separated by commas, and a variable is one of CONTACT, SUBDOMAIN, INVENTORYPARTNERDOMAIN, OWNERDOMAIN or MANAGERDOMAIN with a value. A comment, an empty line and an empty list are refused, naming the line and what is wrong with it, and so is a document that selects the module but names it in no entry, where the lines would never be written. The stock list offers the module after the page modules and lists its middleware in the fetch phase, the example configuration shows its tables and entry, the cargo aliases build and test it with the other modules, and the crate is the project's own, which its manifest states. The tests cover each refusal, the lines the grammar accepts, the file with and without a final line break, an empty file, a file read in pieces, and the module's place in a registry.
The configuration guide gains the section, its module's table, how the file is made and what is refused, with the entry that places the middleware, and lists the module in its key sections and placing tables. The rules guide names the section among the types and in its table. The changelog records the module.
`cmp.inmobi`, `cmp.didomi` and `cmp.sourcepoint` declare both phases, so an entry of either phase may name them. What a prompt writes is the same for every reader, so the fetch phase stores it in the page every reader is served from, which is what a generated configuration places, and the serve phase writes it on each reader's copy for a publisher who wants the stored page without it. Selecting the InMobi module with no entry of either phase naming it is still refused. The stock list keeps each prompt's row in the fetch phase, which is the phase `ts audit generate` writes, and its test allows a middleware that runs in several phases to be listed in one of them, checking that what is listed for a phase is registered for it and that everything registered is listed somewhere.
The InMobi guide places the prompt by an entry of either phase and says what each phase does with it, the configuration guide's middleware table says the same for every consent management platform, and the changelog records the change.
…ts path When the origin answers `404 Not Found` for a path a `[[fetch]]` entry with a path names middleware for, core runs that middleware on an empty document of the entry's media type and its output is the file, stored and served as a fetched one is. A GET and a HEAD are answered alike. With no such entry, on any other answer from the origin, or where only the entry covering every path applies, the origin's answer stands, because a page the origin does not have is the origin's answer. `PhaseEntries::creating` finds the entry. The sellers crate says its file is made from the lines alone where the publisher has none, which its middleware already does on an empty document.
The configuration guide says it with the entries for other media types and in the ads.txt section, the integration guide tells a middleware it may be run on an empty document, and the changelog records the rule.
`Middleware::prepare` answers a future for a document whose decision needs the request's services or has to wait, on the key-value store or the network. The request's handler runs it before the document is processed, once per phase the document goes through, as the middleware's module and with the permissions it declares, and keeps what it answers under the middleware's name in the context's `Prepared`, where `create` takes it as the type it prepared. `create` and every streaming path stay synchronous, so the Fastly adapter is as it was. A middleware that prepares nothing is asked `create` as before. A preparation may answer a `Refusal` instead, with a status, an optional `Retry-After` and a message. The handler answers the request with that status and the message as plain text, marks the response private and unstorable, and processes no document, so a middleware responsible for a file fails closed when it cannot make it. The first refusal ends the preparations of a document. A copy read from the store prepares its serve chain the same way before it is assembled. The middleware fixture gains one that prepares and one that refuses, and the tests cover what is prepared reaching `create` in either phase, the refusal's status, headers and body, the chain keeping each answer under its name, and the first refusal ending the preparation.
The section says when a middleware prepares, what it is called with, where its answer goes, how a refusal is answered, and what each phase's preparation must not depend on. The changelog records the addition.
`PlatformLoadingCache` in `platform/loading_cache.rs` takes a key and a loader and returns the value, running the loader at most once per key at a time, so however many requests ask for a missing value at the same moment the source is asked once and every one of them continues with its value or its error. A failed load is not stored, so the next caller loads again. `NoLoadingCache` holds nothing and runs every load, and is what `RuntimeServices` carries when no module supplies a cache. Every key is a `StoreKey`, being the publisher's `ServerKeyPrefix`, the cache's name and the caller's key. The prefix is made in one place, `platform/server_key.rs`, from the publisher's domain in the loaded settings with a single leading `www.` folded and `ts.` in front, so two publishers sharing a store never share an entry. `PlatformValueCache` is the host's shared cache of small values with lifetimes, which a host supplies through `RuntimeServices` when it has one, with `lookup_or_reserve` handing the caller a `ValueReservation` where the host can make other callers for a key wait, and `remove`. A loading cache keeps its shared copies there where the host has one and in the key-value store otherwise. An `IntegrationBuilder` offers a loading cache with `with_loading_cache`, and `loading_cache_from` gives an adapter the one its linked builders offer, refusing two, so no adapter names the crate that supplies it. Core's tests get `MemoryLoadingCache`, which keeps what it loads for the lifetime the load gave and records every call.
A contributor's answer is loaded through `RuntimeServices::load_cached`, so the contributor is asked once however many requests want its rules at the same moment, and the copy in the host's shared store answers every instance until the contributor's refresh is due. The answer's lifetime in the cache is the contributor's refresh, and `[cache.robots-txt]` sets how long the copies last with `memory_seconds`, `store_seconds` and `store_idle_seconds`, defaulting to a minute, a week and 30 days. A memory lifetime may be 0, the other two must be at least 1, and none may be more than a year, which `LoadingCacheConfig::check` refuses as the settings load, naming the setting. The last answer a contributor gave is also kept apart from the cache for 30 days, in the host's value cache where it has one and in the key-value store otherwise, and read only when the contributor fails, because yesterday's rules are a better answer than none. Every key starts with the publisher's `ServerKeyPrefix`, so two publishers sharing a store never share an answer. The handler hands the settings through in place of the publisher's domain and the time, and a selection carries the name the contributor was declared under. The tests use `MemoryLoadingCache` and cover the one load for a held answer, the key naming the publisher, changed settings asking again, the kept answer served when the contributor fails, in a second instance and from a value cache, and a failure with nothing kept being an error that is not held.
Each adapter takes the cache from the builders it is given with `loading_cache_from` when it builds its state, and hands it to every request's services, so every request in a process shares the cache's memory copy and waits for one load. Axum, Cloudflare and Spin take it from the stock list followed by the builders a deployment passes, as they take every vendor module, and hold nothing without one. No adapter names the crate that supplies it. On Fastly the finishing steps' services carry the same cache as the request, so a lookup there shares the request's memory copy, and the entry point builds those services once from the state it has, or with the unavailable store and a cache that holds nothing when no state was built. Fastly supplies `FastlyValueCache` over Fastly's cache as the value cache, which needs nothing created or linked on the service. A transaction lookup reserves a key nothing holds for this instance and makes every other instance at the location wait for it to be filled, a stale entry is reserved for a fresh load rather than served, and a reservation dropped unfilled is released so a waiting instance takes it over. Each entry is written with a surrogate key, a hash of its key, which `remove` purges. Its tests run under Viceroy and cover the reservation, its release, a stale entry and the purge.
`crates/loading-cache/51degrees` implements `PlatformLoadingCache` with the published `fiftyone-caching` package, version 4.5.8-49, with its default features off so it builds for every host. Each cache is a least recently used copy in process memory with a short lifetime over the host's shared store, being its value cache where it has one and its key-value store otherwise, written in the package's entry format so a store that deletes late or keeps no lifetimes never returns an entry past its drop time. The memory layer is shared by every request in a process and loads a missing key once, covering the store read and the source together. The store layer is made for each call from that request's stores. A load a cache allows to run apart from its caller runs on a pool of threads on a native host and with the request's `wait_until` on Cloudflare, and every load runs in its caller on Fastly and Spin. The crate is the module `loading-cache.51degrees`, named by its folder, and `builder()` offers the cache whatever the settings say, because a loading cache is how a deployment caches rather than a module a document selects. The stock list carries the builder, so every adapter takes the cache from it with `loading_cache_from` and names no crate, and a test holds the list to offering one cache that keeps what it loads. The crate carries the maintainers table, the workspace lists it, and the Axum aliases lint and test it, where its own tests run natively. The crate's tests ask for one key from 16 callers at once on 16 threads with the pool, on 16 threads with loads in the caller, and on one thread with each load a task of its own, and the source runs once each time. Other tests cover a failure reaching every waiter and not being kept, the store answering a second process, the brief memory copy, idle expiry, a value's own lifetime, a reserving value cache being filled and released, two publishers kept apart, removal, the module's name and the builder's offer.
Spin's key-value store refuses a write with a lifetime, so a robots.txt answer kept for a later failure, and a loading cache entry in the module's key-value layer, are written without one there, as the held answer was before. The entry format carries each entry's own drop time, so a copy in a store that keeps no lifetimes still ends when it should, and a kept answer stays until it is replaced, which is what the configuration guide says of Spin. The robots.txt load carries the name the contributor was declared under as the static text the module context takes, and the handler hands it over from the selection. Tests cover the kept answer's lifetime where the store takes one, the answer kept where it refuses one, and a store that refuses lifetimes still answering a second process through the module.
The configuration guide gains "Loading caches" under the cache section, saying what is held, where the two copies live, how the keys start, which crate a stock build caches with and the `[cache.robots-txt]` lifetimes, and its robots.txt section says each answer is held through it. The integration guide gains "Supplying a loading cache", being how a crate offers one with `with_loading_cache` and how code loads through one with `load_cached`, a `CacheSpec`, a `StoreKey` and a `Loader`, and the builder table lists the offer. The example configuration shows `[cache.robots-txt]`, and the changelog records the cache, the lifetimes, the key prefix and the value cache under Unreleased.
The crate keeps a load running with the request's `wait_until` only where the Worker's handling of the request is wrapped in `scope_request`, and nothing on an adapter wraps it, so the crate's documentation and the changelog say so rather than that the load keeps running after the response on Cloudflare.
The item exists only on the Cloudflare target, so a link to it from the crate's documentation is reported broken on every other target's documentation build. The name is written as plain code instead.
The first build adds the published fiftyone-caching crate, hashlink and tokio-util to the lock, which a build with --locked refuses until they are committed.
A feature that holds answers in a loading cache names, in its own `[cache.<name>]` table, the module whose cache holds them, as the module's short name or its name in full. `loading_caches_from` resolves every feature's selection against the caches the linked builders offer, makes each module's cache once however many features select it, and refuses a name no builder offers a cache from, naming the feature and the modules that do. The services carry the selections as `LoadingCaches`, and a feature that selected none loads through `NoLoadingCache`. Before this the stock build's one offered cache held every feature's copies, and a deployment linking two caches was refused. Now each feature decides, so the asset cache and any later feature can choose a cache of its own. `[cache.robots-txt] module = "51degrees"` selects the stock cache for the robots.txt modules' answers. The guides, the example configuration and the changelog say so, and the adapters pass the selections where they passed the one cache.
Core's own middleware `robots_txt`, in `robots_txt/middleware.rs`, makes the file `[robots-txt]` describes where a `[[fetch]]` or `[[serve]]` entry for `text/plain` on `/robots.txt` names it. Its preparation does what the route's handler did, a refusal of every crawler from nothing and otherwise every selected contributor's held answer assembled, and answers a refusal with `503` and `Retry-After` when a contributor fails with nothing held, so nothing is stored and no empty file is served. Its action replaces whatever the origin sent with the file, and the creating rule makes the file where the origin has none. The registry makes the middleware itself once every registration is in, because the contributors it asks are known only then and a middleware cannot reach the registry. The settings refuse a `[robots-txt]` section that no entry places, because the file would never be made. The route goes from the four adapters, with `handle_robots_txt`, the response builder and the route's own cache lifetime. The file takes the deployment's cache policy for the path, as every file does. The adapters' route tests go with it, and each now proves the path is no route of this server; the file itself is proven through the publisher path in core. The stock list carries the middleware on `text/plain`, and the example configuration places it.
The configuration guide's robots.txt section says the file is made by the `robots_txt` middleware where an entry names it, in place of the origin's file or where the origin has none, that a section no entry places is refused, and that the response takes the deployment's cache policy for the path. Its example carries the entry, and the placing table has the middleware's row. The changelog records the middleware and the route's removal.
`[shared.<vendor>]` holds what every module of one vendor reads, such as an endpoint and a license key, so the key is written once. Core declares the tables and reads none of them. A vendor's crates read their own as typed settings through `Settings::shared_config`, which refuses a table that cannot be read as the type or fails its validation, naming the table. A table whose name is not a folder name is refused when the settings load. A builder marked with `with_shared_settings` is run by its vendor's table rather than by a selection, so a document without the table runs none of that vendor's modules, and a selection naming one of them is refused with the absent table named. A table that no builder reads is refused when the registry is built, the one place every builder is known. A secret in the table is declared with the builder's secret settings at its path inside the table and resolved when the table is present, as a module's own secrets are. The seam probe gains a second builder its table runs, whose route answers the table's marker, so the Axum adapter proves the table from outside core.
The configuration guide lists `[shared]` among the sections and describes the tables, what runs a builder, the refusals and how a secret in a table is written. The integration guide's builder table gains `.with_shared_settings(...)` and a section on reading the table as typed settings. The example configuration notes the table beside its list of sections, and the changelog records the addition.
Core gains a prefix module, `PrefixModule`, which gives two things for the publisher a request resolved to: a public prefix that starts every name the browser holds, being cookies and first-party storage keys, and a server key prefix that starts every key in a store publishers share. Core's own module gives `ts` and `ts.<publisher.domain>` and runs when `[prefix] module` selects nothing, so a stock build's names are what they were. The registry resolves the selection as it does geo and device, refuses a name nothing this deployment runs supplies, and asks the module once when the registry is built, so a module naming a value its call never carries is refused at startup. Every adapter puts the resolved prefixes on the request's services, and the Fastly entry point keeps what its request resolved to for the finishing steps that run after the request has been handed to the router. The robots.txt holding and the loading cache take the prefix from the request's services rather than from the settings.
The cookie constants lose their fixed `ts-` head and name the entry alone, `ec`, `ecr`, `ec-pull-complete`, `eids` and `tester`, and every read and write names the cookie through the request's public prefix, so a deployment whose module gives `_ex` reads and writes `_ex-ec`. The Edge Cookie state carries the prefix it was read under, and a module's response header naming a cookie under that prefix is refused as a cookie under `ts-` was. The page's bundle tag tells the client script the prefix, as `data-ts-public-prefix`, so the script names its own cookies after it and holds no copy. The template fingerprint includes the prefix, so a page stored under one is never served under another, and a key cookie naming one of the request's own identity cookies shares no template.
A prefix's first part starts with a letter or a single `_`, so no cookie name starts with `__`, where browsers give names such as `__Host-` rules of their own and drop a cookie that breaks them. The Edge Cookie test reads and writes under a prefix carrying a publisher's name as well, and a test holds the client script to the attribute the server writes.
# Conflicts: # docs/guide/configuration.md
# Conflicts: # docs/guide/configuration.md
# 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.
jwrosewell
marked this pull request as ready for review
October 14, 2026 12:36
Collaborator
|
Moved into this repository as #1371, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record. |
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.
Stacks on sizes by viewport width (#1317). The additions that follow the seven pull requests open here are listed in #777.
Compare
split/29-viewport-sizeswithsplit/30-google-tagto see only this pull request's change.
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: 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: 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
CI passed on
0831b7e5eon the fork. Not exercised: Google's own handling of the snippets. The tests hold the snippets to the text Google publishes and prove where each lands on the page.References #1288.