Skip to content

Analytics Plugin #74

@olliethedev

Description

@olliethedev

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 onRouteRender hook)
  • Custom event tracking (track("button_clicked", { label: "CTA" }))
  • IP anonymisation (last octet stripped before storage)
  • Bot/crawler filtering (user-agent allowlist)
  • Referer capture 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
  • SetupanalyticsBackendPlugin + analyticsClientPlugin
  • Auto page view tracking — explain onRouteRender integration
  • Custom eventstrack() usage
  • Dashboard — screenshot + route docs
  • Schema referenceAutoTypeTable for config + hooks
  • Aggregation API — embedding stats in other pages (e.g. view counts on blog posts)

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions