socorro_cli/commands/
correlations.rs1use reqwest::StatusCode;
6use sha1::{Digest, Sha1};
7
8use crate::models::{CorrelationsResponse, CorrelationsTotals};
9use crate::output::{compact, json, markdown, OutputFormat};
10use crate::{Error, Result};
11
12const CDN_BASE: &str =
13 "https://analysis-output.telemetry.mozilla.org/top-signatures-correlations/data";
14
15pub fn signature_hash(sig: &str) -> String {
16 let mut hasher = Sha1::new();
17 hasher.update(sig.as_bytes());
18 format!("{:x}", hasher.finalize())
19}
20
21fn fetch_totals(client: &reqwest::blocking::Client) -> Result<CorrelationsTotals> {
22 let url = format!("{}/all.json.gz", CDN_BASE);
23 let response = client.get(&url).send()?;
24
25 match response.status() {
26 StatusCode::OK => {
27 let text = response.text()?;
28 serde_json::from_str(&text)
29 .map_err(|e| Error::ParseError(format!("{}: {}", e, &text[..text.len().min(200)])))
30 }
31 _ => Err(Error::Http(response.error_for_status().unwrap_err())),
32 }
33}
34
35fn fetch_signature_correlations(
36 client: &reqwest::blocking::Client,
37 signature: &str,
38 channel: &str,
39) -> Result<CorrelationsResponse> {
40 let hash = signature_hash(signature);
41 let url = format!("{}/{}/{}.json.gz", CDN_BASE, channel, hash);
42 let response = client.get(&url).send()?;
43
44 match response.status() {
45 StatusCode::OK => {
46 let text = response.text()?;
47 serde_json::from_str(&text)
48 .map_err(|e| Error::ParseError(format!("{}: {}", e, &text[..text.len().min(200)])))
49 }
50 StatusCode::NOT_FOUND => Err(Error::NotFound(format!(
51 "No correlation data for signature \"{}\" on channel \"{}\". \
52 Correlations are only available for the top ~200 signatures per channel.",
53 signature, channel
54 ))),
55 _ => Err(Error::Http(response.error_for_status().unwrap_err())),
56 }
57}
58
59pub fn execute(signature: &str, channel: &str, format: OutputFormat) -> Result<()> {
60 let client = reqwest::blocking::Client::builder().gzip(true).build()?;
61
62 let totals = fetch_totals(&client)?;
63
64 if totals.total_for_channel(channel).is_none() {
65 return Err(Error::ParseError(format!(
66 "Unknown channel \"{}\". Valid channels: release, beta, nightly, esr",
67 channel
68 )));
69 }
70
71 let response = fetch_signature_correlations(&client, signature, channel)?;
72
73 let output = match format {
74 OutputFormat::Compact => {
75 let summary = response.to_summary(signature, channel, &totals);
76 compact::format_correlations(&summary)
77 }
78 OutputFormat::Json => json::format_correlations(&response)?,
79 OutputFormat::Markdown => {
80 let summary = response.to_summary(signature, channel, &totals);
81 markdown::format_correlations(&summary)
82 }
83 };
84
85 print!("{}", output);
86 Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn test_signature_hash() {
95 assert_eq!(
96 signature_hash("UiaNode::ProviderInfo::~ProviderInfo"),
97 "4361bb82d8d8c7f34466f8b7589fbd6c920da702"
98 );
99 }
100
101 #[test]
102 fn test_signature_hash_oom() {
103 let hash = signature_hash("OOM | small");
104 assert!(!hash.is_empty());
105 assert_eq!(hash.len(), 40);
106 }
107}