# FluidVoice Analytics Service Architecture and Event Tracking Implementation

> Explore the FluidVoice analytics service architecture and discover its event tracking implementation. Understand how this privacy-focused pipeline dispatches and flushes data to PostHog.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: architecture
- Published: 2026-07-03

---

**FluidVoice implements a privacy-focused, client-side analytics pipeline using a singleton service facade that dispatches events to an actor-based core, which buffers and flushes data to PostHog in fire-and-forget batches.**

FluidVoice is a macOS dictation application that collects usage metrics through a lightweight, entirely client-side architecture. The system leverages Swift's actor model to ensure thread-safe event queuing without blocking the UI thread, while respecting user consent through a persistent opt-out mechanism. This implementation is contained within the `altic-dev/FluidVoice` repository and utilizes strongly-typed events with a bounded in-memory queue.

## Core Architecture Components

The analytics stack consists of several coordinated components that handle everything from consent management to network transmission.

### AnalyticsService (Singleton)

The **`AnalyticsService`** serves as the public façade throughout the application. Located in [`Sources/Fluid/Analytics/AnalyticsService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Analytics/AnalyticsService.swift), this singleton manages the user's consent status, builds common properties (app version, OS version, hardware snapshot), and forwards events to the core processing layer. It checks the **SettingsStore** for the "Share Anonymous Analytics" flag before transmitting any data and generates an anonymous install identifier via **AnalyticsIdentityStore**.

### AnalyticsCore (Actor)

Within the same file, a **`private actor AnalyticsCore`** runs off the main thread to prevent UI blocking. The actor maintains a bounded in-memory queue with a capacity of **200 events** (`maxQueuedEvents`). It triggers a flush when the queue reaches **20 events** (`flushAt`) or when a periodic timer fires every **30 seconds** (`flushIntervalSeconds`). The core constructs JSON batches and dispatches them to the PostHog endpoint using a fire-and-forget `URLSession` request with an **8-second timeout**.

### AnalyticsConfig

The **`AnalyticsConfig`** struct, defined in [`Sources/Fluid/Analytics/AnalyticsConfig.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Analytics/AnalyticsConfig.swift), reads PostHog credentials from the app's `Info.plist` bundle. It provides a default EU-based host and exposes an `isConfigured` property to verify that valid API keys are present before initialization.

### AnalyticsEvent Enum

All event names are strongly typed in **`AnalyticsEvent`** (located in [`Sources/Fluid/Analytics/AnalyticsEvent.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Analytics/AnalyticsEvent.swift)). This enumeration guarantees low-cardinality schema consistency across the codebase, preventing typos in event strings and enabling compile-time checking.

### Consent and Identity Management

User preferences are persisted in **[`SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsStore.swift)**, which maintains the opt-out toggle. The **AnalyticsIdentityStore** generates a stable `anonymousInstallID` for each installation, ensuring unique user tracking without collecting personally identifiable information.

## Data Flow from Capture to PostHog

The pipeline operates through four distinct phases from application startup to network transmission.

1. **Bootstrap** – During app initialization, `AnalyticsService.bootstrap()` is called (typically from [`AppDelegate.swift`](https://github.com/altic-dev/FluidVoice/blob/main/AppDelegate.swift)). This method reads the consent flag, loads the configuration, and starts the core's flush loop only if analytics are enabled.

2. **Capture** – Any component calls `AnalyticsService.shared.capture(event, properties:)`. The method constructs common low-cardinality properties, merges caller-provided context, verifies consent, and dispatches an asynchronous task to `AnalyticsCore.capture`.

3. **Core Handling** – The actor stores the event in its bounded queue. When the batch threshold or timer triggers, the core drains the queue and calls `sendBatch`.

4. **Network Transmission** – The `sendBatch` method constructs a JSON payload containing the API key, host, and event batch, then executes a **detached background task** to POST to `/batch`. No response is awaited, keeping the UI completely non-blocking.

## Complete List of Tracked Events

The **`AnalyticsEvent`** enum enumerates all trackable interactions, organized by functional category.

**App Lifecycle Events:**
- `app_first_open` – Recorded on the application's first launch
- `app_open` – Recorded on subsequent launches
- `analytics_consent_changed` – Fired when the user toggles the anonymous analytics switch in [`SettingsView.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsView.swift)

**Dictation and Transcription Events:**
- `transcription_completed` – A dictation run finishes (captured in `ASRService`)
- `transcription_chunk_processed` – A streaming audio chunk is processed
- `dictation_post_processing_completed` – AI post-processing finishes
- `output_delivered` – Final text is displayed or copied to clipboard
- `post_transcription_edit` – User edits a completed transcription (tracked by `PostTranscriptionEditTracker`)

**Mode-Specific Events:**
- `command_mode_run_completed` – A command-mode interaction ends (from [`CommandModeService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/CommandModeService.swift))
- `rewrite_run_completed` – A rewrite operation finishes (from [`RewriteModeService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/RewriteModeService.swift))
- `meeting_transcription_completed` – A multi-speaker meeting transcription ends (from [`MeetingTranscriptionService.swift`](https://github.com/altic-dev/FluidVoice/blob/main/MeetingTranscriptionService.swift))
- `custom_prompt_used` – A user-defined prompt is executed

**Error Tracking:**
- `error_occurred` – Captures unexpected errors from ASR, LLM, or hotkey subsystems

## Code Examples: Emitting Events

Developers integrate analytics by calling the singleton with strongly-typed events and optional property dictionaries.

Record a completed dictation with duration and word count buckets:

```swift
// From ContentView.swift
AnalyticsService.shared.capture(
    .transcriptionCompleted,
    properties: [
        "duration_seconds": duration,
        "words_bucket": AnalyticsBuckets.bucketWords(wordCount),
        "audio_duration_bucket": AnalyticsBuckets.bucketSeconds(audioDuration)
    ]
)

```

Track a rewrite operation with latency metrics:

```swift
// From RewriteModeService.swift
AnalyticsService.shared.capture(
    .rewriteRunCompleted,
    properties: [
        "mode": AnalyticsMode.rewrite.rawValue,
        "method": AnalyticsOutputMethod.typed.rawValue,
        "latency_bucket": AnalyticsBuckets.bucketSeconds(Date().timeIntervalSince(startTime))
    ]
)

```

Log consent changes with the new state:

```swift
// From SettingsView.swift
AnalyticsService.shared.capture(
    .analyticsConsentChanged,
    properties: ["enabled": enabled]
)

```

## Summary

- **FluidVoice** uses a client-side **AnalyticsService** singleton that dispatches to an **actor-based core** for thread-safe, non-blocking operation.
- The system respects user privacy through a **SettingsStore**-backed opt-out mechanism and **anonymousInstallID** tracking.
- Events are strongly typed via the **AnalyticsEvent** enum and flushed to **PostHog** in batches of 20 or every 30 seconds.
- The architecture supports **14 distinct event types** covering app lifecycle, dictation flows, command modes, and error conditions.
- All network requests are **fire-and-forget** with 8-second timeouts to ensure zero UI latency.

## Frequently Asked Questions

### How does FluidVoice handle user consent for analytics?

The service checks a persistent flag stored in **[`SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsStore.swift)** before transmitting any events. When the user toggles the "Share Anonymous Analytics" switch in the settings UI, the app emits an `analytics_consent_changed` event and immediately respects the new preference, halting or resuming the flush loop accordingly.

### What are the queue limits and flush intervals?

The **AnalyticsCore** actor maintains a bounded queue capable of holding **200 events**. It automatically flushes when the queue reaches **20 events** or when **30 seconds** have elapsed since the last transmission, whichever occurs first. This prevents memory bloat while ensuring timely data delivery.

### Which analytics provider does FluidVoice use?

The pipeline sends data to **PostHog** using the standard `/batch` endpoint. Configuration including the API key and host URL is read from `Info.plist` via **[`AnalyticsConfig.swift`](https://github.com/altic-dev/FluidVoice/blob/main/AnalyticsConfig.swift)**, with a default EU host provided for GDPR compliance.

### How are events structured to maintain data quality?

All event names are defined as cases in the **`AnalyticsEvent`** enum, ensuring compile-time safety and low-cardinality schemas. The **AnalyticsService** automatically injects common properties (app version, OS version, hardware model) into every payload, while callers provide only context-specific data through strongly-typed dictionaries.