What gets measured gets managed. Yet most Chrome extension developers fly blindβ€”shipping updates without knowing if users love or hate them, pricing without understanding willingness to pay, and marketing without tracking what actually drives installs.

This guide changes that. You'll learn exactly how to instrument your extension, what metrics actually matter, and how to turn raw data into actionable insights that grow your user base and revenue.


πŸ“‘ Table of Contents

  1. Why Extension Analytics Matter
  2. The Extension Analytics Stack
  3. Core Metrics Every Extension Should Track
  4. Setting Up Analytics Infrastructure
  5. User Behavior Tracking
  6. Engagement & Retention Analytics
  7. Conversion & Monetization Metrics
  8. Chrome Web Store Analytics
  9. Privacy-Compliant Tracking
  10. Building Custom Dashboards
  11. A/B Testing for Extensions
  12. Analytics by Extension Type
  13. Common Analytics Mistakes
  14. Tools & Platforms Compared
  15. Case Study: Data-Driven Growth
  16. FAQ

🎯 Why Extension Analytics Matter {#why-extension-analytics-matter}

Chrome extensions operate in a unique environment with specific challenges:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  THE EXTENSION VISIBILITY PROBLEM               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Traditional Web App          Chrome Extension                 β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚   β”‚  Server Logs    β”‚          β”‚  No Server?     β”‚             β”‚
β”‚   β”‚  Full Analytics β”‚          β”‚  Limited Access β”‚             β”‚
β”‚   β”‚  User Sessions  β”‚          β”‚  Background     β”‚             β”‚
β”‚   β”‚  A/B Testing    β”‚          β”‚  Scripts Only   β”‚             β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β”‚                                                                 β”‚
β”‚   You see everything    β†’    You see almost nothing             β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Cost of Flying Blind

Scenario Without Analytics With Analytics
Feature shipped "Hope users like it" "42% adoption in 48 hours"
Users churning "No idea why" "Drop-off at onboarding step 3"
Pricing decision "Guess $5/month?" "Willingness to pay peaks at $7"
Marketing spend "Try everything" "Reddit converts 3x vs Twitter"
Bug reports "Works on my machine" "Crash on Chrome 119 + Windows"

What You'll Learn to Track

βœ… Acquisition β€” Where users come from, what converts them βœ… Activation β€” First-use experience, onboarding completion βœ… Engagement β€” Feature usage, session frequency, depth βœ… Retention β€” Daily/weekly/monthly active users, churn signals βœ… Revenue β€” Conversion rates, LTV, upgrade triggers βœ… Technical β€” Errors, performance, compatibility issues


πŸ—οΈ The Extension Analytics Stack {#the-extension-analytics-stack}

A complete analytics setup for Chrome extensions requires multiple components:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    EXTENSION ANALYTICS STACK                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                    DATA COLLECTION                       β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚   β”‚
β”‚  β”‚  β”‚ Popup   β”‚  β”‚ Content β”‚  β”‚Backgroundβ”‚  β”‚ Options β”‚    β”‚   β”‚
β”‚  β”‚  β”‚ Events  β”‚  β”‚ Script  β”‚  β”‚  Script  β”‚  β”‚  Page   β”‚    β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                           β”‚                                     β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                              β”‚
β”‚                    β”‚   Message   β”‚                              β”‚
β”‚                    β”‚   Passing   β”‚                              β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β”‚                           β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                  ANALYTICS SERVICE                       β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚  β”‚  β”‚  Your Backend / Analytics Platform                β”‚   β”‚   β”‚
β”‚  β”‚  β”‚  - Event ingestion                                β”‚   β”‚   β”‚
β”‚  β”‚  β”‚  - User identification                            β”‚   β”‚   β”‚
β”‚  β”‚  β”‚  - Session stitching                              β”‚   β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                           β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                    VISUALIZATION                         β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚   β”‚
β”‚  β”‚  β”‚Dashboardβ”‚  β”‚ Alerts  β”‚  β”‚ Reports β”‚  β”‚ Exports β”‚    β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Stack Components

Component Purpose Options
Event Collection Capture user actions Custom code, Segment, Amplitude SDK
Transport Layer Send data to server Fetch API, Beacon API, WebSocket
Backend Storage Store raw events PostgreSQL, ClickHouse, BigQuery
Processing Aggregate & analyze dbt, custom scripts, real-time
Visualization Display insights Metabase, Grafana, custom dashboard
Alerting Notify on anomalies PagerDuty, Slack webhooks, email

πŸ“ˆ Core Metrics Every Extension Should Track {#core-metrics-every-extension-should-track}

The AARRR Framework for Extensions

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    PIRATE METRICS (AARRR)                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  ACQUISITION ──► ACTIVATION ──► RETENTION ──► REVENUE ──► REFERRAL
β”‚       β”‚              β”‚              β”‚            β”‚           β”‚  β”‚
β”‚       β–Ό              β–Ό              β–Ό            β–Ό           β–Ό  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚   β”‚Installsβ”‚    β”‚ First β”‚     β”‚ DAU/  β”‚    β”‚Upgradeβ”‚   β”‚ Share β”‚β”‚β”‚
β”‚   β”‚Sources β”‚    β”‚ Use   β”‚     β”‚ MAU   β”‚    β”‚ Rate  β”‚   β”‚ Rate  β”‚β”‚β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tier 1: Must-Track Metrics

These metrics are non-negotiable for any extension:

πŸ“₯ Acquisition Metrics

Metric Definition Target
Daily Installs New users per day Growing week-over-week
Install Source Where users found you Diversified sources
Install-to-Active Rate % who actually use it >60% day 1
Cost Per Install Marketing spend / installs <$0.50 for free, <$2 for paid

⚑ Activation Metrics

Metric Definition Target
Onboarding Completion % completing setup >70%
Time to First Value Minutes to "aha moment" <2 minutes
Feature Discovery Rate % finding core features >80% for main feature
Setup Abandonment Point Where users quit Identify & fix friction

πŸ“Š Engagement Metrics

Metric Definition Target
DAU (Daily Active Users) Unique users per day Stable or growing
WAU (Weekly Active Users) Unique users per week Growing
MAU (Monthly Active Users) Unique users per month Growing
DAU/MAU Ratio Daily stickiness >20% good, >40% excellent
Session Frequency Times opened per day Depends on use case
Feature Usage % using each feature Core features >50%

πŸ”„ Retention Metrics

Metric Definition Target
Day 1 Retention % returning next day >40%
Day 7 Retention % returning after week >25%
Day 30 Retention % returning after month >15%
Churn Rate % uninstalling per period <5% monthly
Resurrection Rate Inactive users returning Track trends

πŸ’° Revenue Metrics (if monetized)

Metric Definition Target
Conversion Rate Free to paid % >2% good, >5% excellent
ARPU Average Revenue Per User Growing
LTV Lifetime Value >3x CAC
MRR Monthly Recurring Revenue Growing
Upgrade Triggers What causes upgrades Identify & amplify

Tier 2: Advanced Metrics

Once basics are solid, add these:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    ADVANCED METRICS MATRIX                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  BEHAVIORAL                    TECHNICAL                        β”‚
β”‚  ─────────                     ─────────                        β”‚
β”‚  β–‘ Feature correlation         β–‘ Load time by component         β”‚
β”‚  β–‘ Usage patterns by persona   β–‘ Error rate by Chrome version   β”‚
β”‚  β–‘ Power user indicators       β–‘ Memory consumption             β”‚
β”‚  β–‘ Abandonment flows           β–‘ API latency                    β”‚
β”‚  β–‘ Feature request frequency   β–‘ Crash reports                  β”‚
β”‚                                                                 β”‚
β”‚  BUSINESS                      COMPETITIVE                      β”‚
β”‚  ────────                      ───────────                      β”‚
β”‚  β–‘ Willingness to pay signals  β–‘ Market share estimates         β”‚
β”‚  β–‘ Expansion revenue           β–‘ Feature gap analysis           β”‚
β”‚  β–‘ Support ticket correlation  β–‘ Review sentiment vs competitorsβ”‚
β”‚  β–‘ NPS by user segment         β–‘ Switching behavior             β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

βš™οΈ Setting Up Analytics Infrastructure {#setting-up-analytics-infrastructure}

Option 1: Self-Hosted (Full Control)

Best for: Privacy-conscious extensions, custom needs, cost control at scale

// analytics.js - Core analytics module
class ExtensionAnalytics {
  constructor(config) {
    this.endpoint = config.endpoint;
    this.userId = null;
    this.sessionId = this.generateSessionId();
    this.queue = [];
    this.flushInterval = config.flushInterval || 30000;

    this.init();
  }

  async init() {
    // Get or create anonymous user ID
    const stored = await chrome.storage.local.get('analyticsUserId');
    if (stored.analyticsUserId) {
      this.userId = stored.analyticsUserId;
    } else {
      this.userId = this.generateUserId();
      await chrome.storage.local.set({ analyticsUserId: this.userId });
    }

    // Start flush interval
    setInterval(() => this.flush(), this.flushInterval);

    // Track session start
    this.track('session_start', {
      referrer: document.referrer,
      chrome_version: this.getChromeVersion()
    });
  }

  track(event, properties = {}) {
    const payload = {
      event,
      properties: {
        ...properties,
        extension_version: chrome.runtime.getManifest().version,
        timestamp: new Date().toISOString()
      },
      userId: this.userId,
      sessionId: this.sessionId,
      context: {
        platform: navigator.platform,
        language: navigator.language,
        screen: `${screen.width}x${screen.height}`
      }
    };

    this.queue.push(payload);

    // Flush immediately for important events
    if (this.isImportantEvent(event)) {
      this.flush();
    }
  }

  async flush() {
    if (this.queue.length === 0) return;

    const events = [...this.queue];
    this.queue = [];

    try {
      // Use sendBeacon for reliability
      const blob = new Blob([JSON.stringify(events)], { type: 'application/json' });
      navigator.sendBeacon(this.endpoint, blob);
    } catch (error) {
      // Fallback to fetch
      try {
        await fetch(this.endpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(events)
        });
      } catch (fetchError) {
        // Re-queue failed events
        this.queue = [...events, ...this.queue];
      }
    }
  }

  isImportantEvent(event) {
    const important = ['purchase', 'upgrade', 'error', 'uninstall_intent'];
    return important.includes(event);
  }

  generateUserId() {
    return 'u_' + crypto.randomUUID();
  }

  generateSessionId() {
    return 's_' + crypto.randomUUID();
  }

  getChromeVersion() {
    const match = navigator.userAgent.match(/Chrome\/(\d+)/);
    return match ? match[1] : 'unknown';
  }
}

// Initialize
const analytics = new ExtensionAnalytics({
  endpoint: 'https://your-api.com/events',
  flushInterval: 30000
});

export default analytics;

Backend Schema (PostgreSQL)

-- Events table
CREATE TABLE extension_events (
    id BIGSERIAL PRIMARY KEY,
    event_name VARCHAR(100) NOT NULL,
    user_id VARCHAR(50) NOT NULL,
    session_id VARCHAR(50) NOT NULL,
    properties JSONB,
    context JSONB,
    extension_version VARCHAR(20),
    created_at TIMESTAMPTZ DEFAULT NOW(),

    -- Indexes for common queries
    INDEX idx_events_user (user_id),
    INDEX idx_events_name (event_name),
    INDEX idx_events_created (created_at),
    INDEX idx_events_user_created (user_id, created_at)
);

-- Daily aggregates (materialized for performance)
CREATE MATERIALIZED VIEW daily_metrics AS
SELECT
    DATE(created_at) as date,
    COUNT(DISTINCT user_id) as dau,
    COUNT(DISTINCT session_id) as sessions,
    COUNT(*) FILTER (WHERE event_name = 'feature_used') as feature_uses,
    COUNT(*) FILTER (WHERE event_name = 'error') as errors
FROM extension_events
WHERE created_at > NOW() - INTERVAL '90 days'
GROUP BY DATE(created_at);

-- Refresh daily
CREATE OR REPLACE FUNCTION refresh_daily_metrics()
RETURNS void AS $$
BEGIN
    REFRESH MATERIALIZED VIEW CONCURRENTLY daily_metrics;
END;
$$ LANGUAGE plpgsql;

Option 2: Third-Party Analytics Platform

Best for: Quick setup, built-in visualizations, less maintenance

Amplitude Setup

// amplitude-integration.js
import * as amplitude from '@amplitude/analytics-browser';

class AmplitudeAnalytics {
  constructor(apiKey) {
    amplitude.init(apiKey, undefined, {
      defaultTracking: {
        sessions: true,
        pageViews: false, // Extensions don't have page views
        formInteractions: false
      }
    });
  }

  identify(userId, traits = {}) {
    const identifyObj = new amplitude.Identify();
    Object.entries(traits).forEach(([key, value]) => {
      identifyObj.set(key, value);
    });
    amplitude.identify(identifyObj);
    amplitude.setUserId(userId);
  }

  track(event, properties = {}) {
    amplitude.track(event, {
      ...properties,
      extension_version: chrome.runtime.getManifest().version
    });
  }

  setUserProperty(key, value) {
    const identifyObj = new amplitude.Identify();
    identifyObj.set(key, value);
    amplitude.identify(identifyObj);
  }

  revenue(productId, price, quantity = 1) {
    const revenue = new amplitude.Revenue()
      .setProductId(productId)
      .setPrice(price)
      .setQuantity(quantity);
    amplitude.revenue(revenue);
  }
}

export default new AmplitudeAnalytics('YOUR_API_KEY');

Mixpanel Setup

// mixpanel-integration.js
import mixpanel from 'mixpanel-browser';

class MixpanelAnalytics {
  constructor(token) {
    mixpanel.init(token, {
      debug: process.env.NODE_ENV === 'development',
      track_pageview: false,
      persistence: 'localStorage'
    });
  }

  identify(userId) {
    mixpanel.identify(userId);
  }

  setProfile(properties) {
    mixpanel.people.set(properties);
  }

  track(event, properties = {}) {
    mixpanel.track(event, {
      ...properties,
      $browser_version: this.getChromeVersion(),
      extension_version: chrome.runtime.getManifest().version
    });
  }

  trackCharge(amount, properties = {}) {
    mixpanel.people.track_charge(amount, properties);
  }

  getChromeVersion() {
    const match = navigator.userAgent.match(/Chrome\/(\d+)/);
    return match ? match[1] : 'unknown';
  }
}

export default new MixpanelAnalytics('YOUR_TOKEN');

Option 3: Google Analytics 4

Best for: Free, familiar, good for basic needs

// ga4-integration.js
class GA4Analytics {
  constructor(measurementId) {
    this.measurementId = measurementId;
    this.clientId = null;
    this.init();
  }

  async init() {
    // Get or create client ID
    const stored = await chrome.storage.local.get('ga_client_id');
    if (stored.ga_client_id) {
      this.clientId = stored.ga_client_id;
    } else {
      this.clientId = this.generateClientId();
      await chrome.storage.local.set({ ga_client_id: this.clientId });
    }
  }

  async track(eventName, params = {}) {
    const payload = {
      client_id: this.clientId,
      events: [{
        name: eventName,
        params: {
          ...params,
          engagement_time_msec: 100,
          extension_version: chrome.runtime.getManifest().version
        }
      }]
    };

    try {
      await fetch(
        `https://www.google-analytics.com/mp/collect?measurement_id=${this.measurementId}&api_secret=YOUR_SECRET`,
        {
          method: 'POST',
          body: JSON.stringify(payload)
        }
      );
    } catch (error) {
      console.error('GA4 tracking failed:', error);
    }
  }

  generateClientId() {
    return Math.random().toString(36).substring(2) + Date.now().toString(36);
  }
}

export default new GA4Analytics('G-XXXXXXXXXX');

πŸ‘€ User Behavior Tracking {#user-behavior-tracking}

Event Taxonomy

Structure your events consistently:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    EVENT NAMING CONVENTION                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  Format: [object]_[action]                                      β”‚
β”‚                                                                 β”‚
β”‚  LIFECYCLE              FEATURES              UI                β”‚
β”‚  ─────────              ────────              ──                β”‚
β”‚  extension_installed    feature_used          popup_opened      β”‚
β”‚  extension_updated      feature_discovered    settings_opened   β”‚
β”‚  extension_uninstalled  feature_configured    tab_switched      β”‚
β”‚  session_started        export_completed      modal_shown       β”‚
β”‚  session_ended          import_started        tooltip_viewed    β”‚
β”‚                                                                 β”‚
β”‚  CONVERSION             ERRORS                ENGAGEMENT        β”‚
β”‚  ──────────             ──────                ──────────        β”‚
β”‚  upgrade_viewed         error_occurred        shortcut_used     β”‚
β”‚  upgrade_started        crash_detected        search_performed  β”‚
β”‚  upgrade_completed      api_failed            item_created      β”‚
β”‚  trial_started          permission_denied     item_deleted      β”‚
β”‚  trial_expired                                                  β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Comprehensive Event Implementation

// events.js - Define all trackable events
const EVENTS = {
  // Lifecycle
  INSTALLED: 'extension_installed',
  UPDATED: 'extension_updated',
  UNINSTALL_SURVEY: 'uninstall_survey_shown',

  // Onboarding
  ONBOARDING_STARTED: 'onboarding_started',
  ONBOARDING_STEP_COMPLETED: 'onboarding_step_completed',
  ONBOARDING_COMPLETED: 'onboarding_completed',
  ONBOARDING_SKIPPED: 'onboarding_skipped',

  // Core Features
  FEATURE_USED: 'feature_used',
  FEATURE_DISCOVERED: 'feature_discovered',
  FEATURE_ERROR: 'feature_error',

  // UI Interactions
  POPUP_OPENED: 'popup_opened',
  POPUP_CLOSED: 'popup_closed',
  SETTINGS_OPENED: 'settings_opened',
  SETTINGS_CHANGED: 'settings_changed',

  // Engagement
  SHORTCUT_USED: 'keyboard_shortcut_used',
  CONTEXT_MENU_USED: 'context_menu_used',

  // Conversion
  UPGRADE_PROMPT_SHOWN: 'upgrade_prompt_shown',
  UPGRADE_CLICKED: 'upgrade_clicked',
  UPGRADE_COMPLETED: 'upgrade_completed',

  // Errors
  ERROR_OCCURRED: 'error_occurred',
  API_ERROR: 'api_error'
};

// Track with context
function trackFeatureUse(featureName, metadata = {}) {
  analytics.track(EVENTS.FEATURE_USED, {
    feature_name: featureName,
    trigger: metadata.trigger || 'unknown', // 'popup', 'shortcut', 'context_menu'
    duration_ms: metadata.duration,
    success: metadata.success !== false,
    ...metadata
  });
}

// Track onboarding funnel
function trackOnboardingStep(step, total, action) {
  analytics.track(EVENTS.ONBOARDING_STEP_COMPLETED, {
    step_number: step,
    total_steps: total,
    step_name: action,
    time_on_step_ms: getTimeOnStep()
  });
}

// Track errors with context
function trackError(error, context = {}) {
  analytics.track(EVENTS.ERROR_OCCURRED, {
    error_message: error.message,
    error_stack: error.stack?.substring(0, 500),
    error_type: error.name,
    url: context.url,
    action: context.action,
    chrome_version: getChromeVersion()
  });
}

Session Tracking

// session-manager.js
class SessionManager {
  constructor(analytics, timeout = 30 * 60 * 1000) { // 30 min default
    this.analytics = analytics;
    this.timeout = timeout;
    this.sessionStart = null;
    this.lastActivity = null;
    this.pageViews = 0;
    this.featureUses = 0;

    this.init();
  }

  init() {
    this.startSession();

    // Listen for activity
    chrome.runtime.onMessage.addListener((message) => {
      if (message.type === 'USER_ACTIVITY') {
        this.recordActivity();
      }
    });

    // Check for session timeout
    setInterval(() => this.checkTimeout(), 60000);
  }

  startSession() {
    this.sessionStart = Date.now();
    this.lastActivity = Date.now();
    this.pageViews = 0;
    this.featureUses = 0;

    this.analytics.track('session_started', {
      session_id: this.analytics.sessionId
    });
  }

  recordActivity() {
    const now = Date.now();

    // Check if session expired
    if (now - this.lastActivity > this.timeout) {
      this.endSession();
      this.startSession();
    }

    this.lastActivity = now;
  }

  recordPageView() {
    this.pageViews++;
    this.recordActivity();
  }

  recordFeatureUse() {
    this.featureUses++;
    this.recordActivity();
  }

  checkTimeout() {
    if (Date.now() - this.lastActivity > this.timeout) {
      this.endSession();
    }
  }

  endSession() {
    const duration = Date.now() - this.sessionStart;

    this.analytics.track('session_ended', {
      duration_seconds: Math.round(duration / 1000),
      page_views: this.pageViews,
      feature_uses: this.featureUses,
      session_id: this.analytics.sessionId
    });
  }

  getSessionDuration() {
    return Date.now() - this.sessionStart;
  }
}

πŸ“Š Engagement & Retention Analytics {#engagement-retention-analytics}

Calculating Key Retention Metrics

// retention-calculator.js
class RetentionCalculator {
  constructor(db) {
    this.db = db;
  }

  async calculateCohortRetention(cohortDate, days = [1, 7, 14, 30]) {
    // Get users who installed on cohort date
    const cohortUsers = await this.db.query(`
      SELECT DISTINCT user_id
      FROM extension_events
      WHERE event_name = 'extension_installed'
      AND DATE(created_at) = $1
    `, [cohortDate]);

    const cohortSize = cohortUsers.length;
    if (cohortSize === 0) return null;

    const retention = {};

    for (const day of days) {
      const targetDate = new Date(cohortDate);
      targetDate.setDate(targetDate.getDate() + day);

      const returnedUsers = await this.db.query(`
        SELECT COUNT(DISTINCT user_id) as count
        FROM extension_events
        WHERE user_id = ANY($1)
        AND DATE(created_at) = $2
      `, [cohortUsers.map(u => u.user_id), targetDate]);

      retention[`day_${day}`] = {
        retained: returnedUsers[0].count,
        rate: (returnedUsers[0].count / cohortSize * 100).toFixed(1)
      };
    }

    return {
      cohort_date: cohortDate,
      cohort_size: cohortSize,
      retention
    };
  }

  async calculateDAUMAU() {
    const result = await this.db.query(`
      WITH daily AS (
        SELECT COUNT(DISTINCT user_id) as dau
        FROM extension_events
        WHERE created_at > NOW() - INTERVAL '1 day'
      ),
      monthly AS (
        SELECT COUNT(DISTINCT user_id) as mau
        FROM extension_events
        WHERE created_at > NOW() - INTERVAL '30 days'
      )
      SELECT
        daily.dau,
        monthly.mau,
        ROUND(daily.dau::numeric / NULLIF(monthly.mau, 0) * 100, 1) as stickiness
      FROM daily, monthly
    `);

    return result[0];
  }

  async identifyChurnRisk() {
    // Users who were active but haven't been seen recently
    const atRisk = await this.db.query(`
      SELECT user_id, MAX(created_at) as last_seen
      FROM extension_events
      WHERE user_id IN (
        -- Was active in past 30 days
        SELECT DISTINCT user_id
        FROM extension_events
        WHERE created_at BETWEEN NOW() - INTERVAL '30 days' AND NOW() - INTERVAL '7 days'
      )
      GROUP BY user_id
      HAVING MAX(created_at) < NOW() - INTERVAL '7 days'
      ORDER BY last_seen DESC
    `);

    return atRisk;
  }
}

Retention Visualization Dashboard

<!-- retention-dashboard.html -->
<div class="retention-grid">
  <div class="metric-card">
    <h3>πŸ“ˆ DAU/MAU Ratio</h3>
    <div class="big-number" id="stickiness">--</div>
    <div class="subtext">Target: >20%</div>
  </div>

  <div class="metric-card">
    <h3>πŸ”„ Day 7 Retention</h3>
    <div class="big-number" id="d7-retention">--</div>
    <div class="subtext">Users returning after 1 week</div>
  </div>

  <div class="metric-card">
    <h3>⚠️ Churn Risk</h3>
    <div class="big-number" id="churn-risk">--</div>
    <div class="subtext">Users at risk of churning</div>
  </div>
</div>

<div class="cohort-table">
  <h3>πŸ“… Cohort Retention Matrix</h3>
  <table id="cohort-matrix">
    <thead>
      <tr>
        <th>Cohort</th>
        <th>Size</th>
        <th>Day 1</th>
        <th>Day 7</th>
        <th>Day 14</th>
        <th>Day 30</th>
      </tr>
    </thead>
    <tbody>
      <!-- Populated by JavaScript -->
    </tbody>
  </table>
</div>

Engagement Scoring

// engagement-score.js
class EngagementScorer {
  constructor() {
    // Define scoring weights
    this.weights = {
      session_frequency: 0.25,
      feature_breadth: 0.25,
      feature_depth: 0.20,
      recency: 0.15,
      tenure: 0.15
    };

    this.thresholds = {
      session_frequency: { low: 1, medium: 3, high: 7 }, // per week
      feature_breadth: { low: 1, medium: 3, high: 5 },   // features used
      feature_depth: { low: 5, medium: 20, high: 50 },   // uses per feature
      recency: { low: 7, medium: 3, high: 1 },           // days since last use
      tenure: { low: 7, medium: 30, high: 90 }           // days since install
    };
  }

  async calculateScore(userId, db) {
    const userData = await this.getUserData(userId, db);

    const scores = {
      session_frequency: this.scoreMetric(
        userData.sessions_last_week,
        this.thresholds.session_frequency
      ),
      feature_breadth: this.scoreMetric(
        userData.unique_features_used,
        this.thresholds.feature_breadth
      ),
      feature_depth: this.scoreMetric(
        userData.avg_feature_uses,
        this.thresholds.feature_depth
      ),
      recency: this.scoreMetricInverse(
        userData.days_since_last_use,
        this.thresholds.recency
      ),
      tenure: this.scoreMetric(
        userData.days_since_install,
        this.thresholds.tenure
      )
    };

    // Calculate weighted total
    let total = 0;
    for (const [metric, score] of Object.entries(scores)) {
      total += score * this.weights[metric];
    }

    return {
      total: Math.round(total * 100),
      breakdown: scores,
      segment: this.getSegment(total)
    };
  }

  scoreMetric(value, thresholds) {
    if (value >= thresholds.high) return 1;
    if (value >= thresholds.medium) return 0.66;
    if (value >= thresholds.low) return 0.33;
    return 0;
  }

  scoreMetricInverse(value, thresholds) {
    // Lower is better for recency
    if (value <= thresholds.high) return 1;
    if (value <= thresholds.medium) return 0.66;
    if (value <= thresholds.low) return 0.33;
    return 0;
  }

  getSegment(score) {
    if (score >= 0.8) return 'champion';
    if (score >= 0.6) return 'engaged';
    if (score >= 0.4) return 'casual';
    if (score >= 0.2) return 'at_risk';
    return 'dormant';
  }

  async getUserData(userId, db) {
    return db.query(`
      SELECT
        COUNT(DISTINCT session_id) FILTER (
          WHERE created_at > NOW() - INTERVAL '7 days'
        ) as sessions_last_week,
        COUNT(DISTINCT properties->>'feature_name') FILTER (
          WHERE event_name = 'feature_used'
        ) as unique_features_used,
        AVG(feature_count) as avg_feature_uses,
        EXTRACT(days FROM NOW() - MAX(created_at)) as days_since_last_use,
        EXTRACT(days FROM NOW() - MIN(created_at)) as days_since_install
      FROM extension_events
      WHERE user_id = $1
    `, [userId]);
  }
}

πŸ’° Conversion & Monetization Metrics {#conversion-monetization-metrics}

Tracking the Conversion Funnel

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    CONVERSION FUNNEL                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚              ALL USERS (100%)                    β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚        HIT PAYWALL / LIMIT (40%)                β”‚           β”‚
β”‚  β”‚        Users who encounter upgrade prompt        β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚         VIEWED PRICING (20%)                    β”‚           β”‚
β”‚  β”‚         Clicked to see options                   β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚         STARTED CHECKOUT (8%)                   β”‚           β”‚
β”‚  β”‚         Began payment process                    β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚         COMPLETED PURCHASE (4%)                 β”‚           β”‚
β”‚  β”‚         Successfully converted                   β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Conversion Tracking Implementation

// conversion-tracker.js
class ConversionTracker {
  constructor(analytics) {
    this.analytics = analytics;
  }

  // Track when user hits a paywall/limit
  trackPaywallHit(feature, currentUsage, limit) {
    this.analytics.track('paywall_hit', {
      feature,
      current_usage: currentUsage,
      limit,
      usage_percentage: (currentUsage / limit * 100).toFixed(0)
    });
  }

  // Track upgrade prompt impressions
  trackUpgradePromptShown(trigger, variant) {
    this.analytics.track('upgrade_prompt_shown', {
      trigger, // 'paywall', 'feature_discovery', 'settings_page', 'periodic'
      variant, // For A/B testing different prompts
      timestamp: Date.now()
    });
  }

  // Track pricing page views
  trackPricingViewed(source) {
    this.analytics.track('pricing_viewed', {
      source,
      current_plan: this.getCurrentPlan(),
      days_as_user: this.getDaysAsUser()
    });
  }

  // Track checkout started
  trackCheckoutStarted(plan, price, billingCycle) {
    this.analytics.track('checkout_started', {
      plan,
      price,
      billing_cycle: billingCycle,
      currency: 'USD'
    });
  }

  // Track successful purchase
  trackPurchaseCompleted(transactionId, plan, price, billingCycle) {
    this.analytics.track('purchase_completed', {
      transaction_id: transactionId,
      plan,
      price,
      billing_cycle: billingCycle,
      currency: 'USD',
      ltv_estimate: this.estimateLTV(plan, billingCycle)
    });

    // Also track revenue
    this.analytics.revenue(plan, price);
  }

  // Track checkout abandonment
  trackCheckoutAbandoned(plan, step, reason) {
    this.analytics.track('checkout_abandoned', {
      plan,
      step, // 'payment_info', 'confirmation', etc.
      reason // 'closed', 'error', 'back_button'
    });
  }

  // Track upgrade triggers (what made them upgrade)
  trackUpgradeTrigger(trigger) {
    this.analytics.track('upgrade_trigger', {
      trigger,
      user_tenure_days: this.getDaysAsUser(),
      total_feature_uses: this.getTotalFeatureUses()
    });
  }

  estimateLTV(plan, billingCycle) {
    // Estimate based on historical data
    const avgMonths = {
      monthly: 4.2,
      yearly: 14.5,
      lifetime: 36 // Estimated equivalent
    };

    const prices = {
      basic: { monthly: 5, yearly: 48 },
      pro: { monthly: 9, yearly: 86 }
    };

    if (billingCycle === 'lifetime') {
      return prices[plan].monthly * avgMonths.lifetime;
    }

    const monthlyPrice = billingCycle === 'yearly'
      ? prices[plan].yearly / 12
      : prices[plan].monthly;

    return monthlyPrice * avgMonths[billingCycle];
  }
}

Revenue Analytics Dashboard

// revenue-dashboard.js
class RevenueDashboard {
  constructor(db) {
    this.db = db;
  }

  async getMRR() {
    // Monthly Recurring Revenue
    return this.db.query(`
      SELECT
        SUM(CASE
          WHEN billing_cycle = 'monthly' THEN price
          WHEN billing_cycle = 'yearly' THEN price / 12
          ELSE 0
        END) as mrr
      FROM subscriptions
      WHERE status = 'active'
    `);
  }

  async getARPU() {
    // Average Revenue Per User
    const result = await this.db.query(`
      SELECT
        SUM(amount) / COUNT(DISTINCT user_id) as arpu
      FROM transactions
      WHERE created_at > NOW() - INTERVAL '30 days'
    `);
    return result[0].arpu;
  }

  async getConversionRate(period = '30 days') {
    const result = await this.db.query(`
      WITH total_users AS (
        SELECT COUNT(DISTINCT user_id) as count
        FROM extension_events
        WHERE created_at > NOW() - INTERVAL $1
      ),
      converted_users AS (
        SELECT COUNT(DISTINCT user_id) as count
        FROM transactions
        WHERE created_at > NOW() - INTERVAL $1
      )
      SELECT
        converted_users.count::float / NULLIF(total_users.count, 0) * 100 as rate
      FROM total_users, converted_users
    `, [period]);

    return result[0].rate;
  }

  async getUpgradeTriggers() {
    // What features/actions lead to upgrades
    return this.db.query(`
      SELECT
        properties->>'trigger' as trigger,
        COUNT(*) as count,
        ROUND(COUNT(*)::numeric / SUM(COUNT(*)) OVER () * 100, 1) as percentage
      FROM extension_events
      WHERE event_name = 'upgrade_trigger'
      AND created_at > NOW() - INTERVAL '90 days'
      GROUP BY properties->>'trigger'
      ORDER BY count DESC
      LIMIT 10
    `);
  }

  async getLTVByAcquisitionSource() {
    return this.db.query(`
      SELECT
        u.acquisition_source,
        AVG(t.total_spent) as avg_ltv,
        COUNT(DISTINCT u.id) as user_count
      FROM users u
      JOIN (
        SELECT user_id, SUM(amount) as total_spent
        FROM transactions
        GROUP BY user_id
      ) t ON u.id = t.user_id
      GROUP BY u.acquisition_source
      ORDER BY avg_ltv DESC
    `);
  }
}

πŸͺ Chrome Web Store Analytics {#chrome-web-store-analytics}

What CWS Provides

The Chrome Web Store developer dashboard offers limited but useful metrics:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CHROME WEB STORE ANALYTICS                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  AVAILABLE METRICS              NOT AVAILABLE                   β”‚
β”‚  ─────────────────              ─────────────                   β”‚
β”‚  βœ“ Weekly installs              βœ— Install sources (referrers)   β”‚
β”‚  βœ“ Weekly uninstalls            βœ— Geographic breakdown          β”‚
β”‚  βœ“ Weekly active users          βœ— User demographics             β”‚
β”‚  βœ“ Rating over time             βœ— Feature usage                 β”‚
β”‚  βœ“ Review count                 βœ— Session data                  β”‚
β”‚  βœ“ Chrome version breakdown     βœ— Revenue tracking              β”‚
β”‚  βœ“ Impressions (limited)        βœ— A/B testing                   β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Supplementing CWS Data

Since CWS analytics are limited, supplement with your own:

// cws-supplement.js
class CWSAnalyticsSupplement {
  constructor(analytics) {
    this.analytics = analytics;
  }

  // Track install source (when possible)
  async trackInstallSource() {
    // Check URL params if installed from landing page
    const result = await chrome.storage.local.get('install_source');

    if (!result.install_source) {
      // Try to infer source
      const source = await this.inferInstallSource();
      await chrome.storage.local.set({ install_source: source });

      this.analytics.track('install_source_identified', {
        source,
        method: 'inferred'
      });
    }
  }

  async inferInstallSource() {
    // Check if we have referrer data
    const tabs = await chrome.tabs.query({ active: true });
    if (tabs[0]?.url) {
      const url = new URL(tabs[0].url);

      // Check for UTM params
      const utmSource = url.searchParams.get('utm_source');
      if (utmSource) return utmSource;

      // Check for known referrers
      if (url.hostname.includes('producthunt')) return 'producthunt';
      if (url.hostname.includes('twitter') || url.hostname.includes('x.com')) return 'twitter';
      if (url.hostname.includes('reddit')) return 'reddit';
    }

    return 'organic_cws';
  }

  // Track geographic region (privacy-friendly)
  trackRegion() {
    const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
    const language = navigator.language;

    this.analytics.track('user_region', {
      timezone,
      language,
      region: this.timezoneToRegion(timezone)
    });
  }

  timezoneToRegion(tz) {
    if (tz.startsWith('America/')) return 'americas';
    if (tz.startsWith('Europe/')) return 'europe';
    if (tz.startsWith('Asia/')) return 'asia';
    if (tz.startsWith('Australia/') || tz.startsWith('Pacific/')) return 'oceania';
    return 'other';
  }

  // Track Chrome version for compatibility
  trackChromeVersion() {
    const match = navigator.userAgent.match(/Chrome\/(\d+)/);
    const version = match ? parseInt(match[1]) : 'unknown';

    this.analytics.setUserProperty('chrome_version', version);

    // Alert if using old version
    if (version < 100) {
      this.analytics.track('old_chrome_version', { version });
    }
  }
}

Tracking Uninstalls

// background.js
chrome.runtime.setUninstallURL('https://your-site.com/uninstall-survey', () => {
  // URL will open when extension is uninstalled
});

// Pre-uninstall: try to capture why they might leave
chrome.runtime.onSuspend.addListener(() => {
  // Last chance to track something
  analytics.track('extension_suspended', {
    last_action: getLastAction(),
    session_duration: getSessionDuration()
  });
});

Uninstall Survey Landing Page

<!-- uninstall-survey.html -->
<div class="survey-container">
  <h1>😒 We're sorry to see you go!</h1>
  <p>Help us improve by telling us why you uninstalled:</p>

  <form id="uninstall-survey">
    <label>
      <input type="radio" name="reason" value="not_useful">
      It wasn't useful for me
    </label>
    <label>
      <input type="radio" name="reason" value="too_expensive">
      Too expensive
    </label>
    <label>
      <input type="radio" name="reason" value="found_alternative">
      Found a better alternative
    </label>
    <label>
      <input type="radio" name="reason" value="bugs">
      Too many bugs/issues
    </label>
    <label>
      <input type="radio" name="reason" value="privacy">
      Privacy concerns
    </label>
    <label>
      <input type="radio" name="reason" value="other">
      Other: <input type="text" name="other_reason">
    </label>

    <button type="submit">Submit Feedback</button>
  </form>
</div>

<script>
document.getElementById('uninstall-survey').addEventListener('submit', async (e) => {
  e.preventDefault();
  const formData = new FormData(e.target);

  await fetch('/api/uninstall-feedback', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      reason: formData.get('reason'),
      other_reason: formData.get('other_reason'),
      // Get user ID from URL params if available
      user_id: new URLSearchParams(window.location.search).get('uid')
    })
  });

  // Show thank you message
  document.body.innerHTML = '<h1>Thank you for your feedback! πŸ™</h1>';
});
</script>

πŸ”’ Privacy-Compliant Tracking {#privacy-compliant-tracking}

Privacy Principles for Extension Analytics

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  PRIVACY-FIRST ANALYTICS                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  COLLECT                        NEVER COLLECT                   β”‚
β”‚  ───────                        ─────────────                   β”‚
β”‚  βœ“ Anonymous usage patterns     βœ— Browsing history              β”‚
β”‚  βœ“ Feature interactions         βœ— Personal identifiers          β”‚
β”‚  βœ“ Error reports (sanitized)    βœ— Page content                  β”‚
β”‚  βœ“ Aggregate statistics         βœ— Form data                     β”‚
β”‚  βœ“ Opt-in feedback              βœ— Passwords/credentials         β”‚
β”‚  βœ“ Performance metrics          βœ— Other extension data          β”‚
β”‚                                                                 β”‚
β”‚  BEST PRACTICES                                                 β”‚
β”‚  ──────────────                                                 β”‚
β”‚  β€’ Minimize data collection                                     β”‚
β”‚  β€’ Anonymize by default                                         β”‚
β”‚  β€’ Provide opt-out mechanism                                    β”‚
β”‚  β€’ Clear privacy policy                                         β”‚
β”‚  β€’ Data retention limits                                        β”‚
β”‚  β€’ GDPR/CCPA compliance                                         β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Privacy-Compliant Implementation

// privacy-analytics.js
class PrivacyAnalytics {
  constructor(config) {
    this.enabled = true;
    this.anonymousId = null;
    this.config = config;

    this.init();
  }

  async init() {
    // Check user consent
    const consent = await this.getConsent();
    this.enabled = consent.analytics !== false;

    if (this.enabled) {
      this.anonymousId = await this.getOrCreateAnonymousId();
    }
  }

  async getConsent() {
    const stored = await chrome.storage.local.get('privacy_consent');
    return stored.privacy_consent || { analytics: true }; // Opt-out model
  }

  async setConsent(consent) {
    await chrome.storage.local.set({ privacy_consent: consent });
    this.enabled = consent.analytics !== false;

    if (!this.enabled) {
      // Clear existing data
      await this.clearLocalData();
    }
  }

  async getOrCreateAnonymousId() {
    const stored = await chrome.storage.local.get('anonymous_id');
    if (stored.anonymous_id) {
      return stored.anonymous_id;
    }

    // Create anonymous ID (not linked to any personal info)
    const id = 'anon_' + crypto.randomUUID();
    await chrome.storage.local.set({ anonymous_id: id });
    return id;
  }

  track(event, properties = {}) {
    if (!this.enabled) return;

    // Sanitize properties
    const sanitized = this.sanitizeProperties(properties);

    // Send anonymized event
    this.send({
      event,
      properties: sanitized,
      anonymousId: this.anonymousId,
      timestamp: new Date().toISOString()
    });
  }

  sanitizeProperties(props) {
    const sanitized = {};
    const blocked = ['email', 'password', 'token', 'key', 'secret', 'url', 'domain'];

    for (const [key, value] of Object.entries(props)) {
      // Skip blocked fields
      if (blocked.some(b => key.toLowerCase().includes(b))) {
        continue;
      }

      // Sanitize URLs (keep only path structure)
      if (typeof value === 'string' && value.startsWith('http')) {
        sanitized[key] = this.sanitizeUrl(value);
        continue;
      }

      // Truncate long strings
      if (typeof value === 'string' && value.length > 100) {
        sanitized[key] = value.substring(0, 100) + '...';
        continue;
      }

      sanitized[key] = value;
    }

    return sanitized;
  }

  sanitizeUrl(url) {
    try {
      const parsed = new URL(url);
      // Only keep path, not domain or params
      return parsed.pathname;
    } catch {
      return 'invalid_url';
    }
  }

  async clearLocalData() {
    await chrome.storage.local.remove(['anonymous_id', 'analytics_queue']);
  }

  // GDPR data export
  async exportUserData() {
    const data = await chrome.storage.local.get(null);
    return {
      anonymous_id: data.anonymous_id,
      consent: data.privacy_consent,
      settings: data.settings,
      // Don't include analytics queue - that's our data
    };
  }

  // GDPR data deletion
  async deleteUserData() {
    // Clear local storage
    await chrome.storage.local.clear();

    // Request server-side deletion
    await fetch(`${this.config.endpoint}/delete`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ anonymousId: this.anonymousId })
    });
  }
}

Privacy Settings UI

<!-- privacy-settings.html -->
<div class="privacy-settings">
  <h2>πŸ”’ Privacy Settings</h2>

  <div class="setting-group">
    <label class="toggle">
      <input type="checkbox" id="analytics-consent" checked>
      <span class="slider"></span>
      <span class="label">Share anonymous usage data</span>
    </label>
    <p class="description">
      Help us improve by sharing anonymous usage statistics.
      We never collect personal information or browsing history.
    </p>
  </div>

  <div class="setting-group">
    <label class="toggle">
      <input type="checkbox" id="error-reporting" checked>
      <span class="slider"></span>
      <span class="label">Automatic error reporting</span>
    </label>
    <p class="description">
      Automatically send crash reports to help us fix bugs faster.
    </p>
  </div>

  <div class="data-actions">
    <h3>Your Data</h3>
    <button id="export-data">πŸ“₯ Export My Data</button>
    <button id="delete-data" class="danger">πŸ—‘οΈ Delete My Data</button>
  </div>

  <div class="privacy-links">
    <a href="https://your-site.com/privacy">Privacy Policy</a>
    <a href="https://your-site.com/terms">Terms of Service</a>
  </div>
</div>

<script>
document.getElementById('analytics-consent').addEventListener('change', async (e) => {
  await analytics.setConsent({ analytics: e.target.checked });
});

document.getElementById('export-data').addEventListener('click', async () => {
  const data = await analytics.exportUserData();
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  chrome.downloads.download({ url, filename: 'my-extension-data.json' });
});

document.getElementById('delete-data').addEventListener('click', async () => {
  if (confirm('This will delete all your data. Are you sure?')) {
    await analytics.deleteUserData();
    alert('Your data has been deleted.');
  }
});
</script>

πŸ“Š Building Custom Dashboards {#building-custom-dashboards}

Dashboard Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 ANALYTICS DASHBOARD STACK                       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                      FRONTEND                            β”‚   β”‚
β”‚  β”‚  React/Vue Dashboard with real-time updates              β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚   β”‚
β”‚  β”‚  β”‚ Charts  β”‚ β”‚ Tables  β”‚ β”‚ Filters β”‚ β”‚ Alerts  β”‚        β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                        API                               β”‚   β”‚
β”‚  β”‚  /api/metrics, /api/cohorts, /api/funnels, /api/export  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                    QUERY LAYER                           β”‚   β”‚
β”‚  β”‚  Pre-computed aggregates + real-time rollups             β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                            β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                     DATABASE                             β”‚   β”‚
β”‚  β”‚  PostgreSQL / ClickHouse / TimescaleDB                   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Dashboard Components

// dashboard-api.js
const express = require('express');
const router = express.Router();

// Overview metrics
router.get('/api/metrics/overview', async (req, res) => {
  const { period = '7d' } = req.query;

  const metrics = await db.query(`
    SELECT
      COUNT(DISTINCT user_id) as active_users,
      COUNT(DISTINCT session_id) as sessions,
      COUNT(*) FILTER (WHERE event_name = 'feature_used') as feature_uses,
      COUNT(*) FILTER (WHERE event_name = 'error_occurred') as errors,
      COUNT(*) FILTER (WHERE event_name = 'purchase_completed') as purchases,
      SUM((properties->>'price')::numeric) FILTER (
        WHERE event_name = 'purchase_completed'
      ) as revenue
    FROM extension_events
    WHERE created_at > NOW() - INTERVAL '${period}'
  `);

  res.json(metrics[0]);
});

// DAU/MAU trend
router.get('/api/metrics/dau-trend', async (req, res) => {
  const { days = 30 } = req.query;

  const trend = await db.query(`
    SELECT
      DATE(created_at) as date,
      COUNT(DISTINCT user_id) as dau
    FROM extension_events
    WHERE created_at > NOW() - INTERVAL '${days} days'
    GROUP BY DATE(created_at)
    ORDER BY date
  `);

  res.json(trend);
});

// Feature usage breakdown
router.get('/api/metrics/features', async (req, res) => {
  const features = await db.query(`
    SELECT
      properties->>'feature_name' as feature,
      COUNT(*) as uses,
      COUNT(DISTINCT user_id) as unique_users,
      AVG((properties->>'duration_ms')::numeric) as avg_duration
    FROM extension_events
    WHERE event_name = 'feature_used'
    AND created_at > NOW() - INTERVAL '30 days'
    GROUP BY properties->>'feature_name'
    ORDER BY uses DESC
  `);

  res.json(features);
});

// Conversion funnel
router.get('/api/metrics/funnel', async (req, res) => {
  const funnel = await db.query(`
    WITH funnel_events AS (
      SELECT
        user_id,
        MAX(CASE WHEN event_name = 'paywall_hit' THEN 1 ELSE 0 END) as hit_paywall,
        MAX(CASE WHEN event_name = 'pricing_viewed' THEN 1 ELSE 0 END) as viewed_pricing,
        MAX(CASE WHEN event_name = 'checkout_started' THEN 1 ELSE 0 END) as started_checkout,
        MAX(CASE WHEN event_name = 'purchase_completed' THEN 1 ELSE 0 END) as completed_purchase
      FROM extension_events
      WHERE created_at > NOW() - INTERVAL '30 days'
      GROUP BY user_id
    )
    SELECT
      SUM(hit_paywall) as hit_paywall,
      SUM(viewed_pricing) as viewed_pricing,
      SUM(started_checkout) as started_checkout,
      SUM(completed_purchase) as completed_purchase
    FROM funnel_events
  `);

  res.json(funnel[0]);
});

// Cohort retention
router.get('/api/metrics/cohorts', async (req, res) => {
  const cohorts = await db.query(`
    WITH cohorts AS (
      SELECT
        user_id,
        DATE(MIN(created_at)) as cohort_date
      FROM extension_events
      WHERE event_name = 'extension_installed'
      GROUP BY user_id
    ),
    activity AS (
      SELECT
        c.user_id,
        c.cohort_date,
        DATE(e.created_at) - c.cohort_date as days_since_install
      FROM cohorts c
      JOIN extension_events e ON c.user_id = e.user_id
    )
    SELECT
      cohort_date,
      COUNT(DISTINCT user_id) as cohort_size,
      COUNT(DISTINCT user_id) FILTER (WHERE days_since_install = 1) as d1,
      COUNT(DISTINCT user_id) FILTER (WHERE days_since_install = 7) as d7,
      COUNT(DISTINCT user_id) FILTER (WHERE days_since_install = 30) as d30
    FROM activity
    WHERE cohort_date > NOW() - INTERVAL '60 days'
    GROUP BY cohort_date
    ORDER BY cohort_date
  `);

  res.json(cohorts);
});

Dashboard Frontend Example

// Dashboard.jsx
import React, { useState, useEffect } from 'react';
import { LineChart, BarChart, FunnelChart } from './Charts';

function AnalyticsDashboard() {
  const [period, setPeriod] = useState('7d');
  const [metrics, setMetrics] = useState(null);
  const [dauTrend, setDauTrend] = useState([]);
  const [features, setFeatures] = useState([]);
  const [funnel, setFunnel] = useState(null);

  useEffect(() => {
    fetchData();
  }, [period]);

  async function fetchData() {
    const [metricsRes, dauRes, featuresRes, funnelRes] = await Promise.all([
      fetch(`/api/metrics/overview?period=${period}`),
      fetch(`/api/metrics/dau-trend?days=${parseInt(period)}`),
      fetch('/api/metrics/features'),
      fetch('/api/metrics/funnel')
    ]);

    setMetrics(await metricsRes.json());
    setDauTrend(await dauRes.json());
    setFeatures(await featuresRes.json());
    setFunnel(await funnelRes.json());
  }

  if (!metrics) return <div>Loading...</div>;

  return (
    <div className="dashboard">
      <header>
        <h1>πŸ“Š Extension Analytics</h1>
        <select value={period} onChange={e => setPeriod(e.target.value)}>
          <option value="7d">Last 7 days</option>
          <option value="30d">Last 30 days</option>
          <option value="90d">Last 90 days</option>
        </select>
      </header>

      <div className="metrics-grid">
        <MetricCard
          title="Active Users"
          value={metrics.active_users}
          icon="πŸ‘₯"
        />
        <MetricCard
          title="Sessions"
          value={metrics.sessions}
          icon="πŸ“ˆ"
        />
        <MetricCard
          title="Feature Uses"
          value={metrics.feature_uses}
          icon="⚑"
        />
        <MetricCard
          title="Revenue"
          value={`$${metrics.revenue?.toFixed(2) || 0}`}
          icon="πŸ’°"
        />
      </div>

      <div className="charts-grid">
        <div className="chart-card">
          <h3>Daily Active Users</h3>
          <LineChart data={dauTrend} xKey="date" yKey="dau" />
        </div>

        <div className="chart-card">
          <h3>Feature Usage</h3>
          <BarChart data={features} xKey="feature" yKey="uses" />
        </div>

        <div className="chart-card">
          <h3>Conversion Funnel</h3>
          <FunnelChart data={[
            { stage: 'Hit Paywall', value: funnel.hit_paywall },
            { stage: 'Viewed Pricing', value: funnel.viewed_pricing },
            { stage: 'Started Checkout', value: funnel.started_checkout },
            { stage: 'Purchased', value: funnel.completed_purchase }
          ]} />
        </div>
      </div>
    </div>
  );
}

πŸ§ͺ A/B Testing for Extensions {#ab-testing-for-extensions}

A/B Testing Framework

// ab-testing.js
class ABTesting {
  constructor(analytics) {
    this.analytics = analytics;
    this.experiments = new Map();
    this.assignments = new Map();
  }

  async init() {
    // Load experiment configurations
    const config = await this.fetchExperiments();
    config.forEach(exp => this.experiments.set(exp.id, exp));

    // Load user's assignments
    const stored = await chrome.storage.local.get('ab_assignments');
    if (stored.ab_assignments) {
      Object.entries(stored.ab_assignments).forEach(([id, variant]) => {
        this.assignments.set(id, variant);
      });
    }
  }

  async fetchExperiments() {
    // Fetch from your backend or use local config
    return [
      {
        id: 'upgrade_prompt_v2',
        variants: ['control', 'urgent', 'social_proof', 'discount'],
        weights: [0.25, 0.25, 0.25, 0.25],
        active: true
      },
      {
        id: 'onboarding_flow',
        variants: ['original', 'simplified', 'gamified'],
        weights: [0.33, 0.33, 0.34],
        active: true
      }
    ];
  }

  getVariant(experimentId) {
    // Check if already assigned
    if (this.assignments.has(experimentId)) {
      return this.assignments.get(experimentId);
    }

    const experiment = this.experiments.get(experimentId);
    if (!experiment || !experiment.active) {
      return null;
    }

    // Assign variant based on weights
    const variant = this.weightedRandom(experiment.variants, experiment.weights);

    // Store assignment
    this.assignments.set(experimentId, variant);
    this.saveAssignments();

    // Track assignment
    this.analytics.track('experiment_assigned', {
      experiment_id: experimentId,
      variant
    });

    return variant;
  }

  weightedRandom(items, weights) {
    const total = weights.reduce((a, b) => a + b, 0);
    let random = Math.random() * total;

    for (let i = 0; i < items.length; i++) {
      random -= weights[i];
      if (random <= 0) {
        return items[i];
      }
    }

    return items[items.length - 1];
  }

  async saveAssignments() {
    const obj = Object.fromEntries(this.assignments);
    await chrome.storage.local.set({ ab_assignments: obj });
  }

  trackConversion(experimentId, value = 1) {
    const variant = this.assignments.get(experimentId);
    if (!variant) return;

    this.analytics.track('experiment_conversion', {
      experiment_id: experimentId,
      variant,
      value
    });
  }
}

// Usage
const ab = new ABTesting(analytics);
await ab.init();

// Get variant for upgrade prompt
const upgradeVariant = ab.getVariant('upgrade_prompt_v2');

// Render based on variant
switch (upgradeVariant) {
  case 'urgent':
    showUrgentUpgradePrompt();
    break;
  case 'social_proof':
    showSocialProofPrompt();
    break;
  case 'discount':
    showDiscountPrompt();
    break;
  default:
    showControlPrompt();
}

// Track conversion when user upgrades
function onUpgrade() {
  ab.trackConversion('upgrade_prompt_v2');
}

Analyzing A/B Test Results

-- Calculate conversion rates by variant
WITH experiment_users AS (
  SELECT
    user_id,
    properties->>'variant' as variant
  FROM extension_events
  WHERE event_name = 'experiment_assigned'
  AND properties->>'experiment_id' = 'upgrade_prompt_v2'
),
conversions AS (
  SELECT DISTINCT user_id
  FROM extension_events
  WHERE event_name = 'purchase_completed'
)
SELECT
  e.variant,
  COUNT(DISTINCT e.user_id) as users,
  COUNT(DISTINCT c.user_id) as conversions,
  ROUND(
    COUNT(DISTINCT c.user_id)::numeric /
    NULLIF(COUNT(DISTINCT e.user_id), 0) * 100,
    2
  ) as conversion_rate
FROM experiment_users e
LEFT JOIN conversions c ON e.user_id = c.user_id
GROUP BY e.variant
ORDER BY conversion_rate DESC;

πŸ“‹ Analytics by Extension Type {#analytics-by-extension-type}

Productivity Extensions

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           PRODUCTIVITY EXTENSION METRICS                        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  KEY METRICS                    WHAT TO OPTIMIZE                β”‚
β”‚  ───────────                    ────────────────                β”‚
β”‚  β–‘ Time saved per session       β†’ Feature that saves most time  β”‚
β”‚  β–‘ Tasks completed              β†’ Friction points in workflow   β”‚
β”‚  β–‘ Automation runs              β†’ Automation failure rates      β”‚
β”‚  β–‘ Keyboard shortcut usage      β†’ Discoverability of shortcuts  β”‚
β”‚  β–‘ Integration usage            β†’ Most valuable integrations    β”‚
β”‚                                                                 β”‚
β”‚  ENGAGEMENT SIGNALS             CHURN INDICATORS                β”‚
β”‚  ──────────────────             ────────────────                β”‚
β”‚  β–‘ Daily streak length          β–‘ Decreasing usage frequency    β”‚
β”‚  β–‘ Feature depth (power user)   β–‘ Only using basic features     β”‚
β”‚  β–‘ Template creation            β–‘ No new templates created      β”‚
β”‚  β–‘ Sharing/export               β–‘ Error rate increasing         β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Content/SEO Extensions

Metric Why It Matters
Pages analyzed Core value delivery
Recommendations viewed Engagement depth
Recommendations applied Action rate
Reports exported Power user indicator
API calls made Usage intensity

Ad Blockers / Privacy Extensions

Metric Why It Matters
Ads blocked Core value (show this to users!)
Trackers blocked Secondary value metric
Sites whitelisted Engagement/customization
Filter list updates Technical health
Page load improvement Performance value

Social Media Extensions

Metric Why It Matters
Posts enhanced Feature usage
Automation runs Time saved
Accounts managed Scale of usage
Scheduled posts Future engagement
Analytics views Value realization

Developer Tools

Metric Why It Matters
Inspections performed Core usage
Bugs found/fixed Value delivered
Code snippets used Feature adoption
Export/share actions Collaboration value
Environment switches Power user behavior

⚠️ Common Analytics Mistakes {#common-analytics-mistakes}

Mistake 1: Tracking Everything

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    THE DATA GRAVEYARD                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  ❌ WRONG: Track 500 events "just in case"                      β”‚
β”‚                                                                 β”‚
β”‚  Result:                                                        β”‚
β”‚  β€’ 95% of data never viewed                                     β”‚
β”‚  β€’ Analysis paralysis                                           β”‚
β”‚  β€’ Storage costs balloon                                        β”‚
β”‚  β€’ Privacy liability increases                                  β”‚
β”‚  β€’ Performance degrades                                         β”‚
β”‚                                                                 β”‚
β”‚  βœ… RIGHT: Track 20-30 events that answer specific questions    β”‚
β”‚                                                                 β”‚
β”‚  Questions to ask before adding an event:                       β”‚
β”‚  1. What decision will this data inform?                        β”‚
β”‚  2. How often will I look at this?                              β”‚
β”‚  3. What action will I take based on it?                        β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Mistake 2: Ignoring Context

// ❌ Bad: Raw numbers without context
analytics.track('button_clicked');

// βœ… Good: Event with actionable context
analytics.track('upgrade_button_clicked', {
  location: 'paywall_modal',
  trigger: 'daily_limit_reached',
  current_plan: 'free',
  days_as_user: 14,
  feature_that_triggered: 'export'
});

Mistake 3: Not Tracking Negative Signals

// ❌ Only tracking successes
analytics.track('feature_used');

// βœ… Also track failures and frustration
analytics.track('feature_used', { success: true });
analytics.track('feature_failed', {
  error: error.message,
  context: userContext
});
analytics.track('feature_abandoned', {
  step: abandonedAt,
  time_spent_ms: timeOnFeature
});

Mistake 4: Vanity Metrics Focus

Vanity Metric Better Alternative
Total installs Active users (DAU/MAU)
Page views Time on feature
Total features used Core feature retention
Sign-ups Activated users
Raw revenue LTV, net revenue retention

Mistake 5: No Segmentation

// ❌ Looking at all users the same
const avgRetention = calculateRetention(allUsers);

// βœ… Segment by meaningful dimensions
const retentionBySource = {
  organic: calculateRetention(organicUsers),
  paid: calculateRetention(paidUsers),
  referral: calculateRetention(referralUsers)
};

const retentionByPlan = {
  free: calculateRetention(freeUsers),
  pro: calculateRetention(proUsers)
};

πŸ› οΈ Tools & Platforms Compared {#tools-platforms-compared}

Analytics Platforms

Platform Best For Pricing Extension Support
Amplitude Product analytics, funnels Free up to 10M events βœ… Good
Mixpanel Event tracking, cohorts Free up to 1M events βœ… Good
PostHog Open source, self-host Free self-hosted βœ… Good
Google Analytics 4 Basic metrics, free Free ⚠️ Limited
Heap Auto-capture Expensive ⚠️ Limited
Segment Data pipeline Per event pricing βœ… Good
Plausible Privacy-focused $9/month ⚠️ Limited

Self-Hosted Options

Solution Database Best For
PostHog ClickHouse Full-featured, open source
Matomo MySQL GA alternative
Umami PostgreSQL Simple, lightweight
Custom + Metabase Any SQL Full control

Decision Framework

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CHOOSING YOUR ANALYTICS STACK                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  START HERE: What's your priority?                              β”‚
β”‚                                                                 β”‚
β”‚  β”œβ”€β–Ί SPEED TO VALUE                                             β”‚
β”‚  β”‚   └─► Amplitude or Mixpanel (free tiers available)           β”‚
β”‚  β”‚                                                              β”‚
β”‚  β”œβ”€β–Ί PRIVACY / COMPLIANCE                                       β”‚
β”‚  β”‚   └─► PostHog self-hosted or custom solution                 β”‚
β”‚  β”‚                                                              β”‚
β”‚  β”œβ”€β–Ί COST CONTROL AT SCALE                                      β”‚
β”‚  β”‚   └─► Self-hosted (PostHog, custom)                          β”‚
β”‚  β”‚                                                              β”‚
β”‚  β”œβ”€β–Ί SIMPLICITY                                                 β”‚
β”‚  β”‚   └─► GA4 + custom events (limited but free)                 β”‚
β”‚  β”‚                                                              β”‚
β”‚  └─► FULL CONTROL                                               β”‚
β”‚      └─► Custom: PostgreSQL + your own API + Metabase           β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“– Case Study: Data-Driven Growth {#case-study-data-driven-growth}

Extension: Tab Manager Pro (Hypothetical)

Challenge: 50,000 installs but only 0.5% conversion to paid

Analytics Implementation:

  1. Funnel Analysis Revealed:
  2. 80% never discovered the "save session" feature
  3. 60% who discovered it didn't know it was premium
  4. 90% of upgrade page visitors bounced immediately

  5. Changes Made:

  6. Added "save session" prompt after 10 tabs open
  7. Clear "PRO" badge on premium features
  8. Redesigned upgrade page with feature comparison

  9. A/B Test Results:

Variant Conversion Rate Lift
Original 0.5% -
Feature Discovery 1.2% +140%
Clear PRO Badge 1.8% +260%
New Upgrade Page 2.4% +380%
All Combined 3.2% +540%
  1. Retention Analysis:
  2. Users who saved 3+ sessions had 80% D30 retention
  3. Users who didn't save any had 12% D30 retention
  4. Created onboarding flow to guide session saving

  5. Results After 3 Months:

  6. Conversion rate: 0.5% β†’ 3.2%
  7. MRR: $2,500 β†’ $16,000
  8. D30 retention: 18% β†’ 35%
  9. DAU/MAU: 12% β†’ 28%

Key Learnings: - Most users never found the core value - Price wasn't the issueβ€”awareness was - Small UX changes had massive impact - Retention and conversion are linked


❓ FAQ {#faq}

General Questions

Q: How much does analytics affect extension performance? A: Minimal if implemented correctly. Batch events, use sendBeacon for non-blocking transmission, and limit event volume. Impact should be <5ms per interaction.

Q: Should I track users who opt out of analytics? A: No. Respect user privacy preferences. Track aggregate counts if needed (e.g., "X users opted out") but no individual data.

Q: How long should I retain analytics data? A: 90 days for detailed events, 2 years for aggregates. This balances insight needs with privacy and storage costs.

Q: Can analytics get my extension rejected from CWS? A: Yes, if you collect excessive data or violate privacy policies. Only track what you need, disclose in privacy policy, and never collect PII without consent.

Technical Questions

Q: How do I track across popup, content script, and background? A: Use message passing to centralize events in the background script, then batch send to your analytics endpoint.

Q: What if my analytics endpoint is blocked by ad blockers? A: Use a first-party subdomain (analytics.yourdomain.com) or proxy through your main API endpoint.

Q: How do I handle users with multiple devices? A: Use anonymous device IDs by default. If you have auth, link devices to user accounts server-side.

Q: Should I use localStorage or chrome.storage for analytics? A: Use chrome.storage.local for extension dataβ€”it syncs across browser sessions and isn't cleared by "clear browsing data."

Business Questions

Q: What's a good conversion rate for freemium extensions? A: 2-5% is typical. Above 5% is excellent. Below 1% means either pricing or value proposition needs work.

Q: How do I know if my retention is good? A: D1 >40%, D7 >25%, D30 >15% are solid benchmarks. But compare to similar extension typesβ€”daily-use tools need higher retention than occasional-use tools.

Q: When should I start A/B testing? A: Once you have 1,000+ weekly active users. Below that, sample sizes are too small for statistical significance.


Free tool: Estimate potential earnings with our Chrome extension revenue calculator -- no signup required.


πŸ“ Summary

Chrome extension analytics is both an art and a science. The key takeaways:

  1. Start simple β€” Track 20-30 events that answer real questions
  2. Focus on decisions β€” Every metric should inform an action
  3. Respect privacy β€” Minimize data, anonymize, provide opt-out
  4. Build dashboards β€” Make data accessible and actionable
  5. Test and iterate β€” Use A/B tests to validate hypotheses
  6. Segment everything β€” Averages hide the truth
  7. Track the full funnel β€” Acquisition through revenue

The extensions that win are the ones that understand their users deeply. Analytics is your window into that understanding.

Ready to validate your extension idea before building? Try NicheCheck β†’