The Trinity Beast — Website Analytics Guide

Privacy-first website analytics — page views, user interactions, and content engagement tracking

April 2026 Region: us-east-2 3 Endpoints 2 Tables 5 Event Types Zero Cookies

1. Overview

The Website Analytics system provides lightweight, privacy-first analytics for the CPMP website. It tracks page views and user interaction events without cookies, fingerprinting, or collecting any personally identifiable information (PII). All data stays in Aurora PostgreSQL within the project's AWS account — no third-party services involved.

Cookies
0
JS Dependencies
0
Third-Party Services
0
Event Types
5
DB Tables
2
DB Indexes
6

What It Tracks

Key Principles

System Components

ComponentRoleLocation
analytics.jsClient-side tracking snippet (IIFE, zero deps)cpmp-redesign/js/analytics.js
POST /analytics/pageviewRecords page views to AuroraLPO Server — analytics.go
POST /analytics/eventRecords interaction events to AuroraLPO Server — analytics.go
GET /admin/page-analyticsAggregated analytics query (admin auth)LPO Server — analytics.go
page_views tableStores every page viewAurora PostgreSQL
page_events tableStores every interaction eventAurora PostgreSQL
TBCC DashboardVisual analytics widgetCommand Center
KCC ScriptCLI analytics checkbash scripts/kcc.sh

2. Architecture

The analytics system follows a simple ingest-store-query pattern. The client-side beacon fires page views and events to two public endpoints on the LPO server. The server validates, truncates, and inserts directly into Aurora PostgreSQL. The admin endpoint runs aggregation queries on demand — no pre-computed materialized views, no caching layer, no background jobs.

Why No Cache Layer?

Analytics queries hit Aurora directly rather than ElastiCache. The data volume is low (website traffic, not API usage logs), the queries are simple aggregations with time-range filters, and the indexes are purpose-built. Adding a cache layer would add complexity without meaningful performance gain at current scale.

Contrast with Usage Analytics: The /admin/usage-analytics endpoint reports on LPO API usage (price queries, rate limits, billing). That data flows through the SQS queued write pipeline to Aurora. Website analytics is a completely separate system — different tables, different endpoints, different data.

Data Flow

Diagram 2.1 — Data Flow: Beacon to Dashboard
flowchart LR
    A["🌐 Browser
analytics.js"] -->|"sendBeacon / fetch
POST JSON"| B["⚡ LPO Server
analytics.go"] B -->|"INSERT"| C["🗄️ Aurora PostgreSQL
page_views + page_events"] C -->|"SELECT aggregations"| D["🔒 Admin Endpoint
GET /admin/page-analytics"] D -->|"JSON response"| E["📊 TBCC Dashboard
+ KCC CLI"] style A fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0 style B fill:#064e3b,stroke:#10b981,color:#e2e8f0 style C fill:#334155,stroke:#FF9900,color:#e2e8f0 style D fill:#4c1d95,stroke:#a855f7,color:#e2e8f0 style E fill:#78350f,stroke:#f59e0b,color:#e2e8f0

Request Path Detail

StepComponentWhat Happens
1Browseranalytics.js fires on DOMContentLoaded. Sends page view via sendBeacon (or fetch fallback). Fire-and-forget — no callback, no retry.
2CloudFrontPasses request through to ALB. Adds CloudFront-Viewer-Country header with 2-letter country code.
3LPO ServerValidates JSON, truncates all fields to max lengths, extracts User-Agent from request header and country from CloudFront header.
4Aurora PostgreSQLDirect INSERT into page_views or page_events table. No batch buffering — each request is one row.
5Admin QueryGET /admin/page-analytics?days=7 runs 8 aggregation queries against the tables and returns a single JSON response.

3. Client-Side Beacon (analytics.js)

The tracking snippet is a self-contained IIFE with zero dependencies. It lives at cpmp-redesign/js/analytics.js and is included on every page via a <script> tag before the closing </body>.

Visitor & Session Identity

IDStoragePrefixLifetimePurpose
Visitor IDlocalStorage (tb_vid)v_Persists until user clears storageCount unique visitors across sessions
Session IDsessionStorage (tb_sid)s_Resets when tab closesGroup page views within a single browsing session

ID format: Both IDs are generated as prefix + Math.random().toString(36).substr(2,12) + Date.now().toString(36). They are random strings with no connection to any user identity. Example: v_k8f2m9x3a1b7lxyz123.

Transport

Auto Page View

On every page load, analytics.js automatically sends a page view with:

FieldSourceExample
page_pathlocation.pathname/freedom-moments.html
page_titledocument.titleFreedom Moments — CPMP
referrerdocument.referrerhttps://google.com/
screen_widthwindow.innerWidth1440
screen_heightwindow.innerHeight900
visitor_idlocalStoragev_k8f2m9x3a1b7lxyz123
session_idsessionStorages_p4r7n2w8c5d1mdef456

Public API: tbTrack()

For interaction events, pages call the global tbTrack function:

// Signature
window.tbTrack(eventType, target, data)

// eventType — one of: video_click, map_pin_click, doc_click, cta_click, link_click
// target   — what was clicked (string)
// data     — optional object with additional context

Usage Examples

// Map pin click (from map.html)
tbTrack('map_pin_click', 'Lahore Medical Camp', {
  impact_type: 'medical-camps',
  media_type: 'video'
});

// Video play (from index.html)
tbTrack('video_click', 'hero-video', { duration: '2:30' });

// Document library click
tbTrack('doc_click', 'Trinity-Beast-API-Reference');

// CTA button click
tbTrack('cta_click', 'join-us-hero');

Including on a Page

<!-- Place before closing </body> tag, after i18n.js -->
<script src="js/i18n.js"></script>
<script src="js/analytics.js"></script>

Beacon Lifecycle

Diagram 3.1 — Beacon Lifecycle: Page Load to Event Tracking
sequenceDiagram
    participant B as Browser
    participant JS as analytics.js
    participant LS as localStorage
    participant SS as sessionStorage
    participant API as LPO Server
    participant DB as Aurora PostgreSQL

    B->>JS: Page loads, IIFE executes
    JS->>LS: getItem('tb_vid')
    alt Visitor ID exists
        LS-->>JS: Return existing ID
    else First visit
        JS->>LS: setItem('tb_vid', 'v_...')
    end
    JS->>SS: getItem('tb_sid')
    alt Session ID exists
        SS-->>JS: Return existing ID
    else New tab
        JS->>SS: setItem('tb_sid', 's_...')
    end
    JS->>API: sendBeacon POST /analytics/pageview
    API->>DB: INSERT INTO page_views
    Note over B,JS: Later — user clicks map pin
    B->>JS: tbTrack('map_pin_click', ...)
    JS->>API: sendBeacon POST /analytics/event
    API->>DB: INSERT INTO page_events
      

4. Database Schema

Both tables are auto-created at server startup via MigrateAnalyticsTables() in analytics.go. The migration is idempotent — safe to run on every deploy. All CREATE TABLE and CREATE INDEX statements use IF NOT EXISTS.

page_views

ColumnTypeDefaultDescription
idUUID (PK)gen_random_uuid()Auto-generated unique identifier
page_pathVARCHAR(500)URL path (e.g. /freedom-moments.html)
page_titleVARCHAR(500)''Document title at time of view
referrerVARCHAR(1000)''HTTP referrer URL (empty for direct visits)
user_agentVARCHAR(1000)''Browser user-agent string (from request header, not from JS)
visitor_idVARCHAR(64)''Anonymous visitor ID from localStorage
session_idVARCHAR(64)''Session ID from sessionStorage (resets per tab)
screen_widthINT0Viewport width in pixels
screen_heightINT0Viewport height in pixels
countryVARCHAR(10)''2-letter country code from CloudFront-Viewer-Country header (fallback: X-Country)
created_atTIMESTAMPTZNOW()Server-side timestamp (UTC)

page_events

ColumnTypeDefaultDescription
idUUID (PK)gen_random_uuid()Auto-generated unique identifier
page_pathVARCHAR(500)URL path where the event occurred
event_typeVARCHAR(100)Validated event type (see Section 5)
event_targetVARCHAR(500)''Target element or resource name
event_dataJSONB'{}'Additional structured data (max 2KB validated)
visitor_idVARCHAR(64)''Anonymous visitor ID
session_idVARCHAR(64)''Session ID
created_atTIMESTAMPTZNOW()Server-side timestamp (UTC)

Indexes (6 total)

Index NameTableColumn(s)Purpose
idx_page_views_page_pathpage_viewspage_pathTop pages aggregation
idx_page_views_created_atpage_viewscreated_at DESCTime-range filtering
idx_page_views_visitor_idpage_viewsvisitor_idUnique visitor counting
idx_page_events_event_typepage_eventsevent_typeFilter by event type
idx_page_events_created_atpage_eventscreated_at DESCTime-range filtering
idx_page_events_page_pathpage_eventspage_pathEvents per page

5. Event Types & Validation

The server validates incoming events against a whitelist of known types. Any event_type not in this list is rejected with 400 Bad Request. To add a new event type, update the validTypes map in analytics.go and redeploy.

Event TypeDescriptionTypical TargetEvent Data ExampleUsed On
video_click Video play events Video filename or ID {"duration": "2:30"} index.html, impact pages
map_pin_click Impact Map pin clicks Pin title (e.g. "Lahore Medical Camp") {"impact_type": "medical-camps", "media_type": "video"} map.html
doc_click Document library card clicks Document name Optional docs/index.html
cta_click Call-to-action button clicks Button label or ID Optional Any page with CTAs
link_click General outbound/internal link tracking Link text or URL Optional Any page

Adding a new event type: Edit the validTypes map in trinity-beast-lpo-server/internal/handlers/analytics.go, add the new key with true, and redeploy the ECS services. No database migration needed — the event_type column is a free-form VARCHAR validated at the application layer.

6. API Endpoints

Three endpoints power the analytics system. Two are public (no auth, fire-and-forget) and one is admin-protected.

POST /analytics/pageview

Auth: None — public, fire-and-forget

Records a single page view. Called automatically by analytics.js on every page load.

Request Body

FieldTypeRequiredMax LengthDescription
page_pathstringYes500URL path (e.g. /freedom-moments.html)
page_titlestringNo500Document title
referrerstringNo1000HTTP referrer
visitor_idstringNo64Anonymous visitor ID
session_idstringNo64Session ID
screen_widthintNoViewport width in pixels
screen_heightintNoViewport height in pixels

Server-added fields (not in request body):

Response: 200 OK

{"recorded": "ok"}

POST /analytics/event

Auth: None — public, fire-and-forget

Records a user interaction event. Called via tbTrack() in client code.

Request Body

FieldTypeRequiredMax LengthDescription
page_pathstringYes500URL path where event occurred
event_typestringYes100Must be in the whitelist (Section 5)
event_targetstringNo500Target element or resource
event_dataobjectNo2000 bytesAdditional JSON context
visitor_idstringNo64Anonymous visitor ID
session_idstringNo64Session ID

Response: 200 OK

{"recorded": "ok"}

Error responses:

GET /admin/page-analytics

Auth: X-Admin-Key header required

Returns aggregated analytics data for the specified time period. Runs 8 SQL queries against Aurora and returns a single JSON response.

Query Parameters

ParamAccepted ValuesDefaultDescription
days1, 7, 30, 907Lookback period in days

Example Request

curl -s -H "X-Admin-Key: <admin-key>" \
  "https://api.cpmp-site.org/admin/page-analytics?days=30"

7. Admin Response Reference

The GET /admin/page-analytics endpoint returns a JSON object with 10 top-level fields. Each field is the result of a separate SQL aggregation query.

Response Structure

{
  "status": "✅ [LPO] [us-east-2] [BeastMirror] [/admin/page-analytics] [200]",
  "status_code": 200,
  "endpoint": "/admin/page-analytics",
  "cluster_node": "BeastMirror",
  "region": "us-east-2",
  "timestamp": "2026-04-29T13:20:52Z",
  "data": {
    "period_days": 7,
    "total_views": 1234,
    "unique_visitors": 456,
    "total_events": 89,
    "top_pages": [ ... ],
    "daily_trend": [ ... ],
    "top_referrers": [ ... ],
    "events_by_type": [ ... ],
    "top_event_targets": [ ... ],
    "map_pin_clicks": [ ... ],
    "doc_clicks": [ ... ]
  }
}

Field Reference

FieldTypeMax RowsDescription
period_daysintThe lookback period requested (1, 7, 30, or 90)
total_viewsintTotal page views in the period
unique_visitorsintCount of distinct visitor_id values (excludes empty)
total_eventsintTotal interaction events in the period
top_pagesarray25Most-viewed pages: page_path, page_title, views, unique_visitors
daily_trendarray90One entry per day: date (EST), views, unique_visitors
top_referrersarray15Traffic sources: referrer (or "(direct)"), count
events_by_typearray5Event counts grouped by event_type
top_event_targetsarray25Most-interacted targets: event_type, event_target, count
map_pin_clicksarrayMap pin clicks grouped by impact_type and media_type (from event_data JSONB)
doc_clicksarrayDocument library clicks: document (from event_target), count

Timezone note: The daily_trend dates are computed in EST (America/New_York) to match the project's convention. All created_at timestamps in the database are stored in UTC.

SQL Queries Behind Each Field

FieldQuery Pattern
total_viewsSELECT COUNT(*) FROM page_views WHERE created_at >= $since
unique_visitorsSELECT COUNT(DISTINCT visitor_id) FROM page_views WHERE created_at >= $since AND visitor_id != ''
top_pagesGROUP BY page_path ORDER BY views DESC LIMIT 25
daily_trendGROUP BY TO_CHAR(created_at AT TIME ZONE 'America/New_York', 'YYYY-MM-DD') ORDER BY day
top_referrersGROUP BY referrer ORDER BY cnt DESC LIMIT 15 (empty referrer → "(direct)")
total_eventsSELECT COUNT(*) FROM page_events WHERE created_at >= $since
events_by_typeGROUP BY event_type ORDER BY cnt DESC
top_event_targetsGROUP BY event_type, event_target ORDER BY cnt DESC LIMIT 25
map_pin_clicksWHERE event_type = 'map_pin_click' GROUP BY event_data->>'impact_type', event_data->>'media_type'
doc_clicksWHERE event_type = 'doc_click' GROUP BY event_target ORDER BY cnt DESC

8. Security & Abuse Prevention

The analytics endpoints are public (no auth) by design — the beacon must fire without API keys. Several server-side protections prevent abuse.

Input Truncation

Every string field is truncated to its maximum column length before insertion. This prevents oversized payloads from consuming storage or causing insert failures.

FieldMax LengthTruncation
page_path500 charsServer-side truncate()
page_title500 charsServer-side truncate()
referrer1000 charsServer-side truncate()
visitor_id64 charsServer-side truncate()
session_id64 charsServer-side truncate()
user_agent1000 charsServer-side truncate()
event_type100 charsServer-side truncate()
event_target500 charsServer-side truncate()

Event Type Whitelist

The /analytics/event endpoint validates event_type against a hardcoded map of allowed values. Unknown types return 400 Bad Request. This prevents arbitrary event injection.

Event Data Size Cap

The event_data JSONB field is validated in two ways:

Required Field Validation

Rate Limiting

Protected: Analytics endpoints are rate limited at the ALB WAF level. The RateLimit-Analytics-300 rule blocks any single IP that sends more than 300 requests to /analytics/* within a 5-minute window. This prevents beacon spam and data pollution while allowing normal visitor traffic. The global WAF rate limit (2,000 req/5min per IP) also applies as a secondary cap.

9. Page Coverage

The analytics.js script is included on all public-facing pages. It is placed before the closing </body> tag, after i18n.js.

Pages with analytics.js

PageFileCustom Events
Homepageindex.htmlvideo_click on video play
Givegive.html
Donatedonate.html
Subscribe (LPO)subscribe-listener.html
Impact Mapmap.htmlmap_pin_click with impact_type + media_type
Freedom Momentsfreedom-moments.html
Wheelchairswheelchairs.html
Clean Waterclean-water.html
Medical Campsmedical-camps.html
Provisionsprovisions.html
Trainingtraining.html
Word of Lifeword-of-life.html
Teamteam.html
Origin Storyorigin-story.html
Newslettersnewsletters.html
Supportsupport.html
Privacy Policyprivacy.html
Terms of Useterms.html
Authorityauthority.html
Copyrightcopyright.html
Thank You (Donation)thank-you.html
Thank You (Subscription)thank-you-listener.html
Partner Applicationpartner-apply.html
Partner Statuspartner-status.html

24 pages tracked. All public-facing pages include the beacon. The Document Library (docs/index.html) and individual doc pages do not include it — they are technical reference pages, not public content.

10. KCC & TBCC Integration

Website analytics data is accessible through two operational interfaces: the Kiro Command Center (KCC) CLI and the Trinity Beast Command Center (TBCC) web dashboard.

KCC — Command Line

The KCC verify script includes the page-analytics endpoint in its health check sweep:

# Full endpoint verification (includes page-analytics)
bash scripts/kcc.sh verify

# Direct curl for analytics data
curl -s -H "X-Admin-Key: <admin-key>" \
  "https://api.cpmp-site.org/admin/page-analytics?days=7" | python3 -m json.tool

# 30-day lookback
curl -s -H "X-Admin-Key: <admin-key>" \
  "https://api.cpmp-site.org/admin/page-analytics?days=30" | python3 -m json.tool

TBCC — Web Dashboard

The Page Analytics widget in the TBCC dashboard provides a visual interface for reviewing analytics data.

Period Selector

Four time periods available:

PeriodAPI ParameterUse Case
1 day?days=1Today's traffic snapshot
7 days?days=7 (default)Weekly review
30 days?days=30Monthly trends
90 days?days=90Quarterly analysis

Dashboard Components

ComponentData SourceDescription
Summary Cardstotal_views, unique_visitors, total_eventsThree headline numbers for the selected period
Top Pages Tabletop_pagesRanked list of most-viewed pages with view count and unique visitors
Daily Trend Chartdaily_trendBar chart showing views and unique visitors per day
Top Referrerstop_referrersTraffic sources ranked by volume
Events by Typeevents_by_typeBreakdown of interaction events by type
Top Event Targetstop_event_targetsMost-interacted elements across all event types
Map Pin Clicksmap_pin_clicksMap interactions grouped by impact type and media type
Doc Clicksdoc_clicksMost-clicked documents in the document library

Real-time data: The widget fetches fresh data from GET /admin/page-analytics each time the period is changed or the dashboard is loaded. No caching — always live from Aurora.

Public Analytics Dashboard

A standalone, no-auth analytics dashboard is available at cpmp-site.org/docs/analytics-dashboard.html. It provides:

The dashboard calls GET /analytics/summary?days=7 (public, no auth) and renders all data client-side. Period selector supports 24h, 7d, 14d, 30d, and 90d windows.

Public Endpoint

GET /analytics/summary?days=7

# Returns: total_views, unique_visitors, total_sessions, avg_dwell_seconds,
# daily_trend, top_pages (with avg dwell), events_by_type, top_targets,
# scroll_depth, top_referrers, countries, hourly_heatmap, dwell_by_page

Event Types Tracked

EventSourceDescription
video_playAuto (analytics.js)Any <video> play event
cta_clickAuto (analytics.js).btn-primary clicks
form_submitAuto (analytics.js)Any form submission
download_clickAuto (analytics.js)Download link clicks
outbound_clickAuto (analytics.js)External link clicks
scroll_depthAuto (analytics.js)25%, 50%, 75%, 100% milestones
page_dwellAuto (analytics.js)Active reading time on page leave
doc_clickdocs/index.htmlDocument library card clicks
map_pin_clickmap.htmlImpact map pin clicks
video_clickindex.htmlHomepage video plays
error_naverror.htmlError page navigation clicks

11. Privacy

The analytics system is designed with privacy as a core principle. It collects the minimum data needed for useful analytics without compromising visitor privacy.

What We Do NOT Do

What We Do

Data PointHow It WorksUser Control
Visitor IDRandom string (v_...) in localStorage. Not linked to any identity.Clear browser storage at any time
Session IDRandom string (s_...) in sessionStorage. Resets when tab closes.Automatic — closes with tab
Country2-letter code from CloudFront-Viewer-Country header. Set by AWS at the edge. No IP geolocation lookup by our code.N/A — derived from CDN routing
User AgentStored as-is from the HTTP request header. Used for browser/device statistics only.Browser-controlled
Screen SizeViewport dimensions from window.innerWidth/Height. Used for responsive design insights.Browser-controlled

Data Residency

GDPR/CCPA friendly: Because no PII is collected and no cookies are used, the system operates without requiring consent banners or opt-in mechanisms.