Skip to content

Commit 65e9724

Browse files
codydeclaude
andcommitted
feat(telemetry): propagate caller/agent-session as HTTP headers on the up upload
The CLI's `railway up` uploads its build tarball to backboard's `POST /project/.../environment/.../up` REST handler. That handler emits the `CLI - Create Up` telemetry event (warehouse: `cli_create_up`) via trackCLIEvent — about 107k events in a typical 48h window. But the request only carries `Content-Type` today, so backboard has nothing to attribute the event to: 100% of `cli_create_up` events land in the warehouse with caller / agent_session_id / session_id all NULL. Add the cliEventTrack envelope as HTTP headers on the upload request, mirroring the propagation contract railway-skills already uses for X-Railway-Skill-* / X-Railway-Agent-Session: X-Railway-CLI-Version X-Railway-Session (CLI session_id from telemetry::session_id()) X-Railway-Caller (resolved caller, when non-empty) X-Railway-Agent-Session (agent_session_id, when set) Headers are gated by `is_telemetry_disabled()` so disabled clients stay opted out across both surfaces. Caller / agent-session header omission mirrors the cliEventTrack mutation's null-skipping for those fields. This is the producer half of a two-PR fix. Companion mono backboard PR (forthcoming) reads these headers in the `up` handler and passes them to `trackCLIEvent` properties + adds the missing `deploymentId`. Both can ship independently — backboard reading absent headers will fall back to null (today's behavior); CLI sending headers to a backboard that doesn't read them is harmless. Verified: - `cargo build --bin railway` clean. - 21/21 telemetry tests pass (unchanged). - No new clippy warnings on the touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1ca8048 commit 65e9724

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

src/controllers/upload.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,18 @@ pub async fn upload_deploy_tarball(
107107
}
108108

109109
let body_len = body.len();
110-
let res = client
110+
let mut request = client
111111
.post(url.to_string())
112-
.header("Content-Type", "application/gzip")
113-
.body(body)
114-
.send()
115-
.await?;
112+
.header("Content-Type", "application/gzip");
113+
// Propagate the cliEventTrack telemetry envelope as HTTP headers so
114+
// backboard's POST .../up handler can attribute the `CLI - Create Up`
115+
// event it emits to the correct caller / agent session / CLI session.
116+
// Without these the resulting `cli_create_up` event lands in the
117+
// warehouse with null caller, breaking per-cohort deploy attribution.
118+
for (name, value) in crate::telemetry::http_telemetry_headers() {
119+
request = request.header(name, value);
120+
}
121+
let res = request.body(body).send().await?;
116122

117123
let status = res.status();
118124
if status != 200 {

src/telemetry.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,44 @@ async fn post_telemetry_body(client: &reqwest::Client, url: String, body: Value)
13281328
matches!(result, Ok(Ok(true)))
13291329
}
13301330

1331+
/// HTTP headers that carry the same telemetry envelope as the `cliEventTrack`
1332+
/// GraphQL mutation, for REST endpoints (e.g. the backboard `POST .../up`
1333+
/// upload handler) that emit their own analytics events server-side.
1334+
/// Today those server-side events lack caller / agent_session_id / cli
1335+
/// session_id because the request lacks any way to pass them — these
1336+
/// headers close the gap so backboard can attribute the resulting
1337+
/// `CLI - Create Up` / etc. event to the correct caller cohort.
1338+
///
1339+
/// Returns an empty Vec when telemetry is disabled so disabled clients
1340+
/// stay opted out across both telemetry surfaces.
1341+
///
1342+
/// Header names mirror the propagation contract used by railway-skills
1343+
/// (X-Railway-Skill-Id / X-Railway-Skill-Version / X-Railway-Agent-Session)
1344+
/// to keep one envelope across surfaces.
1345+
pub fn http_telemetry_headers() -> Vec<(&'static str, String)> {
1346+
if is_telemetry_disabled() {
1347+
return Vec::new();
1348+
}
1349+
let configs = match Configs::new() {
1350+
Ok(c) => c,
1351+
Err(_) => return Vec::new(),
1352+
};
1353+
let context = TelemetryContext::current(&configs);
1354+
let mut headers = Vec::with_capacity(4);
1355+
headers.push((
1356+
"X-Railway-CLI-Version",
1357+
env!("CARGO_PKG_VERSION").to_string(),
1358+
));
1359+
headers.push(("X-Railway-Session", context.session_id));
1360+
if !context.caller.is_empty() {
1361+
headers.push(("X-Railway-Caller", context.caller));
1362+
}
1363+
if let Some(asid) = context.agent_session_id {
1364+
headers.push(("X-Railway-Agent-Session", asid));
1365+
}
1366+
headers
1367+
}
1368+
13311369
pub async fn send(event: CliTrackEvent) {
13321370
send_with_caller_override(event, None).await;
13331371
}

0 commit comments

Comments
 (0)