@shopbb/helium 0.7.7 → 0.8.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,18 @@
1
+ /**
2
+ * @shopbb/helium/analytics — 埋点 SDK 公开 API
3
+ *
4
+ * 浏览器侧用法:
5
+ * - <TrackingProvider>:包在 root,初始化 session、起 flush 定时器、自动 page_view
6
+ * - useTracking():在组件内拿到 track() 函数
7
+ *
8
+ * 服务端不要用本模块——服务端事件(checkout_paid)由 platform-api 直接 INSERT。
9
+ *
10
+ * 注意:本模块与 components/AnalyticsProvider 是两个独立产品:
11
+ * - AnalyticsProvider(components):客户端事件总线,挂 GA / 自定义 reporter
12
+ * - TrackingProvider(本模块):把 5 个核心事件批量发到 /api/events,给 agent 用
13
+ */
14
+ export { TrackingProvider, useTracking } from './react';
15
+ export type { TrackingProviderProps } from './react';
16
+ export { AnalyticsQueue } from './queue';
17
+ export type { AnalyticsEvent, AnalyticsConfig, EventType, PageViewProps, ProductViewProps, AddToCartProps, CheckoutStartProps, TrackFn, } from './types';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analytics/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACxD,YAAY,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,YAAY,EACV,cAAc,EACd,eAAe,EACf,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EAClB,OAAO,GACR,MAAM,SAAS,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @shopbb/helium/analytics — 埋点 SDK 公开 API
3
+ *
4
+ * 浏览器侧用法:
5
+ * - <TrackingProvider>:包在 root,初始化 session、起 flush 定时器、自动 page_view
6
+ * - useTracking():在组件内拿到 track() 函数
7
+ *
8
+ * 服务端不要用本模块——服务端事件(checkout_paid)由 platform-api 直接 INSERT。
9
+ *
10
+ * 注意:本模块与 components/AnalyticsProvider 是两个独立产品:
11
+ * - AnalyticsProvider(components):客户端事件总线,挂 GA / 自定义 reporter
12
+ * - TrackingProvider(本模块):把 5 个核心事件批量发到 /api/events,给 agent 用
13
+ */
14
+ export { TrackingProvider, useTracking } from './react';
15
+ export { AnalyticsQueue } from './queue';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analytics/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * AnalyticsQueue — 浏览器侧事件缓冲队列
3
+ *
4
+ * 职责:
5
+ * 1. 把 track() 调用收集到内存队列
6
+ * 2. 定时(3s)或满批(20)触发 flush
7
+ * 3. flush 调 POST /api/events,失败重试 1 次
8
+ * 4. beforeunload 时用 navigator.sendBeacon 兜底刷出
9
+ *
10
+ * 注意:这是浏览器侧逻辑。SSR 阶段调 track() 会被丢弃(typeof window === 'undefined' 检查)。
11
+ */
12
+ import type { AnalyticsConfig, AnalyticsEvent } from './types';
13
+ export declare class AnalyticsQueue {
14
+ private readonly config;
15
+ private queue;
16
+ private timer;
17
+ private sessionId;
18
+ private flushing;
19
+ constructor(config: AnalyticsConfig);
20
+ start(): void;
21
+ stop(): void;
22
+ setBuyerId(id: string | null): void;
23
+ track(event: AnalyticsEvent): void;
24
+ /** 主动 flush。返回是否实际发送了请求 */
25
+ flush(): Promise<boolean>;
26
+ /** 用 sendBeacon 兜底,页面卸载场景 */
27
+ private flushBeacon;
28
+ private headers;
29
+ private log;
30
+ }
31
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../../src/analytics/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAe,MAAM,SAAS,CAAC;AAgC5E,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGrB;IACF,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,eAAe;IAWnC,KAAK,IAAI,IAAI;IAqBb,IAAI,IAAI,IAAI;IAOZ,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAInC,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAoClC,2BAA2B;IACrB,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IA0C/B,6BAA6B;IAC7B,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,GAAG;CAMZ"}
@@ -0,0 +1,203 @@
1
+ /**
2
+ * AnalyticsQueue — 浏览器侧事件缓冲队列
3
+ *
4
+ * 职责:
5
+ * 1. 把 track() 调用收集到内存队列
6
+ * 2. 定时(3s)或满批(20)触发 flush
7
+ * 3. flush 调 POST /api/events,失败重试 1 次
8
+ * 4. beforeunload 时用 navigator.sendBeacon 兜底刷出
9
+ *
10
+ * 注意:这是浏览器侧逻辑。SSR 阶段调 track() 会被丢弃(typeof window === 'undefined' 检查)。
11
+ */
12
+ const SESSION_COOKIE = 'sbb_sid';
13
+ const SESSION_TTL_DAYS = 365;
14
+ /**
15
+ * 读 sbb_sid cookie,没有则生成一个新的并 set。
16
+ * 仅浏览器环境调用。
17
+ */
18
+ function getOrCreateSessionId() {
19
+ if (typeof document === 'undefined')
20
+ return '';
21
+ const match = document.cookie.match(/(?:^|;\s*)sbb_sid=([^;]+)/);
22
+ if (match)
23
+ return match[1];
24
+ // 生成 UUID(不依赖 crypto.randomUUID 以兼容老浏览器)
25
+ const sid = crypto?.randomUUID?.() ?? generateFallbackUuid();
26
+ const maxAge = SESSION_TTL_DAYS * 24 * 60 * 60;
27
+ document.cookie = `${SESSION_COOKIE}=${sid}; max-age=${maxAge}; path=/; samesite=lax`;
28
+ return sid;
29
+ }
30
+ function generateFallbackUuid() {
31
+ // RFC4122 v4 简化版
32
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
33
+ const r = (Math.random() * 16) | 0;
34
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
35
+ return v.toString(16);
36
+ });
37
+ }
38
+ export class AnalyticsQueue {
39
+ config;
40
+ queue = [];
41
+ timer = null;
42
+ sessionId = '';
43
+ flushing = false;
44
+ constructor(config) {
45
+ this.config = {
46
+ apiBase: config.apiBase.replace(/\/+$/, ''),
47
+ publicAccessToken: config.publicAccessToken,
48
+ buyerId: config.buyerId ?? null,
49
+ debug: !!config.debug,
50
+ flushIntervalMs: config.flushIntervalMs ?? 3000,
51
+ flushBatchSize: config.flushBatchSize ?? 20,
52
+ };
53
+ }
54
+ start() {
55
+ if (typeof window === 'undefined')
56
+ return;
57
+ this.sessionId = getOrCreateSessionId();
58
+ if (this.timer)
59
+ return;
60
+ this.timer = setInterval(() => {
61
+ void this.flush();
62
+ }, this.config.flushIntervalMs);
63
+ // 页面卸载兜底
64
+ window.addEventListener('pagehide', () => this.flushBeacon());
65
+ window.addEventListener('beforeunload', () => this.flushBeacon());
66
+ if (typeof document !== 'undefined') {
67
+ document.addEventListener('visibilitychange', () => {
68
+ if (document.visibilityState === 'hidden') {
69
+ this.flushBeacon();
70
+ }
71
+ });
72
+ }
73
+ }
74
+ stop() {
75
+ if (this.timer) {
76
+ clearInterval(this.timer);
77
+ this.timer = null;
78
+ }
79
+ }
80
+ setBuyerId(id) {
81
+ this.config.buyerId = id;
82
+ }
83
+ track(event) {
84
+ if (typeof window === 'undefined') {
85
+ this.log('skip-ssr', event.type);
86
+ return;
87
+ }
88
+ if (!this.sessionId) {
89
+ this.sessionId = getOrCreateSessionId();
90
+ }
91
+ const queued = {
92
+ type: event.type,
93
+ occurredAt: event.occurredAt ?? Math.floor(Date.now() / 1000),
94
+ path: event.path !== undefined
95
+ ? event.path
96
+ : typeof location !== 'undefined'
97
+ ? location.pathname + location.search
98
+ : null,
99
+ referrer: event.referrer !== undefined
100
+ ? event.referrer
101
+ : typeof document !== 'undefined'
102
+ ? document.referrer || null
103
+ : null,
104
+ props: event.props ?? {},
105
+ };
106
+ this.queue.push(queued);
107
+ this.log('queued', queued.type, this.queue.length);
108
+ if (this.queue.length >= this.config.flushBatchSize) {
109
+ void this.flush();
110
+ }
111
+ }
112
+ /** 主动 flush。返回是否实际发送了请求 */
113
+ async flush() {
114
+ if (this.flushing)
115
+ return false;
116
+ if (this.queue.length === 0)
117
+ return false;
118
+ if (!this.config.publicAccessToken) {
119
+ this.log('skip-no-token');
120
+ return false;
121
+ }
122
+ this.flushing = true;
123
+ const batch = this.queue.splice(0, this.config.flushBatchSize);
124
+ const body = JSON.stringify({
125
+ events: batch.map((e) => ({
126
+ type: e.type,
127
+ occurredAt: e.occurredAt,
128
+ path: e.path ?? undefined,
129
+ referrer: e.referrer ?? undefined,
130
+ props: e.props,
131
+ })),
132
+ });
133
+ try {
134
+ const res = await fetch(`${this.config.apiBase}/api/events`, {
135
+ method: 'POST',
136
+ headers: this.headers(),
137
+ body,
138
+ keepalive: true,
139
+ });
140
+ if (!res.ok) {
141
+ this.log('flush-failed-status', res.status);
142
+ // 失败不重试入队(避免堆积),demo 阶段简化
143
+ return false;
144
+ }
145
+ this.log('flushed', batch.length);
146
+ return true;
147
+ }
148
+ catch (err) {
149
+ this.log('flush-error', err);
150
+ return false;
151
+ }
152
+ finally {
153
+ this.flushing = false;
154
+ }
155
+ }
156
+ /** 用 sendBeacon 兜底,页面卸载场景 */
157
+ flushBeacon() {
158
+ if (this.queue.length === 0)
159
+ return;
160
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon)
161
+ return;
162
+ if (!this.config.publicAccessToken)
163
+ return;
164
+ const batch = this.queue.splice(0, this.config.flushBatchSize);
165
+ const body = JSON.stringify({
166
+ events: batch.map((e) => ({
167
+ type: e.type,
168
+ occurredAt: e.occurredAt,
169
+ path: e.path ?? undefined,
170
+ referrer: e.referrer ?? undefined,
171
+ props: e.props,
172
+ })),
173
+ // sendBeacon 无法设 header,所以把 token / session 塞进 body 作为兜底
174
+ // platform-api handler 优先认 header;body 字段用于 beacon 场景
175
+ _auth: {
176
+ token: this.config.publicAccessToken,
177
+ sessionId: this.sessionId,
178
+ buyerId: this.config.buyerId,
179
+ },
180
+ });
181
+ const blob = new Blob([body], { type: 'application/json' });
182
+ const ok = navigator.sendBeacon(`${this.config.apiBase}/api/events?beacon=1`, blob);
183
+ this.log('beacon', ok, batch.length);
184
+ }
185
+ headers() {
186
+ const h = {
187
+ 'Content-Type': 'application/json',
188
+ 'X-Storefront-Access-Token': this.config.publicAccessToken,
189
+ 'X-Session-Id': this.sessionId,
190
+ };
191
+ if (this.config.buyerId) {
192
+ h['X-Buyer-Id'] = this.config.buyerId;
193
+ }
194
+ return h;
195
+ }
196
+ log(...args) {
197
+ if (this.config.debug) {
198
+ // eslint-disable-next-line no-console
199
+ console.log('[helium-analytics]', ...args);
200
+ }
201
+ }
202
+ }
203
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../../src/analytics/queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * TrackingProvider + useTracking — React 绑定(后端事件埋点)
3
+ *
4
+ * 与 components/AnalyticsProvider 不同:
5
+ * - AnalyticsProvider(旧):客户端事件总线,商家挂 GA / Plausible 等 reporter
6
+ * - TrackingProvider(新):把 5 个核心事件发到 platform-api /api/events
7
+ *
8
+ * 用法:
9
+ * <TrackingProvider
10
+ * apiBase={store.apiBase}
11
+ * publicAccessToken={store.publicAccessToken}
12
+ * buyerId={buyer?.id}
13
+ * debug
14
+ * >
15
+ * <App />
16
+ * </TrackingProvider>
17
+ *
18
+ * const { track } = useTracking();
19
+ * track({ type: 'add_to_cart', props: { productId, quantity: 1 } });
20
+ *
21
+ * 自动事件:
22
+ * - 首次挂载发一次 page_view
23
+ * - 监听 history pushState/replaceState/popstate,每次 URL 变化补发 page_view
24
+ */
25
+ import * as React from 'react';
26
+ import type { AnalyticsConfig, TrackFn } from './types';
27
+ interface ContextValue {
28
+ track: TrackFn;
29
+ setBuyerId: (id: string | null) => void;
30
+ }
31
+ export interface TrackingProviderProps extends AnalyticsConfig {
32
+ children: React.ReactNode;
33
+ /** 是否自动发 page_view(默认 true) */
34
+ autoPageview?: boolean;
35
+ }
36
+ export declare function TrackingProvider(props: TrackingProviderProps): React.ReactElement;
37
+ /**
38
+ * 在组件里拿到 track 函数。
39
+ *
40
+ * 在 TrackingProvider 之外调用会得到一个 no-op,不报错(方便在测试/SSR 中使用)。
41
+ */
42
+ export declare function useTracking(): ContextValue;
43
+ export {};
44
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../../src/analytics/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAkB,eAAe,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAExE,UAAU,YAAY;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACzC;AAID,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,+BAA+B;IAC/B,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,KAAK,CAAC,YAAY,CAuFjF;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,YAAY,CAI1C"}
@@ -0,0 +1,114 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * TrackingProvider + useTracking — React 绑定(后端事件埋点)
4
+ *
5
+ * 与 components/AnalyticsProvider 不同:
6
+ * - AnalyticsProvider(旧):客户端事件总线,商家挂 GA / Plausible 等 reporter
7
+ * - TrackingProvider(新):把 5 个核心事件发到 platform-api /api/events
8
+ *
9
+ * 用法:
10
+ * <TrackingProvider
11
+ * apiBase={store.apiBase}
12
+ * publicAccessToken={store.publicAccessToken}
13
+ * buyerId={buyer?.id}
14
+ * debug
15
+ * >
16
+ * <App />
17
+ * </TrackingProvider>
18
+ *
19
+ * const { track } = useTracking();
20
+ * track({ type: 'add_to_cart', props: { productId, quantity: 1 } });
21
+ *
22
+ * 自动事件:
23
+ * - 首次挂载发一次 page_view
24
+ * - 监听 history pushState/replaceState/popstate,每次 URL 变化补发 page_view
25
+ */
26
+ import * as React from 'react';
27
+ import { AnalyticsQueue } from './queue';
28
+ const TrackingCtx = React.createContext(null);
29
+ export function TrackingProvider(props) {
30
+ const { children, autoPageview = true, apiBase, publicAccessToken, buyerId, debug, flushIntervalMs, flushBatchSize, } = props;
31
+ // 用 ref 持有 queue,避免每次 re-render 重建
32
+ const queueRef = React.useRef(null);
33
+ if (queueRef.current === null) {
34
+ queueRef.current = new AnalyticsQueue({
35
+ apiBase,
36
+ publicAccessToken,
37
+ buyerId,
38
+ debug,
39
+ flushIntervalMs,
40
+ flushBatchSize,
41
+ });
42
+ }
43
+ // buyerId 变化时同步到 queue
44
+ React.useEffect(() => {
45
+ queueRef.current?.setBuyerId(buyerId ?? null);
46
+ }, [buyerId]);
47
+ // 启动 queue(只在浏览器侧)
48
+ React.useEffect(() => {
49
+ if (typeof window === 'undefined')
50
+ return;
51
+ const q = queueRef.current;
52
+ if (!q)
53
+ return;
54
+ q.start();
55
+ return () => q.stop();
56
+ }, []);
57
+ // 自动 page_view
58
+ React.useEffect(() => {
59
+ if (!autoPageview)
60
+ return;
61
+ if (typeof window === 'undefined')
62
+ return;
63
+ const q = queueRef.current;
64
+ if (!q)
65
+ return;
66
+ const send = () => {
67
+ q.track({
68
+ type: 'page_view',
69
+ props: { title: typeof document !== 'undefined' ? document.title : undefined },
70
+ });
71
+ };
72
+ // 首次
73
+ send();
74
+ // patch history methods 以捕获 SPA 导航
75
+ const origPush = history.pushState;
76
+ const origReplace = history.replaceState;
77
+ history.pushState = function (...args) {
78
+ origPush.apply(this, args);
79
+ send();
80
+ };
81
+ history.replaceState = function (...args) {
82
+ origReplace.apply(this, args);
83
+ send();
84
+ };
85
+ const onPop = () => send();
86
+ window.addEventListener('popstate', onPop);
87
+ return () => {
88
+ history.pushState = origPush;
89
+ history.replaceState = origReplace;
90
+ window.removeEventListener('popstate', onPop);
91
+ };
92
+ }, [autoPageview]);
93
+ const value = React.useMemo(() => ({
94
+ track: (e) => queueRef.current?.track(e),
95
+ setBuyerId: (id) => queueRef.current?.setBuyerId(id),
96
+ }), []);
97
+ return _jsx(TrackingCtx.Provider, { value: value, children: children });
98
+ }
99
+ /**
100
+ * 在组件里拿到 track 函数。
101
+ *
102
+ * 在 TrackingProvider 之外调用会得到一个 no-op,不报错(方便在测试/SSR 中使用)。
103
+ */
104
+ export function useTracking() {
105
+ const ctx = React.useContext(TrackingCtx);
106
+ if (ctx)
107
+ return ctx;
108
+ return NOOP;
109
+ }
110
+ const NOOP = {
111
+ track: () => { },
112
+ setBuyerId: () => { },
113
+ };
114
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../../src/analytics/react.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Analytics SDK 类型
3
+ *
4
+ * Helium 的埋点接口。与 platform-api 的事件 schema 保持一致。
5
+ */
6
+ export type EventType = 'page_view' | 'product_view' | 'add_to_cart' | 'checkout_start' | 'checkout_paid';
7
+ /** 单条事件输入 */
8
+ export interface AnalyticsEvent {
9
+ /** 事件类型 */
10
+ type: EventType;
11
+ /** 客户端发生时间(秒,Unix)。默认 Date.now()/1000 */
12
+ occurredAt?: number;
13
+ /** 页面 path(默认 window.location.pathname) */
14
+ path?: string;
15
+ /** referrer(默认 document.referrer) */
16
+ referrer?: string;
17
+ /** 事件附加数据 */
18
+ props?: Record<string, unknown>;
19
+ }
20
+ /** 各事件的 props 形状 */
21
+ export interface PageViewProps {
22
+ title?: string;
23
+ }
24
+ export interface ProductViewProps {
25
+ productId: string;
26
+ variantId?: string;
27
+ priceCents?: number;
28
+ currency?: string;
29
+ }
30
+ export interface AddToCartProps {
31
+ productId: string;
32
+ variantId?: string;
33
+ quantity: number;
34
+ priceCents?: number;
35
+ currency?: string;
36
+ }
37
+ export interface CheckoutStartProps {
38
+ cartId?: string;
39
+ itemCount: number;
40
+ subtotalCents: number;
41
+ currency: string;
42
+ }
43
+ /** AnalyticsProvider 初始化配置 */
44
+ export interface AnalyticsConfig {
45
+ /** 平台 API base URL,例如 https://api.oxygen-demo.cloudc.top */
46
+ apiBase: string;
47
+ /** Storefront public access token */
48
+ publicAccessToken: string;
49
+ /** 当前买家 ID(已登录时传入) */
50
+ buyerId?: string | null;
51
+ /** 是否开启 debug 日志 */
52
+ debug?: boolean;
53
+ /** flush 间隔(毫秒),默认 3000 */
54
+ flushIntervalMs?: number;
55
+ /** 批量上限(条),默认 20 */
56
+ flushBatchSize?: number;
57
+ }
58
+ /** track 函数签名 */
59
+ export type TrackFn = (event: AnalyticsEvent) => void;
60
+ /** 内部队列条目 */
61
+ export interface QueuedEvent {
62
+ type: EventType;
63
+ occurredAt: number;
64
+ path: string | null;
65
+ referrer: string | null;
66
+ props: Record<string, unknown>;
67
+ }
68
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/analytics/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,SAAS,GACjB,WAAW,GACX,cAAc,GACd,aAAa,GACb,gBAAgB,GAChB,eAAe,CAAC;AAEpB,aAAa;AACb,MAAM,WAAW,cAAc;IAC7B,WAAW;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,oBAAoB;AACpB,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,oBAAoB;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2BAA2B;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,iBAAiB;AACjB,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AAEtD,aAAa;AACb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Analytics SDK 类型
3
+ *
4
+ * Helium 的埋点接口。与 platform-api 的事件 schema 保持一致。
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/analytics/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -17,6 +17,7 @@
17
17
  * 监听全局 fetcher 状态。Hydrogen demo store 用 `useAside().open()` 模式。
18
18
  */
19
19
  import * as React from 'react';
20
+ import { type CartLineInput } from './CartForm';
20
21
  export interface AddToCartButtonProps {
21
22
  /** variant GID */
22
23
  variantId: string;
@@ -39,6 +40,12 @@ export interface AddToCartButtonProps {
39
40
  route?: string;
40
41
  /** 按钮 children */
41
42
  children?: React.ReactNode;
43
+ /**
44
+ * 按钮被点击时触发(form 提交之前)。
45
+ * 用于埋点 / dataLayer / 自定义分析回调。
46
+ * 不影响表单提交流程。
47
+ */
48
+ onAdd?: (line: CartLineInput) => void;
42
49
  }
43
50
  export declare function AddToCartButton(props: AddToCartButtonProps): import("react/jsx-runtime").JSX.Element;
44
51
  //# sourceMappingURL=AddToCartButton.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,WAAW,oBAAoB;IACnC,kBAAkB;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,kDAAkD;IAClD,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,kBAAkB;IAClB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,sBAAsB;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAwC1D"}
1
+ {"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,oBAAoB;IACnC,kBAAkB;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,kDAAkD;IAClD,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,kBAAkB;IAClB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC,sBAAsB;IACtB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;CACvC;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAoD1D"}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { CartForm } from './CartForm';
3
3
  export function AddToCartButton(props) {
4
- const { variantId, quantity = 1, attributes, selectedVariant, unavailableText = '缺货', loadingText = '加入中...', disabled = false, className, route = '/cart', children = '加入购物车', } = props;
4
+ const { variantId, quantity = 1, attributes, selectedVariant, unavailableText = '缺货', loadingText = '加入中...', disabled = false, className, route = '/cart', children = '加入购物车', onAdd, } = props;
5
5
  const line = { merchandiseId: variantId, quantity };
6
6
  if (attributes && attributes.length > 0)
7
7
  line.attributes = attributes;
@@ -9,7 +9,18 @@ export function AddToCartButton(props) {
9
9
  line.selectedVariant = selectedVariant;
10
10
  return (_jsx(CartForm, { route: route, action: CartForm.ACTIONS.LinesAdd, inputs: { lines: [line] }, children: (fetcher) => {
11
11
  const submitting = fetcher.state !== 'idle';
12
- return (_jsx("button", { type: "submit", className: className, disabled: disabled || submitting, "data-add-to-cart": true, "data-loading": submitting ? '' : undefined, children: submitting ? loadingText : disabled ? unavailableText : children }));
12
+ return (_jsx("button", { type: "submit", className: className, disabled: disabled || submitting, "data-add-to-cart": true, "data-loading": submitting ? '' : undefined, onClick: () => {
13
+ if (onAdd && !submitting && !disabled) {
14
+ try {
15
+ onAdd(line);
16
+ }
17
+ catch (err) {
18
+ if (typeof console !== 'undefined') {
19
+ console.warn('[AddToCartButton] onAdd threw', err);
20
+ }
21
+ }
22
+ }
23
+ }, children: submitting ? loadingText : disabled ? unavailableText : children }));
13
24
  } }));
14
25
  }
15
26
  //# sourceMappingURL=AddToCartButton.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AddToCartButton.js","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":";AAoBA,OAAO,EAAE,QAAQ,EAAsB,MAAM,YAAY,CAAC;AAuB1D,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,EACJ,SAAS,EACT,QAAQ,GAAG,CAAC,EACZ,UAAU,EACV,eAAe,EACf,eAAe,GAAG,IAAI,EACtB,WAAW,GAAG,QAAQ,EACtB,QAAQ,GAAG,KAAK,EAChB,SAAS,EACT,KAAK,GAAG,OAAO,EACf,QAAQ,GAAG,OAAO,GACnB,GAAG,KAAK,CAAC;IAEV,MAAM,IAAI,GAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACnE,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACtE,IAAI,eAAe;QAAE,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IAE5D,OAAO,CACL,KAAC,QAAQ,IACP,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,QAAQ,EACjC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,YAExB,CAAC,OAAO,EAAE,EAAE;YACX,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC;YAC5C,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,IAAI,UAAU,4CAElB,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,YAExC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,GAC1D,CACV,CAAC;QACJ,CAAC,GACQ,CACZ,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"AddToCartButton.js","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":";AAoBA,OAAO,EAAE,QAAQ,EAAsB,MAAM,YAAY,CAAC;AA6B1D,MAAM,UAAU,eAAe,CAAC,KAA2B;IACzD,MAAM,EACJ,SAAS,EACT,QAAQ,GAAG,CAAC,EACZ,UAAU,EACV,eAAe,EACf,eAAe,GAAG,IAAI,EACtB,WAAW,GAAG,QAAQ,EACtB,QAAQ,GAAG,KAAK,EAChB,SAAS,EACT,KAAK,GAAG,OAAO,EACf,QAAQ,GAAG,OAAO,EAClB,KAAK,GACN,GAAG,KAAK,CAAC;IAEV,MAAM,IAAI,GAAkB,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACnE,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACtE,IAAI,eAAe;QAAE,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IAE5D,OAAO,CACL,KAAC,QAAQ,IACP,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,QAAQ,EACjC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,YAExB,CAAC,OAAO,EAAE,EAAE;YACX,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,KAAK,MAAM,CAAC;YAC5C,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,QAAQ,IAAI,UAAU,4CAElB,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EACzC,OAAO,EAAE,GAAG,EAAE;oBACZ,IAAI,KAAK,IAAI,CAAC,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACtC,IAAI,CAAC;4BACH,KAAK,CAAC,IAAI,CAAC,CAAC;wBACd,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,IAAI,OAAO,OAAO,KAAK,WAAW,EAAE,CAAC;gCACnC,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE,GAAG,CAAC,CAAC;4BACrD,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC,YAEA,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,GAC1D,CACV,CAAC;QACJ,CAAC,GACQ,CACZ,CAAC;AACJ,CAAC"}
package/dist/react.d.ts CHANGED
@@ -95,4 +95,9 @@ export interface ReactRendererOptions {
95
95
  onError?: (err: unknown, request: Request) => Response | Promise<Response>;
96
96
  }
97
97
  export declare function createReactRenderer(opts: ReactRendererOptions): (request: Request, env: any, executionContext: ExecutionContext) => Promise<Response>;
98
+ /**
99
+ * 把 head + react stream + tail 拼成一个 ReadableStream<Uint8Array>
100
+ */
101
+ export { TrackingProvider, useTracking, AnalyticsQueue, } from './analytics';
102
+ export type { TrackingProviderProps, AnalyticsEvent, AnalyticsConfig, EventType as AnalyticsEventType, TrackFn, PageViewProps, ProductViewProps, AddToCartProps, CheckoutStartProps, } from './analytics';
98
103
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAO/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAQ7C,wBAAgB,qBAAqB,CAAC,EACpC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,6EAMA;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAQhD;AAMD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE;QACX,GAAG,EAAE,aAAa,CAAC;QACnB,GAAG,EAAE,GAAG,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;KAClB,KAAK,KAAK,CAAC,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAEvD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QACX,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,IAE1D,SAAS,OAAO,EAChB,KAAK,GAAG,EACR,kBAAkB,gBAAgB,KACjC,OAAO,CAAC,QAAQ,CAAC,CAkHrB"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAO/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAQ7C,wBAAgB,qBAAqB,CAAC,EACpC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,6EAMA;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAQhD;AAMD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,GAAG,EAAE,CAAC,KAAK,EAAE;QACX,GAAG,EAAE,aAAa,CAAC;QACnB,GAAG,EAAE,GAAG,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;KAClB,KAAK,KAAK,CAAC,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAEvD;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,UAAU,CAAC,EAAE;QACX,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF;;OAEG;IACH,IAAI,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC5E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,IAE1D,SAAS,OAAO,EAChB,KAAK,GAAG,EACR,kBAAkB,gBAAgB,KACjC,OAAO,CAAC,QAAQ,CAAC,CAkHrB;AAYD;;GAEG;AAUH,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,SAAS,IAAI,kBAAkB,EAC/B,OAAO,EACP,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,GACnB,MAAM,aAAa,CAAC"}
package/dist/react.js CHANGED
@@ -148,6 +148,16 @@ function escapeHtml(s) {
148
148
  /**
149
149
  * 把 head + react stream + tail 拼成一个 ReadableStream<Uint8Array>
150
150
  */
151
+ // ============================================================
152
+ // Tracking SDK re-export
153
+ // ============================================================
154
+ // 把 5 个核心事件(page_view / product_view / add_to_cart / checkout_start / checkout_paid)
155
+ // 批量发到 platform-api 的 /api/events。
156
+ //
157
+ // 与 components/AnalyticsProvider 解耦:
158
+ // - AnalyticsProvider 是商家用的事件总线(自定义 reporter)
159
+ // - TrackingProvider 是平台的标准埋点(发到 platform-api)
160
+ export { TrackingProvider, useTracking, AnalyticsQueue, } from './analytics';
151
161
  function composeStream(head, body, tail) {
152
162
  const encoder = new TextEncoder();
153
163
  return new ReadableStream({
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbb/helium",
3
- "version": "0.7.7",
3
+ "version": "0.8.0",
4
4
  "description": "shopbb storefront framework — components, React SSR, GraphQL client, cart handler, cache for Cloudflare Workers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @shopbb/helium/analytics — 埋点 SDK 公开 API
3
+ *
4
+ * 浏览器侧用法:
5
+ * - <TrackingProvider>:包在 root,初始化 session、起 flush 定时器、自动 page_view
6
+ * - useTracking():在组件内拿到 track() 函数
7
+ *
8
+ * 服务端不要用本模块——服务端事件(checkout_paid)由 platform-api 直接 INSERT。
9
+ *
10
+ * 注意:本模块与 components/AnalyticsProvider 是两个独立产品:
11
+ * - AnalyticsProvider(components):客户端事件总线,挂 GA / 自定义 reporter
12
+ * - TrackingProvider(本模块):把 5 个核心事件批量发到 /api/events,给 agent 用
13
+ */
14
+
15
+ export { TrackingProvider, useTracking } from './react';
16
+ export type { TrackingProviderProps } from './react';
17
+ export { AnalyticsQueue } from './queue';
18
+ export type {
19
+ AnalyticsEvent,
20
+ AnalyticsConfig,
21
+ EventType,
22
+ PageViewProps,
23
+ ProductViewProps,
24
+ AddToCartProps,
25
+ CheckoutStartProps,
26
+ TrackFn,
27
+ } from './types';
@@ -0,0 +1,224 @@
1
+ /**
2
+ * AnalyticsQueue — 浏览器侧事件缓冲队列
3
+ *
4
+ * 职责:
5
+ * 1. 把 track() 调用收集到内存队列
6
+ * 2. 定时(3s)或满批(20)触发 flush
7
+ * 3. flush 调 POST /api/events,失败重试 1 次
8
+ * 4. beforeunload 时用 navigator.sendBeacon 兜底刷出
9
+ *
10
+ * 注意:这是浏览器侧逻辑。SSR 阶段调 track() 会被丢弃(typeof window === 'undefined' 检查)。
11
+ */
12
+
13
+ import type { AnalyticsConfig, AnalyticsEvent, QueuedEvent } from './types';
14
+
15
+ const SESSION_COOKIE = 'sbb_sid';
16
+ const SESSION_TTL_DAYS = 365;
17
+
18
+ /**
19
+ * 读 sbb_sid cookie,没有则生成一个新的并 set。
20
+ * 仅浏览器环境调用。
21
+ */
22
+ function getOrCreateSessionId(): string {
23
+ if (typeof document === 'undefined') return '';
24
+
25
+ const match = document.cookie.match(/(?:^|;\s*)sbb_sid=([^;]+)/);
26
+ if (match) return match[1];
27
+
28
+ // 生成 UUID(不依赖 crypto.randomUUID 以兼容老浏览器)
29
+ const sid = crypto?.randomUUID?.() ?? generateFallbackUuid();
30
+
31
+ const maxAge = SESSION_TTL_DAYS * 24 * 60 * 60;
32
+ document.cookie = `${SESSION_COOKIE}=${sid}; max-age=${maxAge}; path=/; samesite=lax`;
33
+ return sid;
34
+ }
35
+
36
+ function generateFallbackUuid(): string {
37
+ // RFC4122 v4 简化版
38
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
39
+ const r = (Math.random() * 16) | 0;
40
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
41
+ return v.toString(16);
42
+ });
43
+ }
44
+
45
+ export class AnalyticsQueue {
46
+ private readonly config: Required<Omit<AnalyticsConfig, 'buyerId' | 'debug'>> & {
47
+ buyerId: string | null;
48
+ debug: boolean;
49
+ };
50
+ private queue: QueuedEvent[] = [];
51
+ private timer: ReturnType<typeof setInterval> | null = null;
52
+ private sessionId = '';
53
+ private flushing = false;
54
+
55
+ constructor(config: AnalyticsConfig) {
56
+ this.config = {
57
+ apiBase: config.apiBase.replace(/\/+$/, ''),
58
+ publicAccessToken: config.publicAccessToken,
59
+ buyerId: config.buyerId ?? null,
60
+ debug: !!config.debug,
61
+ flushIntervalMs: config.flushIntervalMs ?? 3000,
62
+ flushBatchSize: config.flushBatchSize ?? 20,
63
+ };
64
+ }
65
+
66
+ start(): void {
67
+ if (typeof window === 'undefined') return;
68
+ this.sessionId = getOrCreateSessionId();
69
+
70
+ if (this.timer) return;
71
+ this.timer = setInterval(() => {
72
+ void this.flush();
73
+ }, this.config.flushIntervalMs);
74
+
75
+ // 页面卸载兜底
76
+ window.addEventListener('pagehide', () => this.flushBeacon());
77
+ window.addEventListener('beforeunload', () => this.flushBeacon());
78
+ if (typeof document !== 'undefined') {
79
+ document.addEventListener('visibilitychange', () => {
80
+ if (document.visibilityState === 'hidden') {
81
+ this.flushBeacon();
82
+ }
83
+ });
84
+ }
85
+ }
86
+
87
+ stop(): void {
88
+ if (this.timer) {
89
+ clearInterval(this.timer);
90
+ this.timer = null;
91
+ }
92
+ }
93
+
94
+ setBuyerId(id: string | null): void {
95
+ this.config.buyerId = id;
96
+ }
97
+
98
+ track(event: AnalyticsEvent): void {
99
+ if (typeof window === 'undefined') {
100
+ this.log('skip-ssr', event.type);
101
+ return;
102
+ }
103
+
104
+ if (!this.sessionId) {
105
+ this.sessionId = getOrCreateSessionId();
106
+ }
107
+
108
+ const queued: QueuedEvent = {
109
+ type: event.type,
110
+ occurredAt: event.occurredAt ?? Math.floor(Date.now() / 1000),
111
+ path:
112
+ event.path !== undefined
113
+ ? event.path
114
+ : typeof location !== 'undefined'
115
+ ? location.pathname + location.search
116
+ : null,
117
+ referrer:
118
+ event.referrer !== undefined
119
+ ? event.referrer
120
+ : typeof document !== 'undefined'
121
+ ? document.referrer || null
122
+ : null,
123
+ props: event.props ?? {},
124
+ };
125
+
126
+ this.queue.push(queued);
127
+ this.log('queued', queued.type, this.queue.length);
128
+
129
+ if (this.queue.length >= this.config.flushBatchSize) {
130
+ void this.flush();
131
+ }
132
+ }
133
+
134
+ /** 主动 flush。返回是否实际发送了请求 */
135
+ async flush(): Promise<boolean> {
136
+ if (this.flushing) return false;
137
+ if (this.queue.length === 0) return false;
138
+ if (!this.config.publicAccessToken) {
139
+ this.log('skip-no-token');
140
+ return false;
141
+ }
142
+
143
+ this.flushing = true;
144
+ const batch = this.queue.splice(0, this.config.flushBatchSize);
145
+ const body = JSON.stringify({
146
+ events: batch.map((e) => ({
147
+ type: e.type,
148
+ occurredAt: e.occurredAt,
149
+ path: e.path ?? undefined,
150
+ referrer: e.referrer ?? undefined,
151
+ props: e.props,
152
+ })),
153
+ });
154
+
155
+ try {
156
+ const res = await fetch(`${this.config.apiBase}/api/events`, {
157
+ method: 'POST',
158
+ headers: this.headers(),
159
+ body,
160
+ keepalive: true,
161
+ });
162
+ if (!res.ok) {
163
+ this.log('flush-failed-status', res.status);
164
+ // 失败不重试入队(避免堆积),demo 阶段简化
165
+ return false;
166
+ }
167
+ this.log('flushed', batch.length);
168
+ return true;
169
+ } catch (err) {
170
+ this.log('flush-error', err);
171
+ return false;
172
+ } finally {
173
+ this.flushing = false;
174
+ }
175
+ }
176
+
177
+ /** 用 sendBeacon 兜底,页面卸载场景 */
178
+ private flushBeacon(): void {
179
+ if (this.queue.length === 0) return;
180
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) return;
181
+ if (!this.config.publicAccessToken) return;
182
+
183
+ const batch = this.queue.splice(0, this.config.flushBatchSize);
184
+ const body = JSON.stringify({
185
+ events: batch.map((e) => ({
186
+ type: e.type,
187
+ occurredAt: e.occurredAt,
188
+ path: e.path ?? undefined,
189
+ referrer: e.referrer ?? undefined,
190
+ props: e.props,
191
+ })),
192
+ // sendBeacon 无法设 header,所以把 token / session 塞进 body 作为兜底
193
+ // platform-api handler 优先认 header;body 字段用于 beacon 场景
194
+ _auth: {
195
+ token: this.config.publicAccessToken,
196
+ sessionId: this.sessionId,
197
+ buyerId: this.config.buyerId,
198
+ },
199
+ });
200
+
201
+ const blob = new Blob([body], { type: 'application/json' });
202
+ const ok = navigator.sendBeacon(`${this.config.apiBase}/api/events?beacon=1`, blob);
203
+ this.log('beacon', ok, batch.length);
204
+ }
205
+
206
+ private headers(): HeadersInit {
207
+ const h: Record<string, string> = {
208
+ 'Content-Type': 'application/json',
209
+ 'X-Storefront-Access-Token': this.config.publicAccessToken,
210
+ 'X-Session-Id': this.sessionId,
211
+ };
212
+ if (this.config.buyerId) {
213
+ h['X-Buyer-Id'] = this.config.buyerId;
214
+ }
215
+ return h;
216
+ }
217
+
218
+ private log(...args: unknown[]): void {
219
+ if (this.config.debug) {
220
+ // eslint-disable-next-line no-console
221
+ console.log('[helium-analytics]', ...args);
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * TrackingProvider + useTracking — React 绑定(后端事件埋点)
3
+ *
4
+ * 与 components/AnalyticsProvider 不同:
5
+ * - AnalyticsProvider(旧):客户端事件总线,商家挂 GA / Plausible 等 reporter
6
+ * - TrackingProvider(新):把 5 个核心事件发到 platform-api /api/events
7
+ *
8
+ * 用法:
9
+ * <TrackingProvider
10
+ * apiBase={store.apiBase}
11
+ * publicAccessToken={store.publicAccessToken}
12
+ * buyerId={buyer?.id}
13
+ * debug
14
+ * >
15
+ * <App />
16
+ * </TrackingProvider>
17
+ *
18
+ * const { track } = useTracking();
19
+ * track({ type: 'add_to_cart', props: { productId, quantity: 1 } });
20
+ *
21
+ * 自动事件:
22
+ * - 首次挂载发一次 page_view
23
+ * - 监听 history pushState/replaceState/popstate,每次 URL 变化补发 page_view
24
+ */
25
+
26
+ import * as React from 'react';
27
+ import { AnalyticsQueue } from './queue';
28
+ import type { AnalyticsEvent, AnalyticsConfig, TrackFn } from './types';
29
+
30
+ interface ContextValue {
31
+ track: TrackFn;
32
+ setBuyerId: (id: string | null) => void;
33
+ }
34
+
35
+ const TrackingCtx = React.createContext<ContextValue | null>(null);
36
+
37
+ export interface TrackingProviderProps extends AnalyticsConfig {
38
+ children: React.ReactNode;
39
+ /** 是否自动发 page_view(默认 true) */
40
+ autoPageview?: boolean;
41
+ }
42
+
43
+ export function TrackingProvider(props: TrackingProviderProps): React.ReactElement {
44
+ const {
45
+ children,
46
+ autoPageview = true,
47
+ apiBase,
48
+ publicAccessToken,
49
+ buyerId,
50
+ debug,
51
+ flushIntervalMs,
52
+ flushBatchSize,
53
+ } = props;
54
+
55
+ // 用 ref 持有 queue,避免每次 re-render 重建
56
+ const queueRef = React.useRef<AnalyticsQueue | null>(null);
57
+
58
+ if (queueRef.current === null) {
59
+ queueRef.current = new AnalyticsQueue({
60
+ apiBase,
61
+ publicAccessToken,
62
+ buyerId,
63
+ debug,
64
+ flushIntervalMs,
65
+ flushBatchSize,
66
+ });
67
+ }
68
+
69
+ // buyerId 变化时同步到 queue
70
+ React.useEffect(() => {
71
+ queueRef.current?.setBuyerId(buyerId ?? null);
72
+ }, [buyerId]);
73
+
74
+ // 启动 queue(只在浏览器侧)
75
+ React.useEffect(() => {
76
+ if (typeof window === 'undefined') return;
77
+ const q = queueRef.current;
78
+ if (!q) return;
79
+ q.start();
80
+ return () => q.stop();
81
+ }, []);
82
+
83
+ // 自动 page_view
84
+ React.useEffect(() => {
85
+ if (!autoPageview) return;
86
+ if (typeof window === 'undefined') return;
87
+ const q = queueRef.current;
88
+ if (!q) return;
89
+
90
+ const send = () => {
91
+ q.track({
92
+ type: 'page_view',
93
+ props: { title: typeof document !== 'undefined' ? document.title : undefined },
94
+ });
95
+ };
96
+
97
+ // 首次
98
+ send();
99
+
100
+ // patch history methods 以捕获 SPA 导航
101
+ const origPush = history.pushState;
102
+ const origReplace = history.replaceState;
103
+ history.pushState = function (...args) {
104
+ origPush.apply(this, args as any);
105
+ send();
106
+ };
107
+ history.replaceState = function (...args) {
108
+ origReplace.apply(this, args as any);
109
+ send();
110
+ };
111
+ const onPop = () => send();
112
+ window.addEventListener('popstate', onPop);
113
+
114
+ return () => {
115
+ history.pushState = origPush;
116
+ history.replaceState = origReplace;
117
+ window.removeEventListener('popstate', onPop);
118
+ };
119
+ }, [autoPageview]);
120
+
121
+ const value = React.useMemo<ContextValue>(
122
+ () => ({
123
+ track: (e: AnalyticsEvent) => queueRef.current?.track(e),
124
+ setBuyerId: (id) => queueRef.current?.setBuyerId(id),
125
+ }),
126
+ [],
127
+ );
128
+
129
+ return <TrackingCtx.Provider value={value}>{children}</TrackingCtx.Provider>;
130
+ }
131
+
132
+ /**
133
+ * 在组件里拿到 track 函数。
134
+ *
135
+ * 在 TrackingProvider 之外调用会得到一个 no-op,不报错(方便在测试/SSR 中使用)。
136
+ */
137
+ export function useTracking(): ContextValue {
138
+ const ctx = React.useContext(TrackingCtx);
139
+ if (ctx) return ctx;
140
+ return NOOP;
141
+ }
142
+
143
+ const NOOP: ContextValue = {
144
+ track: () => {},
145
+ setBuyerId: () => {},
146
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Analytics SDK 类型
3
+ *
4
+ * Helium 的埋点接口。与 platform-api 的事件 schema 保持一致。
5
+ */
6
+
7
+ export type EventType =
8
+ | 'page_view'
9
+ | 'product_view'
10
+ | 'add_to_cart'
11
+ | 'checkout_start'
12
+ | 'checkout_paid';
13
+
14
+ /** 单条事件输入 */
15
+ export interface AnalyticsEvent {
16
+ /** 事件类型 */
17
+ type: EventType;
18
+ /** 客户端发生时间(秒,Unix)。默认 Date.now()/1000 */
19
+ occurredAt?: number;
20
+ /** 页面 path(默认 window.location.pathname) */
21
+ path?: string;
22
+ /** referrer(默认 document.referrer) */
23
+ referrer?: string;
24
+ /** 事件附加数据 */
25
+ props?: Record<string, unknown>;
26
+ }
27
+
28
+ /** 各事件的 props 形状 */
29
+ export interface PageViewProps {
30
+ title?: string;
31
+ }
32
+
33
+ export interface ProductViewProps {
34
+ productId: string;
35
+ variantId?: string;
36
+ priceCents?: number;
37
+ currency?: string;
38
+ }
39
+
40
+ export interface AddToCartProps {
41
+ productId: string;
42
+ variantId?: string;
43
+ quantity: number;
44
+ priceCents?: number;
45
+ currency?: string;
46
+ }
47
+
48
+ export interface CheckoutStartProps {
49
+ cartId?: string;
50
+ itemCount: number;
51
+ subtotalCents: number;
52
+ currency: string;
53
+ }
54
+
55
+ /** AnalyticsProvider 初始化配置 */
56
+ export interface AnalyticsConfig {
57
+ /** 平台 API base URL,例如 https://api.oxygen-demo.cloudc.top */
58
+ apiBase: string;
59
+ /** Storefront public access token */
60
+ publicAccessToken: string;
61
+ /** 当前买家 ID(已登录时传入) */
62
+ buyerId?: string | null;
63
+ /** 是否开启 debug 日志 */
64
+ debug?: boolean;
65
+ /** flush 间隔(毫秒),默认 3000 */
66
+ flushIntervalMs?: number;
67
+ /** 批量上限(条),默认 20 */
68
+ flushBatchSize?: number;
69
+ }
70
+
71
+ /** track 函数签名 */
72
+ export type TrackFn = (event: AnalyticsEvent) => void;
73
+
74
+ /** 内部队列条目 */
75
+ export interface QueuedEvent {
76
+ type: EventType;
77
+ occurredAt: number;
78
+ path: string | null;
79
+ referrer: string | null;
80
+ props: Record<string, unknown>;
81
+ }
@@ -39,6 +39,12 @@ export interface AddToCartButtonProps {
39
39
  route?: string;
40
40
  /** 按钮 children */
41
41
  children?: React.ReactNode;
42
+ /**
43
+ * 按钮被点击时触发(form 提交之前)。
44
+ * 用于埋点 / dataLayer / 自定义分析回调。
45
+ * 不影响表单提交流程。
46
+ */
47
+ onAdd?: (line: CartLineInput) => void;
42
48
  }
43
49
 
44
50
  export function AddToCartButton(props: AddToCartButtonProps) {
@@ -53,6 +59,7 @@ export function AddToCartButton(props: AddToCartButtonProps) {
53
59
  className,
54
60
  route = '/cart',
55
61
  children = '加入购物车',
62
+ onAdd,
56
63
  } = props;
57
64
 
58
65
  const line: CartLineInput = { merchandiseId: variantId, quantity };
@@ -74,6 +81,17 @@ export function AddToCartButton(props: AddToCartButtonProps) {
74
81
  disabled={disabled || submitting}
75
82
  data-add-to-cart
76
83
  data-loading={submitting ? '' : undefined}
84
+ onClick={() => {
85
+ if (onAdd && !submitting && !disabled) {
86
+ try {
87
+ onAdd(line);
88
+ } catch (err) {
89
+ if (typeof console !== 'undefined') {
90
+ console.warn('[AddToCartButton] onAdd threw', err);
91
+ }
92
+ }
93
+ }
94
+ }}
77
95
  >
78
96
  {submitting ? loadingText : disabled ? unavailableText : children}
79
97
  </button>
package/src/react.tsx CHANGED
@@ -274,6 +274,32 @@ function escapeHtml(s: string): string {
274
274
  /**
275
275
  * 把 head + react stream + tail 拼成一个 ReadableStream<Uint8Array>
276
276
  */
277
+ // ============================================================
278
+ // Tracking SDK re-export
279
+ // ============================================================
280
+ // 把 5 个核心事件(page_view / product_view / add_to_cart / checkout_start / checkout_paid)
281
+ // 批量发到 platform-api 的 /api/events。
282
+ //
283
+ // 与 components/AnalyticsProvider 解耦:
284
+ // - AnalyticsProvider 是商家用的事件总线(自定义 reporter)
285
+ // - TrackingProvider 是平台的标准埋点(发到 platform-api)
286
+ export {
287
+ TrackingProvider,
288
+ useTracking,
289
+ AnalyticsQueue,
290
+ } from './analytics';
291
+ export type {
292
+ TrackingProviderProps,
293
+ AnalyticsEvent,
294
+ AnalyticsConfig,
295
+ EventType as AnalyticsEventType,
296
+ TrackFn,
297
+ PageViewProps,
298
+ ProductViewProps,
299
+ AddToCartProps,
300
+ CheckoutStartProps,
301
+ } from './analytics';
302
+
277
303
  function composeStream(
278
304
  head: string,
279
305
  body: ReadableStream<Uint8Array>,