Skip to content

Commit 54a0f3d

Browse files
Improve Prebid auction diagnostics (#671)
1 parent 26e0137 commit 54a0f3d

5 files changed

Lines changed: 409 additions & 61 deletions

File tree

crates/js/lib/build-all.mjs

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* TSJS_PREBID_ADAPTERS — Comma-separated list of Prebid.js bid adapter
1313
* names to include in the bundle (e.g. "rubicon,appnexus,openx").
1414
* Each name must have a corresponding {name}BidAdapter.js module in
15-
* the prebid.js package. Default: "rubicon".
15+
* the prebid.js package. Default: no adapters.
1616
*
1717
* TSJS_PREBID_USER_ID_MODULES — Ignored for production builds. User ID
1818
* modules are selected from src/integrations/prebid/user_id_modules.json
@@ -37,7 +37,8 @@ const integrationsDir = path.join(srcDir, 'integrations');
3737
// Prebid adapter generation
3838
// ---------------------------------------------------------------------------
3939

40-
const DEFAULT_PREBID_ADAPTERS = 'rubicon';
40+
const DEFAULT_PREBID_ADAPTERS = '';
41+
const DEFAULT_PREBID_ADAPTERS_DESCRIPTION = DEFAULT_PREBID_ADAPTERS || 'no adapters';
4142
const ADAPTERS_FILE = path.join(integrationsDir, 'prebid', '_adapters.generated.ts');
4243
const USER_IDS_FILE = path.join(integrationsDir, 'prebid', '_user_ids.generated.ts');
4344

@@ -69,20 +70,12 @@ const LIVE_INTENT_SHIM = path.join(
6970
* logged and skipped.
7071
*/
7172
function generatePrebidAdapters() {
72-
const raw = process.env.TSJS_PREBID_ADAPTERS || DEFAULT_PREBID_ADAPTERS;
73+
const raw = process.env.TSJS_PREBID_ADAPTERS ?? DEFAULT_PREBID_ADAPTERS;
7374
const names = raw
7475
.split(',')
7576
.map((s) => s.trim())
7677
.filter(Boolean);
7778

78-
if (names.length === 0) {
79-
console.warn(
80-
'[build-all] TSJS_PREBID_ADAPTERS is empty, falling back to default:',
81-
DEFAULT_PREBID_ADAPTERS
82-
);
83-
names.push(DEFAULT_PREBID_ADAPTERS);
84-
}
85-
8679
const modulesDir = path.join(__dirname, 'node_modules', 'prebid.js', 'modules');
8780

8881
// Validate each adapter and build import lines
@@ -100,22 +93,26 @@ function generatePrebidAdapters() {
10093
}
10194

10295
if (imports.length === 0) {
103-
console.error(
104-
'[build-all] WARNING: No valid Prebid adapters found, bundle will have no client-side adapters'
105-
);
96+
if (names.length === 0) {
97+
console.log(
98+
'[build-all] No Prebid adapters configured; bundle will have no client-side adapters'
99+
);
100+
} else {
101+
console.error(
102+
'[build-all] WARNING: No valid Prebid adapters found, bundle will have no client-side adapters'
103+
);
104+
}
106105
}
107106

108-
const content = [
107+
const header = [
109108
'// Auto-generated by build-all.mjs — manual edits will be overwritten at build time.',
110109
'//',
111110
'// Controls which Prebid.js bid adapters are included in the bundle.',
112111
'// Set the TSJS_PREBID_ADAPTERS environment variable to a comma-separated list',
113112
'// of adapter names (e.g. "rubicon,appnexus,openx") before building.',
114-
`// Default: "${DEFAULT_PREBID_ADAPTERS}"`,
115-
'',
116-
...imports,
117-
'',
113+
`// Default: ${DEFAULT_PREBID_ADAPTERS_DESCRIPTION}`,
118114
].join('\n');
115+
const content = imports.length === 0 ? `${header}\n` : `${header}\n\n${imports.join('\n')}\n`;
119116

120117
fs.writeFileSync(ADAPTERS_FILE, content);
121118

crates/js/lib/src/integrations/prebid/_adapters.generated.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,4 @@
33
// Controls which Prebid.js bid adapters are included in the bundle.
44
// Set the TSJS_PREBID_ADAPTERS environment variable to a comma-separated list
55
// of adapter names (e.g. "rubicon,appnexus,openx") before building.
6-
// Default: "rubicon"
7-
8-
import 'prebid.js/modules/rubiconBidAdapter.js';
6+
// Default: no adapters

crates/trusted-server-core/src/auction/orchestrator.rs

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,44 @@ use super::config::AuctionConfig;
1212
use super::provider::AuctionProvider;
1313
use super::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, BidStatus};
1414

15+
const PROVIDER_ERROR_MESSAGE_CHARS: usize = 500;
16+
17+
pub(crate) const ERROR_TYPE_HTTP_STATUS: &str = "http_status";
18+
const ERROR_TYPE_PARSE_RESPONSE: &str = "parse_response";
19+
const ERROR_TYPE_LAUNCH_FAILED: &str = "launch_failed";
20+
21+
// SECURITY: the returned string is included verbatim (truncated to
22+
// PROVIDER_ERROR_MESSAGE_CHARS) in the public /auction response via
23+
// ProviderSummary.metadata["message"]. Providers MUST NOT interpolate
24+
// upstream-controlled content (response bodies, parse errors, headers) into
25+
// their TrustedServerError::*.message fields. Use static text and log details
26+
// server-side with `log::warn!` instead.
27+
fn provider_error_message(error: &Report<TrustedServerError>) -> String {
28+
error
29+
.current_context()
30+
.to_string()
31+
.chars()
32+
.take(PROVIDER_ERROR_MESSAGE_CHARS)
33+
.collect()
34+
}
35+
36+
fn provider_error_response(
37+
provider_name: &str,
38+
response_time_ms: u64,
39+
error_type: &str,
40+
error: &Report<TrustedServerError>,
41+
) -> AuctionResponse {
42+
AuctionResponse::error(provider_name, response_time_ms)
43+
.with_metadata("error_type", serde_json::json!(error_type))
44+
.with_metadata("message", serde_json::json!(provider_error_message(error)))
45+
}
46+
47+
fn provider_launch_failed_response(provider_name: &str, response_time_ms: u64) -> AuctionResponse {
48+
AuctionResponse::error(provider_name, response_time_ms)
49+
.with_metadata("error_type", serde_json::json!(ERROR_TYPE_LAUNCH_FAILED))
50+
.with_metadata("message", serde_json::json!("Provider launch failed"))
51+
}
52+
1553
/// Compute the remaining time budget from a deadline.
1654
///
1755
/// Returns the number of milliseconds left before `timeout_ms` is exceeded,
@@ -252,6 +290,7 @@ impl AuctionOrchestrator {
252290
let mut backend_to_provider: HashMap<String, (&str, Instant, &dyn AuctionProvider)> =
253291
HashMap::new();
254292
let mut pending_requests: Vec<PendingRequest> = Vec::new();
293+
let mut responses = Vec::new();
255294

256295
for provider_name in provider_names {
257296
let provider = match self.providers.get(provider_name) {
@@ -331,11 +370,16 @@ impl AuctionOrchestrator {
331370
);
332371
}
333372
Err(e) => {
373+
let response_time_ms = start_time.elapsed().as_millis() as u64;
334374
log::warn!(
335375
"Provider '{}' failed to launch request: {:?}",
336376
provider.provider_name(),
337377
e
338378
);
379+
responses.push(provider_launch_failed_response(
380+
provider.provider_name(),
381+
response_time_ms,
382+
));
339383
}
340384
}
341385
}
@@ -357,7 +401,6 @@ impl AuctionOrchestrator {
357401
// transport timeout fires). Hard deadline enforcement therefore depends
358402
// on every backend's `first_byte_timeout` being set to at most the
359403
// remaining auction budget — which Phase 1 above guarantees.
360-
let mut responses = Vec::new();
361404
let mut remaining = pending_requests;
362405

363406
while !remaining.is_empty() {
@@ -397,8 +440,12 @@ impl AuctionOrchestrator {
397440
provider_name,
398441
e
399442
);
400-
responses
401-
.push(AuctionResponse::error(provider_name, response_time_ms));
443+
responses.push(provider_error_response(
444+
provider_name,
445+
response_time_ms,
446+
ERROR_TYPE_PARSE_RESPONSE,
447+
&e,
448+
));
402449
}
403450
}
404451
} else {
@@ -602,9 +649,11 @@ mod tests {
602649
use crate::auction::config::AuctionConfig;
603650
use crate::auction::test_support::create_test_auction_context;
604651
use crate::auction::types::{
605-
AdFormat, AdSlot, AuctionRequest, Bid, MediaType, PublisherInfo, UserInfo,
652+
AdFormat, AdSlot, AuctionRequest, Bid, BidStatus, MediaType, PublisherInfo, UserInfo,
606653
};
654+
use crate::error::TrustedServerError;
607655
use crate::test_support::tests::crate_test_settings_str;
656+
use error_stack::Report;
608657
use fastly::Request;
609658
use std::collections::{HashMap, HashSet};
610659

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

709+
#[test]
710+
fn provider_error_response_includes_diagnostic_metadata() {
711+
let error = Report::new(TrustedServerError::Auction {
712+
message: "parse failed".to_string(),
713+
})
714+
.attach("internal/source.rs:12:34");
715+
716+
let response =
717+
super::provider_error_response("prebid", 37, super::ERROR_TYPE_PARSE_RESPONSE, &error);
718+
719+
assert_eq!(
720+
response.status,
721+
BidStatus::Error,
722+
"should mark diagnostic provider responses as errors"
723+
);
724+
assert_eq!(
725+
response.metadata["error_type"],
726+
serde_json::json!("parse_response"),
727+
"should include the provider error classification"
728+
);
729+
730+
let message = response.metadata["message"]
731+
.as_str()
732+
.expect("should include provider error message");
733+
assert!(
734+
message.contains("parse failed"),
735+
"should include user-safe diagnostic detail"
736+
);
737+
assert!(
738+
!message.contains("internal/source.rs"),
739+
"should not include attached internal details"
740+
);
741+
}
742+
743+
#[test]
744+
fn launch_failed_response_has_safe_static_message() {
745+
let response = super::provider_launch_failed_response("prebid", 58);
746+
747+
assert_eq!(
748+
response.status,
749+
BidStatus::Error,
750+
"should mark launch failures as errors"
751+
);
752+
assert_eq!(
753+
response.metadata["error_type"],
754+
serde_json::json!("launch_failed"),
755+
"should include launch_failed classification"
756+
);
757+
assert_eq!(
758+
response.metadata["message"],
759+
serde_json::json!("Provider launch failed"),
760+
"should use a safe, stable public launch failure message"
761+
);
762+
}
763+
764+
#[test]
765+
fn provider_error_message_truncates_user_safe_context() {
766+
let long_message = "x".repeat(super::PROVIDER_ERROR_MESSAGE_CHARS + 100);
767+
let error = Report::new(TrustedServerError::Auction {
768+
message: long_message,
769+
});
770+
771+
let message = super::provider_error_message(&error);
772+
773+
assert_eq!(
774+
message.chars().count(),
775+
super::PROVIDER_ERROR_MESSAGE_CHARS,
776+
"should cap provider error messages"
777+
);
778+
assert!(
779+
message.starts_with("Auction error: "),
780+
"should preserve the current context display text"
781+
);
782+
}
783+
660784
#[test]
661785
fn filters_winning_bids_below_floor() {
662786
let orchestrator = AuctionOrchestrator::new(AuctionConfig::default());

0 commit comments

Comments
 (0)