-
Notifications
You must be signed in to change notification settings - Fork 12
Decouple the prebid tsjs shim from the bundled Prebid.js #967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
001ad38
f63f31a
2ddd193
33c0663
637b21f
3c4135c
ba6d4a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,29 +11,56 @@ | |
| // The shim on requestBids injects "trustedServer" into every ad unit so all | ||
| // bids flow through the orchestrator. | ||
|
|
||
| import pbjs from 'prebid.js'; | ||
| import adapterManager from 'prebid.js/src/adapterManager.js'; | ||
| import 'prebid.js/modules/consentManagementTcf.js'; | ||
| import 'prebid.js/modules/consentManagementGpp.js'; | ||
| import 'prebid.js/modules/consentManagementUsp.js'; | ||
| import 'prebid.js/modules/userId.js'; | ||
|
|
||
| // Client-side bid adapters — self-register with prebid.js on import. | ||
| // The external bundle generator aliases these placeholder modules to temporary | ||
| // modules built from its --adapters and --user-id-modules options. When a bidder | ||
| // is listed in `client_side_bidders` in trusted-server.toml, the requestBids | ||
| // shim leaves its bids untouched and the corresponding adapter handles them | ||
| // natively in the browser. | ||
| import './_adapters.generated'; | ||
| import type _pbjsDefault from 'prebid.js'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 praise — This is the right seam. A type-only import plus capturing the head-injected |
||
|
|
||
| import { log } from '../../core/log'; | ||
| import { buildAdRequest, parseAuctionResponse } from '../../core/auction'; | ||
| import type { AuctionBid, AuctionEid } from '../../core/auction'; | ||
| import type { AuctionSlot } from '../../core/types'; | ||
|
|
||
| import { INCLUDED_PREBID_USER_ID_MODULES } from './_user_ids.generated'; | ||
| import { PREBID_USER_ID_MODULE_REGISTRY } from './user_id_modules'; | ||
|
|
||
| /** | ||
| * Prebid.js public API surface (type-only; erased at build time). | ||
| * | ||
| * `getUserIdsAsEids` is added by the userId module at runtime, which the base | ||
| * package typing does not model. | ||
| */ | ||
| type PbjsGlobal = typeof _pbjsDefault & { | ||
| getUserIdsAsEids?: () => unknown[]; | ||
| }; | ||
|
|
||
| // Prebid.js itself is NOT bundled into this module. It is served as the | ||
| // external bundle configured via `integrations.prebid.external_bundle_url` | ||
| // (required whenever the prebid integration is enabled) and owns the | ||
| // `window.pbjs` global. The Rust head injector emits a stub | ||
| // (`window.pbjs = window.pbjs || {que:[],cmd:[]}`) before any script runs and | ||
| // Prebid.js installs its API onto that same object, so capturing the reference | ||
| // at module scope is safe regardless of evaluation order. | ||
| const pbjs: PbjsGlobal = ( | ||
| typeof window !== 'undefined' | ||
| ? // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| ((window as any).pbjs ??= { que: [], cmd: [] }) | ||
| : { que: [], cmd: [] } | ||
| ) as PbjsGlobal; | ||
|
|
||
| /** | ||
| * Manifest stamped on `window.__tsjs_prebid_bundle` by the external Prebid.js | ||
| * bundle (see build-prebid-external.mjs): which client-side bid adapters and | ||
| * user ID modules were compiled into it. | ||
| */ | ||
| interface ExternalPrebidBundleManifest { | ||
| adapters?: string[]; | ||
| userIdModules?: string[]; | ||
| } | ||
|
|
||
| function getExternalBundleManifest(): ExternalPrebidBundleManifest | undefined { | ||
| if (typeof window === 'undefined') { | ||
| return undefined; | ||
| } | ||
| return (window as { __tsjs_prebid_bundle?: ExternalPrebidBundleManifest }).__tsjs_prebid_bundle; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — The manifest is page-controlled state read without shape validation.
A pair of |
||
| } | ||
|
|
||
| const ADAPTER_CODE = 'trustedServer'; | ||
| // OpenRTB permits vendor-specific agent types; PAIR uses 571187. | ||
| // Keep this range aligned with the signed 32-bit Rust/OpenRTB representation. | ||
|
|
@@ -142,18 +169,19 @@ function readConfiguredUserIdNames(): string[] { | |
| } | ||
|
|
||
| function recordUserIdModuleDiagnostics(): PrebidUserIdDiagnostics { | ||
| const includedUserIdModules = getExternalBundleManifest()?.userIdModules ?? []; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ♻️ refactor — User ID diagnostics do not distinguish "no manifest" from "module missing". The adapters check at line 1078 explicitly handles Mirroring the adapters handling keeps the two diagnostics consistent and avoids misleading operators during the bundle rollout. |
||
| const configuredUserIdNames = [...new Set(readConfiguredUserIdNames())].sort(); | ||
| const coveredConfigNames = new Set( | ||
| PREBID_USER_ID_MODULE_REGISTRY.filter((entry) => | ||
| INCLUDED_PREBID_USER_ID_MODULES.includes(entry.moduleName) | ||
| includedUserIdModules.includes(entry.moduleName) | ||
| ).flatMap((entry) => entry.configNames) | ||
| ); | ||
| const missingConfiguredUserIdNames = configuredUserIdNames.filter( | ||
| (name) => !coveredConfigNames.has(name) | ||
| ); | ||
|
|
||
| const diagnostics: PrebidUserIdDiagnostics = { | ||
| includedModules: [...INCLUDED_PREBID_USER_ID_MODULES], | ||
| includedModules: [...includedUserIdModules], | ||
| configuredUserIdNames, | ||
| missingConfiguredUserIdNames, | ||
| }; | ||
|
|
@@ -804,6 +832,18 @@ function collectAuctionEids(): AuctionEid[] | undefined { | |
| * 2. `config` argument — explicit overrides from the publisher's JS | ||
| */ | ||
| export function installPrebidNpm(config?: Partial<PrebidNpmConfig>): typeof pbjs { | ||
| // The prebid integration requires the external Prebid.js bundle | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — No idempotency guard for the rollout window. The deployment note says the currently deployed bundle still carries the old baked-in shim, so running the new server against it installs the adapter twice and wraps const installedFlag = window as { __tsjsPrebidShimInstalled?: boolean };
if (installedFlag.__tsjsPrebidShimInstalled) return pbjs;
installedFlag.__tsjsPrebidShimInstalled = true;That removes the ordering constraint from the deploy runbook entirely. |
||
| // (integrations.prebid.external_bundle_url). When it failed to load (network | ||
| // error, SRI mismatch) window.pbjs is still the head-injected stub with no | ||
| // API — installing the adapter is impossible, so bail out loudly. | ||
| if (typeof (pbjs as { registerBidAdapter?: unknown }).registerBidAdapter !== 'function') { | ||
| log.error( | ||
| '[tsjs-prebid] window.pbjs has no Prebid.js API — the external Prebid bundle ' + | ||
| 'failed to load. Prebid integration disabled.' | ||
| ); | ||
| return pbjs; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 wrench — Bail-out is partial: the refresh handler still installs when the external bundle is missing.
Net effect when the bundle fails to load (network error, SRI mismatch, blocked request): server-side SSAT targeting applied by This failure mode is new to this PR. On Fix — gate the rest of self-init on a successful install: const prebidApiAvailable =
typeof (pbjs as { registerBidAdapter?: unknown }).registerBidAdapter === 'function';
if (typeof window !== 'undefined') {
installPrebidNpm();
if (prebidApiAvailable) {
installRefreshHandler();
// ...user ID module setup
}
} |
||
| } | ||
|
|
||
| publisherAdUnitSnapshots = new Map(); | ||
| pendingPublisherBids = new Map(); | ||
| pendingPublisherCodes = new Map(); | ||
|
|
@@ -1030,24 +1070,27 @@ export function installPrebidNpm(config?: Partial<PrebidNpmConfig>): typeof pbjs | |
| pbjs.processQueue(); | ||
| recordUserIdModuleDiagnostics(); | ||
|
|
||
| // Validate that every client-side bidder has its adapter registered. | ||
| // Adapters self-register on import, so a missing adapter means the bidder | ||
| // was listed in client_side_bidders but not included in the generated | ||
| // external Prebid bundle. Without the adapter the bidder is silently dropped | ||
| // from both server-side and client-side auctions. | ||
| for (const bidder of clientSideBidders) { | ||
| try { | ||
| if (!adapterManager.getBidAdapter(bidder)) { | ||
| // Validate that every client-side bidder has its adapter compiled into the | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 thinking — The external bundle deliberately does not call Before this PR a single artifact made this atomic. Worth considering a watchdog in the generated bundle entry that calls |
||
| // external Prebid.js bundle. The bundle stamps its adapter list on | ||
| // window.__tsjs_prebid_bundle; a missing adapter means the bidder was listed | ||
| // in client_side_bidders but not included in the generated bundle, so it is | ||
| // silently dropped from both server-side and client-side auctions. | ||
| const bundledAdapters = getExternalBundleManifest()?.adapters; | ||
| if (bundledAdapters === undefined) { | ||
| if (clientSideBidders.size > 0) { | ||
| log.warn( | ||
| '[tsjs-prebid] external Prebid bundle did not stamp an adapter manifest; ' + | ||
| 'cannot verify client_side_bidders adapters' | ||
| ); | ||
| } | ||
| } else { | ||
| for (const bidder of clientSideBidders) { | ||
| if (!bundledAdapters.includes(bidder)) { | ||
| log.error( | ||
| `[tsjs-prebid] client-side bidder "${bidder}" has no adapter loaded. ` + | ||
| `Add it to build-prebid-external.mjs --adapters.` | ||
| `[tsjs-prebid] client-side bidder "${bidder}" has no adapter in the external ` + | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⛏ nitpick — This message names the wrong operator surface. The documented flow is |
||
| `Prebid bundle. Add it to build-prebid-external.mjs --adapters.` | ||
| ); | ||
| } | ||
| } catch { | ||
| log.error( | ||
| `[tsjs-prebid] client-side bidder "${bidder}" has no adapter loaded. ` + | ||
| `Add it to build-prebid-external.mjs --adapters.` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
♻️ refactor — Injection order is now a correctness invariant, and it is untested.
Both scripts are
defer, so they execute in document order: the external bundle tag (emitted byhead_inserts) must precede thetsjs-prebiddeferred tag (emitted afterwards byhtml_processor). If that order ever reverses, the shim runs first, finds noregisterBidAdapter, and disables the integration outright — a hard failure with no test to catch it.This test asserts both tags are present but not their relative position. Suggest tightening it: