@happyvertical/smrt-analytics
GA4 / Plausible / Matomo analytics with server-side event tracking, scheduled reports, and tenant-overridable AI prompts via smrt-prompts.
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
npm install @happyvertical/smrt-analyticsQuick Start
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
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
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
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
// 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
// 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.
// 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 improvingPrompt 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.
| Key | Method | Variables (PII-conscious) |
|---|---|---|
smrtAnalytics.property.analyzePerformance | AnalyticsProperty.analyzePerformance() | period, propertyDisplayName, propertyProvider |
smrtAnalytics.report.analyzeResults | AnalyticsReport.analyzeResults() | reportName, reportDimensions, reportMetrics, dateRangeStart, dateRangeEnd, rowCount, reportData |
smrtAnalytics.report.hasPositiveTrends | AnalyticsReport.hasPositiveTrends() | reportMetrics, reportData |
Excluded from variables (never reach the AI provider)
id,propertyId,tenantId,externalIdโ internal FKs / UUIDs that link to identifying recordsapiSecret,measurementId,siteDomainโ provider credentials and platform-specific IDs (GA4 API secrets, MatomoidSite, customG-XXXXmeasurement IDs)providerMetadataโ extensible JSON that may contain credentials or account IDslastError, rawdimensionFilter/metricFilterJSON โ 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).
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.