From ec6fb61b1f0c97e99675025b1fea059f6a570c39 Mon Sep 17 00:00:00 2001
From: Aram Grigoryan <132480+aram356@users.noreply.github.com>
Date: Thu, 5 Feb 2026 09:56:09 -0800
Subject: [PATCH] Add GAM interceptor integration - backend
- Add `GamIntegrationConfig` with enabled, bidders, and force_render options
- Implement `IntegrationHeadInjector` to inject GAM config script into
- Register GAM integration in the integration builder system
- Add unit tests for config script generation
Configuration example:
```toml
[integrations.gam]
enabled = true
bidders = ["mocktioneer"] # Only intercept these bidders, empty = all
force_render = false # Force render even if GAM has a line item
```
The injected script sets `window.tsGamConfig` which is picked up by the
client-side GAM interceptor on initialization.
---
crates/common/src/integrations/gam.rs | 160 ++++++++++++++++++++++++++
crates/common/src/integrations/mod.rs | 8 +-
2 files changed, 165 insertions(+), 3 deletions(-)
create mode 100644 crates/common/src/integrations/gam.rs
diff --git a/crates/common/src/integrations/gam.rs b/crates/common/src/integrations/gam.rs
new file mode 100644
index 00000000..4042196f
--- /dev/null
+++ b/crates/common/src/integrations/gam.rs
@@ -0,0 +1,160 @@
+//! GAM (Google Ad Manager) Interceptor Integration
+//!
+//! This integration forces Prebid creatives to render when GAM doesn't have
+//! matching line items configured. It's a client-side only integration that
+//! works by intercepting GPT's `slotRenderEnded` event.
+//!
+//! # Configuration
+//!
+//! ```toml
+//! [integrations.gam]
+//! enabled = true
+//! bidders = ["mocktioneer"] # Only intercept these bidders, empty = all
+//! force_render = false # Force render even if GAM has a line item
+//! ```
+//!
+//! # Environment Variables
+//!
+//! ```bash
+//! TRUSTED_SERVER__INTEGRATIONS__GAM__ENABLED=true
+//! TRUSTED_SERVER__INTEGRATIONS__GAM__BIDDERS="mocktioneer,appnexus"
+//! TRUSTED_SERVER__INTEGRATIONS__GAM__FORCE_RENDER=false
+//! ```
+
+use serde::{Deserialize, Serialize};
+use validator::Validate;
+
+use crate::settings::IntegrationConfig;
+
+use super::{IntegrationHeadInjector, IntegrationHtmlContext, IntegrationRegistration};
+
+const GAM_INTEGRATION_ID: &str = "gam";
+
+/// GAM interceptor configuration.
+#[derive(Debug, Clone, Default, Deserialize, Serialize, Validate)]
+pub struct GamIntegrationConfig {
+ /// Enable the GAM interceptor. Defaults to false.
+ #[serde(default)]
+ pub enabled: bool,
+
+ /// Only intercept bids from these bidders. Empty = all bidders.
+ #[serde(default, deserialize_with = "crate::settings::vec_from_seq_or_map")]
+ pub bidders: Vec,
+
+ /// Force render Prebid creative even if GAM returned a line item.
+ #[serde(default)]
+ pub force_render: bool,
+}
+
+impl IntegrationConfig for GamIntegrationConfig {
+ fn is_enabled(&self) -> bool {
+ self.enabled
+ }
+}
+
+/// Generate the JavaScript config script tag for GAM integration.
+/// Sets window.tsGamConfig which is picked up by the GAM integration on init.
+#[must_use]
+pub fn gam_config_script_tag(config: &GamIntegrationConfig) -> String {
+ let bidders_json = if config.bidders.is_empty() {
+ "[]".to_string()
+ } else {
+ format!(
+ "[{}]",
+ config
+ .bidders
+ .iter()
+ .map(|b| format!("\"{}\"", b))
+ .collect::>()
+ .join(",")
+ )
+ };
+
+ format!(
+ r#""#,
+ bidders_json, config.force_render
+ )
+}
+
+pub struct GamIntegration {
+ config: GamIntegrationConfig,
+}
+
+impl GamIntegration {
+ #[must_use]
+ pub fn new(config: GamIntegrationConfig) -> Self {
+ Self { config }
+ }
+}
+
+impl IntegrationHeadInjector for GamIntegration {
+ fn integration_id(&self) -> &'static str {
+ GAM_INTEGRATION_ID
+ }
+
+ fn head_inserts(&self, _ctx: &IntegrationHtmlContext<'_>) -> Vec {
+ vec![gam_config_script_tag(&self.config)]
+ }
+}
+
+/// Register the GAM integration if enabled.
+#[must_use]
+pub fn register(settings: &crate::settings::Settings) -> Option {
+ use std::sync::Arc;
+
+ let config: GamIntegrationConfig =
+ settings.integrations.get_typed(GAM_INTEGRATION_ID).ok()??;
+
+ log::info!(
+ "GAM integration enabled: bidders={:?}, force_render={}",
+ config.bidders,
+ config.force_render
+ );
+
+ let integration = Arc::new(GamIntegration::new(config));
+
+ Some(
+ IntegrationRegistration::builder(GAM_INTEGRATION_ID)
+ .with_head_injector(integration)
+ .build(),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn gam_config_script_tag_with_bidders() {
+ let config = GamIntegrationConfig {
+ enabled: true,
+ bidders: vec!["mocktioneer".to_string(), "appnexus".to_string()],
+ force_render: false,
+ };
+ let tag = gam_config_script_tag(&config);
+ assert!(tag.contains("window.tsGamConfig="));
+ assert!(tag.contains("enabled:true"));
+ assert!(tag.contains(r#"bidders:["mocktioneer","appnexus"]"#));
+ assert!(tag.contains("forceRender:false"));
+ }
+
+ #[test]
+ fn gam_config_script_tag_empty_bidders() {
+ let config = GamIntegrationConfig {
+ enabled: true,
+ bidders: vec![],
+ force_render: true,
+ };
+ let tag = gam_config_script_tag(&config);
+ assert!(tag.contains("bidders:[]"));
+ assert!(tag.contains("forceRender:true"));
+ }
+
+ #[test]
+ fn gam_config_disabled_by_default() {
+ let config = GamIntegrationConfig::default();
+ assert!(!config.enabled);
+ assert!(config.bidders.is_empty());
+ assert!(!config.force_render);
+ }
+}
diff --git a/crates/common/src/integrations/mod.rs b/crates/common/src/integrations/mod.rs
index af1b5ea1..8b8cabf2 100644
--- a/crates/common/src/integrations/mod.rs
+++ b/crates/common/src/integrations/mod.rs
@@ -5,6 +5,7 @@ use crate::settings::Settings;
pub mod adserver_mock;
pub mod aps;
pub mod didomi;
+pub mod gam;
pub mod lockr;
pub mod nextjs;
pub mod permutive;
@@ -15,9 +16,9 @@ pub mod testlight;
pub use registry::{
AttributeRewriteAction, AttributeRewriteOutcome, IntegrationAttributeContext,
IntegrationAttributeRewriter, IntegrationDocumentState, IntegrationEndpoint,
- IntegrationHtmlContext, IntegrationHtmlPostProcessor, IntegrationMetadata, IntegrationProxy,
- IntegrationRegistration, IntegrationRegistrationBuilder, IntegrationRegistry,
- IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction,
+ IntegrationHeadInjector, IntegrationHtmlContext, IntegrationHtmlPostProcessor,
+ IntegrationMetadata, IntegrationProxy, IntegrationRegistration, IntegrationRegistrationBuilder,
+ IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction,
};
type IntegrationBuilder = fn(&Settings) -> Option;
@@ -30,5 +31,6 @@ pub(crate) fn builders() -> &'static [IntegrationBuilder] {
permutive::register,
lockr::register,
didomi::register,
+ gam::register,
]
}