Tracking Events
All backend operations emit structured wide events to Axiom. Each event is a single JSON object capturing the full lifecycle of a unit of work — webhook receipt, cache invalidation batch, or deploy build.
Storefront per-request telemetry is not in scope. Only admin/system operations are instrumented.
SHOPIFY CF-TRK CF-CLIENT-DEPLOY─────── ────── ────────────────webhook ──▶ webhook:received ──▶ webhook:invalidate │ enrichAndEmit() │ └─ D1 lookup (page_deps) │ └─ affectedUrls[] │ deploy:started │ deploy:complete └─ attribution webhooks: skip Axiom
THEME WORKER (cf-client-site)──────────────────────────────render ──▶ DependencyTracker ──▶ flushDeps() ──▶ D1 page_deps tracks products, waitUntil() (shop_id, url, collections, pages res_type, res_id)Datasets
Section titled “Datasets”Two Axiom datasets, each with its own scoped API token:
| Dataset | Env Var | Workers | Purpose |
|---|---|---|---|
lk-event-logs | AXIOM_EVENT_API_TOKEN + AXIOM_EVENT_DATASET_NAME | cf-trk, cf-client-deploy | Wide operational events |
lk-build-logs | AXIOM_BUILD_API_TOKEN + AXIOM_BUILD_DATASET_NAME | cf-client-deploy (container) | Granular per-step build logs |
The event logs dataset captures one event per operation with scalar aggregates. The build logs dataset captures many events per build with per-step detail (see Build Logging).
Event shape
Section titled “Event shape”Every event follows the LKEvent interface from @lk/config/observability:
interface LKEvent { source: 'cf-trk' | 'cf-client-deploy' | 'cf-app' | 'agent' action: string // e.g. 'webhook:received', 'deploy:complete' clientId?: string // client slug when applicable correlationId: string // UUID for cross-referencing _time: string // ISO timestamp durationMs: number // wall-clock duration ok: boolean // false on error errors: string[] // error messages if !ok}Actions extend this with domain-specific fields.
Events reference
Section titled “Events reference”webhook:received (cf-trk)
Section titled “webhook:received (cf-trk)”Emitted for every Shopify webhook except attribution webhooks (orders/checkouts), which set _skip = true and are not sent to Axiom.
The webhook handler always returns HTTP 200 to Shopify regardless of internal errors — Shopify disables webhook endpoints after repeated non-200 responses.
Emission uses waitUntil() via enrichAndEmit() to avoid blocking the <100ms response requirement. Before emitting, the function performs an async D1 lookup against the page_deps table to resolve which storefront URLs would be affected by the resource change.
| Field | Type | Description |
|---|---|---|
shopId | string | Shopify myshopify domain |
topic | string | Shopify webhook topic (e.g. products/update) |
hmacValid | boolean | HMAC validation result |
handler | string | Resolved handler: attribution, deploy, invalidation, or ignored |
enqueued | boolean | Whether an invalidation event was queued |
deployBuildEnqueued | boolean | Whether a staging rebuild was triggered |
resType | string | Resource type: product, collection, theme, etc. |
resId | string | Shopify numeric resource ID (or * for nuclear) |
resTitle | string? | Resource title from webhook payload (e.g. product title) |
resHandle | string? | Resource handle from webhook payload |
affectedUrls | string[] | Storefront URLs that depend on this resource (from page_deps D1) |
webhook:invalidate (cf-trk)
Section titled “webhook:invalidate (cf-trk)”Emitted per queue batch. Purge failures are non-fatal — if the CDN purge API returns an error, the event is logged but the batch is not retried (content stays cached until TTL).
| Field | Type | Description |
|---|---|---|
batchSize | number | Raw messages in batch |
nuclearCount | number | Nuclear (full-shop) invalidations |
targetedCount | number | Targeted (per-resource) invalidations |
totalPurged | number | CDN cache entries purged |
totalKvEvicted | number | KV cache keys evicted |
skipped | number | Events skipped (unknown shop, no deps) |
deploy:started (cf-client-deploy)
Section titled “deploy:started (cf-client-deploy)”Beacon emitted immediately when a build begins. Fire-and-forget — no additional fields beyond LKEvent.
deploy:complete (cf-client-deploy)
Section titled “deploy:complete (cf-client-deploy)”Wide event emitted after the container stream finishes or errors.
| Field | Type | Description |
|---|---|---|
buildId | string | Build UUID |
httpStatus | number | Container response status code |
streamedOk | boolean | Whether NDJSON stream was received |
Dependency tracking
Section titled “Dependency tracking”The theme worker (cf-client-site) maintains a dependency map in D1 that records which Shopify resources each rendered page depends on. This map powers the affectedUrls field on webhook:received events and drives targeted cache invalidation.
How it works
Section titled “How it works”- During SSR, the
DependencyTrackerrecords every product, collection, page, blog, and article loaded from KV - After the response is sent,
flushDeps()writes the deps to thepage_depsD1 table viawaitUntil() - When a webhook fires,
enrichAndEmit()queriespage_depsto find which URLs reference the changed resource - The queue consumer (
processEvent) uses the same lookup to determine which URLs to purge from the CDN cache
D1 schema (layerkick-deps)
Section titled “D1 schema (layerkick-deps)”CREATE TABLE page_deps ( shop_id TEXT NOT NULL, url TEXT NOT NULL, res_type TEXT NOT NULL, res_id TEXT NOT NULL, PRIMARY KEY (shop_id, url, res_type, res_id));Each row maps a storefront URL to a single resource dependency. A page rendering 50 products creates 50 rows (plus collections, theme, etc.).
Path filtering
Section titled “Path filtering”Only real storefront routes are recorded. The worker validates the request path before flushing:
const isStorefrontPath = depPath === "/" || /^\/(collections|products|pages|blogs|cart|search)(\/|$)/.test(depPath);This prevents bot/scanner probes (/.env, /.git/config, encoded asset URLs) from polluting the dependency map.
Dashboard Activity tab
Section titled “Dashboard Activity tab”The dashboard (cf-app) consumes these Axiom events via the Activity tab. Events are fetched from the lk-event-logs dataset and displayed in a day-grouped feed.
For webhook:received events, the headline shows the resource title (e.g. “Product updated: Farm Suit Pro”) and the detail panel shows resource metadata, handle, ID, and the affected pages list from the dependency map.
The createObservability() factory from @lk/config/observability produces event() and emit() functions scoped to a source:
import { createObservability } from '@lk/config/observability'
const lk = createObservability('cf-trk', env)const ev = lk.event('webhook:received', { clientId: slug })
// ... enrich ev with domain fields ...
ev.ok = falseev.errors.push('something broke')
// emit computes durationMs and strips internal _start fieldawait lk.emit(ev)emit() swallows fetch errors — observability must never crash the operation it’s observing. In Workers, wrap the emit in waitUntil() when the response has already been sent.
Async enrichment pattern
Section titled “Async enrichment pattern”For webhook:received, emission is wrapped in enrichAndEmit() which runs a D1 lookup before emitting. The lookup is non-fatal — if it fails, the base event is emitted without affectedUrls:
async function enrichAndEmit(ev, db, lk) { try { if (ev.resType && ev.resId && ev.resId !== '*' && ev.shopId) { ev.affectedUrls = await lookupAffectedUrls(db, ev.shopId, ev.resType, ev.resId); } } catch { /* emit base event anyway */ } await lk.emit(ev);}What already exists (unchanged)
Section titled “What already exists (unchanged)”- Analytics Engine in cf-trk:
ANALYTICSbinding writesinvalidation_eventsvialogInvalidation(). Per-URL granular data for invalidation debugging. - Axiom build logs from Docker container:
scripts/lib/axiom.tsbuffers and flushes per-step build events tolk-build-logs. Node.js only, runs inside the container. - ClientHub DO + WebSocket: broadcasts NDJSON build events to dashboard viewers in real-time.
Key files
Section titled “Key files”| File | Purpose |
|---|---|
packages/config/src/observability/types.ts | LKEvent type, EventSource union |
packages/config/src/observability/emit.ts | createObservability() factory |
packages/cf-trk/src/webhooks/handler.ts | webhook:received event + enrichAndEmit() |
packages/cf-trk/src/webhooks/resource-map.ts | Topic → resource type mapping + ID extraction |
packages/cf-trk/src/queue/consumer.ts | webhook:invalidate event |
packages/cf-trk/src/queue/process-event.ts | Per-event processing (URL lookup → purge) |
packages/cf-trk/src/queue/purge.ts | lookupAffectedUrls() D1 query |
packages/cf-client-site/src/lib/dependency-tracker.ts | DependencyTracker class + flushDeps() |
packages/cf-client-deploy/src/worker.ts | deploy:started + deploy:complete events |
packages/cf-app/src/components/stats/activity-tab.tsx | Dashboard Activity tab UI |
packages/cf-app/src/components/stats/data.ts | ActivityEvent type + headline formatting |
packages/cf-app/src/lib/event-data.ts | Axiom query + response mapping |
packages/cf-trk/src/analytics/log.ts | Analytics Engine (unchanged) |
scripts/lib/axiom.ts | Build log buffer (unchanged) |