use std::cell::RefCell;
use std::env::consts::{ARCH, OS};
use std::env::var;
use std::process::Command;
use std::str;
use std::sync::Mutex;
use anyhow::anyhow;
use lazy_static::lazy_static;
use maplit::hashmap;
use reqwest::Client;
use tracing::{debug, warn};
use uuid::Uuid;
static CIS: &'static [&str] = &[
"CI",
"CONTINUOUS_INTEGRATION",
"BSTRUSE_BUILD_DIR",
"APPVEYOR",
"BUDDY_WORKSPACE_URL",
"BUILDKITE",
"CF_BUILD_URL",
"CIRCLECI",
"CODEBUILD_BUILD_ARN",
"CONCOURSE_URL",
"DRONE",
"GITLAB_CI",
"GO_SERVER_URL",
"JENKINS_URL",
"PROBO_ENVIRONMENT",
"SEMAPHORE",
"SHIPPABLE",
"TDDIUM",
"TEAMCITY_VERSION",
"TF_BUILD",
"TRAVIS",
"WERCKER_ROOT"
];
pub enum MetricEvent {
ConsumerTestRun {
interactions: usize,
test_framework: String,
app_name: String,
app_version: String
},
ProviderVerificationRan {
tests_run: usize,
test_framework: String,
app_name: String,
app_version: String
}
}
impl MetricEvent {
pub(crate) fn app_name(&self) -> &str {
match self {
MetricEvent::ConsumerTestRun { app_name, .. } => app_name.as_str(),
MetricEvent::ProviderVerificationRan { app_name, .. } => app_name.as_str()
}
}
pub(crate) fn app_version(&self) -> &str {
match self {
MetricEvent::ConsumerTestRun { app_version, .. } => app_version.as_str(),
MetricEvent::ProviderVerificationRan { app_version, .. } => app_version.as_str()
}
}
pub(crate) fn test_framework(&self) -> &str {
match self {
MetricEvent::ConsumerTestRun { test_framework, .. } => test_framework.as_str(),
MetricEvent::ProviderVerificationRan { test_framework, .. } => test_framework.as_str()
}
}
pub(crate) fn name(&self) -> &str {
match self {
MetricEvent::ConsumerTestRun { .. } => "Pact consumer tests ran",
MetricEvent::ProviderVerificationRan { .. } => "Pacts verified"
}
}
pub(crate) fn category(&self) -> &str {
match self {
MetricEvent::ConsumerTestRun { .. } => "ConsumerTest",
MetricEvent::ProviderVerificationRan { .. } => "ProviderTest"
}
}
pub(crate) fn action(&self) -> &str {
match self {
MetricEvent::ConsumerTestRun { .. } => "Completed",
MetricEvent::ProviderVerificationRan { .. } => "Completed"
}
}
pub(crate) fn value(&self) -> String {
match self {
MetricEvent::ConsumerTestRun { interactions, .. } => interactions.to_string(),
MetricEvent::ProviderVerificationRan { tests_run, .. } => tests_run.to_string()
}
}
}
const GA_ACCOUNT: &str = "UA-117778936-1";
const GA_URL: &str = "https://www.google-analytics.com/collect";
lazy_static! {
static ref WARNING_LOGGED: Mutex<RefCell<bool>> = Mutex::new(RefCell::new(false));
}
pub fn send_metrics(event: MetricEvent) {
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
send_metrics_async(event).await
});
},
Err(err) => {
debug!("Could not get the tokio runtime, will not send metrics - {}", err)
}
}
}
pub async fn send_metrics_async(event: MetricEvent) {
if do_not_track() {
debug!("'PACT_DO_NOT_TRACK' environment variable is set, will not send metrics");
} else {
log_warning();
let ci_context = if CIS.iter()
.any(|n| var(n).map(|val| !val.is_empty()).unwrap_or(false)) {
"CI"
} else {
"unknown"
};
let osarch = format!("{}-{}", OS, ARCH);
let uid = hostname_hash();
let value = event.value();
let event_payload = hashmap! {
"v" => "1", "t" => "event", "tid" => GA_ACCOUNT, "cid" => uid.as_str(), "an" => event.app_name(), "aid" => event.app_name(), "av" => event.app_version(), "aip" => "true", "ds" => "client", "cd2" => ci_context, "cd3" => osarch.as_str(), "cd6" => event.test_framework(), "cd7" => env!("CARGO_PKG_VERSION"), "el" => event.name(), "ec" => event.category(), "ea" => event.action(), "ev" => value.as_str() };
debug!("Sending event to GA - {:?}", event_payload);
let result = Client::new().post(GA_URL)
.form(&event_payload)
.send()
.await;
if let Err(err) = result {
debug!("Failed to post event - {}", err);
}
}
}
fn log_warning() {
let mut guard = WARNING_LOGGED.lock().unwrap();
let warning_logged = (*guard).get_mut();
if *warning_logged == false {
warn!(
"\n\nPlease note:\n\
We are tracking events anonymously to gather important usage statistics like Pact version \
and operating system. To disable tracking, set the 'PACT_DO_NOT_TRACK' environment \
variable to 'true'.\n\n"
);
*warning_logged = true;
}
}
fn do_not_track() -> bool {
var("PACT_DO_NOT_TRACK")
.or_else(|_| var("pact_do_not_track"))
.map(|v| v == "true")
.unwrap_or(false)
}
fn hostname_hash() -> String {
let host_name = if OS == "windows" {
var("COMPUTERNAME")
} else {
var("HOSTNAME")
}.or_else(|_| {
exec_hostname_command()
}).unwrap_or_else(|_| {
Uuid::new_v4().to_string()
});
let digest = md5::compute(host_name.as_bytes());
format!("{:x}", digest)
}
fn exec_hostname_command() -> anyhow::Result<String> {
match Command::new("hostname").output() {
Ok(output) => if output.status.success() {
Ok(str::from_utf8(&*output.stdout)?.to_string())
} else {
Err(anyhow!("Failed to invoke hostname command: status {}", output.status))
}
Err(err) => Err(anyhow!("Failed to invoke hostname command: {}", err))
}
}