we0-analyze-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ const VISITOR_ID_KEY = 'we0:distinct_id';
2
+ const SESSION_ID_KEY = 'we0:session_id';
3
+ const VISIT_COUNT_KEY = 'we0:visit_count';
4
+ export function resolveVisitor() {
5
+ const existingId = localStorage.getItem(VISITOR_ID_KEY);
6
+ if (existingId) {
7
+ return {
8
+ distinctId: existingId,
9
+ isNew: false,
10
+ };
11
+ }
12
+ const distinctId = crypto.randomUUID();
13
+ localStorage.setItem(VISITOR_ID_KEY, distinctId);
14
+ return {
15
+ distinctId,
16
+ isNew: true,
17
+ };
18
+ }
19
+ export function resolveSession() {
20
+ const existingId = sessionStorage.getItem(SESSION_ID_KEY);
21
+ if (existingId) {
22
+ return {
23
+ sessionId: existingId,
24
+ isNew: false,
25
+ visitCount: Number(localStorage.getItem(VISIT_COUNT_KEY)),
26
+ };
27
+ }
28
+ const sessionId = crypto.randomUUID();
29
+ const visitCount = Number(localStorage.getItem(VISIT_COUNT_KEY) ?? '0') + 1;
30
+ sessionStorage.setItem(SESSION_ID_KEY, sessionId);
31
+ localStorage.setItem(VISIT_COUNT_KEY, String(visitCount));
32
+ return {
33
+ sessionId,
34
+ isNew: true,
35
+ visitCount,
36
+ };
37
+ }
@@ -0,0 +1,135 @@
1
+ # 行为告警设计
2
+
3
+ ## 目标
4
+
5
+ 行为告警用于在关键业务行为发生时通知团队,例如高危操作、支付成功、大额订单、权限变更等。
6
+
7
+ 第一版不在浏览器 SDK 内判断报警。SDK 只负责采集行为事件,后端接收事件后按规则判断是否触发告警,再通过 Webhook 推送到飞书、企微、钉钉或自建服务。
8
+
9
+ ## 为什么不放在 SDK
10
+
11
+ 报警规则应该放在后端,原因是:
12
+
13
+ - 规则需要随时调整,不应该依赖业务前端重新发版。
14
+ - Webhook 地址和通知密钥不应该暴露在浏览器。
15
+ - 去重、冷却、告警记录需要服务端状态。
16
+ - 浏览器环境不稳定,不能作为告警投递的可靠执行点。
17
+
18
+ SDK 侧继续保持窄 API:业务只调用 `capture`、`conversion`、`identify` 标记事实,不把告警规则写进前端。
19
+
20
+ ## 数据流
21
+
22
+ 1. 业务代码调用 `client.capture()` 或 `client.conversion()`。
23
+ 2. SDK 组装 `We0Event` payload。
24
+ 3. SDK 输出彩色 `console.log`;配置 Supabase 后写入项目表。
25
+ 4. 后端事件接收层读取事件。
26
+ 5. 告警规则引擎匹配 `projectId`、`kind`、`event` 和 `properties`。
27
+ 6. 命中规则后写入告警记录。
28
+ 7. 后端调用规则里的 Webhook 地址。
29
+
30
+ 告警是事件消费能力,不改变 SDK 事件结构。
31
+
32
+ ## 业务埋点方式
33
+
34
+ 高危行为用 `capture`:
35
+
36
+ ```ts
37
+ we0.capture('risk_action', {
38
+ action: 'delete_project',
39
+ role: 'admin',
40
+ })
41
+ ```
42
+
43
+ 业务转化用 `conversion`:
44
+
45
+ ```ts
46
+ we0.conversion('pay_success', {
47
+ plan: 'pro',
48
+ amount: 299,
49
+ })
50
+ ```
51
+
52
+ 这类事件本身只是事实记录。是否报警由后端规则决定。
53
+
54
+ ## 规则配置
55
+
56
+ 推荐的最小规则字段:
57
+
58
+ ```ts
59
+ type BehaviorAlertRule = {
60
+ projectId: string
61
+ kind: 'capture' | 'conversion'
62
+ eventName: string
63
+ propertyFilters: Record<string, unknown>
64
+ cooldownSeconds: number
65
+ webhookUrl: string
66
+ enabled: boolean
67
+ }
68
+ ```
69
+
70
+ 字段含义:
71
+
72
+ - `projectId`:项目 ID。
73
+ - `kind`:匹配 `capture` 或 `conversion`。
74
+ - `eventName`:匹配事件名,例如 `risk_action`。
75
+ - `propertyFilters`:匹配事件属性,例如 `{ action: 'delete_project' }`。
76
+ - `cooldownSeconds`:同一规则的冷却时间,避免重复刷屏。
77
+ - `webhookUrl`:告警通知地址。
78
+ - `enabled`:是否启用规则。
79
+
80
+ ## 告警示例
81
+
82
+ 删除项目报警:
83
+
84
+ ```ts
85
+ {
86
+ projectId: 'demo',
87
+ kind: 'capture',
88
+ eventName: 'risk_action',
89
+ propertyFilters: {
90
+ action: 'delete_project',
91
+ },
92
+ cooldownSeconds: 60,
93
+ webhookUrl: 'https://example.com/webhook',
94
+ enabled: true,
95
+ }
96
+ ```
97
+
98
+ 当后端收到下面的事件时触发告警:
99
+
100
+ ```ts
101
+ {
102
+ kind: 'capture',
103
+ event: 'risk_action',
104
+ projectId: 'demo',
105
+ properties: {
106
+ action: 'delete_project',
107
+ role: 'admin',
108
+ },
109
+ }
110
+ ```
111
+
112
+ 支付成功报警:
113
+
114
+ ```ts
115
+ {
116
+ projectId: 'demo',
117
+ kind: 'conversion',
118
+ eventName: 'pay_success',
119
+ propertyFilters: {},
120
+ cooldownSeconds: 0,
121
+ webhookUrl: 'https://example.com/webhook',
122
+ enabled: true,
123
+ }
124
+ ```
125
+
126
+ ## 第一版范围
127
+
128
+ 第一版只做行为告警:
129
+
130
+ - 不做 PV、UV、转化率的异常检测。
131
+ - 不在 SDK 里内置报警规则。
132
+ - 不在 SDK 配置里暴露 Webhook。
133
+ - 不新增浏览器端重试、队列或兜底投递。
134
+
135
+ 后续如果需要指标异常告警,应基于后端聚合结果单独实现,例如按分钟或小时统计 PV、UV、转化率后再做阈值判断。
@@ -0,0 +1,82 @@
1
+ # 数据流向
2
+
3
+ ## 当前实现
4
+
5
+ 1. 应用调用 `init(config)`。
6
+ 2. SDK 从 `localStorage` 读取或创建 `distinctId`。
7
+ 3. SDK 从 `sessionStorage` 读取或创建 `sessionId`,并维护当前浏览器的 `visitCount`。
8
+ 4. SDK 采集页面、归因、设备和停留时长数据。
9
+ 5. SDK 组装事件 payload。
10
+ 6. SDK 更新本地 metrics snapshot。
11
+ 7. SDK 按事件类型输出彩色 console。
12
+ 8. 如果配置了 Supabase,SDK 会写入对应项目表。
13
+
14
+ ## Payload 结构
15
+
16
+ ```ts
17
+ type We0Event = {
18
+ kind: 'pageview' | 'pageleave' | 'capture' | 'identify' | 'conversion'
19
+ event: string
20
+ projectId: string
21
+ distinctId: string
22
+ sessionId: string
23
+ timestamp: string
24
+ properties?: Record<string, unknown>
25
+ }
26
+ ```
27
+
28
+ ## 页面访问数据
29
+
30
+ Pageview payload 包含:
31
+
32
+ - `url`
33
+ - `path`
34
+ - `title`
35
+ - `referrer`
36
+ - `pageviewId`
37
+ - `pv`
38
+ - `uv`
39
+ - `visitCount`
40
+ - `sessionPageviewCount`
41
+ - `attribution`
42
+ - `device`
43
+
44
+ 每次 pageview 事件的 `pv` 固定为 `1`。
45
+
46
+ 只有当前浏览器在 `localStorage` 中没有已有 `distinctId` 时,`uv` 才为 `1`。按日期范围聚合真实 UV 的逻辑后续由后端实现。
47
+
48
+ ## 页面离开数据
49
+
50
+ Pageleave payload 包含:
51
+
52
+ - `path`
53
+ - `pageviewId`
54
+ - `stayDurationMs`
55
+ - `bounced`
56
+ - `sessionEnded`
57
+ - `sessionPageviewCount`
58
+ - `sessionConversionCount`
59
+
60
+ `stayDurationMs` 用于计算平均停留时长。`bounced` 用于计算跳出率。
61
+
62
+ ## Metrics Snapshot
63
+
64
+ 每次 pageview、pageleave、conversion 后,SDK 会额外打印当前本地统计快照:
65
+
66
+ - `totalPv`
67
+ - `totalUv`
68
+ - `visitCount`
69
+ - `averageStayDurationMs`
70
+ - `bounceRate`
71
+ - `conversionCount`
72
+ - `pages`
73
+ - `trafficSources`
74
+ - `devices`
75
+
76
+ ## Supabase 状态
77
+
78
+ 当前默认保留带颜色区分的 `console.log`。如果配置 `supabaseUrl` 和 `supabaseAnonKey`,SDK 会用 Supabase PostgREST 写入 `{PROJECT_ID}____we0_pageviews` 和 `{PROJECT_ID}____we0_events`。Supabase 落表契约见 [Supabase 持久化方案](./supabase-persistence.md)。
79
+
80
+ ## 行为告警
81
+
82
+ 行为告警属于后端消费事件后的能力。SDK 继续上报 `capture` 和 `conversion` 事件,后端按规则匹配事件并通过 Webhook 通知。设计方案见 [行为告警设计](./behavior-alerting.md)。
@@ -0,0 +1,133 @@
1
+ # 数据来源与统计口径
2
+
3
+ 本 SDK 参考 PostHog Browser SDK 的事件思路:浏览器端采集 `$pageview`、`$pageleave`、自定义事件、`$identify`,并把 visitor、session、页面、来源、设备等维度放进事件 payload。所有事件和本地 metrics snapshot 都会通过带颜色的 `console.log` 输出;配置 Supabase 后,事件也会写入项目表。
4
+
5
+ ## 核心事件
6
+
7
+ ### `$pageview`
8
+
9
+ 触发时机:`init({ autoTrackPageview: true })` 初始化时触发一次,或者业务主动调用 `client.pageview()`。
10
+
11
+ 采集字段:
12
+
13
+ - `distinctId`:访客 ID,来自 `localStorage`。
14
+ - `sessionId`:访问 ID,来自 `sessionStorage`。
15
+ - `pageviewId`:页面访问 ID,用于关联 `$pageview` 和 `$pageleave`。
16
+ - `url`、`path`、`title`、`referrer`:页面信息。
17
+ - `pv`:本次页面浏览记为 `1`。
18
+ - `uv`:首次生成 `distinctId` 时记为 `1`,否则记为 `0`。
19
+ - `visitCount`:当前浏览器累计访问次数。
20
+ - `sessionPageviewCount`:当前访问内的页面浏览次数。
21
+ - `attribution`:流量来源信息。
22
+ - `device`:设备信息。
23
+
24
+ ### `$pageleave`
25
+
26
+ 触发时机:页面进入 `visibilityState = hidden` 时触发;页面 `pagehide` 时也会触发;或者下一次 `client.pageview()` 触发前,先结算上一个页面的停留时长。
27
+
28
+ 采集字段:
29
+
30
+ - `path`:离开的页面路径。
31
+ - `pageviewId`:当前页面访问 ID。
32
+ - `stayDurationMs`:从 pageview 到 pageleave 的停留毫秒数。
33
+ - `bounced`:当前访问只有 1 次 pageview 且没有 conversion 时为 `true`。
34
+ - `sessionEnded`:浏览器页面结束时为 `true`,普通路由切页时为 `false`。
35
+ - `sessionPageviewCount`:当前访问内的页面浏览次数。
36
+ - `sessionConversionCount`:当前访问内的转化次数。
37
+
38
+ 如果配置了 Supabase,`$pageleave` 会更新当前 `pageviewId` 对应的页面行。
39
+
40
+ ### 自定义事件
41
+
42
+ 触发时机:业务主动调用 `client.capture(event, properties)`。
43
+
44
+ 用途:记录按钮点击、表单提交等业务行为。当前不自动把普通 capture 计入转化。
45
+
46
+ ### 转化事件
47
+
48
+ 触发时机:业务主动调用 `client.conversion(event, properties)`。
49
+
50
+ 用途:记录注册、下单、提交线索等转化行为。每次调用计入 1 次转化。
51
+
52
+ ## 指标统计方式
53
+
54
+ ### 总访问量 PV
55
+
56
+ 数据来源:`$pageview`。
57
+
58
+ 统计方式:统计 `$pageview` 事件数量,或对 `$pageview.properties.pv` 求和。
59
+
60
+ 当前本地 snapshot 字段:`totalPv`。
61
+
62
+ ### 独立访客 UV
63
+
64
+ 数据来源:`$pageview.distinctId`。
65
+
66
+ 统计方式:按统计周期对 `distinctId` 去重。当前 console 阶段用首次生成 `distinctId` 时的 `uv = 1` 表示新访客。
67
+
68
+ 当前本地 snapshot 字段:`totalUv`。
69
+
70
+ ### 访问次数
71
+
72
+ 数据来源:`sessionId`。
73
+
74
+ 统计方式:按统计周期对 `sessionId` 去重。当前浏览器用 `sessionStorage` 保存当前 `sessionId`,新 session 会让 `visitCount` 加 1。
75
+
76
+ 当前本地 snapshot 字段:`visitCount`。
77
+
78
+ ### 平均停留时长
79
+
80
+ 数据来源:`$pageleave.properties.stayDurationMs`。
81
+
82
+ 统计方式:`sum(stayDurationMs) / count($pageleave)`。
83
+
84
+ 当前本地 snapshot 字段:`averageStayDurationMs`。
85
+
86
+ ### 跳出率
87
+
88
+ 数据来源:`$pageleave.properties.bounced`。
89
+
90
+ 统计方式:`bounced session 数 / 已结束 session 数`。当前 SDK 判断 bounce 的条件是:该 session 结束时只有 1 次 pageview,且没有 conversion。
91
+
92
+ 当前本地 snapshot 字段:`bounceRate`。
93
+
94
+ ### 转化次数
95
+
96
+ 数据来源:`conversion` 事件。
97
+
98
+ 统计方式:统计 `client.conversion()` 上报的事件数量。
99
+
100
+ 当前本地 snapshot 字段:`conversionCount`。
101
+
102
+ ### 页面浏览排行
103
+
104
+ 数据来源:`$pageview.properties.path` 和 `$pageleave.properties.path`。
105
+
106
+ 统计方式:
107
+
108
+ - 页面 PV:按 `path` 分组统计 `$pageview` 数量。
109
+ - 页面 UV:按 `path` 分组后对 `distinctId` 去重。当前本地 snapshot 用当前 SDK 实例内的页面去重结果表示。
110
+ - 页面停留时长:按 `path` 分组计算 `sum(stayDurationMs)` 和平均值。
111
+ - 页面跳出率:按 `path` 分组计算 `page bounces / page exits`。
112
+
113
+ 当前本地 snapshot 字段:`pages[path]`。
114
+
115
+ ### 流量来源分布(归因)
116
+
117
+ 数据来源:`$pageview.properties.attribution`。
118
+
119
+ 统计方式:
120
+
121
+ - URL 有 `utm_source` 时,来源为 `utm_source`,归因类型为 `utm`。
122
+ - 没有 `utm_source` 但有 `document.referrer` 时,来源为 referrer host,归因类型为 `referrer`。
123
+ - 两者都没有时,来源为 `direct`,归因类型为 `direct`。
124
+
125
+ 当前本地 snapshot 字段:`trafficSources[source]`。
126
+
127
+ ### 设备分布
128
+
129
+ 数据来源:`$pageview.properties.device`。
130
+
131
+ 统计方式:按 `device.type` 分组统计 `$pageview` 数量。当前设备类型为 `desktop`、`tablet`、`mobile`。
132
+
133
+ 当前本地 snapshot 字段:`devices[type]`。
@@ -0,0 +1,43 @@
1
+ # 上报时机
2
+
3
+ ## 初始化
4
+
5
+ `init(config)` 会创建 client 实例,并解析当前浏览器访客身份和访问身份。
6
+
7
+ 当 `autoTrackPageview` 为 `true` 时,SDK 会在初始化时立即上报一次 `$pageview` 事件。
8
+
9
+ ## 页面访问
10
+
11
+ 调用 `client.pageview()` 时,会立即上报一次 `$pageview` 事件。
12
+
13
+ 如果当前已有未结算页面,SDK 会先上报上一页的 `$pageleave`,再上报新的 `$pageview`。这用于手动记录 SPA 路由切换。
14
+
15
+ 当前 payload 来自浏览器 location、document title、referrer、访客身份、访问身份、归因信息和设备信息。
16
+
17
+ ## 页面离开
18
+
19
+ 浏览器页面进入 hidden 状态时,SDK 会上报一次 `$pageleave` 事件。浏览器触发 `pagehide` 时也会尝试上报;如果 hidden 已经结算过,`pagehide` 不会重复上报。
20
+
21
+ 业务也可以主动调用 `client.pageleave()` 结算当前页面。
22
+
23
+ `$pageleave` 用于记录停留时长和跳出判断。
24
+
25
+ 当前阶段仍然保留 `console.log`。如果配置了 Supabase,`$pageleave` 会通过 `fetch` 更新当前 `pageviewId` 对应的页面行。
26
+
27
+ ## 自定义事件
28
+
29
+ 调用 `client.capture(event, properties)` 时,会立即上报一次自定义事件。
30
+
31
+ ## 转化事件
32
+
33
+ 调用 `client.conversion(event, properties)` 时,会立即上报一次转化事件,并让当前访问的转化次数加 1。
34
+
35
+ ## 用户识别
36
+
37
+ 调用 `client.identify(userId, properties)` 时,会立即上报一次 `$identify` 事件。
38
+
39
+ ## 暂不实现
40
+
41
+ - 事件批量上报。
42
+ - 重试逻辑。
43
+ - 自动 SPA 路由监听。
@@ -0,0 +1,109 @@
1
+ -- Replace this value before running the SQL.
2
+ do $$
3
+ declare
4
+ project_id text := 'daafaf71-aca9-4eb6-a4ab-6c472f32a53e';
5
+ placeholder_project_id text := 'YOUR_' || 'PROJECT_ID';
6
+ pageviews_table text := project_id || '____we0_pageviews';
7
+ events_table text := project_id || '____we0_events';
8
+ begin
9
+ if project_id = placeholder_project_id or length(trim(project_id)) = 0 then
10
+ raise exception 'Set project_id before running we0 mock data SQL';
11
+ end if;
12
+
13
+ -- 清理旧 mock
14
+ execute format(
15
+ 'delete from public.%I where id in (
16
+ ''bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1'',
17
+ ''bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2'',
18
+ ''bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb3'',
19
+ ''bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb4'',
20
+ ''bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb5'',
21
+ ''bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb6''
22
+ )',
23
+ events_table
24
+ );
25
+
26
+ execute format(
27
+ 'delete from public.%I where id in (
28
+ ''11111111-1111-4111-8111-111111111111'',
29
+ ''22222222-2222-4222-8222-222222222222'',
30
+ ''33333333-3333-4333-8333-333333333333'',
31
+ ''44444444-4444-4444-8444-444444444444'',
32
+ ''55555555-5555-4555-8555-555555555555'',
33
+ ''66666666-6666-4666-8666-666666666666'',
34
+ ''77777777-7777-4777-8777-777777777777'',
35
+ ''88888888-8888-4888-8888-888888888888'',
36
+ ''99999999-9999-4999-8999-999999999999'',
37
+ ''aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa''
38
+ )',
39
+ pageviews_table
40
+ );
41
+
42
+ -- 写入页面
43
+ execute format(
44
+ $sql$
45
+ insert into public.%I (
46
+ id, viewed_at, left_at, project_id, distinct_id, session_id, path, title,
47
+ uv, visit_count, session_pageview_count, session_conversion_count,
48
+ stay_duration_ms, bounced, session_ended, attribution_type, traffic_source,
49
+ traffic_medium, traffic_campaign, traffic_content, traffic_term,
50
+ referrer_host, device_type, browser_language, viewport_width,
51
+ viewport_height, screen_width, screen_height
52
+ )
53
+ select
54
+ v.id::uuid, v.viewed_at, v.left_at, %L, v.distinct_id, v.session_id,
55
+ v.path, v.title, v.uv::smallint, v.visit_count, v.session_pageview_count,
56
+ v.session_conversion_count, v.stay_duration_ms, v.bounced, v.session_ended,
57
+ v.attribution_type, v.traffic_source, v.traffic_medium, v.traffic_campaign,
58
+ v.traffic_content, v.traffic_term, v.referrer_host, v.device_type,
59
+ v.browser_language, v.viewport_width, v.viewport_height, v.screen_width,
60
+ v.screen_height
61
+ from (
62
+ values
63
+ ('11111111-1111-4111-8111-111111111111', now() - interval '6 days 4 hours', now() - interval '6 days 3 hours 58 minutes', 'mock-visitor-001', 'mock-session-001', '/', 'Home | Demo', 1, 1, 1, 0, 120000, true, true, 'direct', 'direct', 'none', null, null, null, null, 'desktop', 'en-US', 1440, 900, 1440, 900),
64
+ ('22222222-2222-4222-8222-222222222222', now() - interval '5 days 5 hours', now() - interval '5 days 4 hours 55 minutes', 'mock-visitor-002', 'mock-session-002', '/', 'Home | Demo', 1, 1, 1, 1, 300000, false, false, 'utm', 'google', 'cpc', 'spring_launch', 'hero_cta', 'analytics_sdk', null, 'desktop', 'en-US', 1366, 768, 1366, 768),
65
+ ('33333333-3333-4333-8333-333333333333', now() - interval '5 days 4 hours 54 minutes', now() - interval '5 days 4 hours 48 minutes', 'mock-visitor-002', 'mock-session-002', '/pricing', 'Pricing | Demo', 0, 1, 2, 1, 360000, false, true, 'utm', 'google', 'cpc', 'spring_launch', 'hero_cta', 'analytics_sdk', null, 'desktop', 'en-US', 1366, 768, 1366, 768),
66
+ ('44444444-4444-4444-8444-444444444444', now() - interval '4 days 3 hours', now() - interval '4 days 2 hours 59 minutes', 'mock-visitor-003', 'mock-session-003', '/docs', 'Docs | Demo', 1, 1, 1, 0, 60000, true, true, 'referrer', 'github.com', 'referral', null, null, null, 'github.com', 'mobile', 'en-US', 390, 844, 390, 844),
67
+ ('55555555-5555-4555-8555-555555555555', now() - interval '3 days 6 hours', now() - interval '3 days 5 hours 57 minutes', 'mock-visitor-004', 'mock-session-004', '/', 'Home | Demo', 1, 1, 1, 0, 180000, false, false, 'utm', 'x', 'social', 'sdk_demo', 'post_card', null, null, 'mobile', 'en-US', 414, 896, 414, 896),
68
+ ('66666666-6666-4666-8666-666666666666', now() - interval '3 days 5 hours 56 minutes', now() - interval '3 days 5 hours 50 minutes', 'mock-visitor-004', 'mock-session-004', '/docs', 'Docs | Demo', 0, 1, 2, 0, 360000, false, true, 'utm', 'x', 'social', 'sdk_demo', 'post_card', null, null, 'mobile', 'en-US', 414, 896, 414, 896),
69
+ ('77777777-7777-4777-8777-777777777777', now() - interval '2 days 2 hours', now() - interval '2 days 1 hour 54 minutes', 'mock-visitor-005', 'mock-session-005', '/pricing', 'Pricing | Demo', 1, 1, 1, 1, 360000, false, false, 'referrer', 'producthunt.com', 'referral', null, null, null, 'producthunt.com', 'tablet', 'en-US', 820, 1180, 820, 1180),
70
+ ('88888888-8888-4888-8888-888888888888', now() - interval '2 days 1 hour 53 minutes', now() - interval '2 days 1 hour 47 minutes', 'mock-visitor-005', 'mock-session-005', '/checkout', 'Checkout | Demo', 0, 1, 2, 1, 360000, false, true, 'referrer', 'producthunt.com', 'referral', null, null, null, 'producthunt.com', 'tablet', 'en-US', 820, 1180, 820, 1180),
71
+ ('99999999-9999-4999-8999-999999999999', now() - interval '1 day 3 hours', now() - interval '1 day 2 hours 59 minutes', 'mock-visitor-006', 'mock-session-006', '/', 'Home | Demo', 1, 1, 1, 0, 60000, true, true, 'direct', 'direct', 'none', null, null, null, null, 'desktop', 'zh-CN', 1920, 1080, 1920, 1080),
72
+ ('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', now() - interval '3 hours', now() - interval '2 hours 56 minutes', 'mock-visitor-002', 'mock-session-007', '/docs', 'Docs | Demo', 0, 2, 1, 0, 240000, false, true, 'utm', 'newsletter', 'email', 'weekly_digest', 'docs_link', null, null, 'desktop', 'en-US', 1366, 768, 1366, 768)
73
+ ) as v(
74
+ id, viewed_at, left_at, distinct_id, session_id, path, title, uv,
75
+ visit_count, session_pageview_count, session_conversion_count,
76
+ stay_duration_ms, bounced, session_ended, attribution_type, traffic_source,
77
+ traffic_medium, traffic_campaign, traffic_content, traffic_term,
78
+ referrer_host, device_type, browser_language, viewport_width,
79
+ viewport_height, screen_width, screen_height
80
+ )
81
+ $sql$,
82
+ pageviews_table,
83
+ project_id
84
+ );
85
+
86
+ -- 写入事件
87
+ execute format(
88
+ $sql$
89
+ insert into public.%I (
90
+ id, event_at, project_id, kind, event_name, distinct_id, session_id,
91
+ user_id, properties
92
+ )
93
+ select
94
+ v.id::uuid, v.event_at, %L, v.kind, v.event_name, v.distinct_id,
95
+ v.session_id, v.user_id, v.properties
96
+ from (
97
+ values
98
+ ('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1', now() - interval '5 days 4 hours 56 minutes', 'capture', 'pricing_cta_click', 'mock-visitor-002', 'mock-session-002', null, '{"path":"/","source":"hero","label":"View pricing"}'::jsonb),
99
+ ('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2', now() - interval '5 days 4 hours 51 minutes', 'conversion', 'signup', 'mock-visitor-002', 'mock-session-002', null, '{"plan":"pro","value":29}'::jsonb),
100
+ ('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb3', now() - interval '5 days 4 hours 50 minutes', 'identify', '$identify', 'mock-visitor-002', 'mock-session-002', 'mock-user-002', '{"userId":"mock-user-002","plan":"pro"}'::jsonb),
101
+ ('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb4', now() - interval '3 days 5 hours 58 minutes', 'capture', 'docs_search', 'mock-visitor-004', 'mock-session-004', null, '{"query":"supabase","path":"/docs"}'::jsonb),
102
+ ('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb5', now() - interval '2 days 1 hour 55 minutes', 'capture', 'checkout_submit', 'mock-visitor-005', 'mock-session-005', null, '{"path":"/checkout","plan":"team"}'::jsonb),
103
+ ('bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb6', now() - interval '2 days 1 hour 54 minutes', 'conversion', 'purchase', 'mock-visitor-005', 'mock-session-005', null, '{"plan":"team","value":99}'::jsonb)
104
+ ) as v(id, event_at, kind, event_name, distinct_id, session_id, user_id, properties)
105
+ $sql$,
106
+ events_table,
107
+ project_id
108
+ );
109
+ end $$;