How OpenWork's Event System and Analytics Collection Work: Architecture and Implementation
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. 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.
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, the system dispatches openwork-new-providers-available when AI providers update via cloud sync or local config:
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 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. 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).
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. 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). 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.tsmanages server-side workspace refresh events with 750ms debouncing and cursor-based incremental polling. - Client events use DOM
CustomEventfor provider updates (provider-events.ts) and session changes (den-session-events.ts), enabling loose coupling between components. - Analytics in
apps/app/src/app/lib/analytics.tsimplements 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
analyticsEnabledor 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →