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
11 changes: 9 additions & 2 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ where
Fut: Future<Output = Result<Response, Report<TrustedServerError>>>,
{
let services = build_runtime_services(&ctx);
let req = ctx.into_request();
let mut req = ctx.into_request();
if let Err(error) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&state.settings,
&mut req,
) {
return Ok(http_error(&error));
}
Ok(handler(state, services, req)
.await
.unwrap_or_else(|e| http_error(&e)))
Expand Down Expand Up @@ -170,8 +176,9 @@ fn build_ec_context(state: &AppState, services: &RuntimeServices, req: &Request)
async fn dispatch_fallback(
state: &AppState,
services: &RuntimeServices,
req: Request,
mut req: Request,
) -> Result<Response, Report<TrustedServerError>> {
trusted_server_core::integrations::gpt_diagnostics::prepare_request(&state.settings, &mut req)?;
let path = req.uri().path().to_string();
let method = req.method().clone();

Expand Down
16 changes: 14 additions & 2 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,13 @@ where
let f = f.clone();
Box::pin(async move {
let services = build_per_request_services(&ctx);
let req = ctx.into_request();
let mut req = ctx.into_request();
if let Err(error) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&s.settings,
&mut req,
) {
return Ok(http_error(&error));
}
Ok(f(s, services, req).await.unwrap_or_else(|e| http_error(&e)))
})
}
Expand Down Expand Up @@ -361,7 +367,13 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
ctx: RequestContext,
) -> Result<Response, EdgeError> {
let services = build_per_request_services(&ctx);
let req = ctx.into_request();
let mut req = ctx.into_request();
if let Err(error) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&state.settings,
&mut req,
) {
return Ok(http_error(&error));
}
let path = req.uri().path().to_owned();
let method = req.method().clone();
// tsjs assets are served for GET only, matching the Axum/Fastly adapters.
Expand Down
14 changes: 14 additions & 0 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,13 @@ async fn execute_named(
return Ok(run_batch_sync(&state, &services, req));
}

if let Err(report) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&state.settings,
&mut req,
) {
return Ok(http_error(&report));
}

let mut ec = build_ec_request_state(&state.settings, &services, &req);
// EcContext creation errors short-circuit before filters, mirroring legacy:
// the legacy path returns its error response before running filter_request.
Expand Down Expand Up @@ -703,6 +710,13 @@ async fn dispatch_fallback(
let path = req.uri().path().to_string();
let method = req.method().clone();

if let Err(report) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&state.settings,
&mut req,
) {
return http_error(&report);
}

let mut ec = build_ec_request_state(&state.settings, services, &req);
if let Some(report) = ec.setup_error.take() {
let response = http_error(&report);
Expand Down
28 changes: 25 additions & 3 deletions crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,15 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
// `NormalizeMiddleware` before this handler runs, so the signed
// OpenRTB metadata that auction signing derives from
// `RequestInfo::from_request` uses the trusted runtime authority.
let req = ctx.into_request();
let mut req = ctx.into_request();
if let Err(error) =
trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&s.settings,
&mut req,
)
{
return Ok(http_error(&error));
}
// Build the geo-aware EC context so the auction consent gate sees
// the caller's jurisdiction — `EcContext::default()` fails it
// closed for consented users.
Expand All @@ -549,7 +557,15 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
let s = Arc::clone(&s);
async move {
let services = build_runtime_services(&ctx);
let req = ctx.into_request();
let mut req = ctx.into_request();
if let Err(error) =
trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&s.settings,
&mut req,
)
{
return Ok(http_error(&error));
}
let ec_context = build_ec_context(&s.settings, &services, &req);
let auction = AuctionDispatch {
orchestrator: &s.orchestrator,
Expand Down Expand Up @@ -631,7 +647,13 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
ctx: RequestContext,
) -> Result<Response, EdgeError> {
let services = build_runtime_services(&ctx);
let req = ctx.into_request();
let mut req = ctx.into_request();
if let Err(error) = trusted_server_core::integrations::gpt_diagnostics::prepare_request(
&state.settings,
&mut req,
) {
return Ok(http_error(&error));
}

let path = req.uri().path().to_owned();
let method = req.method().clone();
Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-core/benches/html_processor_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fn make_config() -> HtmlProcessorConfig {
ad_slots_script: None,
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
}
}

Expand Down
7 changes: 5 additions & 2 deletions crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ use crate::error::TrustedServerError;
use crate::integrations::{
adserver_mock::AdServerMockConfig, aps::ApsConfig, datadome::DataDomeConfig,
didomi::DidomiIntegrationConfig, google_tag_manager::GoogleTagManagerConfig, gpt::GptConfig,
lockr::LockrConfig, nextjs::NextJsIntegrationConfig, osano::OsanoConfig,
permutive::PermutiveConfig, prebid, sourcepoint::SourcepointConfig, testlight::TestlightConfig,
gpt_diagnostics::GptDiagnosticsConfig, lockr::LockrConfig, nextjs::NextJsIntegrationConfig,
osano::OsanoConfig, permutive::PermutiveConfig, prebid, sourcepoint::SourcepointConfig,
testlight::TestlightConfig,
};
use crate::settings::{IntegrationConfig, Settings};

Expand All @@ -39,6 +40,7 @@ const DEPLOY_VALIDATED_INTEGRATION_IDS: &[&str] = &[
"google_tag_manager",
"datadome",
"gpt",
"gpt_diagnostics",
];

/// Typed app-config root used by the `ts` CLI.
Expand Down Expand Up @@ -155,6 +157,7 @@ fn validate_enabled_integrations(
validate_integration::<GoogleTagManagerConfig>(settings, "google_tag_manager")?;
validate_integration::<DataDomeConfig>(settings, "datadome")?;
validate_integration::<GptConfig>(settings, "gpt")?;
validate_integration::<GptDiagnosticsConfig>(settings, "gpt_diagnostics")?;

Ok(enabled_auction_providers)
}
Expand Down
108 changes: 108 additions & 0 deletions crates/trusted-server-core/src/html_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use lol_html::{
text,
};

use crate::integrations::gpt_diagnostics::GptDiagnosticsRequestDecision;
use crate::integrations::{
AttributeRewriteOutcome, IntegrationAttributeContext, IntegrationDocumentState,
IntegrationHtmlContext, IntegrationHtmlPostProcessor, IntegrationRegistry,
Expand Down Expand Up @@ -172,6 +173,8 @@ pub struct HtmlProcessorConfig {
/// processor aborts. Mirrors `publisher.max_buffered_body_bytes` so the
/// full-document buffering done for post-processors is bounded.
pub max_buffered_body_bytes: usize,
/// Request-scoped conditional diagnostics delivery decision.
pub gpt_diagnostics: Option<GptDiagnosticsRequestDecision>,
}

impl HtmlProcessorConfig {
Expand All @@ -192,6 +195,7 @@ impl HtmlProcessorConfig {
ad_slots_script: None,
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: settings.publisher.max_buffered_body_bytes,
gpt_diagnostics: None,
}
}

Expand All @@ -212,6 +216,13 @@ impl HtmlProcessorConfig {
self.ad_bids_state = ad_bids_state;
self
}

/// Attach the request-scoped conditional diagnostics decision.
#[must_use]
pub fn with_gpt_diagnostics(mut self, decision: Option<GptDiagnosticsRequestDecision>) -> Self {
self.gpt_diagnostics = decision;
self
}
}

/// Create an HTML processor with URL replacement and integration hooks.
Expand Down Expand Up @@ -294,6 +305,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
let script_rewriters = integration_registry.script_rewriters();
let ad_slots_script = config.ad_slots_script.clone();
let ad_bids_state = config.ad_bids_state.clone();
let gpt_diagnostics = config.gpt_diagnostics.clone();

let mut element_content_handlers = vec![
// Inject unified tsjs bundle once at the start of <head>
Expand All @@ -303,6 +315,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
let patterns = patterns.clone();
let document_state = document_state.clone();
let ad_slots_script = ad_slots_script.clone();
let gpt_diagnostics = gpt_diagnostics.clone();
move |el| {
if !injected_tsjs.get() {
let mut snippet = String::new();
Expand All @@ -321,9 +334,23 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
for insert in integrations.head_inserts(&ctx) {
snippet.push_str(&insert);
}
if let Some(bootstrap) = gpt_diagnostics
.as_ref()
.and_then(GptDiagnosticsRequestDecision::bootstrap_script)
{
snippet.push_str(&bootstrap);
}
// Main bundle: core + non-deferred integrations (synchronous).
let immediate_ids = integrations.js_module_ids_immediate();
snippet.push_str(&tsjs::tsjs_script_tag(&immediate_ids));
// Active diagnostics loads synchronously after core so its
// GPT listeners precede publisher scripts in the origin head.
if let Some(module_tag) = gpt_diagnostics
.as_ref()
.and_then(GptDiagnosticsRequestDecision::module_script_tag)
{
snippet.push_str(&module_tag);
}
// Deferred bundles: large modules like prebid loaded after
// HTML parsing completes. Empty when none are enabled.
let deferred_ids = integrations.js_module_ids_deferred();
Expand Down Expand Up @@ -664,6 +691,7 @@ mod tests {
ad_slots_script: None,
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
}
}

Expand Down Expand Up @@ -792,6 +820,80 @@ mod tests {
);
}

#[test]
fn active_gpt_diagnostics_loads_standalone_after_unified_bundle_once() {
let html = "<html><head><title>Test</title></head><body></body></html>";
let mut settings = create_test_settings();
settings
.integrations
.insert_config("gpt_diagnostics", &json!({ "enabled": true }))
.expect("should insert GPT diagnostics config");

let mut request = http::Request::builder()
.method(http::Method::GET)
.uri("https://publisher.example/page?ts_console=1")
.header("sec-fetch-dest", "document")
.body(edgezero_core::body::Body::empty())
.expect("should build activation request");
let decision =
crate::integrations::gpt_diagnostics::prepare_request(&settings, &mut request)
.expect("should prepare diagnostics request");
let mut config = create_test_config();
config.integrations =
IntegrationRegistry::new(&settings).expect("should build integration registry");
config.gpt_diagnostics = Some(decision);

let processor = create_html_processor(config);
let pipeline_config = PipelineConfig {
input_compression: Compression::None,
output_compression: Compression::None,
chunk_size: 8192,
};
let mut pipeline = StreamingPipeline::new(pipeline_config, processor);
let mut output = Vec::new();

pipeline
.process(Cursor::new(html.as_bytes()), &mut output)
.expect("should process HTML");
let processed = String::from_utf8(output).expect("should produce valid UTF-8");
let bootstrap_marker = "__tsjs_gpt_diagnostics_active";
let bundle_marker = "id=\"trustedserver-js\"";
let diagnostics_marker = "tsjs-gpt_diagnostics.min.js";

assert_eq!(
processed.matches(bootstrap_marker).count(),
1,
"should inject the diagnostics bootstrap once"
);
assert_eq!(
processed.matches(bundle_marker).count(),
1,
"should inject the immediate TSJS bundle once"
);
assert_eq!(
processed.matches(diagnostics_marker).count(),
1,
"should inject one standalone diagnostics module"
);
let bootstrap_index = processed
.find(bootstrap_marker)
.expect("should include diagnostics bootstrap");
let bundle_index = processed
.find(bundle_marker)
.expect("should include immediate TSJS bundle");
let diagnostics_index = processed
.find(diagnostics_marker)
.expect("should include standalone diagnostics module");
assert!(
bootstrap_index < bundle_index,
"should activate before core executes"
);
assert!(
bundle_index < diagnostics_index,
"should load diagnostics after core"
);
}

#[test]
fn test_create_html_processor_url_replacement() {
let config = create_test_config();
Expand Down Expand Up @@ -1436,6 +1538,7 @@ mod tests {
),
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1509,6 +1612,7 @@ mod tests {
),
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1544,6 +1648,7 @@ mod tests {
),
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
};
let mut processor = create_html_processor(config);
// Malformed HTML with two <body> elements (common in CMS template pages)
Expand Down Expand Up @@ -1578,6 +1683,7 @@ mod tests {
ad_slots_script: None,
ad_bids_state: std::sync::Arc::new(std::sync::Mutex::new(None)),
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down Expand Up @@ -1630,6 +1736,7 @@ mod tests {
),
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
};
let mut processor = create_html_processor(config);
let output = processor
Expand All @@ -1656,6 +1763,7 @@ mod tests {
ad_slots_script: None,
ad_bids_state: state,
max_buffered_body_bytes: 16 * 1024 * 1024,
gpt_diagnostics: None,
};
let mut processor = create_html_processor(config);
let output = processor
Expand Down
Loading
Loading