posthog_cli/
invocation_context.rs

1use anyhow::Result;
2use inquire::{
3    validator::{ErrorMessage, Validation},
4    CustomUserError,
5};
6use posthog_rs::Event;
7use reqwest::blocking::Client;
8use std::{
9    io::{self, IsTerminal},
10    sync::{Mutex, OnceLock},
11    thread::JoinHandle,
12};
13use tracing::{debug, info, warn};
14
15use crate::{
16    api::client::PHClient,
17    utils::auth::{env_id_validator, get_token, host_validator, token_validator},
18};
19
20// I've decided in my infinite wisdom that global state is fine, actually.
21pub static INVOCATION_CONTEXT: OnceLock<InvocationContext> = OnceLock::new();
22
23pub struct InvocationContext {
24    pub config: InvocationConfig,
25    pub client: PHClient,
26    pub is_terminal: bool,
27
28    handles: Mutex<Vec<JoinHandle<()>>>,
29}
30
31pub fn context() -> &'static InvocationContext {
32    INVOCATION_CONTEXT.get().expect("Context has been set up")
33}
34
35pub fn init_context(host: Option<String>, skip_ssl: bool) -> Result<()> {
36    let token = get_token()?;
37    let config = InvocationConfig {
38        api_key: token.token.clone(),
39        host: host.unwrap_or(token.host.unwrap_or("https://us.i.posthog.com".into())),
40        env_id: token.env_id.clone(),
41        skip_ssl,
42    };
43
44    config.validate()?;
45
46    let client: PHClient = PHClient::from_config(config.clone())?;
47
48    INVOCATION_CONTEXT.get_or_init(|| InvocationContext::new(config, client));
49
50    // This is pulled at compile time, not runtime - we set it at build.
51    if let Some(token) = option_env!("POSTHOG_API_TOKEN") {
52        let ph_config = posthog_rs::ClientOptionsBuilder::default()
53            .api_key(token.to_string())
54            .request_timeout_seconds(5) // It's a CLI, 5 seconds is an eternity
55            .build()
56            .expect("Building PH config succeeds");
57        posthog_rs::init_global(ph_config).expect("Initializing PostHog client");
58    } else {
59        warn!("Posthog api token not set at build time - is this a debug build?");
60    };
61
62    Ok(())
63}
64
65#[derive(Clone)]
66pub struct InvocationConfig {
67    pub api_key: String,
68    pub host: String,
69    pub env_id: String,
70    pub skip_ssl: bool,
71}
72
73impl InvocationConfig {
74    pub fn validate(&self) -> Result<()> {
75        fn handle_validation(
76            validation: Result<Validation, CustomUserError>,
77            context: &str,
78        ) -> Result<()> {
79            let validation = validation.map_err(|err| anyhow::anyhow!("{context}: {err}"))?;
80            if let Validation::Invalid(ErrorMessage::Custom(msg)) = validation {
81                anyhow::bail!("{context}: {msg:?}");
82            }
83            Ok(())
84        }
85
86        handle_validation(token_validator(&self.api_key), "Invalid Personal API key")?;
87        handle_validation(host_validator(&self.host), "Invalid Host")?;
88        handle_validation(env_id_validator(&self.env_id), "Invalid Environment ID")?;
89        Ok(())
90    }
91}
92
93impl InvocationContext {
94    pub fn new(config: InvocationConfig, client: PHClient) -> Self {
95        Self {
96            config,
97            client,
98            is_terminal: io::stdout().is_terminal(),
99            handles: Default::default(),
100        }
101    }
102
103    pub fn build_http_client(&self) -> Result<Client> {
104        let client = Client::builder()
105            .danger_accept_invalid_certs(self.config.skip_ssl)
106            .build()?;
107        Ok(client)
108    }
109
110    pub fn capture_command_invoked(&self, command: &str) {
111        let env_id = self.client.get_env_id();
112        let event_name = "posthog cli command run".to_string();
113        let mut event = Event::new_anon(event_name);
114
115        event
116            .insert_prop("command_name", command)
117            .expect("Inserting command prop succeeds");
118
119        event
120            .insert_prop("env_id", env_id)
121            .expect("Inserting env_id prop succeeds");
122
123        let handle = std::thread::spawn(move || {
124            debug!("Capturing event");
125            let res = posthog_rs::capture(event); // Purposefully ignore errors here
126            if let Err(err) = res {
127                debug!("Failed to capture event: {:?}", err);
128            } else {
129                debug!("Event captured successfully");
130            }
131        });
132
133        self.handles.lock().unwrap().push(handle);
134    }
135
136    pub fn finish(&self) {
137        info!("Finishing up....");
138
139        self.handles
140            .lock()
141            .unwrap()
142            .drain(..)
143            .for_each(|handle| handle.join().unwrap());
144
145        info!("Finished!")
146    }
147}