Skip to content

埋点 面试指南

面试者视角回答

埋点是一种数据采集技术,通过在应用中嵌入代码来收集用户行为和业务数据。埋点是实现数据驱动运营的基础,是产品和运营了解用户行为的核心手段。


核心概念

埋点类型

类型说明适用场景
代码埋点手动在代码中嵌入埋点关键业务节点
全埋点自动采集所有交互页面浏览、点击
可视化埋点通过工具配置选择快速验证
无埋点采集所有数据后筛选数据回溯

埋点数据模型

javascript
{
    event: 'page_view',           // 事件名称
    event_id: 'uuid',              // 事件唯一 ID
    user_id: 'user_123',           // 用户 ID
    device_id: 'device_abc',       // 设备 ID
    timestamp: 1712345678901,      // 时间戳
    platform: 'web',               // 平台
    version: '1.0.0',             // 版本
    channel: 'appstore',          // 渠道
    page: '/home',                // 页面
    element: 'button_pay',        // 元素
    data: {                      // 自定义数据
        product_id: 'P001',
        price: 99.9
    }
}

代码埋点实现

基础埋点 SDK

javascript
class Tracker {
    constructor(options = {}) {
        this.appId = options.appId || "default";
        this.serverUrl = options.serverUrl;
        this.userId = options.userId;
        this设备Id = this.getDeviceId();
        this.isDebug = options.debug || false;
    }

    getDeviceId() {
        let deviceId = localStorage.getItem("__device_id__");
        if (!deviceId) {
            deviceId = "d_" + Math.random().toString(36).substr(2, 9);
            localStorage.setItem("__device_id__", deviceId);
        }
        return deviceId;
    }

    setUserId(userId) {
        this.userId = userId;
    }

    track(eventName, data = {}) {
        const eventData = {
            event: eventName,
            event_id: this.generateId(),
            user_id: this.userId,
            device_id: this.deviceId,
            app_id: this.appId,
            timestamp: Date.now(),
            platform: "web",
            version: window.__APP_VERSION__ || "1.0.0",
            url: location.href,
            user_agent: navigator.userAgent,
            data: data,
        };

        if (this.isDebug) {
            console.log("[Tracker]", eventName, eventData);
        }

        this.send(eventData);
    }

    generateId() {
        return "e_" + Date.now() + "_" + Math.random().toString(36).substr(2, 6);
    }

    send(data) {
        if (navigator.sendBeacon) {
            navigator.sendBeacon(this.serverUrl, JSON.stringify(data));
        } else {
            fetch(this.serverUrl, {
                method: "POST",
                body: JSON.stringify(data),
                keepalive: true,
            });
        }
    }

    // 页面浏览
    pageView(pageName, data = {}) {
        this.track("page_view", {
            page: pageName,
            referrer: document.referrer,
            ...data,
        });
    }

    // 点击事件
    click(elementId, data = {}) {
        this.track("click", {
            element: elementId,
            ...data,
        });
    }

    // 自定义事件
    custom(eventName, data = {}) {
        this.track(eventName, data);
    }
}

// 使用
const tracker = new Tracker({
    appId: "my_app",
    serverUrl: "https://analytics.example.com/collect",
    debug: true,
});

tracker.pageView("home");

// 绑定点击事件
document.querySelector("#btn").addEventListener("click", () => {
    tracker.click("btn_pay", { product_id: "P001" });
});

自动埋点实现

页面自动埋点

javascript
class AutoTracker {
    constructor(tracker) {
        this.tracker = tracker;
        this.initPageTracker();
    }

    initPageTracker() {
        // 页面加载时自动上报
        window.addEventListener("load", () => {
            this.tracker.pageView(location.pathname, {
                title: document.title,
                load_time: performance.now(),
            });
        });

        // 页面离开时上报
        window.addEventListener("beforeunload", () => {
            this.tracker.track("page_leave", {
                page: location.pathname,
                stay_time: Date.now() - this.pageStartTime,
            });
        });

        this.pageStartTime = Date.now();
    }
}

点击自动埋点

javascript
class ClickTracker {
    constructor(tracker, options = {}) {
        this.tracker = tracker;
        this.options = {
            selector: options.selector || "[data-track]",
            excludes: options.excludes || [],
            ...options,
        };
        this.init();
    }

    init() {
        document.addEventListener(
            "click",
            (e) => {
                const target = e.target.closest(this.options.selector);
                if (!target) return;

                // 检查是否排除
                if (this.isExcluded(target)) return;

                const trackId = target.dataset.track;
                const trackData = this.parseTrackData(target);

                this.tracker.click(trackId, trackData);
            },
            true,
        );
    }

    isExcluded(target) {
        return this.options.excludes.some((selector) => {
            return target.matches(selector);
        });
    }

    parseTrackData(target) {
        const data = {};

        // 从 data-* 属性收集
        const attrs = target.dataset;
        for (const key in attrs) {
            if (key.startsWith("track")) {
                const dataKey = key.replace("track", "").toLowerCase();
                data[dataKey] = attrs[key];
            }
        }

        return data;
    }
}

// HTML 使用
// <button data-track="btn_pay" data-track-product-id="P001" data-track-price="99">购买</button>

全埋点方案

无痕埋点实现

javascript
class SilentTracker {
    constructor(tracker) {
        this.tracker = tracker;
        this.setupInterceptors();
        this.setupListeners();
    }

    setupInterceptors() {
        // 拦截 XMLHttpRequest
        const originalXHROpen = XMLHttpRequest.prototype.open;
        const originalXHRSend = XMLHttpRequest.prototype.send;

        XMLHttpRequest.prototype.open = function (method, url) {
            this._method = method;
            this._url = url;
            return originalXHROpen.apply(this, arguments);
        };

        XMLHttpRequest.prototype.send = function (data) {
            this.addEventListener("load", () => {
                if (this._url.includes("/api/")) {
                    this.tracker.track("api_request", {
                        method: this._method,
                        url: this._url,
                        status: this.status,
                    });
                }
            });
            return originalXHRSend.apply(this, arguments);
        };

        // 拦截 fetch
        const originalFetch = window.fetch;
        window.fetch = async (...args) => {
            const startTime = Date.now();
            const response = await originalFetch.apply(this, args);

            this.tracker.track("fetch", {
                url: typeof args[0] === "string" ? args[0] : args[0].url,
                method: args[1]?.method || "GET",
                duration: Date.now() - startTime,
                status: response.status,
            });

            return response;
        };
    }

    setupListeners() {
        // 监控所有点击
        document.addEventListener(
            "click",
            (e) => {
                const target = e.target;
                this.tracker.track("element_click", {
                    tag: target.tagName,
                    id: target.id,
                    class: target.className,
                    text: target.innerText?.substring(0, 50),
                    xpath: this.getXPath(target),
                });
            },
            true,
        );

        // 监控输入
        document.querySelectorAll("input, textarea").forEach((el) => {
            el.addEventListener("input", (e) => {
                this.tracker.track("input_change", {
                    name: el.name || el.id,
                    type: el.type,
                    value_length: el.value.length,
                });
            });
        });

        // 监控路由变化(History API)
        const originalPushState = history.pushState;
        history.pushState = (...args) => {
            originalPushState.apply(history, args);
            this.tracker.pageView(location.pathname);
        };

        window.addEventListener("popstate", () => {
            this.tracker.pageView(location.pathname);
        });
    }

    getXPath(element) {
        if (element.id) return `//*[@id="${element.id}"]`;

        const parts = [];
        while (element && element.nodeType === Node.ELEMENT_NODE) {
            let index = 1;
            let sibling = element.previousSibling;
            while (sibling) {
                if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
                    index++;
                }
                sibling = sibling.previousSibling;
            }
            parts.unshift(`${element.nodeName.toLowerCase()}[${index}]`);
            element = element.parentNode;
        }
        return "/" + parts.join("/");
    }
}

常用埋点事件

页面事件

javascript
// 页面浏览
tracker.pageView("/home", {
    page_type: "首页",
    category: "landing",
});

// 页面停留时长(通过 beforeunload 或 visibilitychange)
document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
        tracker.track("page_stay", {
            page: location.pathname,
            duration: Date.now() - pageStartTime,
        });
    }
});

用户事件

javascript
// 用户登录
tracker.track("user_login", {
    login_type: "phone",
    success: true,
});

// 用户注册
tracker.track("user_register", {
    register_type: "invite",
    channel: "sms",
});

// 用户点击
tracker.track("user_click", {
    element: "nav_search",
    position: "header",
});

业务事件

javascript
// 商品浏览
tracker.track("product_view", {
    product_id: "P001",
    product_name: "iPhone 15",
    category: "手机",
    price: 5999,
    currency: "CNY",
});

// 加入购物车
tracker.track("add_cart", {
    product_id: "P001",
    quantity: 1,
    total_price: 5999,
});

// 下单
tracker.track("create_order", {
    order_id: "O123456",
    products: [{ id: "P001", quantity: 1, price: 5999 }],
    total_amount: 5999,
    payment_method: "wechat",
});

// 支付
tracker.track("payment", {
    order_id: "O123456",
    amount: 5999,
    status: "success",
});

埋点管理系统

埋点配置中心

javascript
class TrackConfig {
    constructor() {
        this.config = {
            events: {
                page_view: { enable: true, sampling: 1 },
                click: { enable: true, sampling: 0.1 },
                custom_purchase: { enable: true, sampling: 1 },
            },
            filters: {
                excludeUrls: ["/admin/**", "/test/**"],
                excludeUsers: ["*@test.com"],
            },
        };
    }

    isEnabled(eventName) {
        const eventConfig = this.config.events[eventName];
        if (!eventConfig || !eventConfig.enable) return false;

        // 采样判断
        if (eventConfig.sampling < 1) {
            return Math.random() < eventConfig.sampling;
        }
        return true;
    }
}

面试题精选

面试题 1:埋点的类型有哪些?各有什么优缺点?

参考答案:

类型原理优点缺点
代码埋点手动在关键位置插入代码精准、可自定义数据工作量大、易遗漏
全埋点自动采集所有点击/浏览全面、覆盖广数据量大、噪声多
可视化埋点通过 UI 工具配置简单、无需编码功能有限
无埋点采集所有数据后端筛选可回溯、数据完整隐私问题、数据量大

实际应用建议:

javascript
// 核心业务采用代码埋点
tracker.track("purchase", { order_id: "xxx", amount: 100 });

// 通用交互采用全埋点
// 通过 [data-track] 属性自动采集

面试题 2:如何实现一个高效的埋点 SDK?

参考答案:

核心设计原则:

  1. 低侵入性
javascript
// 不好:侵入业务代码
function handleClick() {
    sendAnalytics(); // 埋点代码混入业务逻辑
    doBusinessLogic();
}

// 好:非侵入式
class Tracker {
    // 自动拦截业务调用
    intercept(target, method, callback) {
        const original = target[method];
        target[method] = function (...args) {
            callback(args);
            return original.apply(this, args);
        };
    }
}
  1. 批量上报
javascript
class BatchTracker extends Tracker {
    constructor() {
        super();
        this.queue = [];
        this.batchSize = 10;
        this.flushInterval = 5000;

        setInterval(() => this.flush(), this.flushInterval);
    }

    track(event, data) {
        this.queue.push({ event, data, time: Date.now() });

        if (this.queue.length >= this.batchSize) {
            this.flush();
        }
    }

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

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

        navigator.sendBeacon(this.url, JSON.stringify(batch));
    }
}
  1. 离线缓存
javascript
class OfflineTracker extends Tracker {
    constructor() {
        super();
        this.storageKey = "__tracker_queue__";
        this.restoreQueue();
    }

    track(event, data) {
        const item = { event, data, time: Date.now() };

        if (navigator.onLine) {
            this.send(item);
        } else {
            this.saveToLocal(item);
        }
    }

    saveToLocal(item) {
        const queue = this.getQueue();
        queue.push(item);
        localStorage.setItem(this.storageKey, JSON.stringify(queue));
    }

    restoreQueue() {
        window.addEventListener("online", () => {
            const queue = this.getQueue();
            queue.forEach((item) => this.send(item));
            localStorage.removeItem(this.storageKey);
        });
    }

    getQueue() {
        try {
            return JSON.parse(localStorage.getItem(this.storageKey)) || [];
        } catch {
            return [];
        }
    }
}

面试题 3:如何保证埋点数据的准确性?

参考答案:

1. 用户识别

javascript
// 设备 ID 生成
function generateDeviceId() {
    const key = "__device_id__";
    let deviceId = localStorage.getItem(key);

    if (!deviceId) {
        deviceId = "d_" + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
        localStorage.setItem(key, deviceId);
    }
    return deviceId;
}

// 登录时关联用户 ID
tracker.setUserId(userId);

2. 时间同步

javascript
// 校准服务器时间
async function syncTime() {
    const start = Date.now();
    const res = await fetch("/api/time");
    const end = Date.now();

    const serverTime = res.timestamp;
    const delay = (end - start) / 2;

    return {
        offset: serverTime - (start + delay),
        delay: delay,
    };
}

3. 数据校验

javascript
// 上报前校验
function validateEvent(event) {
    const required = ['event', 'timestamp', 'device_id'];
    for (const field of required) {
        if (!event[field]) {
            console.error(`Missing required field: ${field}`);
            return false;
        }
    }
    return true;
}

track(eventName, data) {
    const event = this.buildEvent(eventName, data);
    if (this.validateEvent(event)) {
        this.send(event);
    }
}

面试题 4:埋点数据如何存储和分析?

参考答案:

数据存储架构:

┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│  客户端  │───>│  数据网关 │───>│  Kafka  │───>│  Flink  │
└─────────┘    └─────────┘    └─────────┘    └─────────┘

                                             v
┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│   Hive  │<───│  ClickHouse│<───│实时数据 │    │  离线数据 │
└─────────┘    └─────────┘    └─────────┘    └─────────┘

实时分析(Flink):

java
public class EventProcess {
    public static void main(String[] args) {
        StreamExecutionEnvironment env =
            StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> stream = env
            .addSource(new KafkaSource<>("events_topic"))
            .map(json -> JSON.parseObject(json));

        // 实时统计 UV
        stream.keyBy(event -> event.getString("device_id"))
              .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.minutes(1)))
              .apply((key, window, input, out) -> {
                  out.collect(new UVStat(window.getEnd(), input.size()));
              });

        env.execute();
    }
}

离线分析(Hive):

sql
-- 用户行为分析
SELECT
    date,
    page,
    COUNT(DISTINCT device_id) as uv,
    COUNT(*) as pv,
    AVG(stay_duration) as avg_stay
FROM events
WHERE event = 'page_view'
GROUP BY date, page
ORDER BY uv DESC;

-- 转化漏斗分析
WITH funnels AS (
    SELECT
        user_id,
        MAX(CASE WHEN event = 'page_product' THEN 1 ELSE 0 END) as view_product,
        MAX(CASE WHEN event = 'add_cart' THEN 1 ELSE 0 END) as add_cart,
        MAX(CASE WHEN event = 'create_order' THEN 1 ELSE 0 END) as create_order,
        MAX(CASE WHEN event = 'payment' THEN 1 ELSE 0 END) as payment
    FROM events
    WHERE date BETWEEN '2024-01-01' AND '2024-01-31'
    GROUP BY user_id
)
SELECT
    COUNT(*) as total_users,
    SUM(view_product) as view_product,
    SUM(add_cart) as add_cart,
    SUM(create_order) as create_order,
    SUM(payment) as payment,
    ROUND(SUM(payment) * 100.0 / COUNT(*), 2) as convert_rate
FROM funnels;

面试题 5:如何处理埋点的性能问题?

参考答案:

1. 异步上报

javascript
class AsyncTracker extends Tracker {
    track(event, data) {
        // 使用 requestIdleCallback 不阻塞主线程
        if (window.requestIdleCallback) {
            window.requestIdleCallback(() => {
                this.send({ event, data });
            });
        } else {
            setTimeout(() => this.send({ event, data }), 0);
        }
    }
}

2. 数据压缩

javascript
// 简化字段名
const FIELD_MAP = {
    event: "e",
    user_id: "u",
    device_id: "d",
    timestamp: "t",
    page_url: "p",
};

function compressEvent(event) {
    const compressed = {};
    for (const key in event) {
        const shortKey = FIELD_MAP[key] || key;
        compressed[shortKey] = event[key];
    }
    return compressed;
}

3. 本地采样

javascript
class SampledTracker extends Tracker {
    constructor(options) {
        super(options);
        this.sampleRate = options.sampleRate || 1;
    }

    track(event, data) {
        // 采样过滤
        if (Math.random() > this.sampleRate) {
            return;
        }
        this.send({ event, data });
    }
}

面试题 6:埋点如何保证用户隐私?

参考答案:

1. 数据脱敏

javascript
function sanitizeData(data) {
    const sensitiveFields = ["password", "id_card", "phone", "email"];

    function sanitize(obj) {
        if (typeof obj !== "object") return obj;

        const result = Array.isArray(obj) ? [] : {};
        for (const key in obj) {
            if (sensitiveFields.some((f) => key.toLowerCase().includes(f))) {
                result[key] = "***";
            } else if (typeof obj[key] === "object") {
                result[key] = sanitize(obj[key]);
            } else {
                result[key] = obj[key];
            }
        }
        return result;
    }

    return sanitize(data);
}

2. 用户授权

javascript
async function requestTrackingConsent() {
    const consent = await new Promise((resolve) => {
        // 展示隐私政策
        showPrivacyModal({
            onAccept: () => resolve("accepted"),
            onReject: () => resolve("rejected"),
        });
    });

    if (consent === "accepted") {
        enableFullTracking();
    } else if (consent === "rejected") {
        disableTracking();
        enableMinimalTracking(); // 仅收集必要数据
    }

    localStorage.setItem("tracking_consent", consent);
}

3. 遵循法规

  • GDPR(欧盟):用户知情权、删除权
  • CCPA(加州):选择退出权
  • 中国《个人信息保护法》:最小必要原则

面试题 7:前端埋点有哪些常见问题?

参考答案:

1. 重复上报

javascript
// 场景:快速点击导致多次触发
class DebouncedTracker {
    track(event, data) {
        if (this.pending) return;

        this.pending = true;
        setTimeout(() => {
            this.send({ event, data });
            this.pending = false;
        }, 1000);
    }
}

2. 漏报问题

javascript
// 场景:页面跳转时数据未发送
window.addEventListener("beforeunload", () => {
    // 同步发送未发送的数据
    flushPendingEvents();
});

// 或使用 sendBeacon
navigator.sendBeacon(url, JSON.stringify(data));

3. 跨页面数据传递

javascript
// 场景:页面 A 事件需要传递到页面 B
sessionStorage.setItem(
    "pending_event",
    JSON.stringify({
        event: "form_submit",
        data: { conversion: true },
    }),
);

// 页面 B 加载时检查
const pending = sessionStorage.getItem("pending_event");
if (pending) {
    const event = JSON.parse(pending);
    tracker.track(event.event, event.data);
    sessionStorage.removeItem("pending_event");
}

面试题 8:如何设计一个可扩展的埋点系统?

参考答案:

系统架构:

┌─────────────────────────────────────────────────────────┐
│                     客户端 SDK                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ 代码埋点  │  │ 全埋点   │  │ 可视化埋点│  │ 业务埋点  │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘ │
│       │              │              │              │       │
│       └──────────────┴──────────────┴──────────────┘       │
│                           │                               │
│                    ┌──────▼──────┐                        │
│                    │   SDK Core   │                       │
│                    │  过滤/采样    │                       │
│                    │  压缩/加密    │                       │
│                    └──────┬──────┘                        │
└───────────────────────────┼───────────────────────────────┘

┌───────────────────────────▼───────────────────────────────┐
│                      数据采集层                            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ HTTP API │  │ WebSocket│  │  MQTT    │  │   GRPC   │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
└───────────────────────────┬───────────────────────────────┘

┌───────────────────────────▼───────────────────────────────┐
│                      数据处理层                            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │  实时流   │  │  离线批处理│  │  数据清洗 │  │  数据关联 │ │
│  │  (Kafka) │  │ (Spark)  │  │          │  │          │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
└───────────────────────────┬───────────────────────────────┘

┌───────────────────────────▼───────────────────────────────┐
│                      数据存储层                            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ ClickHouse│  │   HBase  │  │   Hive   │  │   Redis  │ │
│  │  (OLAP)  │  │ (NoSQL)  │  │ (数据湖)  │  │ (缓存)   │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
└───────────────────────────────────────────────────────────┘

SDK 插件化设计:

javascript
class Tracker {
    constructor() {
        this.plugins = [];
    }

    use(plugin) {
        this.plugins.push(plugin);
        return this;
    }

    track(event, data) {
        const context = {
            event,
            data,
            timestamp: Date.now(),
            // 上下文信息
        };

        // 插件链处理
        for (const plugin of this.plugins) {
            if (plugin.process) {
                const result = plugin.process(context);
                if (result === false) return; // 中断
                context = result;
            }
        }

        this.send(context);
    }
}

// 插件示例
const clickPlugin = {
    name: "click",
    process(ctx) {
        if (ctx.event === "click") {
            ctx.data.xpath = generateXPath(ctx.data.element);
        }
        return ctx;
    },
};

tracker.use(clickPlugin);

面试题 9:如何验证埋点数据的正确性?

参考答案:

1. 联调测试

javascript
class DebugTracker extends Tracker {
    constructor() {
        super();
        this.enableDebug();
    }

    enableDebug() {
        window.addEventListener("message", (e) => {
            if (e.data.type === "check_tracker") {
                this.showPendingEvents();
            }
        });
    }

    showPendingEvents() {
        console.table(this.queue);
    }
}

2. 数据校验脚本

python
import json

def validate_event(event):
    required_fields = ['event', 'timestamp', 'device_id', 'user_id']

    # 检查必填字段
    for field in required_fields:
        if field not in event:
            print(f"Missing field: {field}")
            return False

    # 检查数据类型
    if not isinstance(event['timestamp'], (int, float)):
        print(f"Invalid timestamp type: {type(event['timestamp'])}")
        return False

    # 检查时间范围
    now = time.time() * 1000
    if abs(event['timestamp'] - now) > 24 * 60 * 60 * 1000:
        print(f"Suspicious timestamp: {event['timestamp']}")
        return False

    return True

3. 自动化测试

javascript
// Jest 测试
describe("Tracker", () => {
    it("should track page view", () => {
        const tracker = new Tracker({ serverUrl: "http://test" });

        tracker.pageView("/test");

        expect(sendBeacon).toHaveBeenCalledWith("http://test", expect.stringContaining("page_view"));
    });
});

面试题 10:埋点与数据分析的关系是什么?

参考答案:

数据驱动决策流程:

埋点采集 ──> 数据清洗 ──> 数据存储 ──> 数据分析 ──> 业务优化
    │                                              │
    └──────────────── 反馈循环 ───────────────────┘

常见分析场景:

1. 漏斗分析

sql
-- 电商转化漏斗
WITH events AS (
    SELECT
        user_id,
        MAX(CASE WHEN event = 'page_home' THEN 1 ELSE 0 END) as home,
        MAX(CASE WHEN event = 'page_product' THEN 1 ELSE 0 END) as product,
        MAX(CASE WHEN event = 'add_cart' THEN 1 ELSE 0 END) as cart,
        MAX(CASE WHEN event = 'create_order' THEN 1 ELSE 0 END) as order,
        MAX(CASE WHEN event = 'payment' THEN 1 ELSE 0 END) as payment
    FROM user_events
    GROUP BY user_id
)
SELECT
    COUNT(*) as users,
    SUM(home) as home_users,
    SUM(product) as product_users,
    SUM(cart) as cart_users,
    SUM(order) as order_users,
    SUM(payment) as payment_users,
    ROUND(SUM(cart) * 100.0 / SUM(product), 2) as product_to_cart_rate
FROM events;

2. 用户留存分析

sql
-- 次日留存
SELECT
    date,
    COUNT(DISTINCT user_id) as new_users,
    SUM(CASE WHEN retention_1 = 1 THEN 1 ELSE 0 END) as retained,
    ROUND(SUM(CASE WHEN retention_1 = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as retention_rate
FROM (
    SELECT
        first_login_date as date,
        user_id,
        MAX(CASE WHEN event = 'login' AND date >= first_login_date + INTERVAL 1 DAY
                 THEN 1 ELSE 0 END) as retention_1
    FROM user_events
    GROUP BY user_id, first_login_date
) t
GROUP BY date;

3. 路径分析

sql
-- 用户路径
SELECT
    CONCAT(step1, ' -> ', step2, ' -> ', step3) as path,
    COUNT(*) as users
FROM (
    SELECT
        user_id,
        MAX(CASE WHEN step = 1 THEN page ELSE NULL END) as step1,
        MAX(CASE WHEN step = 2 THEN page ELSE NULL END) as step2,
        MAX(CASE WHEN step = 3 THEN page ELSE NULL END) as step3
    FROM (
        SELECT
            user_id,
            page,
            ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY timestamp) as step
        FROM events
        WHERE event = 'page_view'
    ) t
    WHERE step <= 3
    GROUP BY user_id
) t
GROUP BY step1, step2, step3
ORDER BY users DESC;