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
package/AGENTS.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Project Rules
|
|
4
|
+
|
|
5
|
+
- Keep implementation code as small as possible.
|
|
6
|
+
- Do not add fallback, retry, queue, polyfill, adapter, or compatibility code.
|
|
7
|
+
- Only use the most basic `catch` block for surfacing errors.
|
|
8
|
+
- Backend persistence is paused. Use colored `console.log` as the only reporter until the docs are confirmed.
|
|
9
|
+
- Do not add new runtime dependencies without explicit approval.
|
|
10
|
+
- Keep public SDK APIs explicit and narrow.
|
|
11
|
+
|
|
12
|
+
## Current Scope
|
|
13
|
+
|
|
14
|
+
- Browser SDK boilerplate.
|
|
15
|
+
- Pageview event collection.
|
|
16
|
+
- PV and UV fields in the reported payload.
|
|
17
|
+
- Manual custom event capture.
|
|
18
|
+
- Manual user identify.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# we0-analyze-sdk
|
|
2
|
+
|
|
3
|
+
Minimal browser analytics SDK boilerplate for pageview, pageleave, attribution, device, and conversion events.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install we0-analyze-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
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
|
+
we0.capture('signup_click', {
|
|
24
|
+
source: 'home',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
we0.conversion('signup')
|
|
28
|
+
we0.identify('user_id')
|
|
29
|
+
|
|
30
|
+
console.log(we0.getMetrics())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run `docs/supabase-schema.sql` in the Supabase project before enabling database writes. The SDK writes to:
|
|
34
|
+
|
|
35
|
+
- `${PROJECT_ID}____we0_pageviews`
|
|
36
|
+
- `${PROJECT_ID}____we0_events`
|
|
37
|
+
|
|
38
|
+
Colored `console.log` output is still kept for local debugging.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function collectAttribution() {
|
|
2
|
+
const url = new URL(location.href);
|
|
3
|
+
const source = url.searchParams.get('utm_source');
|
|
4
|
+
const referrerHost = document.referrer ? new URL(document.referrer).host : '';
|
|
5
|
+
return {
|
|
6
|
+
type: source ? 'utm' : referrerHost ? 'referrer' : 'direct',
|
|
7
|
+
source: source ?? (referrerHost || 'direct'),
|
|
8
|
+
medium: url.searchParams.get('utm_medium') ?? '',
|
|
9
|
+
campaign: url.searchParams.get('utm_campaign') ?? '',
|
|
10
|
+
content: url.searchParams.get('utm_content') ?? '',
|
|
11
|
+
term: url.searchParams.get('utm_term') ?? '',
|
|
12
|
+
referrer: document.referrer,
|
|
13
|
+
referrerHost,
|
|
14
|
+
};
|
|
15
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { We0Config, We0MetricsSnapshot } from './types.js';
|
|
2
|
+
export declare class We0Client {
|
|
3
|
+
private readonly config;
|
|
4
|
+
private readonly distinctId;
|
|
5
|
+
private readonly sessionId;
|
|
6
|
+
private readonly visitCount;
|
|
7
|
+
private readonly metrics;
|
|
8
|
+
private isNewVisitor;
|
|
9
|
+
private activePage?;
|
|
10
|
+
private sessionPageviewCount;
|
|
11
|
+
private sessionConversionCount;
|
|
12
|
+
private readonly reporter;
|
|
13
|
+
constructor(config: We0Config);
|
|
14
|
+
pageview(properties?: Record<string, unknown>): void;
|
|
15
|
+
pageleave(properties?: Record<string, unknown>): void;
|
|
16
|
+
private leavePage;
|
|
17
|
+
capture(event: string, properties?: Record<string, unknown>): void;
|
|
18
|
+
conversion(event: string, properties?: Record<string, unknown>): void;
|
|
19
|
+
identify(userId: string, properties?: Record<string, unknown>): void;
|
|
20
|
+
getMetrics(): We0MetricsSnapshot;
|
|
21
|
+
private report;
|
|
22
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { collectAttribution } from './attribution.js';
|
|
2
|
+
import { collectDevice } from './device.js';
|
|
3
|
+
import { logEvent, logMetrics } from './logger.js';
|
|
4
|
+
import { MetricsStore } from './metrics.js';
|
|
5
|
+
import { SupabaseReporter } from './supabase-reporter.js';
|
|
6
|
+
import { resolveSession, resolveVisitor } from './visitor.js';
|
|
7
|
+
export class We0Client {
|
|
8
|
+
config;
|
|
9
|
+
distinctId;
|
|
10
|
+
sessionId;
|
|
11
|
+
visitCount;
|
|
12
|
+
metrics;
|
|
13
|
+
isNewVisitor;
|
|
14
|
+
activePage;
|
|
15
|
+
sessionPageviewCount = 0;
|
|
16
|
+
sessionConversionCount = 0;
|
|
17
|
+
reporter;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
const visitor = resolveVisitor();
|
|
21
|
+
const session = resolveSession();
|
|
22
|
+
this.distinctId = visitor.distinctId;
|
|
23
|
+
this.sessionId = session.sessionId;
|
|
24
|
+
this.visitCount = session.visitCount;
|
|
25
|
+
this.metrics = new MetricsStore(session.visitCount);
|
|
26
|
+
this.isNewVisitor = visitor.isNew;
|
|
27
|
+
this.reporter = new SupabaseReporter(config);
|
|
28
|
+
document.addEventListener('visibilitychange', () => {
|
|
29
|
+
if (document.visibilityState === 'hidden') {
|
|
30
|
+
this.pageleave();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
window.addEventListener('pagehide', () => this.pageleave());
|
|
34
|
+
if (config.autoTrackPageview) {
|
|
35
|
+
this.pageview();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
pageview(properties) {
|
|
39
|
+
this.leavePage(false);
|
|
40
|
+
const uv = this.isNewVisitor ? 1 : 0;
|
|
41
|
+
const attribution = collectAttribution();
|
|
42
|
+
const device = collectDevice();
|
|
43
|
+
this.sessionPageviewCount += 1;
|
|
44
|
+
// 关联离开事件
|
|
45
|
+
const pageviewId = crypto.randomUUID();
|
|
46
|
+
this.activePage = {
|
|
47
|
+
pageviewId,
|
|
48
|
+
path: location.pathname,
|
|
49
|
+
startedAt: performance.now(),
|
|
50
|
+
};
|
|
51
|
+
this.metrics.recordPageview(location.pathname, uv, attribution.source, device);
|
|
52
|
+
this.report({
|
|
53
|
+
kind: 'pageview',
|
|
54
|
+
event: '$pageview',
|
|
55
|
+
projectId: this.config.projectId,
|
|
56
|
+
distinctId: this.distinctId,
|
|
57
|
+
sessionId: this.sessionId,
|
|
58
|
+
timestamp: new Date().toISOString(),
|
|
59
|
+
properties: {
|
|
60
|
+
...properties,
|
|
61
|
+
pageviewId,
|
|
62
|
+
url: location.href,
|
|
63
|
+
path: location.pathname,
|
|
64
|
+
title: document.title,
|
|
65
|
+
referrer: document.referrer,
|
|
66
|
+
pv: 1,
|
|
67
|
+
uv,
|
|
68
|
+
visitCount: this.visitCount,
|
|
69
|
+
sessionPageviewCount: this.sessionPageviewCount,
|
|
70
|
+
attribution,
|
|
71
|
+
device,
|
|
72
|
+
},
|
|
73
|
+
}, true);
|
|
74
|
+
this.isNewVisitor = false;
|
|
75
|
+
}
|
|
76
|
+
pageleave(properties) {
|
|
77
|
+
this.leavePage(true, properties);
|
|
78
|
+
}
|
|
79
|
+
leavePage(sessionEnded, properties) {
|
|
80
|
+
if (!this.activePage) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const activePage = this.activePage;
|
|
84
|
+
const stayDurationMs = performance.now() - activePage.startedAt;
|
|
85
|
+
const bounced = sessionEnded && this.sessionPageviewCount === 1 && this.sessionConversionCount === 0;
|
|
86
|
+
this.activePage = undefined;
|
|
87
|
+
this.metrics.recordPageleave(activePage.path, stayDurationMs, bounced, sessionEnded);
|
|
88
|
+
this.report({
|
|
89
|
+
kind: 'pageleave',
|
|
90
|
+
event: '$pageleave',
|
|
91
|
+
projectId: this.config.projectId,
|
|
92
|
+
distinctId: this.distinctId,
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
properties: {
|
|
96
|
+
...properties,
|
|
97
|
+
pageviewId: activePage.pageviewId,
|
|
98
|
+
path: activePage.path,
|
|
99
|
+
stayDurationMs,
|
|
100
|
+
bounced,
|
|
101
|
+
sessionEnded,
|
|
102
|
+
sessionPageviewCount: this.sessionPageviewCount,
|
|
103
|
+
sessionConversionCount: this.sessionConversionCount,
|
|
104
|
+
},
|
|
105
|
+
}, true);
|
|
106
|
+
}
|
|
107
|
+
capture(event, properties) {
|
|
108
|
+
this.report({
|
|
109
|
+
kind: 'capture',
|
|
110
|
+
event,
|
|
111
|
+
projectId: this.config.projectId,
|
|
112
|
+
distinctId: this.distinctId,
|
|
113
|
+
sessionId: this.sessionId,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
properties,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
conversion(event, properties) {
|
|
119
|
+
this.sessionConversionCount += 1;
|
|
120
|
+
this.metrics.recordConversion();
|
|
121
|
+
this.report({
|
|
122
|
+
kind: 'conversion',
|
|
123
|
+
event,
|
|
124
|
+
projectId: this.config.projectId,
|
|
125
|
+
distinctId: this.distinctId,
|
|
126
|
+
sessionId: this.sessionId,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
properties,
|
|
129
|
+
}, true);
|
|
130
|
+
}
|
|
131
|
+
identify(userId, properties) {
|
|
132
|
+
this.report({
|
|
133
|
+
kind: 'identify',
|
|
134
|
+
event: '$identify',
|
|
135
|
+
projectId: this.config.projectId,
|
|
136
|
+
distinctId: this.distinctId,
|
|
137
|
+
sessionId: this.sessionId,
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
properties: {
|
|
140
|
+
...properties,
|
|
141
|
+
userId,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
getMetrics() {
|
|
146
|
+
return this.metrics.snapshot();
|
|
147
|
+
}
|
|
148
|
+
report(event, includeMetrics = false) {
|
|
149
|
+
const metrics = includeMetrics ? this.metrics.snapshot() : undefined;
|
|
150
|
+
if (this.reporter.isEnabled()) {
|
|
151
|
+
void this.reporter.write(event);
|
|
152
|
+
}
|
|
153
|
+
logEvent(event);
|
|
154
|
+
if (metrics) {
|
|
155
|
+
logMetrics(metrics);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/device.d.ts
ADDED
package/dist/device.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function collectDevice() {
|
|
2
|
+
const userAgent = navigator.userAgent;
|
|
3
|
+
return {
|
|
4
|
+
type: getDeviceType(userAgent),
|
|
5
|
+
userAgent,
|
|
6
|
+
language: navigator.language,
|
|
7
|
+
viewportWidth: window.innerWidth,
|
|
8
|
+
viewportHeight: window.innerHeight,
|
|
9
|
+
screenWidth: screen.width,
|
|
10
|
+
screenHeight: screen.height,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function getDeviceType(userAgent) {
|
|
14
|
+
if (/iPad|Tablet/.test(userAgent)) {
|
|
15
|
+
return 'tablet';
|
|
16
|
+
}
|
|
17
|
+
if (/Mobi|Android|iPhone/.test(userAgent)) {
|
|
18
|
+
return 'mobile';
|
|
19
|
+
}
|
|
20
|
+
return 'desktop';
|
|
21
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { We0Client } from './client.js';
|
|
2
|
+
import type { We0Config } from './types.js';
|
|
3
|
+
export type { We0Attribution, We0Config, We0Device, We0Event, We0EventKind, We0MetricsSnapshot, We0PageMetrics, } from './types.js';
|
|
4
|
+
export { We0Client } from './client.js';
|
|
5
|
+
export declare function init(config: We0Config): We0Client;
|
package/dist/index.js
ADDED
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { We0Event, We0MetricsSnapshot } from './types.js';
|
|
2
|
+
export declare function logEvent(event: We0Event): void;
|
|
3
|
+
export declare function logMetrics(metrics: We0MetricsSnapshot): void;
|
|
4
|
+
export declare function logPersistenceSuccess(tableName: string, event: We0Event): void;
|
|
5
|
+
export declare function logPersistenceWarning(message: string, details?: unknown): void;
|
|
6
|
+
export declare function logError(error: unknown): void;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const styles = {
|
|
2
|
+
pageview: 'color:#2563eb;font-weight:700',
|
|
3
|
+
pageleave: 'color:#0f766e;font-weight:700',
|
|
4
|
+
capture: 'color:#16a34a;font-weight:700',
|
|
5
|
+
identify: 'color:#9333ea;font-weight:700',
|
|
6
|
+
conversion: 'color:#c2410c;font-weight:700',
|
|
7
|
+
};
|
|
8
|
+
export function logEvent(event) {
|
|
9
|
+
console.log(`%c[we0:${event.kind}]`, styles[event.kind], event);
|
|
10
|
+
}
|
|
11
|
+
export function logMetrics(metrics) {
|
|
12
|
+
console.log('%c[we0:metrics]', 'color:#ea580c;font-weight:700', metrics);
|
|
13
|
+
}
|
|
14
|
+
export function logPersistenceSuccess(tableName, event) {
|
|
15
|
+
console.log('%c[we0:database:success]', 'color:#16a34a;font-weight:700', {
|
|
16
|
+
tableName,
|
|
17
|
+
kind: event.kind,
|
|
18
|
+
event: event.event,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function logPersistenceWarning(message, details) {
|
|
22
|
+
console.warn('%c[we0:database:warning]', 'color:#f59e0b;font-weight:700', message, details);
|
|
23
|
+
}
|
|
24
|
+
export function logError(error) {
|
|
25
|
+
console.error('%c[we0:error]', 'color:#dc2626;font-weight:700', error);
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { We0Device, We0MetricsSnapshot } from './types.js';
|
|
2
|
+
export declare class MetricsStore {
|
|
3
|
+
private readonly visitCount;
|
|
4
|
+
private totalPv;
|
|
5
|
+
private totalUv;
|
|
6
|
+
private totalStayDurationMs;
|
|
7
|
+
private staySamples;
|
|
8
|
+
private bouncedSessions;
|
|
9
|
+
private endedSessions;
|
|
10
|
+
private conversionCount;
|
|
11
|
+
private readonly pages;
|
|
12
|
+
private readonly trafficSources;
|
|
13
|
+
private readonly devices;
|
|
14
|
+
private readonly seenPages;
|
|
15
|
+
constructor(visitCount: number);
|
|
16
|
+
recordPageview(path: string, uv: number, trafficSource: string, device: We0Device): void;
|
|
17
|
+
recordPageleave(path: string, stayDurationMs: number, bounced: boolean, sessionEnded: boolean): void;
|
|
18
|
+
recordConversion(): void;
|
|
19
|
+
snapshot(): We0MetricsSnapshot;
|
|
20
|
+
private getPage;
|
|
21
|
+
}
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class MetricsStore {
|
|
2
|
+
visitCount;
|
|
3
|
+
totalPv = 0;
|
|
4
|
+
totalUv = 0;
|
|
5
|
+
totalStayDurationMs = 0;
|
|
6
|
+
staySamples = 0;
|
|
7
|
+
bouncedSessions = 0;
|
|
8
|
+
endedSessions = 0;
|
|
9
|
+
conversionCount = 0;
|
|
10
|
+
pages = {};
|
|
11
|
+
trafficSources = {};
|
|
12
|
+
devices = {};
|
|
13
|
+
seenPages = new Set();
|
|
14
|
+
constructor(visitCount) {
|
|
15
|
+
this.visitCount = visitCount;
|
|
16
|
+
}
|
|
17
|
+
recordPageview(path, uv, trafficSource, device) {
|
|
18
|
+
this.totalPv += 1;
|
|
19
|
+
this.totalUv += uv;
|
|
20
|
+
const page = this.getPage(path);
|
|
21
|
+
const pageUv = this.seenPages.has(path) ? 0 : 1;
|
|
22
|
+
this.seenPages.add(path);
|
|
23
|
+
page.pv += 1;
|
|
24
|
+
page.uv += pageUv;
|
|
25
|
+
this.trafficSources[trafficSource] = (this.trafficSources[trafficSource] ?? 0) + 1;
|
|
26
|
+
this.devices[device.type] = (this.devices[device.type] ?? 0) + 1;
|
|
27
|
+
}
|
|
28
|
+
recordPageleave(path, stayDurationMs, bounced, sessionEnded) {
|
|
29
|
+
this.totalStayDurationMs += stayDurationMs;
|
|
30
|
+
this.staySamples += 1;
|
|
31
|
+
const page = this.getPage(path);
|
|
32
|
+
page.stayDurationMs += stayDurationMs;
|
|
33
|
+
page.staySamples += 1;
|
|
34
|
+
if (sessionEnded) {
|
|
35
|
+
this.endedSessions += 1;
|
|
36
|
+
page.exits += 1;
|
|
37
|
+
if (bounced) {
|
|
38
|
+
this.bouncedSessions += 1;
|
|
39
|
+
page.bounces += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
recordConversion() {
|
|
44
|
+
this.conversionCount += 1;
|
|
45
|
+
}
|
|
46
|
+
snapshot() {
|
|
47
|
+
const pages = {};
|
|
48
|
+
for (const path of Object.keys(this.pages)) {
|
|
49
|
+
const page = this.pages[path];
|
|
50
|
+
pages[path] = {
|
|
51
|
+
pv: page.pv,
|
|
52
|
+
uv: page.uv,
|
|
53
|
+
stayDurationMs: page.stayDurationMs,
|
|
54
|
+
averageStayDurationMs: page.staySamples ? page.stayDurationMs / page.staySamples : 0,
|
|
55
|
+
bounceRate: page.exits ? page.bounces / page.exits : 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
totalPv: this.totalPv,
|
|
60
|
+
totalUv: this.totalUv,
|
|
61
|
+
visitCount: this.visitCount,
|
|
62
|
+
averageStayDurationMs: this.staySamples ? this.totalStayDurationMs / this.staySamples : 0,
|
|
63
|
+
bounceRate: this.endedSessions ? this.bouncedSessions / this.endedSessions : 0,
|
|
64
|
+
conversionCount: this.conversionCount,
|
|
65
|
+
pages,
|
|
66
|
+
trafficSources: this.trafficSources,
|
|
67
|
+
devices: this.devices,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
getPage(path) {
|
|
71
|
+
this.pages[path] ??= {
|
|
72
|
+
pv: 0,
|
|
73
|
+
uv: 0,
|
|
74
|
+
stayDurationMs: 0,
|
|
75
|
+
staySamples: 0,
|
|
76
|
+
bounces: 0,
|
|
77
|
+
exits: 0,
|
|
78
|
+
};
|
|
79
|
+
return this.pages[path];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { We0Config, We0Event } from './types.js';
|
|
2
|
+
export declare class SupabaseReporter {
|
|
3
|
+
private readonly config?;
|
|
4
|
+
constructor(config: We0Config);
|
|
5
|
+
isEnabled(): boolean;
|
|
6
|
+
write(event: We0Event): Promise<void>;
|
|
7
|
+
private insert;
|
|
8
|
+
private updatePageview;
|
|
9
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { logPersistenceSuccess, logPersistenceWarning } from './logger.js';
|
|
2
|
+
function readRecord(value) {
|
|
3
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
// 拼接表名
|
|
9
|
+
function tableName(projectId, name) {
|
|
10
|
+
return `${projectId}____${name}`;
|
|
11
|
+
}
|
|
12
|
+
function endpoint(config, table) {
|
|
13
|
+
return `${config.url.replace(/\/$/, '')}/rest/v1/${encodeURIComponent(table)}`;
|
|
14
|
+
}
|
|
15
|
+
function headers(config) {
|
|
16
|
+
return {
|
|
17
|
+
apikey: config.anonKey,
|
|
18
|
+
Authorization: `Bearer ${config.anonKey}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
Prefer: 'return=minimal',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// 解析配置
|
|
24
|
+
function createConfig(config) {
|
|
25
|
+
if (!config.supabaseUrl && !config.supabaseAnonKey) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (!config.projectId) {
|
|
29
|
+
logPersistenceWarning('PROJECT_ID 未配置');
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (!config.supabaseUrl || !config.supabaseAnonKey) {
|
|
33
|
+
logPersistenceWarning('Supabase 配置未完整');
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
projectId: config.projectId,
|
|
38
|
+
url: config.supabaseUrl,
|
|
39
|
+
anonKey: config.supabaseAnonKey,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// 检查写入
|
|
43
|
+
async function checkResponse(response, table) {
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const body = await response.text();
|
|
48
|
+
const message = body || response.statusText;
|
|
49
|
+
if (response.status === 404 || message.includes('Could not find the table')) {
|
|
50
|
+
logPersistenceWarning('Supabase 表未建表', { tableName: table, status: response.status, body });
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
else if (response.status === 401 || response.status === 403) {
|
|
54
|
+
logPersistenceWarning('Supabase 写入权限不足', { tableName: table, status: response.status, body });
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Failed to write ${table}: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
// 页面行
|
|
60
|
+
function pageviewRow(event) {
|
|
61
|
+
const properties = readRecord(event.properties);
|
|
62
|
+
const attribution = readRecord(properties.attribution);
|
|
63
|
+
const device = readRecord(properties.device);
|
|
64
|
+
return {
|
|
65
|
+
id: properties.pageviewId,
|
|
66
|
+
viewed_at: event.timestamp,
|
|
67
|
+
project_id: event.projectId,
|
|
68
|
+
distinct_id: event.distinctId,
|
|
69
|
+
session_id: event.sessionId,
|
|
70
|
+
path: properties.path,
|
|
71
|
+
title: properties.title,
|
|
72
|
+
uv: properties.uv,
|
|
73
|
+
visit_count: properties.visitCount,
|
|
74
|
+
session_pageview_count: properties.sessionPageviewCount,
|
|
75
|
+
attribution_type: attribution.type,
|
|
76
|
+
traffic_source: attribution.source,
|
|
77
|
+
traffic_medium: attribution.medium,
|
|
78
|
+
traffic_campaign: attribution.campaign,
|
|
79
|
+
traffic_content: attribution.content,
|
|
80
|
+
traffic_term: attribution.term,
|
|
81
|
+
referrer_host: attribution.referrerHost,
|
|
82
|
+
device_type: device.type,
|
|
83
|
+
browser_language: device.language,
|
|
84
|
+
viewport_width: device.viewportWidth,
|
|
85
|
+
viewport_height: device.viewportHeight,
|
|
86
|
+
screen_width: device.screenWidth,
|
|
87
|
+
screen_height: device.screenHeight,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// 离开行
|
|
91
|
+
function pageleaveRow(event) {
|
|
92
|
+
const properties = readRecord(event.properties);
|
|
93
|
+
return {
|
|
94
|
+
left_at: event.timestamp,
|
|
95
|
+
stay_duration_ms: Math.round(Number(properties.stayDurationMs)),
|
|
96
|
+
bounced: properties.bounced,
|
|
97
|
+
session_ended: properties.sessionEnded,
|
|
98
|
+
session_pageview_count: properties.sessionPageviewCount,
|
|
99
|
+
session_conversion_count: properties.sessionConversionCount,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// 事件行
|
|
103
|
+
function eventRow(event) {
|
|
104
|
+
const properties = readRecord(event.properties);
|
|
105
|
+
return {
|
|
106
|
+
id: crypto.randomUUID(),
|
|
107
|
+
event_at: event.timestamp,
|
|
108
|
+
project_id: event.projectId,
|
|
109
|
+
kind: event.kind,
|
|
110
|
+
event_name: event.event,
|
|
111
|
+
distinct_id: event.distinctId,
|
|
112
|
+
session_id: event.sessionId,
|
|
113
|
+
user_id: properties.userId,
|
|
114
|
+
properties,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export class SupabaseReporter {
|
|
118
|
+
config;
|
|
119
|
+
constructor(config) {
|
|
120
|
+
this.config = createConfig(config);
|
|
121
|
+
}
|
|
122
|
+
isEnabled() {
|
|
123
|
+
return Boolean(this.config);
|
|
124
|
+
}
|
|
125
|
+
async write(event) {
|
|
126
|
+
if (!this.config) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (event.kind === 'pageview') {
|
|
130
|
+
await this.insert(this.config, tableName(this.config.projectId, 'we0_pageviews'), pageviewRow(event), event);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (event.kind === 'pageleave') {
|
|
134
|
+
await this.updatePageview(this.config, event);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await this.insert(this.config, tableName(this.config.projectId, 'we0_events'), eventRow(event), event);
|
|
138
|
+
}
|
|
139
|
+
async insert(config, table, row, event) {
|
|
140
|
+
const response = await fetch(endpoint(config, table), {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: headers(config),
|
|
143
|
+
keepalive: true,
|
|
144
|
+
body: JSON.stringify(row),
|
|
145
|
+
});
|
|
146
|
+
if (await checkResponse(response, table)) {
|
|
147
|
+
logPersistenceSuccess(table, event);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async updatePageview(config, event) {
|
|
151
|
+
const properties = readRecord(event.properties);
|
|
152
|
+
const table = tableName(config.projectId, 'we0_pageviews');
|
|
153
|
+
const pageviewId = String(properties.pageviewId);
|
|
154
|
+
const response = await fetch(`${endpoint(config, table)}?id=eq.${encodeURIComponent(pageviewId)}`, {
|
|
155
|
+
method: 'PATCH',
|
|
156
|
+
headers: headers(config),
|
|
157
|
+
keepalive: true,
|
|
158
|
+
body: JSON.stringify(pageleaveRow(event)),
|
|
159
|
+
});
|
|
160
|
+
if (await checkResponse(response, table)) {
|
|
161
|
+
logPersistenceSuccess(table, event);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type We0Config = {
|
|
2
|
+
projectId: string;
|
|
3
|
+
autoTrackPageview: boolean;
|
|
4
|
+
supabaseUrl?: string;
|
|
5
|
+
supabaseAnonKey?: string;
|
|
6
|
+
};
|
|
7
|
+
export type We0EventKind = 'pageview' | 'pageleave' | 'capture' | 'identify' | 'conversion';
|
|
8
|
+
export type We0Attribution = {
|
|
9
|
+
type: 'utm' | 'referrer' | 'direct';
|
|
10
|
+
source: string;
|
|
11
|
+
medium: string;
|
|
12
|
+
campaign: string;
|
|
13
|
+
content: string;
|
|
14
|
+
term: string;
|
|
15
|
+
referrer: string;
|
|
16
|
+
referrerHost: string;
|
|
17
|
+
};
|
|
18
|
+
export type We0Device = {
|
|
19
|
+
type: 'desktop' | 'tablet' | 'mobile';
|
|
20
|
+
userAgent: string;
|
|
21
|
+
language: string;
|
|
22
|
+
viewportWidth: number;
|
|
23
|
+
viewportHeight: number;
|
|
24
|
+
screenWidth: number;
|
|
25
|
+
screenHeight: number;
|
|
26
|
+
};
|
|
27
|
+
export type We0Event = {
|
|
28
|
+
kind: We0EventKind;
|
|
29
|
+
event: string;
|
|
30
|
+
projectId: string;
|
|
31
|
+
distinctId: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
properties?: Record<string, unknown>;
|
|
35
|
+
};
|
|
36
|
+
export type We0PageMetrics = {
|
|
37
|
+
pv: number;
|
|
38
|
+
uv: number;
|
|
39
|
+
stayDurationMs: number;
|
|
40
|
+
averageStayDurationMs: number;
|
|
41
|
+
bounceRate: number;
|
|
42
|
+
};
|
|
43
|
+
export type We0MetricsSnapshot = {
|
|
44
|
+
totalPv: number;
|
|
45
|
+
totalUv: number;
|
|
46
|
+
visitCount: number;
|
|
47
|
+
averageStayDurationMs: number;
|
|
48
|
+
bounceRate: number;
|
|
49
|
+
conversionCount: number;
|
|
50
|
+
pages: Record<string, We0PageMetrics>;
|
|
51
|
+
trafficSources: Record<string, number>;
|
|
52
|
+
devices: Record<string, number>;
|
|
53
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Visitor = {
|
|
2
|
+
distinctId: string;
|
|
3
|
+
isNew: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type VisitSession = {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
isNew: boolean;
|
|
8
|
+
visitCount: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function resolveVisitor(): Visitor;
|
|
11
|
+
export declare function resolveSession(): VisitSession;
|