Skip to content
Merged
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
35 changes: 16 additions & 19 deletions crates/js/lib/build-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* 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".
* the prebid.js package. Default: no adapters.
*
* TSJS_PREBID_USER_ID_MODULES — Ignored for production builds. User ID
* modules are selected from src/integrations/prebid/user_id_modules.json
Expand All @@ -37,7 +37,8 @@ const integrationsDir = path.join(srcDir, 'integrations');
// Prebid adapter generation
// ---------------------------------------------------------------------------

const DEFAULT_PREBID_ADAPTERS = 'rubicon';
const DEFAULT_PREBID_ADAPTERS = '';
const DEFAULT_PREBID_ADAPTERS_DESCRIPTION = DEFAULT_PREBID_ADAPTERS || 'no adapters';
const ADAPTERS_FILE = path.join(integrationsDir, 'prebid', '_adapters.generated.ts');
const USER_IDS_FILE = path.join(integrationsDir, 'prebid', '_user_ids.generated.ts');

Expand Down Expand Up @@ -69,20 +70,12 @@ const LIVE_INTENT_SHIM = path.join(
* logged and skipped.
*/
function generatePrebidAdapters() {
const raw = process.env.TSJS_PREBID_ADAPTERS || DEFAULT_PREBID_ADAPTERS;
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
Expand All @@ -100,22 +93,26 @@ function generatePrebidAdapters() {
}

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

const content = [
const header = [
'// 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,
'',
`// Default: ${DEFAULT_PREBID_ADAPTERS_DESCRIPTION}`,
].join('\n');
const content = imports.length === 0 ? `${header}\n` : `${header}\n\n${imports.join('\n')}\n`;

fs.writeFileSync(ADAPTERS_FILE, content);

Expand Down
4 changes: 1 addition & 3 deletions crates/js/lib/src/integrations/prebid/_adapters.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,4 @@
// 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';
// Default: no adapters
132 changes: 128 additions & 4 deletions crates/trusted-server-core/src/auction/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,44 @@ use super::config::AuctionConfig;
use super::provider::AuctionProvider;
use super::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, BidStatus};

const PROVIDER_ERROR_MESSAGE_CHARS: usize = 500;

pub(crate) const ERROR_TYPE_HTTP_STATUS: &str = "http_status";
const ERROR_TYPE_PARSE_RESPONSE: &str = "parse_response";
const ERROR_TYPE_LAUNCH_FAILED: &str = "launch_failed";

// SECURITY: the returned string is included verbatim (truncated to
Comment thread
ChristianPavilonis marked this conversation as resolved.
// PROVIDER_ERROR_MESSAGE_CHARS) in the public /auction response via
// ProviderSummary.metadata["message"]. Providers MUST NOT interpolate
// upstream-controlled content (response bodies, parse errors, headers) into
// their TrustedServerError::*.message fields. Use static text and log details
// server-side with `log::warn!` instead.
fn provider_error_message(error: &Report<TrustedServerError>) -> String {
error
.current_context()
.to_string()
.chars()
.take(PROVIDER_ERROR_MESSAGE_CHARS)
.collect()
}
Comment thread
ChristianPavilonis marked this conversation as resolved.
Comment thread
ChristianPavilonis marked this conversation as resolved.

fn provider_error_response(
provider_name: &str,
response_time_ms: u64,
error_type: &str,
error: &Report<TrustedServerError>,
) -> AuctionResponse {
AuctionResponse::error(provider_name, response_time_ms)
.with_metadata("error_type", serde_json::json!(error_type))
.with_metadata("message", serde_json::json!(provider_error_message(error)))
}

fn provider_launch_failed_response(provider_name: &str, response_time_ms: u64) -> AuctionResponse {
AuctionResponse::error(provider_name, response_time_ms)
.with_metadata("error_type", serde_json::json!(ERROR_TYPE_LAUNCH_FAILED))
.with_metadata("message", serde_json::json!("Provider launch failed"))
Comment thread
ChristianPavilonis marked this conversation as resolved.
}

/// Compute the remaining time budget from a deadline.
///
/// Returns the number of milliseconds left before `timeout_ms` is exceeded,
Expand Down Expand Up @@ -252,6 +290,7 @@ impl AuctionOrchestrator {
let mut backend_to_provider: HashMap<String, (&str, Instant, &dyn AuctionProvider)> =
HashMap::new();
let mut pending_requests: Vec<PendingRequest> = Vec::new();
let mut responses = Vec::new();
Comment thread
ChristianPavilonis marked this conversation as resolved.

for provider_name in provider_names {
let provider = match self.providers.get(provider_name) {
Expand Down Expand Up @@ -331,11 +370,16 @@ impl AuctionOrchestrator {
);
}
Err(e) => {
let response_time_ms = start_time.elapsed().as_millis() as u64;
log::warn!(
"Provider '{}' failed to launch request: {:?}",
provider.provider_name(),
e
);
responses.push(provider_launch_failed_response(
Comment thread
ChristianPavilonis marked this conversation as resolved.
provider.provider_name(),
response_time_ms,
));
}
}
}
Expand All @@ -357,7 +401,6 @@ impl AuctionOrchestrator {
// transport timeout fires). Hard deadline enforcement therefore depends
// on every backend's `first_byte_timeout` being set to at most the
// remaining auction budget — which Phase 1 above guarantees.
let mut responses = Vec::new();
let mut remaining = pending_requests;

while !remaining.is_empty() {
Expand Down Expand Up @@ -397,8 +440,12 @@ impl AuctionOrchestrator {
provider_name,
e
);
responses
.push(AuctionResponse::error(provider_name, response_time_ms));
responses.push(provider_error_response(
provider_name,
response_time_ms,
ERROR_TYPE_PARSE_RESPONSE,
&e,
));
}
}
} else {
Expand Down Expand Up @@ -602,9 +649,11 @@ mod tests {
use crate::auction::config::AuctionConfig;
use crate::auction::test_support::create_test_auction_context;
use crate::auction::types::{
AdFormat, AdSlot, AuctionRequest, Bid, MediaType, PublisherInfo, UserInfo,
AdFormat, AdSlot, AuctionRequest, Bid, BidStatus, MediaType, PublisherInfo, UserInfo,
};
use crate::error::TrustedServerError;
use crate::test_support::tests::crate_test_settings_str;
use error_stack::Report;
use fastly::Request;
use std::collections::{HashMap, HashSet};

Expand Down Expand Up @@ -657,6 +706,81 @@ mod tests {
crate::settings::Settings::from_toml(&settings_str).expect("should parse test settings")
}

#[test]
fn provider_error_response_includes_diagnostic_metadata() {
let error = Report::new(TrustedServerError::Auction {
message: "parse failed".to_string(),
})
.attach("internal/source.rs:12:34");

let response =
super::provider_error_response("prebid", 37, super::ERROR_TYPE_PARSE_RESPONSE, &error);

assert_eq!(
response.status,
BidStatus::Error,
"should mark diagnostic provider responses as errors"
);
assert_eq!(
response.metadata["error_type"],
serde_json::json!("parse_response"),
"should include the provider error classification"
);

let message = response.metadata["message"]
.as_str()
.expect("should include provider error message");
assert!(
message.contains("parse failed"),
"should include user-safe diagnostic detail"
);
assert!(
!message.contains("internal/source.rs"),
"should not include attached internal details"
);
}

#[test]
fn launch_failed_response_has_safe_static_message() {
let response = super::provider_launch_failed_response("prebid", 58);

assert_eq!(
response.status,
BidStatus::Error,
"should mark launch failures as errors"
);
assert_eq!(
response.metadata["error_type"],
serde_json::json!("launch_failed"),
"should include launch_failed classification"
);
assert_eq!(
response.metadata["message"],
serde_json::json!("Provider launch failed"),
"should use a safe, stable public launch failure message"
);
}

#[test]
fn provider_error_message_truncates_user_safe_context() {
let long_message = "x".repeat(super::PROVIDER_ERROR_MESSAGE_CHARS + 100);
let error = Report::new(TrustedServerError::Auction {
message: long_message,
});

let message = super::provider_error_message(&error);

assert_eq!(
message.chars().count(),
super::PROVIDER_ERROR_MESSAGE_CHARS,
"should cap provider error messages"
);
assert!(
message.starts_with("Auction error: "),
"should preserve the current context display text"
);
}

#[test]
fn filters_winning_bids_below_floor() {
let orchestrator = AuctionOrchestrator::new(AuctionConfig::default());
Expand Down
Loading
Loading