@happyvertical/smrt-analytics

GA4 / Plausible / Matomo analytics with server-side event tracking, scheduled reports, and tenant-overridable AI prompts via smrt-prompts.

v0.29.34GA4PlausibleMatomoPrompt Registry

Overview

smrt-analytics covers GA4, Plausible, and Matomo properties, data streams, server-side events, and saved reports. AI methods (analyzePerformance(), analyzeResults(), hasPositiveTrends()) route through smrt-prompts so tenants can override templates and models without code changes. UI components auto-register slots with ModuleUIRegistry from @happyvertical/smrt-svelte.

Installation

bash
npm install @happyvertical/smrt-analytics

Quick Start

typescript
import {
  AnalyticsProperty, AnalyticsPropertyCollection,
  AnalyticsDataStream, AnalyticsDataStreamCollection,
  AnalyticsEvent, AnalyticsEventCollection,
  AnalyticsProvider, DataStreamType
} from '@happyvertical/smrt-analytics';

// Initialize collections
const properties = new AnalyticsPropertyCollection(db);
const streams = new AnalyticsDataStreamCollection(db);
const events = new AnalyticsEventCollection(db);

// Create GA4 property
const property = await properties.create({
  name: 'main-site',
  displayName: 'Main Site Analytics',
  provider: AnalyticsProvider.GA4,
  externalId: 'properties/123456789',
  measurementId: 'G-XXXXXXXXXX',
  apiSecret: 'server-side-secret',
  status: 'active',
});

// Add a web data stream
await streams.create({
  propertyId: property.id,
  displayName: 'Web Traffic',
  streamType: DataStreamType.WEB,
  measurementId: 'G-XXXXXXXXXX',
  defaultUri: 'https://example.com',
  status: 'active',
});

// Track a server-side event with retry support
const event = await events.create({
  propertyId: property.id,
  eventName: 'purchase',
  clientId: 'client-uuid',
  params: JSON.stringify({ value: 99.99, currency: 'USD' }),
  status: 'pending',
});

// After sending: markSent() / markFailed('timeout') mutate in place (sync),
// then persist with save()
event.markSent();
await event.save();

Core Models

AnalyticsProperty

typescript
class AnalyticsProperty extends SmrtObject {
  name: string
  displayName: string
  provider: 'ga4' | 'plausible' | 'matomo'   // AnalyticsProvider enum
  externalId?: string              // e.g. 'properties/123456789'
  measurementId?: string           // GA4 (G-XXXXXXXXXX), Firebase app ID for mobile
  apiSecret?: string               // GA4 server-side secret (plaintext โ€” see Gotchas)
  siteDomain?: string              // Plausible site domain / Matomo idSite
  providerMetadata?: string        // JSON: extensible per-provider config
  status: 'active' | 'inactive' | 'pending'
  lastSyncAt?: Date

  // smrt-prompts: smrtAnalytics.property.analyzePerformance
  async analyzePerformance(options?: { period?: string }): Promise<{ action: string; period: string; analysis: string }>

  // inline is() โ€” NOT registered
  async isPerformingWell(): Promise<boolean>
}

AnalyticsEvent

typescript
class AnalyticsEvent extends SmrtObject {
  propertyId: string
  eventName: string           // 'page_view', 'purchase', 'sign_up', etc.
  clientId: string            // Anonymous client identifier
  userId?: string             // Authenticated user
  params: string              // JSON string (use getter/setter helpers)
  eventTimestamp: Date
  status: 'pending' | 'sent' | 'failed'  // TrackingEventStatus enum
  retryCount: number
  sentAt?: Date
  errorMessage?: string

  isPageview(): boolean
  isConversion(): boolean
  markSent(): void                      // Sync โ€” sets status='sent' + sentAt (call save() to persist)
  markFailed(error: string): void       // Sync โ€” sets status='failed', stores error, increments retryCount
  shouldRetry(maxRetries?: number): boolean  // default maxRetries = 3
  resetForRetry(): void                 // Reset to pending for retry
  toTrackEvent(): SDKTrackEvent         // Convert to SDK payload
}

AnalyticsReport

typescript
class AnalyticsReport extends SmrtObject {
  propertyId: string
  name: string
  dimensions: string          // JSON array of dimension objects
  metrics: string             // JSON array of metric objects
  dimensionFilter?: string    // JSON โ€” excluded from prompt variables
  metricFilter?: string       // JSON โ€” excluded from prompt variables
  dateRangeStart: string      // e.g. '7daysAgo'
  dateRangeEnd: string        // e.g. 'today'
  frequency: 'once' | 'daily' | 'weekly' | 'monthly'  // ReportFrequency
  status: 'draft' | 'scheduled' | 'running' | 'completed' | 'failed'
  resultData?: string         // Cached JSON โ€” FORWARDED VERBATIM to AI (see Prompt Registry)
  rowCount?: number
  lastRunAt?: Date
  nextRunAt?: Date

  isDue(): boolean
  calculateNextRun(): void        // Recomputes and sets nextRunAt in place

  // smrt-prompts: smrtAnalytics.report.analyzeResults (a single AI call;
  // returned response shape includes "insights" mirroring "analysis").
  async analyzeResults(): Promise<{ action: string; analysis: string; insights: string }>

  // smrt-prompts: smrtAnalytics.report.hasPositiveTrends
  async hasPositiveTrends(): Promise<boolean>
}

Event Tracking

typescript
// Track page view
const pageView = await events.create({
  propertyId: property.id,
  eventName: 'page_view',
  clientId: req.cookies.clientId,
  userId: req.user?.id,
  params: {
    page_location: req.url,
    page_title: 'Product Page',
    page_referrer: req.headers.referer
  },
  eventTimestamp: new Date()
});
await pageView.save();

// Track purchase
const purchase = await events.create({
  propertyId: property.id,
  eventName: 'purchase',
  clientId: req.cookies.clientId,
  userId: req.user.id,
  params: {
    transaction_id: order.id,
    value: order.total,
    currency: 'USD',
    items: order.items.map(item => ({
      item_id: item.productId,
      item_name: item.name,
      quantity: item.quantity,
      price: item.price
    }))
  },
  eventTimestamp: new Date()
});
await purchase.save();

// Get stats
const stats = await events.getPropertyStats(property.id);
console.log(`Total: ${stats.total}, Sent: ${stats.sent}`);

Scheduled Reports

typescript
// Create weekly report
const report = await reports.create({
  propertyId: property.id,
  name: 'Weekly Traffic Report',
  dimensions: ['country', 'deviceCategory', 'date'],
  metrics: ['activeUsers', 'sessions', 'bounceRate'],
  dateRange: 'last7Days',
  frequency: 'WEEKLY',
  status: 'SCHEDULED'
});
await report.save();

// Check if due
if (report.isDue()) {
  report.status = 'RUNNING';
  await report.save();

  // Run report (integrate with SDK)
  const results = await runAnalyticsReport(report);

  report.resultData = results;
  report.rowCount = results.length;
  report.lastRunAt = new Date();
  report.status = 'COMPLETED';
  await report.save();

  // AI analysis
  const insights = await report.analyzeResults();
  console.log(insights);
}

AI-Powered Analysis

Three of the four AI methods on this package route through the smrt-prompts registry; isPerformingWell() still uses the inline is() shortcut.

typescript
// Routed through smrt-prompts (tenant-overridable)
const { analysis } = await property.analyzePerformance({ period: '7 days' });
// returns { action, period, analysis }
// analysis: "Traffic up 15% this week. Top sources: organic search (45%),
//  direct (30%). Mobile usage increased to 68%."

// Still uses inline is() โ€” not registered
const isHealthy = await property.isPerformingWell();
// true if metrics trending positively

// Routed through smrt-prompts โ€” and FORWARDS resultData verbatim (see below)
const reportInsights = await report.analyzeResults();
// { action, analysis, insights }  // insights mirrors analysis in the response shape

const hasGrowth = await report.hasPositiveTrends();
// true if key metrics improving

Prompt Registry

Three prompts are registered at module-load time via definePrompt() from @happyvertical/smrt-prompts and re-exported from the package root for tenant override targeting.

KeyMethodVariables (PII-conscious)
smrtAnalytics.property.analyzePerformanceAnalyticsProperty.analyzePerformance()period, propertyDisplayName, propertyProvider
smrtAnalytics.report.analyzeResultsAnalyticsReport.analyzeResults()reportName, reportDimensions, reportMetrics, dateRangeStart, dateRangeEnd, rowCount, reportData
smrtAnalytics.report.hasPositiveTrendsAnalyticsReport.hasPositiveTrends()reportMetrics, reportData

Excluded from variables (never reach the AI provider)

  • id, propertyId, tenantId, externalId โ€” internal FKs / UUIDs that link to identifying records
  • apiSecret, measurementId, siteDomain โ€” provider credentials and platform-specific IDs (GA4 API secrets, Matomo idSite, custom G-XXXX measurement IDs)
  • providerMetadata โ€” extensible JSON that may contain credentials or account IDs
  • lastError, raw dimensionFilter / metricFilter JSON โ€” error strings (may contain auth tokens) and filter expressions that may reference cookie IDs, user-pseudo-IDs, or IP-derived geos

resultData is forwarded verbatim โ€” callers must de-PII

UI Slot Registry

Svelte components live in src/svelte/ and auto-register with ModuleUIRegistry from @happyvertical/smrt-svelte/registry when @happyvertical/smrt-analytics/svelte is imported. Slot declarations are exported via the ./ui subpath.

Slots: analytics-summary, events-table, property-info, property-status-badge, stat-card, trend-badge (see ANALYTICS_UI_SLOTS for icons / categories / display order).

typescript
import '@happyvertical/smrt-analytics/svelte'; // side-effect: registers slots
import { ModuleUIRegistry } from '@happyvertical/smrt-svelte/registry';

const StatCard = ModuleUIRegistry.get(
  '@happyvertical/smrt-analytics',
  'stat-card',
);

Tenancy

Models in this package use a tenancy-fallback pattern: tenantId is omitted from the model surface so any withTenant() context applied at the call site automatically propagates through the SMRT tenancy interceptor โ€” no per-model decorator needed. See smrt-tenancy for the call-site pattern and the canonical rule in docs/content/standards.md ยง7.

Best Practices

Related Modules