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
- Why Extension Analytics Matter
- The Extension Analytics Stack
- Core Metrics Every Extension Should Track
- Setting Up Analytics Infrastructure
- User Behavior Tracking
- Engagement & Retention Analytics
- Conversion & Monetization Metrics
- Chrome Web Store Analytics
- Privacy-Compliant Tracking
- Building Custom Dashboards
- A/B Testing for Extensions
- Analytics by Extension Type
- Common Analytics Mistakes
- Tools & Platforms Compared
- Case Study: Data-Driven Growth
- 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:
- Funnel Analysis Revealed:
- 80% never discovered the "save session" feature
- 60% who discovered it didn't know it was premium
-
90% of upgrade page visitors bounced immediately
-
Changes Made:
- Added "save session" prompt after 10 tabs open
- Clear "PRO" badge on premium features
-
Redesigned upgrade page with feature comparison
-
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% |
- Retention Analysis:
- Users who saved 3+ sessions had 80% D30 retention
- Users who didn't save any had 12% D30 retention
-
Created onboarding flow to guide session saving
-
Results After 3 Months:
- Conversion rate: 0.5% β 3.2%
- MRR: $2,500 β $16,000
- D30 retention: 18% β 35%
- 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.
π Related Resources
- Chrome Extension Monetization Strategies
- Chrome Extension Success Stories
- Browser Extension Market Analysis
- Privacy Policy Generator for Extensions
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:
- Start simple β Track 20-30 events that answer real questions
- Focus on decisions β Every metric should inform an action
- Respect privacy β Minimize data, anonymize, provide opt-out
- Build dashboards β Make data accessible and actionable
- Test and iterate β Use A/B tests to validate hypotheses
- Segment everything β Averages hide the truth
- 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 β
Ready to Validate Your Idea?
Get instant insights on market demand, competition, and revenue potential.
Try NicheCheck Free