找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

281

积分

0

好友

33

主题
发表于 7 天前 | 查看: 18| 回复: 0

基于 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  │
└─────────────┘                 └──────────────────┘

核心设计思路

  1. 写入隔离:客户端通过Edge Function上报事件,Function内部使用高权限的service_role密钥写入数据库,避免在前端暴露写权限。
  2. 读取受控:数据分析仪表盘使用低权限的anon key,结合用户登录认证后读取数据,并通过RLS(行级安全策略)保护数据安全。
  3. 批量上报:客户端采用本地事件队列,配合定时或阈值触发批量上报机制,有效减少网络请求次数。

第一步:创建数据库表

在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不变,支持长期行为追踪。
  • 无需用户登录账号体系,实现匿名分析。
  • 不收集个人身份信息,隐私友好。

批量上报策略

采用 队列缓存 + 双触发机制 以优化网络请求:

  1. 定时发送:每30秒自动发送缓存队列(flushInterval)。
  2. 批量阈值:队列累积达到50条事件时立即发送(flushBatchSize)。
  3. 应用退出保障:退出时调用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)、高性能(批量上报)和易扩展性。它特别适合资源有限但需要快速获得产品洞察的个人开发者或小团队,无需在基础设施上投入过多精力即可搭建一个专业、隐私友好的用户行为分析平台。




上一篇:Chatblade命令行工具详解:将ChatGPT无缝集成到终端工作流
下一篇:Java SPI机制深度解析:从JDBC驱动看如何打破双亲委派
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 22:55 , Processed in 0.254098 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表