posthog_cli/
invocation_context.rs1use 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
20pub 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 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) .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); 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}