use dioxus::prelude::*;
use crate::analytics::event::{DEFAULT_HOST, Event};
#[derive(Clone, Copy)]
pub struct AnalyticsHandle {
config: Signal<AnalyticsConfig>,
}
impl AnalyticsHandle {
pub fn is_enabled(&self) -> bool {
self.config.read().api_key.is_some()
}
pub fn capture(&self, event: Event) {
if !self.is_enabled() {
return;
}
#[cfg(target_arch = "wasm32")]
{
let config = self.config.read();
let Some(key) = config.api_key.clone() else {
return;
};
let body = crate::analytics::event::capture_body(&key, &distinct_id(), &event);
let url = format!("{}/capture/", config.host.trim_end_matches('/'));
wasm_bindgen_futures::spawn_local(async move {
let _ = reqwest::Client::new().post(url).json(&body).send().await;
});
}
#[cfg(not(target_arch = "wasm32"))]
let _ = event;
}
}
#[component]
pub fn AnalyticsProvider(api_key: Option<String>, host: Option<String>, children: Element) -> Element {
let config = use_signal(|| AnalyticsConfig {
api_key,
host: host.unwrap_or_else(|| DEFAULT_HOST.to_string()),
});
use_context_provider(|| AnalyticsHandle { config });
rsx! {
{children}
}
}
pub fn use_analytics() -> AnalyticsHandle {
use_context::<AnalyticsHandle>()
}
#[derive(Clone, PartialEq)]
struct AnalyticsConfig {
api_key: Option<String>,
host: String,
}
#[cfg(target_arch = "wasm32")]
fn distinct_id() -> String {
use wasm_bindgen::JsCast;
fn doc() -> Option<web_sys::HtmlDocument> {
web_sys::window()?.document()?.dyn_into().ok()
}
let read = || -> Option<String> {
let cookie = doc()?.cookie().ok()?;
cookie.split(';').find_map(|pair| {
let (key, value) = pair.trim().split_once('=')?;
(key == "ev_did").then(|| value.to_string())
})
};
if let Some(existing) = read() {
return existing;
}
let id = format!(
"{:08x}{:08x}",
(js_sys::Math::random() * f64::from(u32::MAX)) as u32,
(js_sys::Math::random() * f64::from(u32::MAX)) as u32,
);
if let Some(document) = doc() {
let _ = document.set_cookie(&format!("ev_did={id}; path=/; max-age=31536000; samesite=lax"));
}
id
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analytics::test_util::render;
#[test]
fn provider_exposes_enabled_state() {
#[component]
fn Probe() -> Element {
let analytics = use_analytics();
let label = if analytics.is_enabled() { "on" } else { "off" };
rsx! {
span { {label} }
}
}
fn with_key() -> Element {
rsx! {
AnalyticsProvider { api_key: "phc_x".to_string(), Probe {} }
}
}
fn without_key() -> Element {
rsx! {
AnalyticsProvider { Probe {} }
}
}
assert!(render(with_key).contains("on"));
assert!(render(without_key).contains("off"));
}
#[test]
fn provider_renders_children_unchanged() {
fn app() -> Element {
rsx! {
AnalyticsProvider { api_key: "phc_x".to_string(),
div { "child-content" }
}
}
}
assert!(render(app).contains("child-content"));
}
#[test]
fn host_defaults_when_unset() {
#[component]
fn Probe() -> Element {
let analytics = use_analytics();
let host = analytics.config.read().host.clone();
rsx! {
span { {host} }
}
}
fn default_host() -> Element {
rsx! {
AnalyticsProvider { Probe {} }
}
}
fn explicit_host() -> Element {
rsx! {
AnalyticsProvider { host: "https://eu.i.posthog.com".to_string(), Probe {} }
}
}
assert!(render(default_host).contains(DEFAULT_HOST));
assert!(render(explicit_host).contains("https://eu.i.posthog.com"));
}
#[test]
fn capture_is_noop_on_native_without_panicking() {
#[component]
fn Probe() -> Element {
let analytics = use_analytics();
analytics.capture(Event::new("native_noop_with_key").prop("k", 1));
rsx! {
span { "rendered" }
}
}
#[component]
fn ProbeNoKey() -> Element {
let analytics = use_analytics();
analytics.capture(Event::new("native_noop_no_key"));
rsx! {
span { "rendered" }
}
}
fn with_key() -> Element {
rsx! {
AnalyticsProvider { api_key: "phc_x".to_string(), Probe {} }
}
}
fn without_key() -> Element {
rsx! {
AnalyticsProvider { ProbeNoKey {} }
}
}
assert!(render(with_key).contains("rendered"));
assert!(render(without_key).contains("rendered"));
}
}