/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! Support for legacy telemetry ping creation. The ping support serialization which should be used //! when submitting. use anyhow::Context; use serde::Serialize; use std::collections::BTreeMap; use uuid::Uuid; const TELEMETRY_VERSION: u64 = 4; const PAYLOAD_VERSION: u64 = 1; // Generated by `build.rs`. // static PING_ANNOTATIONS: phf::Set<&'static str>; include!(concat!(env!("OUT_DIR"), "/ping_annotations.rs")); #[derive(Serialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Ping<'a> { Crash { id: Uuid, version: u64, #[serde(with = "time::serde::rfc3339")] creation_date: time::OffsetDateTime, client_id: &'a str, #[serde(skip_serializing_if = "serde_json::Value::is_null")] environment: serde_json::Value, payload: Payload<'a>, application: Application<'a>, }, } time::serde::format_description!(date_format, Date, "[year]-[month]-[day]"); #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct Payload<'a> { session_id: &'a str, version: u64, #[serde(with = "date_format")] crash_date: time::Date, #[serde(with = "time::serde::rfc3339")] crash_time: time::OffsetDateTime, has_crash_environment: bool, crash_id: &'a str, minidump_sha256_hash: Option<&'a str>, process_type: &'a str, #[serde(skip_serializing_if = "serde_json::Value::is_null")] stack_traces: serde_json::Value, metadata: BTreeMap<&'a str, &'a str>, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct Application<'a> { vendor: &'a str, name: &'a str, build_id: &'a str, display_version: String, platform_version: String, version: &'a str, channel: &'a str, #[serde(skip_serializing_if = "Option::is_none")] architecture: Option, #[serde(skip_serializing_if = "Option::is_none")] xpcom_abi: Option, } impl<'a> Ping<'a> { pub fn crash( extra: &'a serde_json::Value, crash_id: &'a str, minidump_sha256_hash: Option<&'a str>, ) -> anyhow::Result { let now: time::OffsetDateTime = crate::std::time::SystemTime::now().into(); let environment: serde_json::Value = extra["TelemetryEnvironment"] .as_str() .and_then(|estr| serde_json::from_str(estr).ok()) .unwrap_or_default(); // The subset of extra file entries (crash annotations) which are allowed in pings. let metadata = extra .as_object() .map(|map| { map.iter() .filter_map(|(k, v)| { PING_ANNOTATIONS .contains(k) .then(|| k.as_str()) .zip(v.as_str()) }) .collect() }) .unwrap_or_default(); let display_version = environment .pointer("/build/displayVersion") .and_then(|s| s.as_str()) .unwrap_or_default() .to_owned(); let platform_version = environment .pointer("/build/platformVersion") .and_then(|s| s.as_str()) .unwrap_or_default() .to_owned(); let architecture = environment .pointer("/build/architecture") .and_then(|s| s.as_str()) .map(ToOwned::to_owned); let xpcom_abi = environment .pointer("/build/xpcomAbi") .and_then(|s| s.as_str()) .map(ToOwned::to_owned); Ok(Ping::Crash { id: crate::std::mock::hook(Uuid::new_v4(), "ping_uuid"), version: TELEMETRY_VERSION, creation_date: now, client_id: extra["TelemetryClientId"] .as_str() .context("missing TelemetryClientId")?, environment, payload: Payload { session_id: extra["TelemetrySessionId"] .as_str() .context("missing TelemetrySessionId")?, version: PAYLOAD_VERSION, crash_date: now.date(), crash_time: now, has_crash_environment: true, crash_id, minidump_sha256_hash, process_type: "main", stack_traces: extra["StackTraces"].clone(), metadata, }, application: Application { vendor: extra["Vendor"].as_str().unwrap_or_default(), name: extra["ProductName"].as_str().unwrap_or_default(), build_id: extra["BuildID"].as_str().unwrap_or_default(), display_version, platform_version, version: extra["Version"].as_str().unwrap_or_default(), channel: extra["ReleaseChannel"].as_str().unwrap_or_default(), architecture, xpcom_abi, }, }) } /// Generate the telemetry URL for submitting this ping. pub fn submission_url(&self, extra: &serde_json::Value) -> anyhow::Result { let url = extra["TelemetryServerURL"] .as_str() .context("missing TelemetryServerURL")?; let id = self.id(); let name = extra["ProductName"] .as_str() .context("missing ProductName")?; let version = extra["Version"].as_str().context("missing Version")?; let channel = extra["ReleaseChannel"] .as_str() .context("missing ReleaseChannel")?; let buildid = extra["BuildID"].as_str().context("missing BuildID")?; Ok(format!("{url}/submit/telemetry/{id}/crash/{name}/{version}/{channel}/{buildid}?v={TELEMETRY_VERSION}")) } /// Get the ping identifier. pub fn id(&self) -> &Uuid { match self { Ping::Crash { id, .. } => id, } } }