Skip to main content

tauri_plugin_better_posthog/
lib.rs

1//! Tauri integration with PostHog.
2//!
3//! This plugin wraps the [`better_posthog`] crate to provide seamless analytics integration for Tauri applications.
4//! It automatically handles:
5//!
6//! - User identity management with configurable strategies
7//! - Session tracking across the application lifecycle
8//! - Tauri-specific context injection (app name, version, webview info)
9//! - Both Rust backend and frontend JavaScript event capture
10//!
11//! # Prerequisites
12//!
13//! The consuming application must initialize the `better_posthog` global client **before** registering the plugin:
14//!
15//! ```ignore
16//! fn main() {
17//!   // Initialize PostHog client first.
18//!   let _guard = better_posthog::init(better_posthog::ClientOptions {
19//!     api_key: Some("phc_your_api_key".into()),
20//!     ..Default::default()
21//!   });
22//!
23//!   tauri::Builder::default()
24//!     .plugin(tauri_plugin_better_posthog::init())
25//!     .run(tauri::generate_context!())
26//!     .expect("error while running tauri application");
27//! }
28//! ```
29//!
30//! # Identity Strategies
31//!
32//! The plugin supports three identity strategies:
33//!
34//! - [`IdentityStrategy::Autogenerated`] (default): Persists a UUID v4 in the app data directory
35//! - [`IdentityStrategy::Custom`]: Use a developer-provided closure to resolve the user ID
36//! - [`IdentityStrategy::Anonymous`]: Each event gets a transient UUID v7
37//!
38//! # Example
39//!
40//! ```ignore
41//! use tauri_plugin_better_posthog::{Builder, IdentityStrategy, PostHogExt, PostHogEvent};
42//!
43//! // Custom identity from your user system.
44//! let plugin = Builder::new()
45//!   .identity(IdentityStrategy::Custom(Box::new(|app_handle| {
46//!     // Return user ID from your app's state.
47//!     Some("user_123".to_string())
48//!   })))
49//!   .build();
50//!
51//! // Capture events from Rust
52//! app.capture_event(MyCustomEvent { ... });
53//! ```
54
55mod commands;
56mod identity;
57mod state;
58
59pub use identity::IdentityStrategy;
60use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
61use tauri::{Manager, Runtime};
62
63/// Initializes the plugin with default settings.
64#[must_use]
65pub fn init<R: Runtime>() -> TauriPlugin<R> {
66  Builder::default().build()
67}
68
69/// Builder for configuring the PostHog plugin.
70pub struct Builder<R: Runtime> {
71  identity_strategy: IdentityStrategy<R>,
72}
73
74impl<R: Runtime> Builder<R> {
75  /// Creates a new plugin builder with default settings.
76  #[must_use]
77  pub fn new() -> Self {
78    Self::default()
79  }
80
81  /// Sets the identity strategy for user identification.
82  ///
83  /// See [`IdentityStrategy`] for available options.
84  #[must_use]
85  pub fn identity(mut self, strategy: IdentityStrategy<R>) -> Self {
86    self.identity_strategy = strategy;
87    self
88  }
89
90  /// Builds the plugin with the configured settings.
91  #[must_use]
92  pub fn build(self) -> TauriPlugin<R> {
93    PluginBuilder::new("better-posthog")
94      .invoke_handler(tauri::generate_handler![commands::capture, commands::batch])
95      .setup(move |app, _api| {
96        let distinct_id = self.identity_strategy.resolve(app);
97
98        let state = state::PluginState::new(distinct_id);
99        app.manage(state);
100
101        Ok(())
102      })
103      .build()
104  }
105}
106
107impl<R: Runtime> Default for Builder<R> {
108  fn default() -> Self {
109    Self {
110      identity_strategy: IdentityStrategy::default(),
111    }
112  }
113}
114
115/// Extension trait for capturing PostHog events.
116///
117/// This trait is automatically implemented for any type that implements [`Manager`](tauri::Manager).
118pub trait PostHogExt<R: Runtime> {
119  /// Captures an event defined via the [`PostHogEvent`] trait.
120  fn capture_event(&self, event: impl PostHogEvent);
121
122  /// Captures a batch of events efficiently.
123  fn batch_events(&self, events: &[impl PostHogEvent]);
124}
125
126impl<R: Runtime, T: Manager<R>> PostHogExt<R> for T {
127  fn capture_event(&self, event: impl PostHogEvent) {
128    self.batch_events(&[event]);
129  }
130
131  fn batch_events(&self, events: &[impl PostHogEvent]) {
132    let state = self.state::<state::PluginState>();
133    let package_info = self.package_info();
134
135    let distinct_id = state.distinct_id();
136    let session_id = state.session_id();
137
138    better_posthog::events::batch(
139      events
140        .iter()
141        .map(|event| {
142          let event_name = event.name();
143          let properties = event.properties();
144
145          #[allow(clippy::option_if_let_else)]
146          let mut event = match distinct_id {
147            Some(id) => better_posthog::Event::new(event_name, id),
148            None => better_posthog::Event::new_anonymous(event_name),
149          };
150
151          for (key, value) in properties {
152            event.insert_property(key, value);
153          }
154
155          event.insert_property("$session_id".to_string(), session_id);
156
157          event.insert_property("$app".to_string(), package_info.name.clone());
158          event.insert_property("$app_version".to_string(), package_info.version.to_string());
159
160          #[cfg(target_os = "windows")]
161          event.insert_property("$browser".to_string(), "webview2");
162          #[cfg(not(target_os = "windows"))]
163          event.insert_property("$browser".to_string(), "webkit");
164          event.insert_property("$browser_version".to_string(), tauri::webview_version().ok());
165
166          event
167        })
168        .collect(),
169    );
170  }
171}
172
173/// Trait for defining custom reusable PostHog events.
174///
175/// # Example
176///
177/// ```ignore
178/// use std::collections::HashMap;
179/// use tauri_plugin_better_posthog::PostHogEvent;
180///
181/// struct ButtonClick {
182///   button_id: String,
183///   page: String,
184/// }
185///
186/// impl PostHogEvent for ButtonClick {
187///   fn name(&self) -> &str {
188///     "button_click"
189///   }
190///
191///   fn properties(&self) -> HashMap<String, serde_json::Value> {
192///     let mut properties = HashMap::new();
193///     properties.insert("button_id".to_string(), self.button_id.clone().into());
194///     properties.insert("page".to_string(), self.page.clone().into());
195///     properties
196///   }
197/// }
198/// ```
199pub trait PostHogEvent {
200  /// Returns the name of the event.
201  fn name(&self) -> &str;
202
203  /// Returns the custom properties associated with the event.
204  fn properties(&self) -> std::collections::HashMap<String, serde_json::Value>;
205}