PostHog Analytics
Loquent uses PostHog for product analytics. The integration tracks page views, identifies users without PII, and captures custom events for key user actions — all through a Rust-to-JS interop layer built on Dioxus’s document::eval().
How it works
Section titled “How it works”Admin sets API key in System Config → Client fetches config via GET /api/analytics/config → AnalyticsProvider initializes PostHog SDK → User identified (member_id, org_id, role — no PII) → Page views tracked on every route change → Custom events fired from feature components → Logout calls posthog.reset()Analytics is disabled by default. If no API key is configured in core_conf, the config endpoint returns None and no JS loads.
Configuration
Section titled “Configuration”PostHog credentials are stored in the core_conf database table, managed through the admin System Config panel.
| Field | Column | Validation |
|---|---|---|
| API Key | posthog_api_key | Must start with phc_ |
| API Host | posthog_api_host | PostHog instance URL |
The PosthogCoreConf type implements the CoreConf trait to read these values:
pub struct PosthogCoreConf { pub posthog_api_key: String, pub posthog_api_host: String,}
impl CoreConf for PosthogCoreConf { fn from_row(row: core_conf::Model) -> Self { Self { posthog_api_key: row.posthog_api_key, posthog_api_host: row.posthog_api_host, } }}Config API
Section titled “Config API”GET /api/analytics/config — returns the PostHog config for client-side SDK initialization.
| Response | Meaning |
|---|---|
Some(PosthogConfig { api_key, api_host }) | Analytics enabled — initialize SDK |
None | Analytics disabled — no JS loaded |
AnalyticsProvider component
Section titled “AnalyticsProvider component”AnalyticsProvider lives in src/shared/analytics/analytics_provider.rs and mounts inside SessionContext in PrivateLayout. It handles three responsibilities:
- Initialize — fetches config, loads the PostHog JS snippet, calls
posthog.init()withcapture_pageview: false(manual tracking) - Identify — calls
posthog.identify()withmember_id,org_id, androle(owner/admin/member) - Track page views — uses
use_effectsubscribed touse_route::<Route>()to fire$pageviewon every SPA navigation
// Simplified — src/shared/analytics/analytics_provider.rs#[component]pub fn AnalyticsProvider(children: Element) -> Element { let config_resource = use_resource(|| async { get_posthog_config_api().await }); let session = use_context::<ClientSession>(); let current_route = use_route::<Route>(); let mut initialized = use_signal(|| false);
use_effect(move || { if let Some(Ok(Some(config))) = &*config_resource.read() { if !*initialized.peek() { init_posthog(&config.api_key, &config.api_host); identify_user(&session.member_id, &session.organization_id, role); initialized.set(true); } track_pageview(&format!("{}", current_route)); } });
children}JS interop functions
Section titled “JS interop functions”All functions in src/shared/analytics/posthog.rs are fire-and-forget — they use document::eval() to execute JS. The PostHog stub queues calls made before the library loads.
| Function | Purpose |
|---|---|
init_posthog(api_key, api_host) | Loads PostHog snippet and initializes with config |
identify_user(member_id, org_id, role) | Associates events with a user (no PII) |
reset_analytics() | Clears identity on logout |
track_event(name, properties) | Captures a custom event with JSON properties |
track_pageview(path) | Captures a $pageview event for SPA navigation |
String values are escaped with escape_js_string (from src/shared/utils/) or serialized via serde_json::to_string to prevent injection.
Custom events
Section titled “Custom events”These events are instrumented across the app:
| Event | Component | Properties |
|---|---|---|
contact_created | create_contact_view | — |
message_sent | message_compose_component | channel (type) |
call_initiated | outbound_call_context | — |
agent_configured | create_agent_view | — |
task_created | create_task_view, contact_tasks_tab | source |
knowledge_uploaded | create_knowledge_view | — |
report_viewed | report_details_view | — |
assistant_opened | assistant sidebar | — |
assistant_message_sent | assistant chat | — |
assistant_session_created | assistant chat | — |
Adding a new event
Section titled “Adding a new event”Use track_event from any component:
use crate::shared::analytics::posthog::track_event;use serde_json::json;
track_event("feature_used", &json!({ "feature": "bulk_import" }));Naming convention: snake_case, verb in past tense (e.g., contact_created, report_viewed). Never include PII (email, name, phone) in properties — use IDs only.
Privacy
Section titled “Privacy”The integration follows a strict no-PII policy:
- Sent:
member_id,organization_id,role, behavioral data (page views, feature usage) - Never sent: email, name, phone number, message content
posthog.reset()runs on logout to clear the identitypersistence: "localStorage"keeps the anonymous ID across page reloads