Skip to main content

Offline Analytics

Offline Analytics extends Coniglio's event-tracking pipeline to capture reading events when users have no internet connection. Events are queued locally on the host application and synchronized in batch when connectivity is restored.

Linear Project: Offline Analytics

Problem

Before this feature, Coniglio operated as an online-only ingestion system. If a user read downloaded content in Fenice (mobile or desktop), all reading events for that session were lost. This affected:

  • Education tenants: students reading offline on transit or in areas without connectivity
  • Mobile users: Fenice app users reading downloaded content
  • Analytics accuracy: session and reading-time reports underrepresented actual usage

Architecture

The solution adds an offline queue layer in the mobile/desktop host (Fenice) between the reader (Volpe) and the analytics backend (Coniglio), using Delfino as the communication bridge. The web host (Farfalla) forwards events directly to Coniglio and does not implement an offline queue.

Component Responsibilities

ComponentRole
VolpeGenerates reading events, sends them to the host via Delfino RPC
DelfinoRPC bridge; analytics.track() handler routes events to the host
FarfallaHost (web); forwards events to Coniglio directly (no offline queue)
FeniceHost (mobile/desktop); queues events in local storage when offline, syncs on reconnect
ConiglioBackend; receives individual or batch event payloads, deduplicates, stores

Data Flow

┌─────────────────────────────────────────────────────────────┐
│ Volpe (iframe) │
│ │
│ reading event fires │
│ ↓ │
│ hostBridge.analytics.track(event) │
└────────────────────┬────────────────────────────────────────┘
│ Delfino RPC (MessageChannel)

┌─────────────────────────────────────────────────────────────┐
│ Host │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Farfalla (web) │ │ Fenice (mobile/desk.)│ │
│ │ │ │ │ │
│ │ Always online; │ │ Online? ──NO──▶ │ │
│ │ forwards events │ │ ┌──────────────┐ │ │
│ │ immediately │ │ │ Local queue │ │ │
│ │ │ │ │ (configurable│ │ │
│ │ │ │ │ retention) │ │ │
│ │ │ │ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ◄───────┘ on │ │
│ │ │ │ reconnect │ │
│ └────────┬─────────┘ └──────────┬───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ POST /track/* POST /track/* (single) │
│ or /track/batch (queued) │
└────────────────────┬────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Coniglio │
│ │
│ /track/batch (new endpoint) │
│ ↓ │
│ Validation + Deduplication (by event UUID) │
│ ↓ │
│ track_events → standard aggregation pipeline │
└─────────────────────────────────────────────────────────────┘

Key Design Decisions

  1. Volpe never talks to Coniglio directly. All events go through Delfino to the host. This decouples the reader from the analytics backend and enables the host to manage connectivity.

  2. Timestamps preserve the real event time. The timestamp field in the tracking payload reflects when the event occurred, not when it was synced. Coniglio uses this original timestamp for session aggregation.

  3. Configurable local retention. Queued events have a configurable time-to-live to prevent unbounded storage growth. Retention is intended to be tenant-configurable from Farfalla (pending full implementation); Fenice applies a local fallback until the tenant-level setting is available.

  4. Batch sync on reconnect. When connectivity is restored, the host sends accumulated events via POST /track/batch instead of individual requests, reducing HTTP overhead.

Events Tracked Offline

EventDescription
session-startUser opens the reader
session-resumeUser returns to reader within 30 minutes
session-endUser closes the reader
page-changeUser navigates to a different page
tab-hiddenReader goes to background
tab-visibleReader returns to foreground

Events NOT tracked offline:

  • heartbeat: sent only when the reader has an active connection; it is not queued locally.
  • Highlights, bookmarks, notes, and other user-generated content interactions: require immediate server sync for conflict resolution.

Delfino Analytics Module

Volpe sends events using the analytics domain in Delfino:

// Volpe side - sending a tracking event
await hostBridge.analytics.track({
eventName: 'session-start',
payload: {
sessionUuid: 'abc-123',
uuid: 'event-uuid-456',
timestamp: Date.now(),
index: 5,
schemaId: 1,
reader: 'volpe',
currentPage: 42,
lastPage: 200,
secondsReading: 120,
},
});

The host implements the analytics.track handler. On Farfalla (web) the handler forwards events directly to Coniglio. On Fenice (mobile/desktop) the handler checks connectivity and enqueues events locally when offline:

// Fenice host - receiving, routing, and queuing events
clientBridge.registerAnalyticsHandlers({
track: async ({ eventName, payload }): Promise<void> => {
if (isOnline()) {
await sendToConiglio(eventName, payload);
} else {
await offlineQueue.enqueue({ eventName, payload });
}
},
});

See the Delfino Architecture doc for the full TrackingPayload and TrackingEventName type definitions.

Coniglio Batch Endpoint

POST /api/v1/track/batch

Accepts an array of tracking events in a single request. Each event in the array follows the same schema as individual POST /track/* requests.

Deduplication: Events include a uuid field. Coniglio uses this to prevent double-counting when the same event is sent both individually (if connectivity was intermittent) and in a batch sync.

Processing: Batch events enter the same validation and storage pipeline as individual events (ValidateTrackRequestTrackEventStorertrack_events table). The aggregation scheduler processes them in subsequent windows.

Host Queue Implementation

Only Fenice implements a local offline queue. Farfalla (web) forwards events directly to Coniglio and has no queuing layer.

Fenice (Mobile/Desktop)

  • Storage: AsyncStorage (React Native) or SQLite
  • Sync trigger: Network state change detection
  • Precompute: Fenice performs heartbeat precomputation on-device to reduce the volume of events sent in bulk
  • Session management: Offline sessions use locally generated session_uuid values
X

Graph View