Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions crates/common/src/integrations/prebid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ pub struct PrebidIntegrationConfig {
deserialize_with = "crate::settings::vec_from_seq_or_map"
)]
pub script_patterns: Vec<String>,
/// Bidders that should run client-side in the browser via native Prebid.js
/// adapters instead of being routed through the server-side auction.
///
/// These bidders are **not** absorbed into the `trustedServer` adapter and
/// remain as standalone bids in each ad unit. The corresponding Prebid.js
/// adapter modules must be statically imported in the JS bundle so they are
/// available at runtime.
///
/// This list is independent of [`bidders`](Self::bidders) — the operator
/// manages both lists explicitly.
#[serde(default, deserialize_with = "crate::settings::vec_from_seq_or_map")]
pub client_side_bidders: Vec<String>,
/// Per-bidder, per-zone param overrides. The outer key is a bidder name, the
/// inner key is a zone name (sent by the JS adapter from `mediaTypes.banner.name`
/// — a non-standard Prebid.js field used as a temporary workaround),
Expand Down Expand Up @@ -218,6 +230,20 @@ fn build(settings: &Settings) -> Option<Arc<PrebidIntegration>> {
log::warn!("Prebid integration disabled: prebid.server_url missing");
return None;
}

// Warn about bidders that appear in both lists — this is likely a config
// mistake. A bidder should be in either `bidders` (server-side) or
// `client_side_bidders` (browser-side), not both.
for bidder in &config.client_side_bidders {
if config.bidders.iter().any(|b| b == bidder) {
log::warn!(
"prebid: bidder \"{}\" is in both bidders and client_side_bidders — \
it will run server-side AND be left for client-side, which is likely unintended",
bidder
);
}
}

Some(PrebidIntegration::new(config))
}

Expand Down Expand Up @@ -306,13 +332,16 @@ impl IntegrationHeadInjector for PrebidIntegration {
timeout: u32,
debug: bool,
bidders: &'a [String],
#[serde(skip_serializing_if = "<[String]>::is_empty")]
client_side_bidders: &'a [String],
}

let payload = InjectedPrebidClientConfig {
account_id: self.config.account_id.as_deref().unwrap_or_default(),
timeout: self.config.timeout_ms,
debug: self.config.debug,
bidders: &self.config.bidders,
client_side_bidders: &self.config.client_side_bidders,
};

// Escape `</` to prevent breaking out of the script tag.
Expand Down Expand Up @@ -1022,6 +1051,7 @@ mod tests {
test_mode: false,
debug_query_params: None,
script_patterns: default_script_patterns(),
client_side_bidders: Vec::new(),
bid_param_zone_overrides: HashMap::new(),
}
}
Expand Down Expand Up @@ -1477,6 +1507,48 @@ server_url = "https://prebid.example"
);
}

#[test]
fn head_injector_omits_client_side_bidders_when_empty() {
let integration = PrebidIntegration::new(base_config());
let document_state = IntegrationDocumentState::default();
let ctx = IntegrationHtmlContext {
request_host: "pub.example",
request_scheme: "https",
origin_host: "origin.example",
document_state: &document_state,
};

let inserts = integration.head_inserts(&ctx);
let script = &inserts[0];
assert!(
!script.contains("clientSideBidders"),
"should omit clientSideBidders when empty: {}",
script
);
}

#[test]
fn head_injector_includes_client_side_bidders_when_configured() {
let mut config = base_config();
config.client_side_bidders = vec!["rubicon".to_string(), "magnite".to_string()];
let integration = PrebidIntegration::new(config);
let document_state = IntegrationDocumentState::default();
let ctx = IntegrationHtmlContext {
request_host: "pub.example",
request_scheme: "https",
origin_host: "origin.example",
document_state: &document_state,
};

let inserts = integration.head_inserts(&ctx);
let script = &inserts[0];
assert!(
script.contains(r#""clientSideBidders":["rubicon","magnite"]"#),
"should include clientSideBidders array: {}",
script
);
}

#[test]
fn to_openrtb_includes_debug_flags_when_enabled() {
let mut config = base_config();
Expand Down
100 changes: 100 additions & 0 deletions crates/js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
* Output (in ../dist/):
* tsjs-core.js — core API (always included)
* tsjs-<integration>.js — one per discovered integration
*
* Environment variables:
* TSJS_PREBID_ADAPTERS — Comma-separated list of Prebid.js bid adapter
* names to include in the bundle (e.g. "rubicon,appnexus,openx").
* Each name must have a corresponding {name}BidAdapter.js module in
* the prebid.js package. Default: "rubicon".
*/

import fs from 'node:fs';
Expand All @@ -19,6 +25,90 @@ const srcDir = path.resolve(__dirname, 'src');
const distDir = path.resolve(__dirname, '..', 'dist');
const integrationsDir = path.join(srcDir, 'integrations');

// ---------------------------------------------------------------------------
// Prebid adapter generation
// ---------------------------------------------------------------------------

const DEFAULT_PREBID_ADAPTERS = 'rubicon';
const ADAPTERS_FILE = path.join(
integrationsDir,
'prebid',
'_adapters.generated.ts',
);

/**
* Generate `_adapters.generated.ts` with import statements for each adapter
* listed in the TSJS_PREBID_ADAPTERS environment variable.
*
* Invalid adapter names (those without a matching module in prebid.js) are
* logged and skipped.
*/
function generatePrebidAdapters() {
const raw = process.env.TSJS_PREBID_ADAPTERS || DEFAULT_PREBID_ADAPTERS;
const names = raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);

if (names.length === 0) {
console.warn(
'[build-all] TSJS_PREBID_ADAPTERS is empty, falling back to default:',
DEFAULT_PREBID_ADAPTERS,
);
names.push(DEFAULT_PREBID_ADAPTERS);
}

const modulesDir = path.join(
__dirname,
'node_modules',
'prebid.js',
'modules',
);

// Validate each adapter and build import lines
const imports = [];
for (const name of names) {
const moduleFile = `${name}BidAdapter.js`;
const modulePath = path.join(modulesDir, moduleFile);
if (!fs.existsSync(modulePath)) {
console.error(
`[build-all] WARNING: Prebid adapter "${name}" not found (expected ${moduleFile}), skipping`,
);
continue;
}
imports.push(`import 'prebid.js/modules/${moduleFile}';`);
}

if (imports.length === 0) {
console.error(
'[build-all] WARNING: No valid Prebid adapters found, bundle will have no client-side adapters',
);
}

const content = [
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
'//',
'// Controls which Prebid.js bid adapters are included in the bundle.',
'// Set the TSJS_PREBID_ADAPTERS environment variable to a comma-separated list',
'// of adapter names (e.g. "rubicon,appnexus,openx") before building.',
`// Default: "${DEFAULT_PREBID_ADAPTERS}"`,
'',
...imports,
'',
].join('\n');

fs.writeFileSync(ADAPTERS_FILE, content);

const adapterNames = names.filter((name) =>
fs.existsSync(path.join(modulesDir, `${name}BidAdapter.js`)),
);
console.log('[build-all] Prebid adapters:', adapterNames);
}

generatePrebidAdapters();

// ---------------------------------------------------------------------------

// Clean dist directory
fs.rmSync(distDir, { recursive: true, force: true });
fs.mkdirSync(distDir, { recursive: true });
Expand Down Expand Up @@ -47,6 +137,16 @@ async function buildModule(name, entryPath) {
await build({
configFile: false,
root: __dirname,
resolve: {
alias: {
// prebid.js doesn't expose src/adapterManager.js via its package
// "exports" map, but we need it for client-side bidder validation.
'prebid.js/src/adapterManager.js': path.resolve(
__dirname,
'node_modules/prebid.js/dist/src/src/adapterManager.js',
),
},
},
build: {
emptyOutDir: false,
outDir: distDir,
Expand Down
23 changes: 13 additions & 10 deletions crates/js/lib/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/js/lib/src/integrations/prebid/_adapters.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.
//
// Controls which Prebid.js bid adapters are included in the bundle.
// Set the TSJS_PREBID_ADAPTERS environment variable to a comma-separated list
// of adapter names (e.g. "rubicon,appnexus,openx") before building.
// Default: "rubicon"

import 'prebid.js/modules/rubiconBidAdapter.js';
Loading
Loading