New bid adapter: Adswag - #15487
Conversation
|
This PR introduces changes that may not work on all browsers. According to Babel, the following polyfills may be needed, and they are not automatically included:
The best way to address this is to provide good test coverage, as normal PR checks run unit tests on older browsers. |
There was a problem hiding this comment.
Pull request overview
Adds the new Adswag bidder adapter to Prebid.js, implementing OpenRTB request/response mapping for banner, video, and (feature-gated) audio, plus consent-aware identity forwarding, user syncs, and lifecycle hooks.
Changes:
- Introduces
modules/adswagBidAdapter.tsimplementing bid validation, request construction, response interpretation, user syncs, and win-notice firing. - Adds a comprehensive unit test suite in
test/spec/modules/adswagBidAdapter_spec.jscovering validation, request mapping, consent/identity behavior, and response mapping. - Adds adapter documentation in
modules/adswagBidAdapter.mdwith parameters, test setup, and GDPR/TCF notes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| test/spec/modules/adswagBidAdapter_spec.js | Adds unit tests validating request/response mapping, consent gating, identity waterfall, and lifecycle callbacks. |
| modules/adswagBidAdapter.ts | Implements the Adswag bidder adapter (OpenRTB 2.6 POST) with consent-aware identity handling, mixed-format imps, user syncs, and win notice firing. |
| modules/adswagBidAdapter.md | Documents the Adswag adapter’s purpose, bid parameters, test parameters, user sync behavior, and GDPR/TCF posture. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| expect(body.user.ext.consent).to.equal("C"); | ||
| expect(body.user.eids).to.be.undefined; | ||
| expect(body.user.eids).to.be.undefined; | ||
| }); |
There was a problem hiding this comment.
Fixed in f87fa6e — duplicate assertion removed.
| const cpm = typeof bid.cpm === "number" && isFinite(bid.cpm) ? String(bid.cpm) : ""; | ||
| // Regex literal rather than a plain string: upstream eslint's | ||
| // no-template-curly-in-string flags "${...}" inside string literals. | ||
| triggerPixel(bid.burl.replace(/\$\{AUCTION_PRICE\}/, cpm)); | ||
| } catch (e) { |
There was a problem hiding this comment.
Fixed in f87fa6e — switched to a global regex so every ${AUCTION_PRICE} occurrence in burl is expanded; added a spec covering a burl with multiple occurrences.
| // as its Device Storage Disclosure URL; consumed by Prebid's metadata | ||
| // pipeline. | ||
| disclosureURL: "https://content.adswag.ai/iab/vendorjson.json", | ||
| supportedMediaTypes: [BANNER, VIDEO, AUDIO], | ||
| isBidRequestValid, |
There was a problem hiding this comment.
Fixed in f87fa6e — supportedMediaTypes now declares AUDIO only when FEATURES.AUDIO is enabled (a compile-time constant in real builds), so audio-less builds advertise exactly what they can bid on, matching the existing audioEnabled() gating in validation and imp building.
| | Name | Scope | Type | Description | Example | | ||
| |---------------|----------|--------|-----------------------------------------------------------------------------------------------|-----------------------| | ||
| | `publisherId` | required | String | Adswag publisher id (issued at onboarding). Resolves the canonical publisher at the edge. | `"pub-nl-news-1"` | | ||
| | `placementId` | optional | String | Explicit placement override. Omit to let Adswag discover the placement from GPID/adUnitCode. | `"plc-homepage-mrec"` | | ||
| | `bidFloor` | optional | Number | Static floor (EUR) used only when the Prebid Price Floors module is not configured. | `0.50` | | ||
| | `video` | optional | Object | Overrides for `mediaTypes.video` ad-unit params (Prebid video-params convention). | `{ maxduration: 15 }` | | ||
|
|
There was a problem hiding this comment.
Fixed in f87fa6e — documented endpoint in the Bid Parameters table plus a paragraph with the exact constraint the code enforces: the override (per-bid params.endpoint or global pbjs.setConfig({ adswag: { endpoint } })) is honored only for hosts on the adswag.ai domain (adswag.ai or *.adswag.ai); any other host is ignored and the request goes to the built-in production endpoint. It exists for Adswag-operated test/staging environments only — publishers never need to set it.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3dca93ad12
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const size = | ||
| (isAudio ? null : isVideo ? req.videoSize : req.bannerSize) || | ||
| [undefined, undefined]; |
There was a problem hiding this comment.
Use response dimensions before backfilling size
On multi-size banner impressions this ignores the OpenRTB bid's w/h and always backfills the first requested size from imp.banner.format. If the server wins a later size such as 300x600 after a request that also included 300x250, Prebid will emit the wrong width/height and hb_size, which can route the win to a mismatched ad-server creative; use b.w/b.h when present before falling back to the requested size.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f87fa6e — interpretResponse now prefers the response's explicit bid.w/bid.h (positive integers) and only backfills from the requested primary size when they are absent. For context: the server currently omits w/h on all bids (verified against production today), so the backfill remains the live path — but the adapter now honors explicit dimensions whenever the server starts setting them. Added specs for the multi-size preference and the malformed-dimensions fallback.
| const res = bid.getFloor({ | ||
| currency: DEFAULT_CURRENCY, | ||
| mediaType, | ||
| size: sizes.length === 1 ? sizes[0] : "*", |
There was a problem hiding this comment.
Pass video playerSize to getFloor
When the requestable media type is video, the sizes argument is still only getBannerSizes(bid), so video-only bids call the floors module with size: '*' instead of the mediaTypes.video.playerSize. Publishers with video floor rules keyed to 640x360 or 640x480 can therefore get a lower/default floor in imp.bidfloor; pass the video player size into getFloor for video bids.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f87fa6e — video-only impressions now call getFloor with the playerSize; audio deliberately keeps size: '*' (audio is a sizeless media type), and mixed-format units keep '*' as before. Specs updated to assert the size passed per media type.
| typeof navigator !== "undefined" && | ||
| navigator && | ||
| isNonEmptyStr(navigator.userAgent) | ||
| ) { | ||
| return navigator.userAgent; |
There was a problem hiding this comment.
Use shared UA access instead of navigator
When ortb2.device.ua is absent, this new vendor adapter reads navigator.userAgent directly. The repo review rules require vendor modules to go through common libraries or methods for navigator access, so this should use the existing UA utility/core-enriched device path instead of introducing a direct navigator read.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f87fa6e — dropped the direct navigator.userAgent fallback entirely. The adapter now passes core-enriched ortb2.device through verbatim; core's FPD enrichment always populates device.ua (plus sua/w/h/language), so the fallback was redundant as well as against the repo rules.
| const cpm = typeof bid.cpm === "number" && isFinite(bid.cpm) ? String(bid.cpm) : ""; | ||
| // Regex literal rather than a plain string: upstream eslint's | ||
| // no-template-curly-in-string flags "${...}" inside string literals. | ||
| triggerPixel(bid.burl.replace(/\$\{AUCTION_PRICE\}/, cpm)); |
There was a problem hiding this comment.
Send win notices with keepalive
On display wins, this fires the win notice with an Image pixel. The adapter comments say this notice is the only way display wins are observed, so navigations or page teardown can undercount wins; repo guidance asks low-priority calls to use ajax/fetch with keepalive instead of triggerPixel when avoidable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f87fa6e — the win notice now fires through ajax from src/ajax.js as a GET with keepalive: true (via a small exported dep indirection so the spec can stub the transport), replacing the Image pixel. The notice now survives page teardown/navigation.
8015a99 to
a3637cc
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
test/spec/modules/adswagBidAdapter_spec.js:379
- Duplicate assertion:
expect(body.user.eids).to.be.undefined;is repeated twice, which is redundant and can be removed to keep the test concise.
expect(body.user.ext.consent).to.equal("C");
expect(body.user.eids).to.be.undefined;
expect(body.user.eids).to.be.undefined;
modules/adswagBidAdapter.ts:468
buildImp()always resolves floors using bannersizes, even for video-only requests. This meansbid.getFloor()will receivesize: '*'instead of the ad unit'splayerSize, so publishers configuring floors by video size may get an incorrect / missingbidflooron video imps. Consider passing the video player size when the imp is video-only.
const floorMediaType = types.length === 1 ? types[0] : "*";
const floor = resolveFloor(bid, sizes, floorMediaType);
if (floor) {
a3637cc to
f87fa6e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
modules/adswagBidAdapter.ts:412
- Forward the Price Floors result without rounding it to two decimals. The floors module deliberately rounds up to four decimals (
modules/priceFloors.ts:368-371); for example, this converts1.3334to1.33, weakening the publisher's configured floor and allowing below-floor bids.
return { floor: round2(res.floor), currency: res.currency || DEFAULT_CURRENCY };
modules/adswagBidAdapter.ts:635
- The linked documentation PR states that publisher
ortb2.userFPD objects are forwarded, but this code only readsuser.eids/user.ext.eids; fields such asuser.data,keywords, or other non-EID FPD are dropped. Either merge the permitted user FPD into the request (while retaining the consent gates for identifiers) or correct the documentation before merge.
for (const list of [
deepAccess(validBidRequests[0], "userIdAsEids"),
deepAccess(ortb2, "user.ext.eids"),
deepAccess(ortb2, "user.eids"),
]) {
modules/adswagBidAdapter.ts:390
- The documented
pbjs.setConfig({ adswag: { endpoint } })namespace has no TypeScriptConfigaugmentation, which is why this value remainsunknownand needs a cast. Add anAdswagConfigtype and augment../src/config, followingmodules/yaleoBidAdapter.ts:38-42, so this public configuration surface is typed consistently with repository guidance.
// Cast: Prebid core types getConfig as unknown for custom namespaces.
const cfg = config.getConfig("adswag") as { endpoint?: unknown } | undefined;
if (cfg && isNonEmptyStr(cfg.endpoint) && isPermittedEndpoint(cfg.endpoint)) {
return cfg.endpoint;
| // the publisher never opted into). All bids ride one request/URL, so | ||
| // resolving once against bids[0] mirrors the url assignment below. | ||
| const endpoint = resolveEndpoint(bids[0]); |
There was a problem hiding this comment.
Fixed in 658c5d6 — good catch, and it was a money bug: with two ad units under different publisherId every impression was reported under whichever publisher account came first, so the wrong publisher would be credited.
buildRequests now groups the biddable bids by (resolved endpoint, publisherId) and emits one ServerRequest per group. Per group: its own imp array, its own site.publisher.id, its own url, and its own client-side bidRequests meta array so interpretResponse still resolves media types and backfills sizes from the right ad units. Everything else (device / consent / identity / schain / source.tid / tmax) is request-level and is applied to every group.
Request ids: Prebid hands the adapter a single bidderRequestId, so the first group keeps it — the single-group case, which is nearly all real traffic, is byte-identical to before — and each further group gets a fresh generateUUID(), since two concurrent requests must not claim the same OpenRTB request id. source.tid is unchanged on every group and remains the shared auction correlator, so the differing ids cost nothing in tracing. The rule is commented at the call site.
Also made the no-endpoint no-bid per group rather than global: a bid whose endpoint does not resolve now drops on its own instead of silencing every sibling Adswag unit on the page, which is the correct reading of fail-open (per ad unit, not per page).
Specs added for two ad units under different publisherId asserting two ServerRequests with the correct per-group site.publisher.id and disjoint imp ids, unique per-group request ids, request-level fields present on every group, same-publisher units staying in one request, splitting on a differing endpoint, the per-group meta array, interpretResponse resolving each group against its own request, and the sibling-survival case. All verified failing against the pre-fix module. Suite is 126/126.
f87fa6e to
658c5d6
Compare
|
Round-2 follow-up for the three suppressed comments (no inline threads, so addressing each by file:line). All fixed or answered in 658c5d6; the inline comment on
I also removed the rounding from the static
The docs page now states plainly that
Verification for all of the above: One note in the interest of not overclaiming: the multi-publisher grouping from the inline comment is proven by unit tests, not by a live bid. Our documented evergreen test placement is seeded with a single publisher, so a two-publisher page is not something I can demonstrate against production; the test params in |
d3f56a1 to
63bb30b
Compare
Adds the Adswag bid adapter (banner, video, audio) for directly-integrated European supply. GVL vendor 1417; EU-hosted endpoint (bid.adswag.ai). Includes bidder docs and unit tests.
63bb30b to
d575e2e
Compare
|
Pushed one more change, Outstream was the gap in this adapter — an in-article slot has no player, so a winning video bid there quietly did not render unless the publisher had brought their own. The adapter now installs a renderer on outstream video wins only. Nothing is downloaded unless such a bid wins, so page weight on every other auction is unchanged, and there are no new dependencies. The script is served from our own CDN. Publisher renderers keep precedence as usual, with one deliberate nuance: a renderer declaring One thing worth flagging since it is visible in the diff: the renderer URL is a channel alias rather than a version-pinned path. That is deliberate — a pinned player version here would mean every player-side fix needs a new Prebid PR and release to reach publishers. The path segment versions the adapter-to-renderer contract, so a breaking change on our side becomes a new path and a new PR here, which is the part that should be slow. Also included: an "Outstream Video" section in The docs PR (prebid/prebid.github.io#6702) is updated with the matching outstream note. Note on the three red checks that came up after this push, so nobody has to dig: two are GitHub infrastructure ( |
|
The three failing checks on the previous run were infrastructure failures from the Aug 17 GitHub incident, not test failures: the Unit ChromeHeadless chunk 3 and Unit EdgeHeadless chunk 3 jobs failed while downloading their GitHub Actions (codeload 429/500s, zero tests ran), and the E2E Firefox job failed at geckodriver session creation before any spec executed. The adapter's banner/instream specs passed wherever a browser session started. I've pushed an empty commit (5850128) to re-run the checks — no code changes. |
Type of change
Description of change
Adds the Adswag bid adapter: banner, video (instream + publisher-rendered outstream) and audio, including mixed-format ad units, for Adswag's directly-integrated European supply. EU-hosted endpoint (
https://bid.adswag.ai/prebid/bid), TCF-first (IAB Europe GVL vendor 1417,gvliddeclared), GPP forwarded, consentless traffic served contextually. DSA transparency supported. The adapter is fail-open in every path: any error degrades to a clean no-bid.Test parameters for validating bids (live, evergreen test campaign — bids €2.50 EUR):
Display test slot must include size 300x250 (the test campaign's display creative size). Video: instream,
mimes: ['video/mp4'], playerSize 640x360. Audio:mediaTypes.audiowithmimes: ['audio/mpeg', 'audio/mp4'].placementIdis optional — the endpoint also discovers the placement from GPID/adUnitCode. Verified against production on 2026-08-13 (single ad units and a mixed banner+video+audio page all return bids).Unit tests: 112 specs for the adapter (
gulp test-only --file test/spec/modules/adswagBidAdapter_spec.js), lint andgulp build --modules=adswagBidAdapterpass. Integration checked with the Hello World sample page against the live endpoint.Other information
Docs PR: prebid/prebid.github.io#6702. Prebid Server (Go and Java) adapter submissions follow separately.