|

Analytics architecture and workflows

Keystone lists, how a hit becomes a visit, the report engine, goals, funnels, segments, public and shared dashboards, imports, deletion, GraphQL operations, and access control.

Application surfaces

SurfaceLocationNotes
Trackerpublic/assets/script.jsServed at /assets/script.js; posts to /api/track
Collectorapp/api/track/route.ts, features/keystone/lib/analytics/collector.tsValidates a hit and writes at most one visit update and one event
Report enginefeatures/keystone/lib/analytics/engine.ts, features/lib/analytics/*SQL reports, dimensions, filters, periods
Operator dashboardapp/dashboard/(admin)/platform/**, features/platformSites, Shared Links, Store Settings, API Keys
Generic model pagesapp/dashboard/(admin)/[listKey]/**, features/dashboardKeystone list and item screens for every list
Public siteapp/(storefront)/(main)/**, features/storefrontPublic dashboard list, /sites/[slug], /share/[slug]
GraphQL/api/graphqlKeystone CRUD where access allows, plus custom queries and mutations
Other HTTP/api/mcp-transport/[transport], /api/completionMCP tools and dashboard AI chat

Data model

Lists live in features/keystone/models.

ListWhat it holds
Sitename, domain (unique hostname; subdomains count), slug (public address), publicId (generated site_... id used by the tracker, read-only), timezone, currency, status (active / paused / archived), isPublic, description
AnalyticsSessionOne visit: hashed visitorId, optional distinctId, hostname, entry and exit path, referrer, five UTM fields, browser, OS, device, screen class, language, country, region, city, pageviews, customEvents, durationSeconds, isBounce, startedAt, lastSeenAt
AnalyticsEventOne hit: type (pageview / custom / vitals), name, path, title, referrer, properties JSON, revenueAmount and revenueCurrency, lcp, inp, cls, fcp, ttfb, occurredAt, and a unique eventKey for idempotency
Goaltype pageview with a pathPattern starting with / (* matches anything), or type event with an eventName
Funnel, FunnelStepAn ordered list of goals from the same site; steps carry a position
SegmentA named, saved list of filters (dimension, operator, value)
SharedLinkA private read-only link: generated slug, optional bcrypt password, isActive, expiresAt, viewCount, lastViewedAt, createdBy, and a virtual isUsable
StoreSettingsSingleton with the name, tagline, description, website, contact email, and sanitized SVG logo shown on the public site
User, Role, ApiKeyPeople, role permission checkboxes, and scoped API keys

Site relationships to visits, events, goals, funnels, segments, and shared links are read-only from the site side; you create those records against the site.

Workflow: a hit becomes a visit

  1. The script sends { type, payload } with the site's publicId, page URL (hash removed), referrer, title, screen size, language, timezone, and an optional identity set by identify().
  2. /api/track rejects bodies over 32 KB and non-JSON bodies, and ignores bots by User-Agent (an empty User-Agent counts as a bot).
  3. The site is looked up by publicId. If the request has an Origin header, its hostname must be the site's domain or a subdomain, or the request gets a 403.
  4. Hits for sites that are not active, or whose URL hostname is outside the domain, are accepted and ignored (202).
  5. The visitor hash is sha256(daily salt, site, IP, User-Agent). The daily salt comes from SESSION_SECRET and the UTC date. The IP is taken from the first X-Forwarded-For entry, then X-Real-IP.
  6. Inside a transaction with an advisory lock per visitor, an open visit (last seen within 30 minutes) is extended, or a new visit is created with attribution, device, language, and country.
  7. One AnalyticsEvent row is written. If the sender gave an eventId, a repeat with the same id returns duplicate: true and writes nothing.

Custom event properties are limited to 30 keys and 8 KB. Keys that look like password, secret, token, cookie, card, cvv, ssn, or email are refused. Revenue must be a decimal amount, and its currency must match the site's currency.

Workflow: reading reports

Every report takes a period (today, yesterday, 24h, 7d, 30d, month, last_month, 6mo, 12mo, or custom with from and to; the default is 30d) in the site's timezone, and up to ten filters. Filter dimensions are page, entry_page, exit_page, hostname, event, source, utm_source, utm_medium, utm_campaign, utm_content, utm_term, country, region, city, browser, os, device, screen, and language; operators are is, is_not, and contains. In the dashboard, clicking a row adds a filter, and the filters can be saved as a Segment.

  • Overview: visitors, visits, pageviews, views per visit, bounce rate, and visit duration, with the previous period and a time series.
  • Breakdown: top values of one dimension, up to 100 rows per page, with optional search.
  • Realtime: visitors active in the last five minutes and pageviews per minute for the last thirty. The dashboard page refreshes every 15 seconds.
  • Visits: a paged list of visits and one visit's event timeline.
  • Goals: conversions, conversion rate, and revenue per goal.
  • Funnels: visits that reached each step in order within one visit.
  • Events: custom events and their property values.
  • Performance: p75 LCP, INP, CLS, FCP, and TTFB, overall and per page.

Workflow: goals, funnels, and segments

  1. Create goals on the site's Goals tab (createGoal). Deleting a goal also deletes the funnel steps that use it.
  2. Build a funnel on the Funnels tab from at least two goals (createFunnel, then createFunnelSteps). A step must use a goal from the funnel's own site.
  3. Save the current filters as a segment from the dashboard filter bar (createSegment). A segment needs at least one valid filter.
  • Public dashboard: turn on isPublic for a site in Settings (needs canManageSharedLinks). The site then appears on / and at /sites/<slug>. Archived sites are never shown.
  • Shared link: create one in Settings with a name and optional password of at least 8 characters. The link is /share/<slug>, where the slug is a generated secret. Turn it off with isActive or delete it. expiresAt can be set through GraphQL or the generic SharedLink page.
  • Password unlock: unlockSharedLink checks the password and returns an HMAC token. The public site stores it in an httpOnly cookie scoped to /share/<slug> for 30 days. Changing the password invalidates the token.
  • What viewers see: publicDashboard serves only aggregate reports (site, overview, breakdown, realtime, goals, funnels, funnel, vitals). Visit lists and visit timelines are never public. Each site call through a shared link adds one to viewCount. Shared-link pages are marked noindex.

Workflow: import and deletion

  • importSiteSessions(siteId, sessions) takes 1 to 500 visits per call, each with a key and 1 to 50 events. Ids are derived from the site and key, so re-sending the same keys is skipped. Onboarding uses this for demo traffic.
  • deleteVisitorData(siteId, distinctId) deletes every visit and event recorded under an identifier the site sent with identify(). It does not find visits without a distinctId.
  • deleteSiteWithData(siteId) deletes a site with its events, visits, funnel steps, funnels, goals, segments, and shared links in one transaction. The dashboard asks you to type the site name first.

Custom GraphQL operations

Queries:

OperationPurpose
siteOverview(siteId, period, from, to, filters)Headline totals, previous period, time series
siteBreakdown(siteId, dimension, ..., limit, offset, search)Top values of one dimension
siteRealtime(siteId)Live visitors and per-minute pageviews
siteSessions(siteId, ..., limit, offset), siteSession(siteId, sessionId)Visit list and one visit's timeline
siteGoals(siteId, ...)Goal conversions and revenue
siteFunnel(funnelId, ...)Funnel steps and drop-off
siteVitals(siteId, ...)Core Web Vitals at p75
siteEventProperties(siteId, eventName, ...)Property values for one custom event
siteSummariesEvery readable site with 30-day visitors and live visitors
publicSitesPublic sites with 30-day visitors, for the home page
publicDashboard(siteSlug or shareSlug, token, report, ...)Aggregate report for a public site or shared link
redirectToInitWhether no users exist yet

Mutations: importSiteSessions, deleteVisitorData, deleteSiteWithData, unlockSharedLink, ensureOnboardingRecord, updateActiveUser. All report results are returned as JSON, so a client must validate their shape itself.

The dashboard also uses Keystone CRUD operations: createSite, updateSite, createGoal, deleteGoal, createFunnel, createFunnelSteps, deleteFunnel, deleteFunnelSteps, createSegment, deleteSegment, createSharedLink, updateSharedLink, deleteSharedLink, createStoreSettings, updateStoreSettings, and the ApiKey list operations.

Access control

Dashboard visibility is not the control. Every list and custom operation checks role permissions on the server.

  • Site, AnalyticsSession, AnalyticsEvent, Goal, Funnel, FunnelStep, and Segment: read with canReadSites; create, update, and delete with canManageSites.
  • Site.isPublic and all of SharedLink: canManageSharedLinks.
  • StoreSettings: anyone can read; changes need canManageSettings.
  • Role: anyone can read; changes need canManageRoles. User: signed-in users see themselves, or everyone with canSeeOtherPeople; role and active status need canManagePeople.
  • ApiKey: a signed-in human can manage only their own keys. API-key sessions cannot manage keys. Scopes must already be granted by the owner's role: read_analytics maps to canReadSites, write_sites to canReadSites and canManageSites, write_shared_links to canReadSites and canManageSharedLinks.
  • Custom report queries check canReadSites and load the site through list access. publicDashboard, publicSites, and unlockSharedLink are public by design and use elevated access internally.

On this page