Skip to content
Open
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
16 changes: 11 additions & 5 deletions src/controllers/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,18 @@ pub async fn upload_deploy_tarball(
}

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

let status = res.status();
if status != 200 {
Expand Down
35 changes: 35 additions & 0 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,41 @@ async fn post_telemetry_body(client: &reqwest::Client, url: String, body: Value)
matches!(result, Ok(Ok(true)))
}

/// HTTP headers that carry the same telemetry envelope as the `cliEventTrack`
/// GraphQL mutation, for REST endpoints (e.g. the backboard `POST .../up`
/// upload handler) that emit their own analytics events server-side.
/// Today those server-side events lack caller / agent_session_id / cli
/// session_id because the request lacks any way to pass them — these
/// headers close the gap so backboard can attribute the resulting
/// `CLI - Create Up` / etc. event to the correct caller cohort.
///
/// Returns an empty Vec when telemetry is disabled so disabled clients
/// stay opted out across both telemetry surfaces.
///
/// Header names mirror the propagation contract used by railway-skills
/// (X-Railway-Skill-Id / X-Railway-Skill-Version / X-Railway-Agent-Session)
/// to keep one envelope across surfaces.
pub fn http_telemetry_headers() -> Vec<(&'static str, String)> {
if is_telemetry_disabled() {
return Vec::new();
}
let configs = match Configs::new() {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let context = TelemetryContext::current(&configs);
let mut headers = Vec::with_capacity(4);
headers.push(("X-Railway-CLI-Version", env!("CARGO_PKG_VERSION").to_string()));
headers.push(("X-Railway-Session", context.session_id));
if !context.caller.is_empty() {
headers.push(("X-Railway-Caller", context.caller));
}
if let Some(asid) = context.agent_session_id {
headers.push(("X-Railway-Agent-Session", asid));
}
headers
}

pub async fn send(event: CliTrackEvent) {
send_with_caller_override(event, None).await;
}
Expand Down
Loading