Analytics
Analytics integration with Amplitude, Google Analytics, session replay, and admin reporting dashboard
Analytics
The platform integrates Amplitude for behavioral analytics with session replay, Google Analytics for traffic tracking, and a custom admin analytics dashboard with sales and order reports.
Architecture
Amplitude
The Amplitude integration (lib/analytics/amplitude.ts) provides:
Initialization
import * as amplitude from '@amplitude/unified'
export const initAmplitude = () => {
if (isInitialized) return true
const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY
if (!apiKey) {
console.warn('Amplitude API key missing — analytics disabled')
return false
}
// Analytics with autocapture, plus session replay on every session.
amplitude.initAll(apiKey, { analytics: { autocapture: true }, sessionReplay: { sampleRate: 1 } })
isInitialized = true
return true
}Event Tracking
export const trackEvent = (
eventName: string,
eventProperties?: Record<string, any>
) => {
amplitude.track(eventName, eventProperties)
}User Identification
export const identifyUser = (
userId: string,
userProperties?: Record<string, any>
) => {
amplitude.setUserId(userId)
if (userProperties) {
const identifyEvent = new amplitude.Identify()
Object.entries(userProperties).forEach(([key, value]) => {
identifyEvent.set(key, value)
})
amplitude.identify(identifyEvent)
}
}Autocapture
initAll turns on Amplitude's autocapture: sessions, page views, clicks, form interactions and file downloads are recorded without per-event code. The one explicit load-time event is Viewed Home Page, sent by components/analytics/home-page-view.tsx when the home page mounts.
Session Replay
@amplitude/unified includes session replay, set to record every session (sampleRate: 1) on the public site, cart and checkout included. Amplitude masks form inputs by default. The SDK is loaded with a dynamic import only after consent, so visitors who have not accepted cookies never download it; trackEvent and the other helpers queue calls made while it downloads.
Agent Analytics
The site's three Claude calls report to Amplitude Agent Analytics through @amplitude/ai, set up in lib/analytics/agent-analytics.ts with the same project key:
| Agent | Call site | Session | Content captured |
|---|---|---|---|
storefront-chat | app/api/ai-chat/route.ts | one UUID per chat-widget conversation | Full text, PII redacted |
inbox-triage | lib/inbox/classifier.ts | the mailbox message id | Metadata only (tokens, latency, cost, model) |
form-capture | lib/form-capture/extract.ts | the capture id | Metadata only |
Inbox and form capture stay metadata-only because their prompts are whole customer emails and photographed order forms. @amplitude/analytics-node is passed in pre-initialized, because under Next's bundler the AI SDK cannot require it itself.
Google Analytics
The google-analytics.tsx component loads the Google Analytics script and provides the global tracking tag. It is included in the root layout for site-wide tracking.
Admin Analytics Dashboard
The admin analytics page (/admin/analytics) provides:
Sales Reports
- Revenue over time (daily, weekly, monthly)
- Order volume trends
- Average order value
Order Analytics
- Orders by status breakdown
- Top-selling products by quantity and revenue
- Order source tracking
Inventory Turnover & Slow Movers
/admin/analytics/inventory-turnover (Analytics → Turnover & Slow Movers) answers how fast
stock is selling and what is sitting still.
- Overall turnover — how many times the shelf sold through in the window, annualised, plus the days on hand needed to clear current stock at that pace.
- Per-product table, ranked slowest-first: products holding stock with no sales lead, then the longest days of supply (current stock ÷ the window's daily selling pace). The slow-mover flag fires past 90 days of supply, or immediately for stock that sold nothing.
Both COGS and inventory are valued at the current cost price (the platform keeps no
historical inventory snapshots to average against), and — as on the margin page — a missing
cost is missing, never zero: uncosted products are excluded from the ratio and value figures
and counted against a stated coverage percentage. The velocity columns (units, days of supply)
need no cost and work regardless. The arithmetic is in lib/analytics/inventory-turnover.ts;
the query in inventory-turnover.server.ts reuses the same sales filter as the margin and
orders reports. Read-only, gated on analytics:read.
Retention & Repeat Purchase
/admin/analytics/retention (Analytics → Retention & Repeat Purchase) answers whether buyers
come back — as one rate and as a cohort grid.
- Repeat-purchase rate — the share of distinct buyers who placed two or more orders, plus orders-per-buyer.
- Cohort retention triangle — buyers grouped by the month they were first seen; each later column is the share of that cohort who ordered again that many months on.
Buyer identity is the signed-in userId when present, otherwise the normalised guest email;
there is no Customer foreign key on Order. Orders with neither cannot be tied to a person
and are excluded (their count is surfaced) rather than counted as one-time buyers, which would
deflate every figure. Two honesty rules the grid depends on: month 0 is the acquisition month and
is 100% by definition, and a cell the data cannot see yet is blank, never 0% (a cohort
acquired last month has no "three months later" column). The math is in
lib/analytics/cohort-retention.ts; the query in cohort-retention.server.ts reuses the same
sales filter as the other reports. Read-only, gated on analytics:read.
Attribution (UTM)
/admin/analytics/attribution (Analytics → Attribution) answers where orders come from, by
first-touch UTM source, medium, campaign, or referrer.
- How it's captured: there is no server-side cart, so attribution rides in a cookie. The
AttributionTrackerclient component (mounted in the public layout) records theutm_*params, the external referrer host, and the landing path into ajms_attributioncookie the first time a visitor lands with anything to attribute — a purely direct visit is left uncaptured so a later campaign click can still be the first touch.POST /api/checkoutreads that cookie and writes the fields onto theOrder(utmSource/utmMedium/utmCampaign/utmTerm/utmContent,referrer,landingPage); parsing never throws, so a bad cookie can't break checkout. - How it reports: orders are grouped by the chosen dimension into orders, revenue, and average order value, ranked by revenue. Orders with no captured source are their own Direct / none row (never folded into a campaign), and the headline states the share of orders that carried any UTM source — attribution is only ever as complete as capture, and offline/pre-feature orders are honestly direct.
Capture logic is in lib/analytics/attribution.ts (pure, tested) + attribution.client.ts; the
report math is in lib/analytics/utm-report.ts and the query in utm-report.server.ts. Read-only,
gated on analytics:read. Migration 20260815120000_add_order_attribution adds the nullable
columns (no backfill — existing orders are simply direct).
Date Range Filtering
The lib/analytics/date-range.ts utility provides predefined date ranges:
- Today, Yesterday
- Last 7 days, Last 30 days
- This month, Last month
- Custom range
Environment Variables
| Variable | Description |
|---|---|
NEXT_PUBLIC_AMPLITUDE_API_KEY | Amplitude browser SDK key |
NEXT_PUBLIC_GA_MEASUREMENT_ID | Google Analytics measurement ID |
Analytics initialization is guarded by API key checks. If keys are not configured, tracking silently degrades without errors in the console (except a single warning for Amplitude).
How is this guide?
Last updated on