Skip to content
Closed
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
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
50 changes: 50 additions & 0 deletions crates/trusted-server-core/src/html_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,56 @@ mod tests {
);
}

#[test]
fn gpt_diagnostics_bootstrap_precedes_immediate_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 config = create_test_config();
config.integrations =
IntegrationRegistry::new(&settings).expect("should build integration registry");

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\"";

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!(
processed
.find(bootstrap_marker)
.expect("should include diagnostics bootstrap")
< processed
.find(bundle_marker)
.expect("should include immediate TSJS bundle"),
"should activate diagnostics before the immediate bundle executes"
);
}

#[test]
fn test_create_html_processor_url_replacement() {
let config = create_test_config();
Expand Down
233 changes: 233 additions & 0 deletions crates/trusted-server-core/src/integrations/gpt_diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! GPT runtime diagnostics integration.
//!
//! This integration only makes the browser diagnostics module available and
//! injects its early tab-activation bootstrap. GPT observation and presentation
//! remain entirely client-side and do not alter ad serving behavior.

use std::sync::Arc;

use error_stack::Report;
use serde::Deserialize;
use validator::Validate;

use crate::error::TrustedServerError;
use crate::settings::{IntegrationConfig, Settings};

use super::{IntegrationHeadInjector, IntegrationHtmlContext, IntegrationRegistration};

const GPT_DIAGNOSTICS_INTEGRATION_ID: &str = "gpt_diagnostics";
const GPT_DIAGNOSTICS_BOOTSTRAP_JS: &str = include_str!("gpt_diagnostics_bootstrap.js");

/// Configuration for the GPT runtime diagnostics integration.
#[derive(Debug, Clone, Deserialize, Validate)]
#[serde(deny_unknown_fields)]
pub struct GptDiagnosticsConfig {
/// Whether the GPT diagnostics browser module is available.
#[serde(default)]
pub enabled: bool,
}

impl IntegrationConfig for GptDiagnosticsConfig {
fn is_enabled(&self) -> bool {
self.enabled
}
}

struct GptDiagnosticsIntegration;

/// Register GPT diagnostics when explicitly enabled.
///
/// # Errors
///
/// Returns an error when the integration configuration cannot be parsed or
/// fails validation.
pub fn register(
settings: &Settings,
) -> Result<Option<IntegrationRegistration>, Report<TrustedServerError>> {
let Some(_config) =
settings.integration_config::<GptDiagnosticsConfig>(GPT_DIAGNOSTICS_INTEGRATION_ID)?
else {
return Ok(None);
};

let integration = Arc::new(GptDiagnosticsIntegration);
Ok(Some(
IntegrationRegistration::builder(GPT_DIAGNOSTICS_INTEGRATION_ID)
.with_head_injector(integration)
.build(),
))
}

impl IntegrationHeadInjector for GptDiagnosticsIntegration {
fn integration_id(&self) -> &'static str {
GPT_DIAGNOSTICS_INTEGRATION_ID
}

fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec<String> {
vec![format!("<script>{GPT_DIAGNOSTICS_BOOTSTRAP_JS}</script>")]
}
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::*;
use crate::integrations::{IntegrationDocumentState, IntegrationRegistry};
use crate::test_support::tests::create_test_settings;

#[test]
fn register_returns_none_without_config() {
let settings = create_test_settings();

let registration = register(&settings).expect("should evaluate diagnostics config");

assert!(
registration.is_none(),
"should not register diagnostics without explicit config"
);
}

#[test]
fn register_returns_none_when_disabled() {
let mut settings = create_test_settings();
settings
.integrations
.insert_config(GPT_DIAGNOSTICS_INTEGRATION_ID, &json!({ "enabled": false }))
.expect("should insert diagnostics config");

let registration = register(&settings).expect("should parse diagnostics config");

assert!(
registration.is_none(),
"should not register disabled diagnostics"
);
}

#[test]
fn register_adds_only_immediate_js_and_head_bootstrap() {
let mut settings = create_test_settings();
settings
.integrations
.insert_config(GPT_DIAGNOSTICS_INTEGRATION_ID, &json!({ "enabled": true }))
.expect("should insert diagnostics config");

let registration = register(&settings)
.expect("should parse diagnostics config")
.expect("should register enabled diagnostics");

assert_eq!(registration.integration_id, GPT_DIAGNOSTICS_INTEGRATION_ID);
assert!(
!registration.js_deferred,
"should load diagnostics immediately"
);
assert!(!registration.js_disabled, "should include diagnostics JS");
assert!(
registration.proxies.is_empty(),
"should register no proxy routes"
);
assert!(
registration.attribute_rewriters.is_empty(),
"should register no attribute rewriters"
);
assert!(
registration.script_rewriters.is_empty(),
"should register no script rewriters"
);
assert!(
registration.html_post_processors.is_empty(),
"should register no HTML post-processors"
);
assert!(
registration.request_filters.is_empty(),
"should register no request filters"
);
assert_eq!(
registration.head_injectors.len(),
1,
"should register one activation bootstrap"
);
}

#[test]
fn enabled_registry_includes_diagnostics_in_immediate_modules() {
let mut settings = create_test_settings();
settings
.integrations
.insert_config(GPT_DIAGNOSTICS_INTEGRATION_ID, &json!({ "enabled": true }))
.expect("should insert diagnostics config");

let registry = IntegrationRegistry::new(&settings).expect("should build registry");

assert!(
registry
.js_module_ids_immediate()
.contains(&GPT_DIAGNOSTICS_INTEGRATION_ID),
"should include diagnostics in the immediate JS bundle"
);
assert!(
!registry
.js_module_ids_deferred()
.contains(&GPT_DIAGNOSTICS_INTEGRATION_ID),
"should not defer diagnostics listener installation"
);
}

#[test]
fn head_bootstrap_only_exposes_private_activation_logic() {
let integration = GptDiagnosticsIntegration;
let document_state = IntegrationDocumentState::default();
let context = IntegrationHtmlContext {
request_host: "edge.example.com",
request_scheme: "https",
origin_host: "origin.example.com",
document_state: &document_state,
};

let inserts = integration.head_inserts(&context);

assert_eq!(inserts.len(), 1, "should emit one bootstrap script");
assert!(
inserts[0].contains("ts_console"),
"should read the activation query parameter"
);
assert!(
inserts[0].contains("sessionStorage"),
"should persist activation within the tab"
);
assert!(
inserts[0].contains("history.replaceState"),
"should remove recognized activation directives"
);
assert!(
inserts[0].contains("__tsjs_gpt_diagnostics_active"),
"should expose only the private document activation flag"
);
assert!(
!inserts[0].contains("gptDiagnostics ="),
"bootstrap should not expose the public diagnostics API"
);
}

#[test]
fn config_rejects_unknown_fields() {
let mut settings = create_test_settings();
settings
.integrations
.insert_config(
GPT_DIAGNOSTICS_INTEGRATION_ID,
&json!({ "enabled": true, "typo": true }),
)
.expect("should insert diagnostics config");

let error = settings
.integration_config::<GptDiagnosticsConfig>(GPT_DIAGNOSTICS_INTEGRATION_ID)
.expect_err("should reject unknown diagnostics config fields");
let error_text = format!("{error:?}");

assert!(
error_text.contains("typo") || error_text.contains("unknown field"),
"error should mention the unknown field: {error:?}"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Early activation bootstrap for the GPT diagnostics integration.
//
// This script intentionally owns only tab-local activation and one-time URL
// cleanup. The TypeScript integration reads the document flag below and owns
// all GPT observation, storage, API, and presentation behavior.
(function () {
if (typeof window === "undefined") return;

var queryName = "ts_console";
var storageKey = "tsjs:gptDiagnostics:active";
var activeFlag = "__tsjs_gpt_diagnostics_active";
var active = false;
var directiveRecognized = false;
var url;

try {
url = new URL(window.location.href);
var value = url.searchParams.get(queryName);

if (value === "1" || value === "true") {
active = true;
directiveRecognized = true;
} else if (value === "0" || value === "false") {
active = false;
directiveRecognized = true;
}
} catch (_) {
url = undefined;
}

if (directiveRecognized) {
try {
window.sessionStorage.setItem(storageKey, active ? "1" : "0");
} catch (_) {
// The recognized directive still applies to this document.
}

if (url) {
url.searchParams.delete(queryName);
try {
window.history.replaceState(
window.history.state,
"",
url.pathname + url.search + url.hash,
);
} catch (_) {
// URL cleanup is optional and must not block diagnostics activation.
}
}
} else {
try {
active = window.sessionStorage.getItem(storageKey) === "1";
} catch (_) {
active = false;
}
}

window[activeFlag] = active;
})();
5 changes: 5 additions & 0 deletions crates/trusted-server-core/src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod datadome;
pub mod didomi;
pub mod google_tag_manager;
pub mod gpt;
pub mod gpt_diagnostics;
pub mod lockr;
pub mod nextjs;
pub mod osano;
Expand Down Expand Up @@ -332,6 +333,10 @@ pub(crate) fn builders() -> &'static [IntegrationBuilder] {
id: "gpt",
build: gpt::register,
},
IntegrationBuilder {
id: "gpt_diagnostics",
build: gpt_diagnostics::register,
},
]
}

Expand Down
Loading
Loading