# How OpenWork's Event System and Analytics Collection Work: Architecture and Implementation

> Explore OpenWork's event system and analytics collection architecture. Discover its server-side reload event store and privacy-first PostHog client for efficient, secure data.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: architecture
- Published: 2026-07-31

---

**OpenWork implements a decoupled event system and privacy-first analytics collection using an in-memory reload event store server-side and a zero-dependency PostHog client that batches events every 10 seconds while never transmitting sensitive user content.**

OpenWork combines a lightweight event architecture with minimal analytics to keep the UI responsive and gather product metrics without compromising privacy. The system separates server-side reload events from client-side lifecycle notifications, while the analytics layer queues events in memory and flushes them to PostHog in configurable batches.

## Event System Architecture

### Server-Side Reload Event Store

At the core of OpenWork's event system is `ReloadEventStore`, implemented in [`apps/server/src/events.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/events.ts). This class maintains an in-memory circular buffer of reload events that signal when a workspace requires UI refresh.

The store records events with a unique `id`, sequential `seq`, `workspaceId`, `reason`, optional `trigger`, and timestamp. To prevent event flooding, the `recordDebounced` method implements a 750ms debounce window, ignoring rapid-repeat events.

```typescript
import { ReloadEventStore } from "./events";

const reloadStore = new ReloadEventStore(200); // Retains last 200 events

// Debounced recording prevents flooding
reloadStore.recordDebounced("workspace-123", "file_change");

```

Clients poll incrementally using `list(workspaceId, since?)`, which returns events newer than the provided cursor, and `cursor()`, which returns the latest sequence number for resume capability.

### Client-Side Event Dispatchers

The client layer uses standard DOM `CustomEvent` for decoupled communication.

**Provider Availability Events**
In [`apps/app/src/app/lib/provider-events.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/provider-events.ts), the system dispatches `openwork-new-providers-available` when AI providers update via cloud sync or local config:

```typescript
import { newProvidersEvent, dispatchNewProviders } from "./provider-events";

window.addEventListener(newProvidersEvent, (e) => {
  const detail = (e as CustomEvent).detail;
  console.log("Providers updated:", detail.providers);
});

```

**Session Lifecycle Events**
File [`apps/app/src/app/lib/den-session-events.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/den-session-events.ts) exports `denSessionUpdatedEvent` and `DenSessionUpdatedDetail` for broadcasting authentication state. Emitted when users sign in via Den, these events allow the analytics layer to link anonymous IDs to user accounts.

### Event Polling and UI Synchronization

The `session-group-event-poller` (located in the app renderer process) periodically polls `ReloadEventStore.list()` and transforms raw reload events into UI notifications. This polling architecture keeps the interface synchronized with server state without blocking the main thread.

## Analytics Collection Architecture

### Privacy-First Design Principles

OpenWork's analytics explicitly exclude sensitive data. The system never transmits message content, file paths, code, or prompts. Instead, it captures high-level metrics like event names, counts, durations, and coarse context (workspace type, provider IDs). Users can disable all network traffic via the `analyticsEnabled` preference, and an empty PostHog API key automatically disables transmission while maintaining local recording for testing.

### Core Analytics Module

The primary implementation resides in [`apps/app/src/app/lib/analytics.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/analytics.ts). The module initializes a memory-based queue that flushes under two conditions: when the queue reaches `MAX_BATCH` (50 events) or when the `FLUSH_INTERVAL_MS` timer fires (every 10 seconds).

```typescript
import { captureAnalyticsEvent, initAnalytics } from "./analytics";

initAnalytics();

captureAnalyticsEvent("task_started", {
  task_name: "generate_outline",
  workspace_id: "ws-42",
  duration_ms: 0,
});

```

The `captureAnalyticsEvent` function records events to both the local inspector (`window.__openwork.record`) and the queue. When flushing, `flushAnalytics()` sends batched POST requests to `https://us.i.posthog.com/batch/`. Failed transmissions are silently dropped to prevent UI disruption.

Each event merges with `baseProperties` containing `app_version` and `platform` identifiers. When `denSessionUpdatedEvent` fires with a successful sign-in, the system calls `identify(denUserId)`, linking the anonymous `distinct_id` (generated via `crypto.randomUUID()`) to the authenticated user for retention tracking.

### Configuration and Key Resolution

API key resolution occurs in [`apps/app/src/app/lib/analytics-key.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/analytics-key.ts). The `resolvePosthogKey` function checks `import.meta.env.DEV` to determine whether to use `VITE_OPENWORK_POSTHOG_KEY` (development) or the packaged production key. If resolution returns an empty string, analytics transmission disables entirely while preserving local inspection capabilities.

The host defaults to `https://us.i.posthog.com` unless overridden by `VITE_OPENWORK_POSTHOG_HOST`.

### Inspector Mirror for Testing

Every captured event flows through `recordInspectorEvent` to a local inspector bridge ([`apps/app/src/app/lib/app-inspector.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/app-inspector.ts)). This mirror enables end-to-end tests, such as those in `evals/flows/analytics-task-events.flow.mjs`, to assert event emission correctness without requiring live PostHog endpoints.

## Summary

- **ReloadEventStore** in [`apps/server/src/events.ts`](https://github.com/different-ai/openwork/blob/main/apps/server/src/events.ts) manages server-side workspace refresh events with 750ms debouncing and cursor-based incremental polling.
- **Client events** use DOM `CustomEvent` for provider updates ([`provider-events.ts`](https://github.com/different-ai/openwork/blob/main/provider-events.ts)) and session changes ([`den-session-events.ts`](https://github.com/different-ai/openwork/blob/main/den-session-events.ts)), enabling loose coupling between components.
- **Analytics** in [`apps/app/src/app/lib/analytics.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/app/lib/analytics.ts) implements a zero-dependency PostHog client with 50-event batching, 10-second flush intervals, and automatic identity linking via Den sessions.
- **Privacy controls** ensure no sensitive content leaves the client, with opt-out via `analyticsEnabled` or empty API keys.
- **Testing integration** through the inspector mirror allows validation of analytics flows without network dependencies.

## Frequently Asked Questions

### What triggers a reload event in OpenWork?

Reload events fire when the workspace state changes and requires UI refresh, such as after file modifications, sync operations, or specific user actions. The `ReloadEventStore.recordDebounced` method captures these with a 750ms debounce to prevent rapid successive updates from flooding the event stream.

### How does OpenWork protect user privacy in analytics?

OpenWork never transmits message content, code, file paths, or prompts. The analytics layer only captures high-level metadata like event names, counts, and durations. Users can disable all transmission via the `analyticsEnabled` preference, and the system defaults to local-only recording if the PostHog API key is empty.

### What is the batch size and flush interval for analytics events?

The analytics queue flushes when it accumulates 50 events (`MAX_BATCH`) or every 10 seconds (`FLUSH_INTERVAL_MS`), whichever occurs first. This batched approach minimizes network overhead while maintaining near real-time telemetry.

### How does OpenWork handle user identification across sessions?

When a user signs in via Den, the `denSessionUpdatedEvent` listener in the analytics module calls `identify(denUserId)`, linking the anonymous distinct ID generated via `crypto.randomUUID()` to the authenticated user account. This linkage enables DAU and retention tracking without exposing personal identifiers in event payloads.