Skip to content

Let a format name the viewport widths at which the page asks for it - #1370

Open
jwrosewell wants to merge 48 commits into
feature/28-device-signalsfrom
feature/29-viewport-sizes
Open

jwrosewell wants to merge 48 commits into
feature/28-device-signalsfrom
feature/29-viewport-sizes

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-js/lib 7 files, 891 lines added, 37 removed
crates/trusted-server-core/src 2 files, 287 lines added, 8 removed
crates/ad-tag/google 3 files, 129 lines added, 21 removed
crates/trusted-server-cli/src 1 file, 52 lines added, 1 removed
docs/guide 1 file, 26 lines added, 0 removed
CHANGELOG.md 1 file, 1 line added, 0 removed

Closes

Closes #1287

Spec

Spec: viewport-sizes

Test plan

CI on this pull request at 4404500 passed all 20 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at 4404500

Checklist

  • Feature PRs: spec linked above, ending in a review log of two or more review rounds
  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

Stacks on device signals (#1369).

Kept current. This update only merges the pull request below (#1369), which brings Tech Lab's main at 7aa5c99 and the changes made lower in the stack.

What this pull request does

[[creative_opportunities.slot]]
id = "billboard"
page_patterns = ["/**"]
formats = [
  { width = 970, height = 250, min_viewport_width = 1040 },
  { width = 728, height = 90, min_viewport_width = 800, max_viewport_width = 1039 },
  { width = 300, height = 250, min_viewport_width = 600, max_viewport_width = 799 },
  { width = 320, height = 50, max_viewport_width = 599 },
]

A format may name the viewport widths, in CSS pixels, at which the page asks for it, so a slot asks for a large size only where the page has room for one. Either bound may be left out, and a minimum above a maximum is refused when the settings load. When any format of a slot names a bound, the slot's page data carries viewport_sizes, one entry for each band of widths, widest first. A slot whose formats name no bound asks for every format at every width, as before.

The bands reach two places in the browser. The Google ad tag's head bootstrap and the bundle's slot definition apply them as a size mapping through defineSizeMapping, so the ad server request carries only the band's sizes. The bundle's refresh auction offers a bidder only the sizes of the band the viewport is in, and leaves a slot whose band asks for no size out of the auction. The server-side auction does not filter its sizes and is still offered every format.

A binary released before these fields refuses a format carrying either of them, which fails every request, so the binary that reads them is promoted before any configuration uses them. The configuration guide says so.

Why

A page that shows a billboard on a wide screen and a small banner on a phone has one slot and several formats, and without bounds the slot asks for every format at every width, so a bidder is offered a billboard the page cannot show. The bounds are the publisher's statement of what fits where. The ad tag passes them to the ad server as its size mapping, and the refresh auction holds a bidder to them.

How it was verified

The ad server's own handling of a size mapping is not exercised. The tests prove the mapping is defined with the bands the page data carries, through the bootstrap and the bundle.

References #1287.

jwrosewell and others added 12 commits October 11, 2026 21:02
`CreativeOpportunityFormat` gains optional `min_viewport_width` and
`max_viewport_width`, in CSS pixels. A slot with a bounded format carries
`viewport_sizes` in its page data, one band per width at which the
formats on offer change, widest first, and the Google ad tag's head
bootstrap and the bundle's slot definition apply it as a size mapping
when they define the slot, so a slot asks for a large size only where
the page has room for one. A slot whose formats name no bound behaves
exactly as before, and a server-side auction is still offered every
format. A binary without the fields refuses a document carrying them,
so it is promoted before any configuration uses them.
The bundle's refresh auction built its banner sizes from every format, so on a phone a bidder could win with a billboard the size mapping would not show. It now offers the sizes of the band the viewport is in, and leaves a slot whose band asks for nothing out of the auction. The band shape is stated once, on CreativeOpportunitySlot::viewport_sizes, and the guide drops the paragraph that repeated it.
The head bootstrap and the bundle defined every slot and left an empty band to the size mapping, after the page had painted. No ad can serve at such a width, and GPT sizes a slot's div when it is displayed if the page's style gives it no size, so the only effect was to move the page. Both now define nothing there and release the first-impression claim, so the container keeps the space the page gave it. The band lookup is shared by the bundle's slot definition and its refresh auction, and the recorded page carries the new bootstrap.
…band

The render bridge sized a winning bid that carried no width or height as the slot's first configured format, so on a phone a slot whose band asks for 320x50 rendered a 970x250 frame. It now takes the first size of the band the viewport is in, and falls back to the first format only where the slot names no band or the band asks for nothing. The comment on the server side that names the fallback says the same.
The refresh auction found a slot's page data by element id and offered the sizes of its band whoever defined the slot, so a slot the publisher defined first, which carries no size mapping, asked the ad server for its own sizes while bidders were offered only the band's, or left it out of the auction where the band was empty. The head bootstrap and the bundle now record each GPT slot they give the mapping, and the refresh auction narrows only those. Every other slot is offered its formats, as before the bands.
The head bootstrap and the bundle each define a slot in two places, the adInit pass and the fallback that runs when a publisher's first-impression claim lapses, and only the adInit pass was tested with bands. A test for each fallback now checks that a band with sizes gives the slot its mapping and records it, and that a band with no size defines nothing and releases the claim.
The calls that build and apply a slot's size mapping ran after defineSlot with nothing to catch an exception, so a throw skipped addService and the release of the first-impression claim, and stopped the rest of the adInit pass. The head bootstrap and the bundle now catch it, log a warning and return the slot as defined, asking for every format and not recorded as mapped, so the refresh auction offers it every format too. The claim-expiry fallback test also types its command queue.
`ts config ad-templates --details` printed each format as its size and media type, so a slot whose formats name viewport bounds looked the same as one whose formats name none. Each format now carries any `min_viewport_width` and `max_viewport_width` it names, under those names, and a test pins the output. The comment on `build_ad_slots_script` names `viewport_sizes` among the properties of the page data.
The changelog entry says that no slot is defined where the band asks for no size, that a failed mapping keeps every format, that the refresh auction narrows only a slot defined with the mapping, that a winning bid with no size takes the band's first size and that the details output prints each format's bounds. The guide says that a slot whose band asks for no size is not defined, so its container keeps the space the page's own style gives it.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:23
@jwrosewell
jwrosewell removed this pull request from stack #1422 October 28, 2026 11:47
@jwrosewell
jwrosewell added this pull request to stack #1465 October 28, 2026 18:28

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A slot asks for every format at every viewport width

2 participants