@@ -68,6 +68,10 @@ pub struct GptConfig {
6868 #[ serde( default = "default_enabled" ) ]
6969 pub enabled : bool ,
7070
71+ /// Enable page-level `ts=true` delivery attribution in GAM.
72+ #[ serde( default ) ]
73+ pub gam_attribution_enabled : bool ,
74+
7175 /// URL for the GPT bootstrap script (default: Google's CDN).
7276 #[ serde( default = "default_script_url" ) ]
7377 #[ validate( url) ]
@@ -487,10 +491,17 @@ impl IntegrationHeadInjector for GptIntegration {
487491 /// route changes (see `auction/endpoints.rs`).
488492 /// The `POST /auction` endpoint is not involved in scroll or refresh flows.
489493 fn head_inserts ( & self , _ctx : & IntegrationHtmlContext < ' _ > ) -> Vec < String > {
494+ let gam_attribution_flag = if self . config . gam_attribution_enabled {
495+ "window.__tsjs_gam_attribution_enabled=true;"
496+ } else {
497+ ""
498+ } ;
499+
490500 let mut scripts = vec ! [
491- "<script>window.__tsjs_gpt_enabled=true;\
492- window.__tsjs_installGptShim&&window.__tsjs_installGptShim();</script>"
493- . to_string( ) ,
501+ format!(
502+ "<script>window.__tsjs_gpt_enabled=true;{gam_attribution_flag}\
503+ window.__tsjs_installGptShim&&window.__tsjs_installGptShim();</script>"
504+ ) ,
494505 format!( "<script>{}</script>" , GPT_BOOTSTRAP_JS ) ,
495506 ] ;
496507
@@ -508,6 +519,14 @@ impl IntegrationHeadInjector for GptIntegration {
508519
509520 scripts
510521 }
522+
523+ fn tsjs_script_tag_attributes ( & self ) -> Vec < ( & ' static str , & ' static str ) > {
524+ if self . config . gam_attribution_enabled {
525+ vec ! [ ( "data-ts-gam-attribution" , "true" ) ]
526+ } else {
527+ Vec :: new ( )
528+ }
529+ }
511530}
512531
513532/// Inline `window.tsjs.adInit` bootstrap injected at `<head>` so the bids
@@ -549,6 +568,7 @@ mod tests {
549568 fn test_config ( ) -> GptConfig {
550569 GptConfig {
551570 enabled : true ,
571+ gam_attribution_enabled : false ,
552572 script_url : default_script_url ( ) ,
553573 cache_ttl_seconds : 3600 ,
554574 rewrite_script : true ,
@@ -573,6 +593,29 @@ mod tests {
573593 . expect ( "should build HTTP request" )
574594 }
575595
596+ #[ test]
597+ fn gam_attribution_defaults_to_disabled ( ) {
598+ let config: GptConfig =
599+ serde_json:: from_value ( serde_json:: json!( { } ) ) . expect ( "should parse defaults" ) ;
600+
601+ assert ! ( !config. gam_attribution_enabled) ;
602+ }
603+
604+ #[ test]
605+ fn gam_attribution_deserializes_explicit_values ( ) {
606+ let disabled: GptConfig = serde_json:: from_value ( serde_json:: json!( {
607+ "gam_attribution_enabled" : false
608+ } ) )
609+ . expect ( "should parse explicit false" ) ;
610+ let enabled: GptConfig = serde_json:: from_value ( serde_json:: json!( {
611+ "gam_attribution_enabled" : true
612+ } ) )
613+ . expect ( "should parse explicit true" ) ;
614+
615+ assert ! ( !disabled. gam_attribution_enabled) ;
616+ assert ! ( enabled. gam_attribution_enabled) ;
617+ }
618+
576619 // -- URL detection --
577620
578621 #[ test]
@@ -1146,6 +1189,38 @@ mod tests {
11461189 "<script>window.__tsjs_gpt_enabled=true;window.__tsjs_installGptShim&&window.__tsjs_installGptShim();</script>" ,
11471190 "should set the enable flag and call the GPT shim activation function"
11481191 ) ;
1192+ assert ! (
1193+ integration. tsjs_script_tag_attributes( ) . is_empty( ) ,
1194+ "should not authorize GAM attribution metadata by default"
1195+ ) ;
1196+ }
1197+
1198+ #[ test]
1199+ fn gam_attribution_true_adds_both_activation_signals_without_a_new_insert ( ) {
1200+ let integration = GptIntegration :: new ( GptConfig {
1201+ gam_attribution_enabled : true ,
1202+ ..test_config ( )
1203+ } ) ;
1204+ let document_state = IntegrationDocumentState :: default ( ) ;
1205+ let context = IntegrationHtmlContext {
1206+ request_host : "edge.example.com" ,
1207+ request_scheme : "https" ,
1208+ origin_host : "origin.example.com" ,
1209+ document_state : & document_state,
1210+ } ;
1211+
1212+ let inserts = integration. head_inserts ( & context) ;
1213+
1214+ assert_eq ! ( inserts. len( ) , 2 , "should not add another head insert" ) ;
1215+ assert ! (
1216+ inserts[ 0 ] . contains( "window.__tsjs_gam_attribution_enabled=true;" ) ,
1217+ "should activate the early bootstrap marker"
1218+ ) ;
1219+ assert_eq ! (
1220+ integration. tsjs_script_tag_attributes( ) ,
1221+ vec![ ( "data-ts-gam-attribution" , "true" ) ] ,
1222+ "should authorize the bundle fallback on the publisher tag"
1223+ ) ;
11491224 }
11501225
11511226 #[ test]
@@ -1352,6 +1427,35 @@ mod tests {
13521427 ) ;
13531428 }
13541429
1430+ #[ test]
1431+ fn head_inserts_queue_gam_attribution_before_guard_and_ad_requests ( ) {
1432+ let targeting_index = GPT_BOOTSTRAP_JS
1433+ . find ( "gpt.setConfig({ targeting: { ts: 'true' } })" )
1434+ . expect ( "should apply the fixed page-level GAM targeting pair" ) ;
1435+ let guard_index = GPT_BOOTSTRAP_JS
1436+ . find ( "if (ts.adInit) return;" )
1437+ . expect ( "should retain the preinstalled adInit guard" ) ;
1438+ let display_index = GPT_BOOTSTRAP_JS
1439+ . find ( "googletag.display(divId);" )
1440+ . expect ( "should retain the executable GPT display call" ) ;
1441+ let refresh_index = GPT_BOOTSTRAP_JS
1442+ . find ( "googletag.pubads().refresh(slotsNeedingRefresh);" )
1443+ . expect ( "should retain the bounded GPT refresh call" ) ;
1444+
1445+ assert ! (
1446+ targeting_index < guard_index,
1447+ "should enqueue attribution before the preinstalled adInit guard"
1448+ ) ;
1449+ assert ! (
1450+ targeting_index < display_index,
1451+ "should enqueue attribution before the executable display call"
1452+ ) ;
1453+ assert ! (
1454+ targeting_index < refresh_index,
1455+ "should enqueue attribution before the executable refresh call"
1456+ ) ;
1457+ }
1458+
13551459 #[ test]
13561460 fn head_injector_integration_id ( ) {
13571461 let integration = GptIntegration :: new ( test_config ( ) ) ;
0 commit comments