基于 Supabase 构建一个轻量级、隐私友好的用户行为分析系统,非常适合个人项目或小型团队快速搭建一套完整的用户追踪方案。
为什么选择 Supabase?
- 免费额度充足:免费版足以支撑小型项目早期需求。
- 开箱即用:PostgreSQL数据库、身份认证和Edge Functions一站式提供,无需自行拼凑技术栈。
- 无需运维:作为托管服务,开发者可以专注于业务逻辑,而非基础设施维护。
- 实时能力:原生支持实时数据订阅,可轻松扩展为实时监控看板。
- 轻量简洁:无需部署类似Sentry这样的大型、重型监控项目,架构简单明了。
整体架构设计
┌─────────────┐ POST /track ┌──────────────────┐
│ 客户端 │ ───────────────▶ │ Edge Function │
│ (App/Web) │ x-track-token │ (Deno Runtime) │
└─────────────┘ └────────┬─────────┘
│ service_role
▼
┌─────────────┐ anon key ┌──────────────────┐
│ 仪表盘 │ ◀────────────── │ PostgreSQL │
│ (静态页面) │ authenticated │ events + views │
└─────────────┘ └──────────────────┘
核心设计思路:
- 写入隔离:客户端通过Edge Function上报事件,Function内部使用高权限的
service_role密钥写入数据库,避免在前端暴露写权限。
- 读取受控:数据分析仪表盘使用低权限的
anon key,结合用户登录认证后读取数据,并通过RLS(行级安全策略)保护数据安全。
- 批量上报:客户端采用本地事件队列,配合定时或阈值触发批量上报机制,有效减少网络请求次数。
第一步:创建数据库表
在Supabase的SQL编辑器中执行以下语句,创建核心事件表。
-- 启用UUID生成扩展
create extension if not exists "uuid-ossp";
-- 事件表
create table if not exists public.events (
id uuid primary key default gen_random_uuid(),
user_id uuid not null, -- 匿名用户ID
session_id uuid not null, -- 会话ID
event text not null, -- 事件名称
ts timestamptz not null, -- 事件时间戳
os_platform text, -- 操作系统
os_release text, -- 系统版本
os_arch text, -- CPU架构
app_version text, -- 应用版本
language text, -- 语言
screen_resolution text, -- 屏幕分辨率
extra jsonb default '{}'::jsonb, -- 扩展字段
inserted_at timestamptz not null default now()
);
-- 为常用查询创建索引以优化性能
create index idx_events_user_ts on public.events (user_id, ts desc);
create index idx_events_ts on public.events (ts desc);
create index idx_events_event on public.events (event);
第二步:配置行级安全策略(RLS)
通过RLS精确控制数据访问权限,这是保障数据库/中间件安全的关键一步。
-- 对events表启用RLS
alter table public.events enable row level security;
-- 策略1:仅允许已认证用户读取数据(用于仪表盘)
create policy "events_read" on public.events
for select to authenticated using (true);
-- 策略2:仅允许service_role角色写入数据(用于Edge Function)
create policy "events_insert" on public.events
for insert to service_role with check (true);
配置完成后:
- 前端使用的
anon key无法直接向表中插入数据。
- 数据写入必须通过我们接下来创建的Edge Function(使用
service_role)来完成。
- 登录后的认证用户可以查询数据,用于在仪表盘中展示。
第三步:创建Edge Function
在项目目录中创建 supabase/functions/track/index.ts 文件,该Node.js函数负责接收并处理上报请求。
import { serve } from 'https://deno.land/std@0.223.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.2'
const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const TRACK_TOKEN = Deno.env.get('TRACK_TOKEN')!
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
const MAX_BATCH = 200
serve(async (req) => {
// 仅允许POST方法
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 })
}
// 基于自定义Token进行鉴权
const token = req.headers.get('x-track-token')
if (token !== TRACK_TOKEN) {
return new Response('Unauthorized', { status: 401 })
}
// 解析请求体
let events
try {
events = await req.json()
if (!Array.isArray(events)) throw new Error('Body must be an array')
} catch (err) {
return new Response(`Invalid JSON: ${err}`, { status: 400 })
}
// 批量大小限制
if (events.length > MAX_BATCH) {
return new Response(`Too many events (max ${MAX_BATCH})`, { status: 413 })
}
// 数据清洗与验证
const sanitized = events.filter(ev =>
ev.user_id && ev.session_id && ev.event && ev.ts
).map((ev) => {
// 限制extra字段大小,防止过大JSON
if (ev.extra && JSON.stringify(ev.extra).length > 4000) {
ev.extra = { warning: 'truncated' }
}
return ev
})
if (sanitized.length === 0) {
return new Response('No valid events', { status: 400 })
}
// 写入数据库
const { error } = await supabase.from('events').insert(sanitized)
if (error) {
return new Response(`Insert failed: ${error.message}`, { status: 500 })
}
return new Response(
JSON.stringify({ inserted: sanitized.length }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
})
第四步:部署Edge Function
使用Supabase CLI完成函数的部署和环境变量配置。
# 登录Supabase CLI
supabase login
# 设置环境变量(替换为你的实际值)
supabase secrets set \
SUPABASE_URL=https://your-project.supabase.co \
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key \
TRACK_TOKEN=your_custom_token \
--project-ref your_project_ref
# 部署函数(跳过JWT验证,使用我们自定义的Token鉴权)
supabase functions deploy track --project-ref your_project_ref --no-verify-jwt
第五步:创建数据分析视图
为了方便查询,可以在数据库中创建一些预聚合的视图。
-- 用户画像视图
create or replace view public.user_profile as
select
user_id,
min(ts) as first_seen_at,
max(ts) as last_seen_at,
count(*) filter (where event = 'app_start') as total_sessions,
count(*) as total_events,
count(distinct date(ts)) as active_days
from public.events
group by user_id;
-- 用户分层与生命周期视图
create or replace view public.user_tags as
with activity as (
select
user_id,
max(ts) as last_seen_at,
min(ts) as first_seen_at,
count(distinct date(ts)) filter (where ts >= now() - interval '7 days') as active_days_7d
from public.events
group by user_id
)
select
user_id,
case
when active_days_7d >= 5 then 'heavy'
when active_days_7d between 2 and 4 then 'medium'
else 'light'
end as active_level,
case
when first_seen_at >= now() - interval '7 days' then 'new_user'
when last_seen_at >= now() - interval '7 days' then 'retained'
when last_seen_at >= now() - interval '30 days' then 'churn_risk'
else 'churned'
end as lifecycle_stage
from activity;
项目实践:Electron应用埋点分析
以一个名为xiaozhi-desktop的Electron桌面应用为例,其埋点上报流程如下:
┌─────────────────────┐
│ 渲染进程 │
│ useAnalytics() │
│ trackEvent() │
└─────────┬───────────┘
│ IPC (analytics:track)
┌─────────▼───────────┐
│ 主进程 │
│ AnalyticsService │
│ - 收集系统信息 │
│ - 缓存事件队列 │
│ - 批量上报 │
└─────────┬───────────┘
│ POST /track
┌─────────▼───────────┐
│ Supabase │
│ Edge Function │
│ Token 鉴权 │
└─────────┬───────────┘
│ service_role
┌─────────▼───────────┐
│ PostgreSQL │
│ events 表 │
└─────────────────────┘
核心代码结构
1. 主进程服务:AnalyticsService.ts
负责生成用户标识、收集环境信息、管理事件队列和批量上报。
// src/main/services/AnalyticsService.ts
export class AnalyticsService {
private config: AnalyticsConfig = {
enabled: true,
endpoint: 'https://your-project.supabase.co/functions/v1/track',
token: 'your_custom_token',
flushInterval: 30000, // 30秒
flushBatchSize: 50, // 50条事件
}
// 上报事件
track(event: string, extra?: Record<string, unknown>): void {
const payload: EventPayload = {
user_id: this.userId, // 基于设备指纹生成的稳定UUID
session_id: this.sessionId, // 每次启动生成新的会话ID
event,
ts: new Date().toISOString(),
...this.systemInfo, // 系统信息
extra, // 自定义数据
}
this.eventQueue.push(payload)
// 队列达到批量大小时立即发送
if (this.eventQueue.length >= this.config.flushBatchSize) {
this.flush()
}
}
}
2. IPC通信桥接
在主进程中暴露埋点相关的能力给渲染进程。
// src/main/ipc/analyticsIpc.ts
ipcMain.handle('analytics:track', async (_event, eventName: string, extra?: Record<string, unknown>) => {
const analytics = getAnalyticsService()
analytics.track(eventName, extra)
return { success: true }
})
3. 前端(渲染进程)封装
在Vue组件中,通过Composable简化调用。
// src/renderer/src/composables/useAnalytics.ts
export function trackEvent(event: string, extra?: Record<string, unknown>): void {
window.api
.analyticsTrack(event, extra)
.catch(error => console.error('埋点上报失败:', error))
}
export function useAnalytics() {
return {
track: trackEvent,
}
}
4. Preload脚本安全暴露API
// src/preload/modules/analytics.ts
export const analyticsApi = {
track: async (event: string, extra?: Record<string, unknown>) => {
return await ipcRenderer.invoke('analytics:track', event, extra)
},
// ... 其他方法 (getConfig, updateConfig, flush)
}
数据字段与用户标识策略
每个事件默认包含以下系统信息字段:
| 字段 |
类型 |
来源 |
示例 |
user_id |
UUID |
设备指纹+UUID v5生成 |
550e8400-e29b-... |
session_id |
UUID |
每次应用启动生成 |
123e4567-e89b-... |
event |
string |
事件名称 |
app_start |
ts |
ISO 8601 |
事件时间戳 |
2025-12-09T10:30:00.000Z |
os_platform |
string |
process.platform |
darwin / win32 |
os_arch |
string |
process.arch |
arm64 / x64 |
app_version |
string |
app.getVersion() |
1.0.5-beta.7 |
language |
string |
app.getLocale() |
zh-CN |
screen_resolution |
string |
主显示器分辨率 |
1920x1080 |
extra |
jsonb |
自定义扩展数据 |
{ "attempts": 3 } |
用户标识生成策略(隐私友好):
// 基于设备指纹生成稳定的用户ID
private async generateUserId(): Promise<void> {
const fingerprintManager = await DeviceFingerprintManager.getInstanceAsync()
const serialNumber = fingerprintManager.getSerialNumber()
// 使用UUID v5生成稳定的UUID (同一设备始终相同)
const UUID_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'
this.userId = uuidv5(serialNumber, UUID_NAMESPACE)
}
优点:
- 同一设备卸载重装后
user_id不变,支持长期行为追踪。
- 无需用户登录账号体系,实现匿名分析。
- 不收集个人身份信息,隐私友好。
批量上报策略
采用 队列缓存 + 双触发机制 以优化网络请求:
- 定时发送:每30秒自动发送缓存队列(
flushInterval)。
- 批量阈值:队列累积达到50条事件时立即发送(
flushBatchSize)。
- 应用退出保障:退出时调用
shutdown()方法,确保所有剩余事件发送完成。
// 批量发送逻辑
async flush(): Promise<void> {
if (this.eventQueue.length === 0) return
const events = [...this.eventQueue]
this.eventQueue = []
try {
await this.sendToSupabase(events)
this.logger.debug(`成功发送 ${events.length} 个事件`)
} catch (error) {
this.logger.error('发送事件失败,保存到离线缓存', error)
await this.saveOffline(events) // 可扩展离线重试逻辑
}
}
最佳实践与建议
1. 埋点设计原则
建议对事件进行分类管理:
| 分类 |
示例事件 |
说明 |
| 生命周期 |
app_start, app_quit, window_opened |
应用/窗口生命周期事件 |
| 功能使用 |
camera_capture, music_play |
用户主动触发的核心功能 |
| 用户交互 |
button_click, menu_opened |
界面交互行为 |
| 业务转化 |
activation_verified, purchase_success |
关键业务流程节点 |
| 错误异常 |
error_occurred, api_failed |
错误和异常记录 |
| 性能指标 |
startup_time, api_latency |
性能数据监控 |
2. 扩展:错误监控实践
统一错误上报函数
// src/main/utils/errorTracking.ts
export function trackError(error: Error, context?: Record<string, unknown>) {
const analytics = getAnalyticsService()
analytics.track('error_occurred', {
error_name: error.name,
error_message: error.message,
error_stack: error.stack?.substring(0, 500), // 限制长度
...context,
})
}
3. 数据分析查询示例
利用SQL直接进行数据分析,快速获取洞察。
活跃用户统计
-- 日活用户 (DAU)
select count(distinct user_id) as dau
from public.events
where date(ts) = current_date;
-- 周活用户 (WAU)
select count(distinct user_id) as wau
from public.events
where ts >= current_date - interval '7 days';
-- 月活用户 (MAU)
select count(distinct user_id) as mau
from public.events
where ts >= current_date - interval '30 days';
功能使用排行(最近7天)
select
extra->>'feature' as feature_name,
count(*) as usage_count,
count(distinct user_id) as unique_users
from public.events
where event = 'feature_used'
and ts >= now() - interval '7 days'
group by extra->>'feature'
order by usage_count desc
limit 10;
留存率分析(次日留存)
with first_day_users as (
select distinct
user_id,
date(ts) as first_day
from public.events
where event = 'app_start'
and date(ts) = '2025-12-08'
)
select
count(distinct e.user_id)::float / count(distinct f.user_id) * 100 as retention_rate
from first_day_users f
left join public.events e on f.user_id = e.user_id
and date(e.ts) = f.first_day + interval '1 day';
总结
通过Supabase构建的这套轻量级埋点系统,完美结合了其开箱即用的后端与架构服务(PostgreSQL、Auth、Edge Functions),实现了数据安全(RLS)、高性能(批量上报)和易扩展性。它特别适合资源有限但需要快速获得产品洞察的个人开发者或小团队,无需在基础设施上投入过多精力即可搭建一个专业、隐私友好的用户行为分析平台。