Skip to content

Analytics Infrastructure โ€‹

A guide to how Lantern's analytics system is architected โ€” from the shared event registry through client and server SDKs to Firestore and BigQuery.


Overview โ€‹

Lantern uses a monoconfig pattern: all event definitions live in one shared package consumed by every layer of the stack. This means a single source of truth drives event validation on the client, the API, and the admin UI simultaneously.

@lantern/shared/analytics   โ† Single source of truth
        โ†“                          โ†“
 Flash SDK (browser)         Forge SDK (backends)
        โ†“                          โ†“
 Analytics Cloud Run       Direct write (no HTTP hop)
 (proxy + auth gate)               โ†“
        โ†“               BigQuery  (default)
     Forge SDK          Firestore + BigQuery  (destination: 'realtime')
        โ†“
 Firestore + BigQuery

The analytics Cloud Run is a proxy for browser events only. Browsers can't authenticate with BigQuery or Firestore using service accounts, so Flash events are relayed through the analytics API, which uses Forge internally to write them. Backend services (Cloud Run APIs, Cloud Functions) use Forge directly โ€” no HTTP hop needed.

SDK Roles โ€‹

SDKWhereWrites to
FlashBrowserโ†’ Analytics Cloud Run โ†’ Forge โ†’ Firestore + BigQuery
ForgeBackend servicesโ†’ BigQuery (default), or Firestore + BigQuery with realtime

The Monoconfig โ€” @lantern/shared/analytics โ€‹

File: packages/shared/analytics/index.js

This is the only place where events are defined. All other code imports from here.

What it contains โ€‹

ExportPurpose
EVENT_REGISTRYAll 24+ built-in events (auto + registered) with full metadata
DYNAMIC_EVENT_REGISTRYIn-memory registry for custom events loaded from Firestore at runtime
ENTITY_TYPESValid entity type strings (venue, offer, lantern, wave, chat, feature)
PARAMETER_TEMPLATESReusable parameter definitions (entityId, entityType, metadata, reason, etc.)
EVENT_NAMING_RULESNaming constraints (snake_case, max 64 chars)
validateEventName()Gate that rejects unregistered or non-active events
getEventDefinition()Lookup by name โ€” checks dynamic registry first, then built-ins
registerRemoteEvent()Loads a Firestore event definition into DYNAMIC_EVENT_REGISTRY
defineEvent()Strictly-validated API for registering new custom events

Event tiers โ€‹

  • auto โ€” Fired automatically by Flash (session_start, page_view). No manual call needed.
  • registered โ€” Pre-defined events with a specific intent (lantern_lit, wave_sent, etc.). Call flash.track() or forge.track() explicitly.

Event status lifecycle โ€‹

pending โ†’ submitted โ†’ in_progress โ†’ installed โ†’ active โ†’ archived
                                                    โ†‘
                         Only these two are trackable

Events not in active or installed status are rejected at validation time. Manage lifecycle in the admin Registered Events UI.


Client SDK โ€” Flash โ€‹

File: apps/web/src/lib/flash.js

The browser-side analytics SDK. Imports the shared taxonomy and adds:

  • Batching โ€” Up to 10 events buffered, flushed every 30 seconds or on page hide
  • Auto events โ€” session_start and page_view fire automatically on lifecycle hooks
  • Auth attachment โ€” Firebase auth token attached; userId resolved server-side
  • Client-side sanitization โ€” PII stripped before events leave the browser
  • Custom event sync โ€” Loads Firestore-registered custom events at startup

Usage โ€‹

js
import { flash } from '@/lib/flash'

// Simple
flash.track('lantern_lit')

// With entity context
flash.track('lantern_lit', {
  entityId: '<venue_id>',
  entityType: 'venue',
})

// With metadata
flash.track('offer_claimed', {
  entityId: '<offer_id>',
  entityType: 'offer',
  metadata: {
    discount_pct: 20,
    merchant_id: '<merchant_id>',
  },
})

Only events in VALID_EVENT_NAMES (built-ins) or DYNAMIC_EVENT_REGISTRY (custom, loaded from Firestore) are accepted. Unregistered events are rejected.


Server SDK โ€” Forge โ€‹

Package: packages/forge/ (@lantern/forge)

Used by Cloud Run APIs and Cloud Functions to emit events from the backend.

Pipeline โ€‹

forge.track(payload)
  โ†’ validate(eventName, userId/serviceId, entityType)
  โ†’ checkRateLimit(userId)          // 30 events/min per user
  โ†’ sanitizeMetadata(metadata)       // strip PII, enforce limits
  โ†’ writeToBigQuery()                // always
  โ†’ writeToFirestore()               // only if destination: 'realtime'

Usage โ€‹

js
import { forge } from '@lantern/forge'

// System/service event โ€” BigQuery only (default)
await forge.track({
  serviceId: 'venues-api',
  eventName: 'venue_searched',
  entityType: 'venue',
  metadata: { result_count: 12, city: 'Austin' },
})

// User event with real-time presence โ€” Firestore + BigQuery
await forge.track({
  userId: '<firebase_uid>',
  eventName: 'lantern_lit',
  entityId: '<venue_id>',
  entityType: 'venue',
  metadata: { source: 'map' },
}, { destination: 'realtime' })

Destination options โ€‹

OptionBehaviour
'analytics' (default)BigQuery only. For server-to-server, system, or high-frequency events.
'realtime'Firestore + BigQuery. For user-facing events that need live dashboards.

Rate limiting โ€‹

  • 30 events per user per minute (sliding window)
  • Per-Cloud-Run-instance only (in-memory). For multi-instance distributed limiting, Redis/Firestore would be needed.
  • Service events (serviceId) are not rate-limited.

Metadata sanitization โ€‹

  • Max 50 keys, max 500 chars per string value
  • Only primitives allowed (string, number, boolean) โ€” no nested objects
  • Forbidden keys stripped automatically: email, phone, name, address, password, token, secret, apikey, credential, session, and ~30 more

Data Routing โ€‹

When creating a new event in the admin, choose the Data Destination:

OptionBehaviour
RealtimeWrites to Firestore โ†’ automatically forwarded to BigQuery. Best for user-facing events where you need live dashboards.
Analytics OnlyWrites directly to BigQuery, skips Firestore. Best for server-to-server or high-frequency events that don't need real-time presence.

The data destination setting maps directly to Forge's destination option:

  • Realtime events โ†’ { destination: 'realtime' }
  • Analytics-only events โ†’ default ({ destination: 'analytics' })

Firestore Storage โ€‹

Collection: analytics_events

FieldTypeNotes
eventNamestringFrom taxonomy
eventTierstringauto or registered
userIdstring?Firebase UID
serviceIdstring?Backend service identifier
entityIdstring?Related entity ID
entityTypestring?venue, offer, lantern, etc.
metadatamap?Sanitized event-specific data
environmentstringproduction or development
createdAttimestampServer timestamp
expiresAttimestamp90-day TTL (Firestore TTL policy)

BigQuery Storage โ€‹

Dataset: analytics โ†’ Table: events

  • Partitioned by timestamp (daily)
  • Expiration: 90 days per partition
  • Schema: See tooling/schemas/bigquery-events.json
  • Views: recent_user_events (last 7d, userId not null), recent_system_events (last 7d, serviceId not null)

Setup โ€‹

Run once per Firebase project (dev + prod):

bash
./tooling/scripts/setup-bigquery.sh

Requires the bq CLI and appropriate GCP credentials.


Admin UI โ€‹

The admin Analytics section lives at /analytics/ and provides:

RouteComponentPurpose
/analytics/overviewAnalyticsOverviewThis documentation
/analytics/events/taxonomyEventTaxonomyBrowse all events (built-in + custom)
/analytics/events/createEventCreatorRegister and manage custom events
/analytics/events/infoEventTrackingInfoEvent tracking reference

Creating a new event โ€‹

  1. Go to Registered Events โ†’ + New Event
  2. Fill in Basics (name, category, description, trigger, source, entity types)
  3. Add Parameters if the event carries data
  4. Set Data Destination (Realtime or Analytics Only)
  5. In Ticket/Task, submit to GitHub to create an implementation issue
  6. Develop the tracking call, mark as Installed, then Active
  7. Only active and installed events are accepted by the validation gate

Mirroring built-in events to Firestore โ€‹

To allow editing built-in events from the admin UI:

bash
node tooling/scripts/sync-events-to-firestore.mjs --dry-run   # preview
node tooling/scripts/sync-events-to-firestore.mjs             # write (skip existing)
node tooling/scripts/sync-events-to-firestore.mjs --force     # overwrite all

Built-in events in Firestore are marked built-in in the taxonomy table. Edits made in the admin take precedence over the code defaults at runtime.


API Reference โ€‹

The analytics Cloud Run service exposes a REST API. The OpenAPI spec is served at /openapi.json on each deployment; interactive docs are rendered by the Lantern admin portal under API Reference โ†’ Analytics:

  • Dev spec: https://analytics-api-dev.ourlantern.app/openapi.json
  • Prod spec: https://analytics-api.ourlantern.app/openapi.json

Key endpoints:

MethodPathDescription
POST/analytics/trackTrack a single event
POST/analytics/batchTrack up to 10 events
GET/analytics/eventsList registered custom events
GET/openapi.jsonOpenAPI spec

Built with VitePress