-
Notifications
You must be signed in to change notification settings - Fork 20
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Overview
Add a first-party analytics plugin that provides privacy-friendly, self-hosted page view and event tracking — no third-party scripts, no cookies, no GDPR headaches. Think a minimal Plausible or Fathom built directly into the stack.
This plugin ships two halves:
- Backend: ingestion endpoints + DB schema for events, plus a query API for aggregation
- Client: a lightweight script-less tracker (beacon calls) + an admin dashboard with charts
Core Features
Event Ingestion
- Page view tracking (fired automatically on route change via
onRouteRenderhook) - Custom event tracking (
track("button_clicked", { label: "CTA" })) - IP anonymisation (last octet stripped before storage)
- Bot/crawler filtering (user-agent allowlist)
-
Referercapture for referrer analytics
Dashboard
- Unique visitors, page views, bounce rate, session duration
- Top pages table (with trend sparklines)
- Top referrers table
- Top custom events table
- Date range picker (today / 7d / 30d / custom)
- Real-time visitor count (polling or SSE)
Data
- Retention policy (configurable TTL, e.g. 90 days)
- CSV export of raw events
- Aggregation API for embedding stats in other pages (e.g. "1.2k views" badge on a blog post)
Schema
// db.ts
import { createDbPlugin } from "@btst/stack/plugins/api"
export const analyticsSchema = createDbPlugin("analytics", {
pageView: {
modelName: "pageView",
fields: {
path: { type: "string", required: true },
referrer: { type: "string", required: false },
country: { type: "string", required: false },
device: { type: "string", required: false }, // "mobile" | "tablet" | "desktop"
browser: { type: "string", required: false },
sessionId: { type: "string", required: true }, // anonymous session hash
timestamp: { type: "date", defaultValue: () => new Date() },
},
},
customEvent: {
modelName: "customEvent",
fields: {
name: { type: "string", required: true },
path: { type: "string", required: true },
properties: { type: "string", required: false }, // JSON blob
sessionId: { type: "string", required: true },
timestamp: { type: "date", defaultValue: () => new Date() },
},
},
})Plugin Structure
src/plugins/analytics/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│ ├── plugin.ts # defineBackendPlugin, ingest + query endpoints
│ ├── getters.ts # aggregatePageViews, topPages, topReferrers, etc.
│ ├── mutations.ts # recordPageView, recordEvent
│ ├── query-key-defs.ts
│ ├── serializers.ts
│ └── index.ts
└── client/
├── plugin.tsx # defineClientPlugin — dashboard route + auto-tracking hook
├── tracker.ts # track() helper + beacon sender (works without React)
├── overrides.ts # AnalyticsPluginOverrides
├── index.ts
├── hooks/
│ ├── use-analytics.tsx # useDashboardStats, useTopPages, useTopReferrers
│ └── index.tsx
└── components/
└── pages/
├── dashboard-page.tsx
├── dashboard-page.internal.tsx
└── charts/
├── views-chart.tsx
├── top-pages-table.tsx
└── top-referrers-table.tsx
Routes
| Route | Path | Description |
|---|---|---|
dashboard |
/analytics |
Main stats dashboard |
Auto-tracking Integration
The client plugin hooks into the existing onRouteRender lifecycle to fire page views automatically:
// client/plugin.tsx
export const analyticsClientPlugin = (config: AnalyticsClientConfig) =>
defineClientPlugin({
name: "analytics",
hooks: {
onRouteRender: async ({ path }) => {
await track("pageview", { path }, config)
},
},
routes: () => ({ dashboard: createRoute("/analytics", ...) }),
})Consumers can also call track() directly for custom events:
import { track } from "@btst/stack/plugins/analytics/client"
await track("signup_completed", { plan: "pro" })Backend API Surface
// Available via myStack.api.analytics.*
const stats = await myStack.api.analytics.getDashboardStats({ range: "30d" })
const pages = await myStack.api.analytics.getTopPages({ limit: 10, range: "7d" })
const referrers = await myStack.api.analytics.getTopReferrers({ limit: 10, range: "7d" })Hooks
analyticsBackendPlugin({
onBeforeIngest?: (event, ctx) => Promise<void> // throw to reject/filter an event
onAfterIngest?: (event, ctx) => Promise<void>
})SSG Support
Dashboard is auth-gated / dynamic by nature — prefetchForRoute is not needed. dynamic = "force-dynamic" on the dashboard page.
Consumer Setup
// lib/stack.ts
import { analyticsBackendPlugin } from "@btst/stack/plugins/analytics/api"
analytics: analyticsBackendPlugin()// lib/stack-client.tsx
import { analyticsClientPlugin } from "@btst/stack/plugins/analytics/client"
analytics: analyticsClientPlugin({
apiBaseURL: "",
apiBasePath: "/api/data",
siteBasePath: "/pages",
queryClient,
})Non-Goals (v1)
- Funnels and conversion tracking
- A/B testing
- Heatmaps / session recording
- Multi-site / multi-tenant dashboards
- Email reports
Plugin Configuration Options
| Option | Type | Description |
|---|---|---|
apiBaseURL |
string |
Base URL for API calls |
apiBasePath |
string |
API route prefix |
siteBasePath |
string |
Mount path (e.g. "/pages") |
queryClient |
QueryClient |
Shared React Query client |
retentionDays |
number |
How long to keep raw events (default: 90) |
hooks |
AnalyticsPluginHooks |
onBeforeIngest, onAfterIngest |
Documentation
Add docs/content/docs/plugins/analytics.mdx covering:
- Overview — first-party, self-hosted, privacy-friendly
- Setup —
analyticsBackendPlugin+analyticsClientPlugin - Auto page view tracking — explain
onRouteRenderintegration - Custom events —
track()usage - Dashboard — screenshot + route docs
- Schema reference —
AutoTypeTablefor config + hooks - Aggregation API — embedding stats in other pages (e.g. view counts on blog posts)
Related Issues
- Job Board Plugin #58 Job Board Plugin
- Calendar Booking Plugin #40 Calendar Booking Plugin
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request