Skip to content

Replace Plausible Analytics with OpenTelemetry Browser Instrumentation #19

Description

@visoedhwa

Replace Plausible Analytics with OpenTelemetry Browser Instrumentation

Plausible Analytics is currently used for usage tracking across all pages. This issue tracks replacing it with OpenTelemetry browser instrumentation, sending structured log events through our own observability stack instead of a third-party service.

Target pipeline: Browser (OTel JS SDK) → Nginx reverse-proxy (/v1/logs) → OpenTelemetry Collector → Loki → Grafana


1. Architecture Overview

┌──────────────────┐  POST /v1/logs   ┌──────────────────┐   proxy_pass   ┌──────────────────────────────────────────┐
│  Static HTML      │ ──────────────► │  Nginx            │ ────────────► │  opentelemetry-collector                 │
│  (Browser)        │  (same origin)  │  (nginx-unpriv)   │               │  .observability.svc:4318                 │
│                   │                 │  port 8080        │               │                                          │
│  Vanilla JS       │                 │  conf.d/          │               │  receivers: otlp (http)                  │
│  (no bundler)     │                 │   default.conf    │               │  exporters: loki                         │
└──────────────────┘                 └──────────────────┘               └──────────────────────────────────────────┘

Current state

  • Plausible is loaded via an inline <script> block + external utils.js on every page
  • Custom events are fired from js/page-setup.js via trackPlausible(): content_section, nav_click, asset_download, external_click
  • Plausible also auto-captures page views

Constraints

This is a static multi-page site (Gulp + jQuery). There is no JS bundler — Gulp only copies raw .js files to dist/. To use OTel's npm packages in the browser, we need to add a minimal bundling step.


2. Approach: Minimal esbuild Bundling Step

Add a single esbuild step to the Gulp pipeline that bundles only the OTel analytics module (js/analytics/init.js) into a self-contained IIFE file (dist/js/otel-analytics.js). The rest of the build pipeline stays unchanged — all other JS files continue to be copied as-is.


3. What to Instrument (User Analytics Events)

Automatic (from OTel browser instrumentations)

These come free from @opentelemetry/browser-instrumentation:

  • Navigation — route changes (multi-page navigations)
  • Navigation Timing — page load performance metrics
  • Web Vitals — LCP, FID, CLS, TTFB
  • Errors — uncaught JS errors

Custom Events (replaces existing Plausible events + new)

Event Name Trigger Attributes Replaces Plausible?
page_view Page load (each HTML page) url, page_type, referrer, user_agent, screen_resolution ✅ replaces auto page view
content_section Scroll into section / anchor click page_type, content_section ✅ replaces trackPlausible("content_section")
nav_click Click on sidebar/navbar link page_type, nav_link, content_section ✅ replaces trackPlausible("nav_click")
asset_download Click on downloadable asset link page_type, asset_type, asset_name ✅ replaces trackPlausible("asset_download")
external_click Click on external link page_type, external_link ✅ replaces trackPlausible("external_click")
view_toggle Click PREVIEW/WORD/HTML/NOTES button page_type, view_mode, element_index ❌ New
session_start Page load (once per session) session_id, timestamp ❌ New
session_heartbeat Every 60s while tab is visible session_id, duration_seconds ❌ New
copy_code User copies a code snippet page_type, snippet_language ❌ New

4. Implementation Steps

Phase 1 — Add OTel dependencies and esbuild bundling step

  • Install OTel npm packages:
    @opentelemetry/api-logs
    @opentelemetry/sdk-logs
    @opentelemetry/exporter-logs-otlp-http
    @opentelemetry/resources
    @opentelemetry/semantic-conventions
    @opentelemetry/browser-instrumentation
    @opentelemetry/instrumentation
    
  • Install esbuild as a dev dependency
  • Add a bundleAnalytics Gulp task that runs esbuild to bundle js/analytics/init.jsdist/js/otel-analytics.js as an IIFE
  • Wire bundleAnalytics into the existing build and watch tasks

Phase 2 — Create the analytics module (js/analytics/init.js)

  • Create js/analytics/init.js:
    • LoggerProvider with BatchLogRecordProcessor (prod) / ConsoleLogRecordExporter (dev, detected via window.location.hostname === 'localhost')
    • OTLPLogExporter({ url: '/v1/logs' }) — relative URL, proxied by Nginx
    • Resource: service.name = 'conversion-guide', service.version from package.json
    • Register browser instrumentations: Navigation, NavigationTiming, WebVitals, Errors
    • Export global window.otelAnalytics.logEvent(eventName, attributes) and window.otelAnalytics.trackEvent(eventName, attributes) (with session ID + common attrs baked in)
    • Session management: generate session_id (UUID) in sessionStorage
    • Common attributes: user_agent, screen_resolution, referrer
    • Session heartbeat: setInterval every 60s (skip if document.hidden)
    • Emit session_start and page_view on init

Phase 3 — Integrate into HTML pages and remove Plausible

  • Add <script src="js/otel-analytics.js"></script> to all 8 pages (before closing </body>, after jQuery but before page-setup.js)
  • Call window.otelAnalytics.init() — or auto-init on script load
  • Remove the Plausible inline <script> block (window.plausible = ...) from all pages
  • Remove the <script defer src="https://common.ltc.bcit.ca/js/utils.js"> tag from all pages

Phase 4 — Instrument existing jQuery event handlers in page-setup.js

Replace all trackPlausible() calls with OTel equivalents and remove the trackPlausible() function:

Location in page-setup.js Current Change
trackPlausible("content_section", ...) (line ~118, ~407) Plausible Replace with window.otelAnalytics.trackEvent("content_section", ...)
trackPlausible("nav_click", ...) (line ~400) Plausible Replace with window.otelAnalytics.trackEvent("nav_click", ...)
trackPlausible("asset_download", ...) (line ~415) Plausible Replace with window.otelAnalytics.trackEvent("asset_download", ...)
trackPlausible("external_click", ...) (line ~424) Plausible Replace with window.otelAnalytics.trackEvent("external_click", ...)
View toggle buttons (.preview-button, .word-button, etc.) No tracking Add window.otelAnalytics.trackEvent("view_toggle", ...)
  • Delete the trackPlausible() function definition from page-setup.js
  • Remove all references to window.plausible from page-setup.js

Phase 5 — Add Nginx reverse-proxy for /v1/logs

  • Add to conf.d/default.conf:
    # OTel log ingestion proxy
    location = /v1/logs {
        proxy_pass http://opentelemetry-collector.observability.svc:4318/v1/logs;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Content-Type $content_type;
        client_max_body_size 64k;
        access_log off;
    }

Phase 6 — Verify & test

  • Run npm run build — confirm dist/js/otel-analytics.js is generated
  • Run npm run watch — confirm analytics loads in browser console (dev mode logs to console)
  • Verify no remaining references to Plausible in dist/ output
  • Deploy to staging — verify logs arrive in Loki/Grafana
  • Confirm no CORS errors (same-origin /v1/logs path)

5. File Changes Summary

Action File Description
NEW js/analytics/init.js OTel analytics module (source, pre-bundle)
EDIT gulpfile.mjs Add bundleAnalytics task using esbuild; wire into build and watch
EDIT package.json Add OTel + esbuild dependencies
EDIT pages/*.html (all 8) Add <script src="js/otel-analytics.js"> tag
EDIT js/page-setup.js Replace trackPlausible() calls with OTel trackEvent(); remove trackPlausible() function; add view_toggle tracking
EDIT conf.d/default.conf Add location = /v1/logs reverse proxy block
EDIT pages/*.html (all 8) Remove Plausible inline script block and utils.js script tag
EDIT .gitignore Add dist/js/otel-analytics.js if not already covered

6. Risks & Considerations

  • Bundle size: The OTel JS SDK adds ~30-50 KB gzipped. Acceptable for a documentation site, but monitor.
  • No bundler today: Adding esbuild is the lightest-touch way to get node_modules imports into a browser-ready IIFE. It only touches the analytics module — all other JS stays as-is.
  • Privacy: No PII collected. All events are anonymous. Same posture as current Plausible setup.
  • Failure isolation: BatchLogRecordProcessor is fire-and-forget. If the collector is unreachable, logs silently drop — the site functions normally.
  • Multi-page vs SPA: Each page navigation is a full page load, so session_start must be deduplicated via sessionStorage (only emit if no existing session). The heartbeat timer resets on each page, which is acceptable.
  • External dependency removal: Removing Plausible eliminates the external utils.js script load from common.ltc.bcit.ca, reducing third-party dependencies.

7. Suggested Implementation Order

  1. Install deps + add esbuild Gulp task (Phase 1)
  2. Create js/analytics/init.js (Phase 2)
  3. Add Nginx /v1/logs proxy (Phase 5) — can be done in parallel
  4. Add <script> tags to all HTML pages (Phase 3)
  5. Instrument page-setup.js with OTel calls (Phase 4)
  6. Test locally with console exporter, then deploy to staging (Phase 6)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions