Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7505ff7
Auction telemetry is a module [analytics] selects
jwrosewell Oct 9, 2026
75fc697
The guides describe [analytics]
jwrosewell Oct 9, 2026
6b0597d
Merge branch 'review/15-diagnostic-headers' into review/16-analytics
Jamesr51d Oct 12, 2026
7de3882
The analytics module is named analytics in every identifier
Jamesr51d Oct 12, 2026
c5aff4e
Merge branch 'review/15-diagnostic-headers' into review/16-analytics
Jamesr51d Oct 12, 2026
8f8bc98
Merge 15-diagnostic-headers into 16-analytics
Jamesr51d Oct 12, 2026
04ee8ad
Merge 15-diagnostic-headers into 16-analytics
jwrosewell Oct 13, 2026
50a9d3d
Merge split/15-diagnostic-headers into split/16-analytics
Jamesr51d Oct 13, 2026
c4f0302
Merge split/15-diagnostic-headers into split/16-analytics
Jamesr51d Oct 13, 2026
7b19336
Merge 15-diagnostic-headers into 16-analytics
jwrosewell Oct 13, 2026
d76489a
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 16, 2026
cdb0ab1
Send no telemetry row about a reader who has not permitted ad measure…
jwrosewell Oct 16, 2026
71fce6d
Warn at startup when an adapter sends no telemetry for [analytics]
jwrosewell Oct 16, 2026
4bf0d8a
Say when telemetry rows are sent and which adapters send none
jwrosewell Oct 16, 2026
0b1ed27
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 16, 2026
f70138e
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 16, 2026
a204858
The rules guide maps `[tinybird]` to the analytics module
jwrosewell Oct 16, 2026
2f06fd7
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 16, 2026
539208f
Merge the review fixes into split/16-analytics
jwrosewell Oct 16, 2026
cdac1ba
Merge the review fixes into split/16-analytics
jwrosewell Oct 16, 2026
f0b8b6e
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 16, 2026
99abf9f
The guides say which adapters send auction and access telemetry
jwrosewell Oct 17, 2026
b1a47b8
A Fastly test shows each request carries the sink [analytics] selects
jwrosewell Oct 17, 2026
4afdadd
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 18, 2026
e642f1f
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 18, 2026
ceec0f6
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 18, 2026
4332c85
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 18, 2026
ca71672
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 19, 2026
e24cd38
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 19, 2026
4299173
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 19, 2026
25af766
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 19, 2026
566334b
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
d619b0b
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
87b417f
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
82f97b3
The analytics startup tests carry the sample policy
jwrosewell Oct 28, 2026
fd1c4ae
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
7ea927f
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
fd97a7a
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
48e36bd
Merge split/15-diagnostic-headers into split/16-analytics
jwrosewell Oct 28, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking:** auction telemetry is selected the way every other module is. `[tinybird]` is now `[analytics.tinybird]`, and `[analytics] module = "tinybird"` selects it, which replaces the `enabled` line. A configuration that still carries `[tinybird]` is refused with directions. A stored configuration written by an earlier version carries the table even where telemetry was off, so push the configuration again with this version. The secret reference is `analytics.tinybird.auction_token_secret`, the table refuses a key it does not know, and a section that selects nothing, or a module this build does not have, is refused. A row is sent only for a reader whose permissions include `analytics.ad_reporting.measure_ad_performance`, so none is sent for a reader the consent gate refuses unless they permit it. The Axum, Cloudflare and Spin adapters send no telemetry, and log a warning at startup when the section selects a module.
- The DataDome module asks its Protection API about a publisher's pages beneath `/admin/` like any other page. It skipped every path beneath `/admin/` and `/_ts/admin/`, which left a publisher's own `/admin/` pages out of bot protection.
- **Breaking:** `ts cache purge` asks the hosting platform to purge instead of calling the service. It takes `--service-id <id>` where it took `--service <url>`, reads a Fastly API token from `FASTLY_API_TOKEN` where it read `TRUSTED_SERVER_ADMIN_PASSWORD`, and no longer takes `--username`. It runs `fastly service purge --key`, which needs Fastly CLI 14.0.0 or later, with the surrogate key it derives by the function the service tags its cached objects with, so `--all` and `--page <url>` purge exactly what they purged before.
- **Breaking:** A module's change to a page runs only on the pages where a `[[fetch]]` or `[[serve]]` entry names its middleware. A deployment adds the entries when it upgrades. Until it does, a module changes no page, and startup logs a warning naming each middleware that no entry names. "Placing page changes" in the configuration guide lists the middleware each module supplies and the phase each runs in. To keep a page as it was, name the middleware of the modules a deployment selects in one `[[fetch]]` entry and one `[[serve]]` entry for `text/html`, in the order `trusted-server.example.toml` shows them, which is the order the modules' hooks ran in.
Expand Down
34 changes: 34 additions & 0 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use core::future::Future;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use edgezero_core::app::Hooks;
use edgezero_core::context::RequestContext;
Expand All @@ -10,6 +11,7 @@ use edgezero_core::http::{
use edgezero_core::router::RouterService;
use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::telemetry::warn_once_when_analytics_is_unsent;
use trusted_server_core::auction::{
AuctionOrchestrator, build_orchestrator_with_plan, compile_auction_plan_with,
};
Expand Down Expand Up @@ -78,6 +80,9 @@ pub struct AppState {
services: Option<RuntimeServices>,
}

/// Set once startup has logged that this adapter sends no auction telemetry.
static UNSENT_ANALYTICS_LOGGED: AtomicBool = AtomicBool::new(false);

/// The permission signal modules this adapter links, in the order they run
/// when configuration names none. Global Privacy Control is first because it
/// is a browser setting with no interface of its own, and the three that
Expand Down Expand Up @@ -213,6 +218,14 @@ fn build_state_with_registrations_and_services(
&settings,
&shipped_signal_modules(),
)?;
// This adapter builds no telemetry sink, so a module `[analytics]` selects
// sends nothing unless the caller's services carry a sink.
warn_once_when_analytics_is_unsent(
&settings,
services.as_ref(),
"Axum",
&UNSENT_ANALYTICS_LOGGED,
);
let permission_policy = settings.required_permission_policy()?;

Ok(Arc::new(AppState {
Expand Down Expand Up @@ -929,6 +942,27 @@ mod tests {
);
}

/// `[analytics]` selecting a module still loads on this adapter, which
/// sends no auction telemetry, and startup says so.
#[test]
fn startup_logs_that_this_adapter_sends_no_auction_telemetry() {
let settings = Settings::from_toml(&format!(
"{}\n{}",
trusted_server_core::test_support::tests::crate_test_settings_str(),
trusted_server_core::test_support::template::documented_block("[analytics]")
))
.expect("should load settings selecting the documented analytics module")
// A loaded domain carries its policy.
.with_permission_policy(trusted_server_core::permissions::PermissionPolicy::sample());

build_state_with_settings(settings).expect("should accept a selected analytics module");

assert!(
UNSENT_ANALYTICS_LOGGED.load(std::sync::atomic::Ordering::Relaxed),
"startup should log that this adapter sends no auction telemetry"
);
}

mod startup_policy {
use std::path::Path;

Expand Down
36 changes: 36 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::future::Future;
use core::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;

use edgezero_core::app::Hooks;
use edgezero_core::context::RequestContext;
Expand All @@ -9,6 +10,7 @@ use edgezero_core::http::{HeaderValue, Method, Request, Response, header};
use edgezero_core::router::RouterService;
use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::telemetry::warn_once_when_analytics_is_unsent;
use trusted_server_core::auction::{
AuctionOrchestrator, build_orchestrator_with_plan, compile_auction_plan_with,
};
Expand Down Expand Up @@ -257,6 +259,14 @@ fn build_state_with_registrations_and_services(
&settings,
&shipped_signal_modules(),
)?;
// This adapter builds no telemetry sink, so a module `[analytics]` selects
// sends nothing unless the caller's services carry a sink.
warn_once_when_analytics_is_unsent(
&settings,
services.as_ref(),
"Cloudflare",
&UNSENT_ANALYTICS_LOGGED,
);
let permission_policy = settings.required_permission_policy()?;

Ok(Arc::new(AppState {
Expand All @@ -270,6 +280,11 @@ fn build_state_with_registrations_and_services(
}))
}

/// Set once startup has logged that this adapter sends no auction telemetry.
/// The state is built for every request, so the flag keeps the warning to
/// one for each instance.
static UNSENT_ANALYTICS_LOGGED: AtomicBool = AtomicBool::new(false);

/// The permission signal modules this adapter links, in the order they run
/// when configuration names none. Global Privacy Control is first because it
/// is a browser setting with no interface of its own, and the three that
Expand Down Expand Up @@ -1131,4 +1146,25 @@ mod tests {
}
}
}

/// `[analytics]` selecting a module still loads on this adapter, which
/// sends no auction telemetry, and startup says so.
#[test]
fn startup_logs_that_this_adapter_sends_no_auction_telemetry() {
let settings = Settings::from_toml(&format!(
"{}\n{}",
trusted_server_core::test_support::tests::crate_test_settings_str(),
trusted_server_core::test_support::template::documented_block("[analytics]")
))
.expect("should load settings selecting the documented analytics module")
// A loaded domain carries its policy.
.with_permission_policy(trusted_server_core::permissions::PermissionPolicy::sample());

build_state_with_settings(settings).expect("should accept a selected analytics module");

assert!(
UNSENT_ANALYTICS_LOGGED.load(std::sync::atomic::Ordering::Relaxed),
"startup should log that this adapter sends no auction telemetry"
);
}
}
35 changes: 35 additions & 0 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,41 @@ mod tests {
);
}

/// The sink startup builds from `[analytics]` is the one each request's
/// services carry, so a selected module sends from the request path and a
/// deployment that selects none sends nothing.
#[test]
fn per_request_services_carry_the_sink_analytics_selects() {
let base = trusted_server_core::test_support::tests::crate_test_settings_str();
let analytics =
trusted_server_core::test_support::template::documented_block("[analytics]");
let context = RequestContext::new(
empty_request(Method::GET, "/article"),
PathParams::default(),
);

for (document, sends) in [
(base.clone(), false),
(format!("{base}\n{analytics}"), true),
] {
let settings = Settings::from_toml(&document)
.expect("should load the settings")
// A loaded domain carries its policy.
.with_permission_policy(
trusted_server_core::permissions::PermissionPolicy::sample(),
);
let state = build_state_from_settings(settings).expect("should build state");

let services = build_per_request_services(&state, &context);

assert_eq!(
services.auction_telemetry_sink().is_enabled(),
sends,
"a request's sink should send exactly when [analytics] selects a module"
);
}
}

/// Builds a router whose `AppState` uses a registry containing the given
/// request filters (and no routes), so dispatch-level request-filter
/// behavior can be exercised without a real integration.
Expand Down
58 changes: 30 additions & 28 deletions crates/trusted-server-adapter-fastly/src/tinybird.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,18 @@ const TINYBIRD_FIRST_BYTE_TIMEOUT: Duration = Duration::from_secs(2);
const TINYBIRD_BETWEEN_BYTES_TIMEOUT: Duration = Duration::from_secs(2);
const TINYBIRD_MAX_ROWS_PER_AUCTION_BATCH: usize = 512;

/// Build the configured auction telemetry sink.
/// Build the auction telemetry sink, which sends only when `[analytics]`
/// selects the analytics module.
#[must_use]
pub(crate) fn auction_sink_from_settings(settings: &Settings) -> Arc<dyn AuctionTelemetrySink> {
if settings.tinybird.enabled {
Arc::new(FastlyTinybirdAuctionTelemetrySink::new(
settings.tinybird.clone(),
))
} else {
Arc::new(NoopAuctionTelemetrySink)
match settings.analytics_module() {
Some(telemetry) => Arc::new(FastlyTinybirdAuctionTelemetrySink::new(telemetry.clone())),
None => Arc::new(NoopAuctionTelemetrySink),
}
}

#[derive(Debug, Clone)]
struct FastlyTinybirdAuctionTelemetrySink {
enabled: bool,
target: TinybirdEventsTarget,
}

Expand All @@ -57,7 +54,7 @@ impl TinybirdEventsTarget {
dataset: config.auction_dataset,
append_token: config
.auction_token_secret
.expect("should contain a resolved Tinybird auction token when enabled"),
.expect("should contain a resolved Tinybird auction token when selected"),
uri,
backend_spec,
max_body_bytes: config.max_body_bytes,
Expand All @@ -67,9 +64,7 @@ impl TinybirdEventsTarget {

impl FastlyTinybirdAuctionTelemetrySink {
fn new(config: TinybirdSettings) -> Self {
let enabled = config.enabled;
Self {
enabled,
target: TinybirdEventsTarget::from_config(config),
}
}
Expand Down Expand Up @@ -149,16 +144,12 @@ impl FastlyTinybirdAuctionTelemetrySink {

#[async_trait::async_trait(?Send)]
impl AuctionTelemetrySink for FastlyTinybirdAuctionTelemetrySink {
fn is_enabled(&self) -> bool {
self.enabled
}

async fn emit_auction_events(
&self,
services: &RuntimeServices,
batch: AuctionEventBatch,
) -> Result<(), Report<TrustedServerError>> {
if !self.enabled || batch.is_empty() {
if batch.is_empty() {
return Ok(());
}

Expand Down Expand Up @@ -431,9 +422,8 @@ mod tests {
.build()
}

fn enabled_config() -> TinybirdSettings {
fn tinybird_settings() -> TinybirdSettings {
TinybirdSettings {
enabled: true,
api_host: "api.us-east.aws.tinybird.co".to_owned(),
secret_store: None,
auction_dataset: "auction_events_raw".to_owned(),
Expand Down Expand Up @@ -480,7 +470,7 @@ mod tests {
Arc::clone(&http_client),
HashMap::new(),
);
let sink = FastlyTinybirdAuctionTelemetrySink::new(enabled_config());
let sink = FastlyTinybirdAuctionTelemetrySink::new(tinybird_settings());

futures::executor::block_on(
sink.emit_auction_events(&services, AuctionEventBatch::new(vec![test_row()])),
Expand Down Expand Up @@ -528,37 +518,49 @@ mod tests {
}

#[test]
fn disabled_sink_does_not_dispatch() {
fn settings_selecting_no_analytics_module_build_a_sink_that_sends_nothing() {
let backend = Arc::new(RecordingBackend::default());
let http_client = Arc::new(RecordingHttpClient::default());
let services = services(
Arc::clone(&backend),
Arc::clone(&http_client),
HashMap::new(),
);
let mut config = enabled_config();
config.enabled = false;
let sink = FastlyTinybirdAuctionTelemetrySink::new(config);
let sink = auction_sink_from_settings(&Settings::default());
assert!(!sink.is_enabled(), "should build the no-op sink");

futures::executor::block_on(
sink.emit_auction_events(&services, AuctionEventBatch::new(vec![test_row()])),
)
.expect("should ignore disabled sink");
.expect("the no-op sink should accept the batch");

assert!(
backend.specs.lock().expect("should lock specs").is_empty(),
"should not ensure backend when disabled"
"should not ensure a backend when nothing is selected"
);
assert!(
http_client
.requests
.lock()
.expect("should lock recorded requests")
.is_empty(),
"should not send when disabled"
"should not send when nothing is selected"
);
}

#[test]
fn settings_selecting_the_analytics_module_build_a_sink_that_sends() {
let mut settings = Settings::default();
settings.analytics = Some(trusted_server_core::settings::AnalyticsConfig {
module: Some("tinybird".to_owned()),
tinybird: Some(tinybird_settings()),
});

let sink = auction_sink_from_settings(&settings);

assert!(sink.is_enabled(), "should build the sink that sends");
}

#[test]
fn empty_batch_does_not_dispatch() {
let backend = Arc::new(RecordingBackend::default());
Expand All @@ -568,7 +570,7 @@ mod tests {
Arc::clone(&http_client),
HashMap::new(),
);
let sink = FastlyTinybirdAuctionTelemetrySink::new(enabled_config());
let sink = FastlyTinybirdAuctionTelemetrySink::new(tinybird_settings());

futures::executor::block_on(
sink.emit_auction_events(&services, AuctionEventBatch::new(Vec::new())),
Expand Down Expand Up @@ -601,7 +603,7 @@ mod tests {
b"append-token".to_vec(),
)]),
);
let sink = FastlyTinybirdAuctionTelemetrySink::new(enabled_config());
let sink = FastlyTinybirdAuctionTelemetrySink::new(tinybird_settings());
let rows = vec![test_row(); TINYBIRD_MAX_ROWS_PER_AUCTION_BATCH + 1];

let result = futures::executor::block_on(
Expand Down
Loading
Loading