Skip to main content

socorro_cli/commands/
correlations.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use 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}