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.
- package/AGENTS.md +18 -0
- package/README.md +38 -0
- package/dist/attribution.d.ts +2 -0
- package/dist/attribution.js +15 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +158 -0
- package/dist/device.d.ts +2 -0
- package/dist/device.js +21 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +26 -0
- package/dist/metrics.d.ts +21 -0
- package/dist/metrics.js +81 -0
- package/dist/supabase-reporter.d.ts +9 -0
- package/dist/supabase-reporter.js +164 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +1 -0
- package/dist/visitor.d.ts +11 -0
- package/dist/visitor.js +37 -0
- package/docs/behavior-alerting.md +135 -0
- package/docs/data-flow.md +82 -0
- package/docs/data-source.md +133 -0
- package/docs/reporting-timing.md +43 -0
- package/docs/supabase-mock-data.sql +109 -0
- package/docs/supabase-persistence.md +193 -0
- package/docs/supabase-schema.sql +115 -0
- package/docs/supabase-test.sql +15 -0
- package/package.json +33 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Supabase 持久化方案
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
SDK 在浏览器采集事件,并使用宿主应用显式传入的 Supabase anon key 直接写入用户自己的 Supabase 项目。SDK 不建表,不读取宿主 `.env`,建表 SQL 由后端或迁移流程在合适时机执行。
|
|
6
|
+
|
|
7
|
+
建表 SQL 见 [`supabase-schema.sql`](./supabase-schema.sql)。
|
|
8
|
+
|
|
9
|
+
## 初始化
|
|
10
|
+
|
|
11
|
+
宿主应用把环境变量读出来,再显式传给 SDK:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { init } from 'we0-analyze-sdk'
|
|
15
|
+
|
|
16
|
+
const we0 = init({
|
|
17
|
+
projectId: process.env.PROJECT_ID!,
|
|
18
|
+
autoTrackPageview: true,
|
|
19
|
+
supabaseUrl: process.env.SUPABASE_URL!,
|
|
20
|
+
supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
SDK 不使用 `SUPABASE_SERVICE_ROLE_KEY`。service role key 只应留在宿主后端或迁移流程里。
|
|
25
|
+
|
|
26
|
+
## 表设计
|
|
27
|
+
|
|
28
|
+
表名由 `projectId` 决定:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
{PROJECT_ID}____we0_pageviews
|
|
32
|
+
{PROJECT_ID}____we0_events
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
推荐保留两张表:
|
|
36
|
+
|
|
37
|
+
- `$pageview`:插入 `{PROJECT_ID}____we0_pageviews`,一条 PV 一行。
|
|
38
|
+
- `$pageleave`:更新同一条 `{PROJECT_ID}____we0_pageviews`,不新增行。
|
|
39
|
+
- `capture`、`conversion`、`$identify`:插入 `{PROJECT_ID}____we0_events`。
|
|
40
|
+
- 页面统计从 pageviews 表聚合。
|
|
41
|
+
- 业务事件统计从 events 表聚合。
|
|
42
|
+
|
|
43
|
+
这个方案把基础场景的行数从 `daily_pv * 2` 降到约 `daily_pv * 1`。页面表不保存完整 `properties jsonb`、完整 `url`、完整 `referrer` 和完整 `userAgent`,避免重复存大字段。
|
|
44
|
+
|
|
45
|
+
## 写入行为
|
|
46
|
+
|
|
47
|
+
SDK 侧行为:
|
|
48
|
+
|
|
49
|
+
- 未配置 Supabase 时,只输出彩色 console 日志。
|
|
50
|
+
- 配置 Supabase 后,所有事件都会尝试写入对应项目表。
|
|
51
|
+
- 每次 `$pageview` 会生成 `pageviewId`,对应 `$pageleave` 复用同一个 `pageviewId`。
|
|
52
|
+
- 写库成功会输出绿色 console。
|
|
53
|
+
- 缺少 `PROJECT_ID`、Supabase 配置不完整、表未创建或权限不足时会输出橘色 console。
|
|
54
|
+
- `metrics` 只用于本地调试,不写入数据库。
|
|
55
|
+
|
|
56
|
+
SDK 不做建表、队列、重试、fallback 或聚合。
|
|
57
|
+
|
|
58
|
+
## SQL 变量说明
|
|
59
|
+
|
|
60
|
+
普通 SQL 参数不能直接替代表名这类 identifier。`supabase-schema.sql` 使用 PL/pgSQL:
|
|
61
|
+
|
|
62
|
+
```sql
|
|
63
|
+
do $$
|
|
64
|
+
declare
|
|
65
|
+
project_id text := 'YOUR_PROJECT_ID';
|
|
66
|
+
pageviews_table text := project_id || '____we0_pageviews';
|
|
67
|
+
events_table text := project_id || '____we0_events';
|
|
68
|
+
begin
|
|
69
|
+
execute format('create table if not exists public.%I (...)', pageviews_table);
|
|
70
|
+
end $$;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
执行前只需要替换 `project_id` 的值。
|
|
74
|
+
|
|
75
|
+
## 字段映射
|
|
76
|
+
|
|
77
|
+
`$pageview` 插入 pageviews 表:
|
|
78
|
+
|
|
79
|
+
| 表字段 | 来源 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `id` | `event.properties.pageviewId` |
|
|
82
|
+
| `viewed_at` | `event.timestamp` |
|
|
83
|
+
| `project_id` | `event.projectId` |
|
|
84
|
+
| `distinct_id` | `event.distinctId` |
|
|
85
|
+
| `session_id` | `event.sessionId` |
|
|
86
|
+
| `path` | `event.properties.path` |
|
|
87
|
+
| `title` | `event.properties.title` |
|
|
88
|
+
| `uv` | `event.properties.uv` |
|
|
89
|
+
| `visit_count` | `event.properties.visitCount` |
|
|
90
|
+
| `session_pageview_count` | `event.properties.sessionPageviewCount` |
|
|
91
|
+
| attribution 字段 | `event.properties.attribution` |
|
|
92
|
+
| device 字段 | `event.properties.device` |
|
|
93
|
+
|
|
94
|
+
`$pageleave` 更新 pageviews 表:
|
|
95
|
+
|
|
96
|
+
| 表字段 | 来源 |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `left_at` | `event.timestamp` |
|
|
99
|
+
| `stay_duration_ms` | `round(event.properties.stayDurationMs)` |
|
|
100
|
+
| `bounced` | `event.properties.bounced` |
|
|
101
|
+
| `session_ended` | `event.properties.sessionEnded` |
|
|
102
|
+
| `session_pageview_count` | `event.properties.sessionPageviewCount` |
|
|
103
|
+
| `session_conversion_count` | `event.properties.sessionConversionCount` |
|
|
104
|
+
|
|
105
|
+
`capture`、`conversion`、`$identify` 插入 events 表:
|
|
106
|
+
|
|
107
|
+
| 表字段 | 来源 |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `event_at` | `event.timestamp` |
|
|
110
|
+
| `project_id` | `event.projectId` |
|
|
111
|
+
| `kind` | `event.kind` |
|
|
112
|
+
| `event_name` | `event.event` |
|
|
113
|
+
| `distinct_id` | `event.distinctId` |
|
|
114
|
+
| `session_id` | `event.sessionId` |
|
|
115
|
+
| `user_id` | `event.properties.userId`,仅 `$identify` 使用 |
|
|
116
|
+
| `properties` | `event.properties` |
|
|
117
|
+
|
|
118
|
+
## 表数据量估算
|
|
119
|
+
|
|
120
|
+
基础场景里没有自定义事件、转化事件和 identify:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
daily_rows ~= daily_pv
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
如果平均每个 PV 还会触发 `n` 个自定义事件:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
daily_rows ~= daily_pv * (1 + n) + daily_conversion_events + daily_identify_events
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
页面表先按每行 `1 KB` 到 `2 KB` 估算;默认用 `1.5 KB` 每行做预算。
|
|
133
|
+
|
|
134
|
+
| 日 PV | 页面表 30 天行数 | 页面表 30 天存储 |
|
|
135
|
+
| --- | ---: | ---: |
|
|
136
|
+
| `1,000` | `~30,000` | `~45 MB` |
|
|
137
|
+
| `10,000` | `~300,000` | `~450 MB` |
|
|
138
|
+
| `50,000` | `~1,500,000` | `~2.25 GB` |
|
|
139
|
+
| `100,000` | `~3,000,000` | `~4.5 GB` |
|
|
140
|
+
| `1,000,000` | `~30,000,000` | `~45 GB` |
|
|
141
|
+
|
|
142
|
+
上线后用真实表校准平均行大小:
|
|
143
|
+
|
|
144
|
+
```sql
|
|
145
|
+
select
|
|
146
|
+
count(*) as rows,
|
|
147
|
+
pg_size_pretty(pg_total_relation_size('public.YOUR_PROJECT_ID____we0_pageviews')) as total_size,
|
|
148
|
+
pg_total_relation_size('public.YOUR_PROJECT_ID____we0_pageviews') / nullif(count(*), 0) as bytes_per_row
|
|
149
|
+
from public.YOUR_PROJECT_ID____we0_pageviews;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`pg_total_relation_size` 包含表、索引和 toast 数据,更适合做 Supabase 容量预算。
|
|
153
|
+
|
|
154
|
+
## 常用统计 SQL
|
|
155
|
+
|
|
156
|
+
PV:
|
|
157
|
+
|
|
158
|
+
```sql
|
|
159
|
+
select count(*) as pv
|
|
160
|
+
from public.YOUR_PROJECT_ID____we0_pageviews
|
|
161
|
+
where viewed_at >= $1
|
|
162
|
+
and viewed_at < $2;
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
UV:
|
|
166
|
+
|
|
167
|
+
```sql
|
|
168
|
+
select count(distinct distinct_id) as uv
|
|
169
|
+
from public.YOUR_PROJECT_ID____we0_pageviews
|
|
170
|
+
where viewed_at >= $1
|
|
171
|
+
and viewed_at < $2;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
平均停留时长:
|
|
175
|
+
|
|
176
|
+
```sql
|
|
177
|
+
select avg(stay_duration_ms) as average_stay_duration_ms
|
|
178
|
+
from public.YOUR_PROJECT_ID____we0_pageviews
|
|
179
|
+
where viewed_at >= $1
|
|
180
|
+
and viewed_at < $2
|
|
181
|
+
and stay_duration_ms is not null;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
事件数:
|
|
185
|
+
|
|
186
|
+
```sql
|
|
187
|
+
select event_name, count(*) as count
|
|
188
|
+
from public.YOUR_PROJECT_ID____we0_events
|
|
189
|
+
where event_at >= $1
|
|
190
|
+
and event_at < $2
|
|
191
|
+
group by event_name
|
|
192
|
+
order by count desc;
|
|
193
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
-- Replace this value before running the SQL.
|
|
2
|
+
do $$
|
|
3
|
+
declare
|
|
4
|
+
project_id text := '619063b3-5f46-4bbc-9c28-1c6ad5852b4e';
|
|
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 schema SQL';
|
|
11
|
+
end if;
|
|
12
|
+
|
|
13
|
+
execute format(
|
|
14
|
+
'create table if not exists public.%I (
|
|
15
|
+
id uuid primary key,
|
|
16
|
+
received_at timestamptz not null default now(),
|
|
17
|
+
viewed_at timestamptz not null,
|
|
18
|
+
left_at timestamptz,
|
|
19
|
+
|
|
20
|
+
project_id text not null,
|
|
21
|
+
distinct_id text not null,
|
|
22
|
+
session_id text not null,
|
|
23
|
+
|
|
24
|
+
path text not null,
|
|
25
|
+
title text,
|
|
26
|
+
|
|
27
|
+
pv smallint not null default 1 check (pv = 1),
|
|
28
|
+
uv smallint not null default 0 check (uv in (0, 1)),
|
|
29
|
+
visit_count integer check (visit_count is null or visit_count >= 0),
|
|
30
|
+
session_pageview_count integer check (
|
|
31
|
+
session_pageview_count is null or session_pageview_count >= 0
|
|
32
|
+
),
|
|
33
|
+
session_conversion_count integer check (
|
|
34
|
+
session_conversion_count is null or session_conversion_count >= 0
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
stay_duration_ms integer check (
|
|
38
|
+
stay_duration_ms is null or stay_duration_ms >= 0
|
|
39
|
+
),
|
|
40
|
+
bounced boolean,
|
|
41
|
+
session_ended boolean,
|
|
42
|
+
|
|
43
|
+
attribution_type text check (
|
|
44
|
+
attribution_type is null or attribution_type in (''utm'', ''referrer'', ''direct'')
|
|
45
|
+
),
|
|
46
|
+
traffic_source text,
|
|
47
|
+
traffic_medium text,
|
|
48
|
+
traffic_campaign text,
|
|
49
|
+
traffic_content text,
|
|
50
|
+
traffic_term text,
|
|
51
|
+
referrer_host text,
|
|
52
|
+
|
|
53
|
+
device_type text check (
|
|
54
|
+
device_type is null or device_type in (''desktop'', ''tablet'', ''mobile'')
|
|
55
|
+
),
|
|
56
|
+
browser_language text,
|
|
57
|
+
viewport_width integer check (viewport_width is null or viewport_width >= 0),
|
|
58
|
+
viewport_height integer check (viewport_height is null or viewport_height >= 0),
|
|
59
|
+
screen_width integer check (screen_width is null or screen_width >= 0),
|
|
60
|
+
screen_height integer check (screen_height is null or screen_height >= 0)
|
|
61
|
+
)',
|
|
62
|
+
pageviews_table
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
execute format(
|
|
66
|
+
'create table if not exists public.%I (
|
|
67
|
+
id uuid primary key,
|
|
68
|
+
received_at timestamptz not null default now(),
|
|
69
|
+
event_at timestamptz not null,
|
|
70
|
+
|
|
71
|
+
project_id text not null,
|
|
72
|
+
kind text not null check (
|
|
73
|
+
kind in (''capture'', ''identify'', ''conversion'')
|
|
74
|
+
),
|
|
75
|
+
event_name text not null,
|
|
76
|
+
distinct_id text not null,
|
|
77
|
+
session_id text not null,
|
|
78
|
+
user_id text,
|
|
79
|
+
|
|
80
|
+
properties jsonb not null default ''{}''::jsonb
|
|
81
|
+
)',
|
|
82
|
+
events_table
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
execute format('alter table public.%I enable row level security', pageviews_table);
|
|
86
|
+
execute format('alter table public.%I enable row level security', events_table);
|
|
87
|
+
|
|
88
|
+
execute format('grant insert, update on table public.%I to anon', pageviews_table);
|
|
89
|
+
execute format('grant insert on table public.%I to anon', events_table);
|
|
90
|
+
|
|
91
|
+
execute format('drop policy if exists we0_pageviews_anon_insert on public.%I', pageviews_table);
|
|
92
|
+
execute format(
|
|
93
|
+
'create policy we0_pageviews_anon_insert on public.%I
|
|
94
|
+
for insert to anon
|
|
95
|
+
with check (true)',
|
|
96
|
+
pageviews_table
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
execute format('drop policy if exists we0_pageviews_anon_update on public.%I', pageviews_table);
|
|
100
|
+
execute format(
|
|
101
|
+
'create policy we0_pageviews_anon_update on public.%I
|
|
102
|
+
for update to anon
|
|
103
|
+
using (true)
|
|
104
|
+
with check (true)',
|
|
105
|
+
pageviews_table
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
execute format('drop policy if exists we0_events_anon_insert on public.%I', events_table);
|
|
109
|
+
execute format(
|
|
110
|
+
'create policy we0_events_anon_insert on public.%I
|
|
111
|
+
for insert to anon
|
|
112
|
+
with check (true)',
|
|
113
|
+
events_table
|
|
114
|
+
);
|
|
115
|
+
end $$;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- SQL Editor smoke test.
|
|
2
|
+
select now() as sql_editor_ok;
|
|
3
|
+
|
|
4
|
+
-- 临时测试
|
|
5
|
+
create temp table we0_sql_editor_test (
|
|
6
|
+
id integer primary key,
|
|
7
|
+
note text not null,
|
|
8
|
+
created_at timestamptz not null default now()
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
insert into we0_sql_editor_test (id, note)
|
|
12
|
+
values (1, 'ok');
|
|
13
|
+
|
|
14
|
+
select *
|
|
15
|
+
from we0_sql_editor_test;
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "we0-analyze-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal browser analytics SDK for pageview, PV, and UV tracking.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"docs",
|
|
17
|
+
"AGENTS.md",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
23
|
+
"prepare": "npm run build",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+ssh://git@github.com/Minf97/we0-analyze-sdk.git"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.8.3"
|
|
32
|
+
}
|
|
33
|
+
}
|