rsclick-log-sdk-web 0.1.0 → 0.1.2
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/README.md +10 -2
- package/dist/index.global.js +260 -186
- package/dist/index.js +260 -186
- package/dist/sdk-client.d.ts +9 -0
- package/dist/sdk-core.d.ts +115 -0
- package/dist/sdk-payload.d.ts +50 -0
- package/dist/sdk-state.d.ts +38 -0
- package/dist/sdk-transport.d.ts +36 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- 自动维护 30 分钟 `session_id`
|
|
9
9
|
- 调用后端 `POST /track/collect` 上报事件
|
|
10
10
|
- 默认初始化后自动发送 `$page_view`
|
|
11
|
+
- 支持 SPA 场景下的路由切换自动发送 `$page_view`
|
|
11
12
|
- 登录态识别仅影响后续事件
|
|
12
13
|
|
|
13
14
|
## 目录
|
|
@@ -52,6 +53,12 @@ await init({
|
|
|
52
53
|
writeKey: "trk_live_xxx",
|
|
53
54
|
endpoint: "https://your-api.example.com/track/collect",
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
默认行为:
|
|
58
|
+
|
|
59
|
+
- 初始化完成后自动发送当前页面的 `$page_view`
|
|
60
|
+
- 在 SPA 中监听 `history.pushState`、`history.replaceState`、`popstate`、`hashchange`
|
|
61
|
+
- 同一 URL 不会重复自动上报
|
|
55
62
|
```
|
|
56
63
|
|
|
57
64
|
关闭默认 `$page_view`:
|
|
@@ -143,8 +150,9 @@ SDK 默认上报以下字段:
|
|
|
143
150
|
|
|
144
151
|
传输策略:
|
|
145
152
|
|
|
146
|
-
1. 优先调用 `navigator.sendBeacon`
|
|
147
|
-
2.
|
|
153
|
+
1. 同源 endpoint 优先调用 `navigator.sendBeacon`
|
|
154
|
+
2. 跨源 endpoint 直接使用 `fetch(..., { mode: "cors", credentials: "omit", keepalive: true })`
|
|
155
|
+
3. 同源 `sendBeacon` 返回 `false` 或抛错时,回退到 `fetch(..., { keepalive: true })`
|
|
148
156
|
|
|
149
157
|
## 发布说明
|
|
150
158
|
|
package/dist/index.global.js
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
TrackingAnalyticsSDK: () => TrackingAnalyticsSDK
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
// src/sdk.ts
|
|
36
|
+
// src/sdk-core.ts
|
|
37
37
|
var DEFAULT_ENDPOINT = "/track/collect";
|
|
38
38
|
var DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
|
|
39
39
|
var DEFAULT_STORAGE_PREFIX = "rs_tracking_sdk";
|
|
@@ -43,128 +43,24 @@
|
|
|
43
43
|
lastActivityAt: "last_activity_at",
|
|
44
44
|
userId: "user_id"
|
|
45
45
|
};
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
var track = async (eventName, options = {}) => getClient().track(eventName, options);
|
|
52
|
-
var identify = (userId) => {
|
|
53
|
-
getClient().identify(userId);
|
|
54
|
-
};
|
|
55
|
-
var page = async (options = {}) => getClient().page(options);
|
|
56
|
-
var reset = () => getClient().reset();
|
|
57
|
-
var getDebugState = () => getClient().getState();
|
|
58
|
-
var createTrackingClient = (options) => {
|
|
59
|
-
const browser = resolveBrowser();
|
|
60
|
-
const config = normalizeConfig(options);
|
|
61
|
-
const storage = createScopedStorage(browser, config.storagePrefix);
|
|
62
|
-
let state = loadState(browser, storage);
|
|
63
|
-
const touchSession = (now) => {
|
|
64
|
-
const timeoutExceeded = now - state.lastActivityAt >= config.sessionTimeoutMs / 1000;
|
|
65
|
-
if (timeoutExceeded) {
|
|
66
|
-
state = {
|
|
67
|
-
...state,
|
|
68
|
-
sessionId: generateId(browser)
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
state = {
|
|
72
|
-
...state,
|
|
73
|
-
lastActivityAt: now
|
|
74
|
-
};
|
|
75
|
-
persistState(storage, state);
|
|
76
|
-
return state;
|
|
77
|
-
};
|
|
78
|
-
const buildEventPayload = (eventName, options2 = {}) => {
|
|
79
|
-
const now = currentUnixTime(browser);
|
|
80
|
-
const nextState = touchSession(now);
|
|
81
|
-
const pageContext = resolvePageContext(browser, options2.page);
|
|
82
|
-
const utm = extractUtm(pageContext.url);
|
|
83
|
-
return {
|
|
84
|
-
event_name: eventName.trim(),
|
|
85
|
-
event_time: options2.eventTime,
|
|
86
|
-
anonymous_id: nextState.anonymousId,
|
|
87
|
-
session_id: nextState.sessionId,
|
|
88
|
-
user_id: nextState.userId ?? undefined,
|
|
89
|
-
page_url: pageContext.url,
|
|
90
|
-
page_path: pageContext.path,
|
|
91
|
-
page_title: pageContext.title,
|
|
92
|
-
referrer: pageContext.referrer,
|
|
93
|
-
utm_source: utm.utm_source,
|
|
94
|
-
utm_medium: utm.utm_medium,
|
|
95
|
-
utm_campaign: utm.utm_campaign,
|
|
96
|
-
properties: sanitizeProperties(options2.properties)
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
const dispatch = async (payload) => {
|
|
100
|
-
const body = JSON.stringify(payload);
|
|
101
|
-
if (canUseBeacon(browser)) {
|
|
102
|
-
try {
|
|
103
|
-
const blob = createJsonBlob(browser, body);
|
|
104
|
-
const sent = browser.navigator?.sendBeacon?.(config.endpoint, blob);
|
|
105
|
-
if (sent) {
|
|
106
|
-
return { transport: "beacon" };
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const response = await postWithFetch(browser, config.endpoint, body);
|
|
112
|
-
return {
|
|
113
|
-
transport: "fetch",
|
|
114
|
-
response
|
|
115
|
-
};
|
|
116
|
-
};
|
|
46
|
+
var normalizeConfig = (options) => {
|
|
47
|
+
const writeKey = options.writeKey.trim();
|
|
48
|
+
if (!writeKey) {
|
|
49
|
+
throw new Error("Tracking writeKey is required");
|
|
50
|
+
}
|
|
117
51
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
track: async (eventName, options2 = {}) => {
|
|
127
|
-
const payload = {
|
|
128
|
-
write_key: config.writeKey,
|
|
129
|
-
events: [buildEventPayload(eventName, options2)]
|
|
130
|
-
};
|
|
131
|
-
return dispatch(payload);
|
|
132
|
-
},
|
|
133
|
-
identify: (userId) => {
|
|
134
|
-
state = {
|
|
135
|
-
...state,
|
|
136
|
-
userId: normalizeOptionalString(userId)
|
|
137
|
-
};
|
|
138
|
-
persistState(storage, state);
|
|
139
|
-
},
|
|
140
|
-
page: async (options2 = {}) => dispatch({
|
|
141
|
-
write_key: config.writeKey,
|
|
142
|
-
events: [
|
|
143
|
-
buildEventPayload("$page_view", {
|
|
144
|
-
properties: options2.properties,
|
|
145
|
-
page: options2
|
|
146
|
-
})
|
|
147
|
-
]
|
|
148
|
-
}),
|
|
149
|
-
reset: () => {
|
|
150
|
-
clearState(storage);
|
|
151
|
-
state = createFreshState(browser);
|
|
152
|
-
persistState(storage, state);
|
|
153
|
-
return { ...state };
|
|
154
|
-
},
|
|
155
|
-
getState: () => ({ ...state })
|
|
52
|
+
writeKey,
|
|
53
|
+
endpoint: normalizeOptionalString(options.endpoint) ?? DEFAULT_ENDPOINT,
|
|
54
|
+
autoPageview: options.autoPageview ?? true,
|
|
55
|
+
sessionTimeoutMs: Math.max(options.sessionTimeoutMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES, 1) * 60000,
|
|
56
|
+
storagePrefix: normalizeOptionalString(options.storagePrefix) ?? DEFAULT_STORAGE_PREFIX
|
|
156
57
|
};
|
|
157
58
|
};
|
|
158
|
-
var getClient = () => {
|
|
159
|
-
if (!trackingClient) {
|
|
160
|
-
throw new Error("Tracking SDK has not been initialized");
|
|
161
|
-
}
|
|
162
|
-
return trackingClient;
|
|
163
|
-
};
|
|
164
59
|
var resolveBrowser = () => {
|
|
165
60
|
const candidate = globalThis;
|
|
166
61
|
return {
|
|
167
62
|
document: candidate.document,
|
|
63
|
+
history: candidate.history,
|
|
168
64
|
location: candidate.location,
|
|
169
65
|
navigator: candidate.navigator,
|
|
170
66
|
screen: candidate.screen,
|
|
@@ -172,22 +68,99 @@
|
|
|
172
68
|
fetch: candidate.fetch?.bind(globalThis),
|
|
173
69
|
crypto: candidate.crypto,
|
|
174
70
|
Blob: candidate.Blob,
|
|
175
|
-
Date
|
|
71
|
+
Date,
|
|
72
|
+
addEventListener: candidate.addEventListener?.bind(globalThis),
|
|
73
|
+
removeEventListener: candidate.removeEventListener?.bind(globalThis),
|
|
74
|
+
setTimeout: candidate.setTimeout?.bind(globalThis),
|
|
75
|
+
clearTimeout: candidate.clearTimeout?.bind(globalThis)
|
|
176
76
|
};
|
|
177
77
|
};
|
|
178
|
-
var
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
78
|
+
var currentUnixTime = (browser) => Math.floor((browser.Date ?? Date).now() / 1000);
|
|
79
|
+
var generateId = (browser) => {
|
|
80
|
+
if (browser.crypto?.randomUUID) {
|
|
81
|
+
return browser.crypto.randomUUID();
|
|
82
|
+
}
|
|
83
|
+
const bytes = new Uint8Array(16);
|
|
84
|
+
if (browser.crypto?.getRandomValues) {
|
|
85
|
+
browser.crypto.getRandomValues(bytes);
|
|
86
|
+
} else {
|
|
87
|
+
for (let index = 0;index < bytes.length; index += 1) {
|
|
88
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
92
|
+
};
|
|
93
|
+
var normalizeOptionalString = (value) => {
|
|
94
|
+
const trimmed = value?.trim();
|
|
95
|
+
return trimmed ? trimmed : null;
|
|
96
|
+
};
|
|
97
|
+
var undefinedIfNull = (value) => value ?? undefined;
|
|
98
|
+
var parseInteger = (value) => {
|
|
99
|
+
if (!value) {
|
|
100
|
+
return null;
|
|
182
101
|
}
|
|
102
|
+
const parsed = Number.parseInt(value, 10);
|
|
103
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/sdk-payload.ts
|
|
107
|
+
var buildTrackingEventPayload = (input) => {
|
|
108
|
+
const pageContext = resolvePageContext(input.browser, input.options?.page);
|
|
109
|
+
const utm = extractUtm(pageContext.url);
|
|
183
110
|
return {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
111
|
+
event_name: input.eventName.trim(),
|
|
112
|
+
event_time: input.options?.eventTime,
|
|
113
|
+
anonymous_id: input.state.anonymousId,
|
|
114
|
+
session_id: input.state.sessionId,
|
|
115
|
+
user_id: input.state.userId ?? undefined,
|
|
116
|
+
page_url: pageContext.url,
|
|
117
|
+
page_path: pageContext.path,
|
|
118
|
+
page_title: pageContext.title,
|
|
119
|
+
referrer: pageContext.referrer,
|
|
120
|
+
utm_source: utm.utm_source,
|
|
121
|
+
utm_medium: utm.utm_medium,
|
|
122
|
+
utm_campaign: utm.utm_campaign,
|
|
123
|
+
properties: sanitizeProperties(input.options?.properties)
|
|
189
124
|
};
|
|
190
125
|
};
|
|
126
|
+
var resolvePageContext = (browser, page = {}) => ({
|
|
127
|
+
url: normalizeOptionalString(page.url) ?? undefinedIfNull(normalizeOptionalString(browser.location?.href)),
|
|
128
|
+
path: normalizeOptionalString(page.path) ?? undefinedIfNull(normalizeOptionalString(browser.location?.pathname)),
|
|
129
|
+
title: normalizeOptionalString(page.title) ?? undefinedIfNull(normalizeOptionalString(browser.document?.title)),
|
|
130
|
+
referrer: normalizeOptionalString(page.referrer) ?? undefinedIfNull(normalizeOptionalString(browser.document?.referrer))
|
|
131
|
+
});
|
|
132
|
+
var extractUtm = (url) => {
|
|
133
|
+
if (!url) {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const parsed = new URL(url, "https://sdk.local");
|
|
138
|
+
return {
|
|
139
|
+
utm_source: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_source"))),
|
|
140
|
+
utm_medium: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_medium"))),
|
|
141
|
+
utm_campaign: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_campaign")))
|
|
142
|
+
};
|
|
143
|
+
} catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var sanitizeProperties = (properties) => {
|
|
148
|
+
if (!properties) {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
return Object.entries(properties).reduce((accumulator, [key, value]) => {
|
|
152
|
+
const normalizedKey = key.trim();
|
|
153
|
+
if (!normalizedKey) {
|
|
154
|
+
return accumulator;
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
157
|
+
accumulator[normalizedKey] = value;
|
|
158
|
+
}
|
|
159
|
+
return accumulator;
|
|
160
|
+
}, {});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/sdk-state.ts
|
|
191
164
|
var createScopedStorage = (browser, prefix) => {
|
|
192
165
|
const nativeStorage = browser.localStorage;
|
|
193
166
|
if (!nativeStorage) {
|
|
@@ -254,68 +227,25 @@
|
|
|
254
227
|
storage.removeItem(STORAGE_KEYS.lastActivityAt);
|
|
255
228
|
storage.removeItem(STORAGE_KEYS.userId);
|
|
256
229
|
};
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
271
|
-
};
|
|
272
|
-
var resolvePageContext = (browser, page2 = {}) => ({
|
|
273
|
-
url: normalizeOptionalString(page2.url) ?? undefinedIfNull(normalizeOptionalString(browser.location?.href)),
|
|
274
|
-
path: normalizeOptionalString(page2.path) ?? undefinedIfNull(normalizeOptionalString(browser.location?.pathname)),
|
|
275
|
-
title: normalizeOptionalString(page2.title) ?? undefinedIfNull(normalizeOptionalString(browser.document?.title)),
|
|
276
|
-
referrer: normalizeOptionalString(page2.referrer) ?? undefinedIfNull(normalizeOptionalString(browser.document?.referrer))
|
|
277
|
-
});
|
|
278
|
-
var extractUtm = (url) => {
|
|
279
|
-
if (!url) {
|
|
280
|
-
return {};
|
|
281
|
-
}
|
|
282
|
-
try {
|
|
283
|
-
const parsed = new URL(url, "https://sdk.local");
|
|
284
|
-
return {
|
|
285
|
-
utm_source: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_source"))),
|
|
286
|
-
utm_medium: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_medium"))),
|
|
287
|
-
utm_campaign: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_campaign")))
|
|
288
|
-
};
|
|
289
|
-
} catch {
|
|
290
|
-
return {};
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
var sanitizeProperties = (properties) => {
|
|
294
|
-
if (!properties) {
|
|
295
|
-
return {};
|
|
296
|
-
}
|
|
297
|
-
return Object.entries(properties).reduce((accumulator, [key, value]) => {
|
|
298
|
-
const normalizedKey = key.trim();
|
|
299
|
-
if (!normalizedKey) {
|
|
300
|
-
return accumulator;
|
|
301
|
-
}
|
|
302
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
303
|
-
accumulator[normalizedKey] = value;
|
|
230
|
+
|
|
231
|
+
// src/sdk-transport.ts
|
|
232
|
+
var dispatchCollect = async (browser, endpoint, payload) => {
|
|
233
|
+
const body = JSON.stringify(payload);
|
|
234
|
+
if (canUseBeacon(browser)) {
|
|
235
|
+
try {
|
|
236
|
+
const blob = createJsonBlob(browser, body);
|
|
237
|
+
const sent = browser.navigator?.sendBeacon?.(endpoint, blob);
|
|
238
|
+
if (sent) {
|
|
239
|
+
return { transport: "beacon" };
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
304
242
|
}
|
|
305
|
-
return accumulator;
|
|
306
|
-
}, {});
|
|
307
|
-
};
|
|
308
|
-
var normalizeOptionalString = (value) => {
|
|
309
|
-
const trimmed = value?.trim();
|
|
310
|
-
return trimmed ? trimmed : null;
|
|
311
|
-
};
|
|
312
|
-
var undefinedIfNull = (value) => value ?? undefined;
|
|
313
|
-
var parseInteger = (value) => {
|
|
314
|
-
if (!value) {
|
|
315
|
-
return null;
|
|
316
243
|
}
|
|
317
|
-
const
|
|
318
|
-
return
|
|
244
|
+
const response = await postWithFetch(browser, endpoint, body);
|
|
245
|
+
return {
|
|
246
|
+
transport: "fetch",
|
|
247
|
+
response
|
|
248
|
+
};
|
|
319
249
|
};
|
|
320
250
|
var canUseBeacon = (browser) => typeof browser.navigator?.sendBeacon === "function";
|
|
321
251
|
var createJsonBlob = (browser, body) => {
|
|
@@ -345,6 +275,150 @@
|
|
|
345
275
|
}
|
|
346
276
|
return await response.json();
|
|
347
277
|
};
|
|
278
|
+
|
|
279
|
+
// src/sdk-client.ts
|
|
280
|
+
var createTrackingClient = (options) => {
|
|
281
|
+
const browser = resolveBrowser();
|
|
282
|
+
const config = normalizeConfig(options);
|
|
283
|
+
const storage = createScopedStorage(browser, config.storagePrefix);
|
|
284
|
+
let state = loadState(browser, storage);
|
|
285
|
+
let routeListenerCleanup = null;
|
|
286
|
+
const routeRuntime = {
|
|
287
|
+
timer: null,
|
|
288
|
+
lastAutoPageUrl: normalizeOptionalString(browser.location?.href)
|
|
289
|
+
};
|
|
290
|
+
const touchSession = (now) => {
|
|
291
|
+
const timeoutExceeded = now - state.lastActivityAt >= config.sessionTimeoutMs / 1000;
|
|
292
|
+
if (timeoutExceeded) {
|
|
293
|
+
state = {
|
|
294
|
+
...state,
|
|
295
|
+
sessionId: generateId(browser)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
state = {
|
|
299
|
+
...state,
|
|
300
|
+
lastActivityAt: now
|
|
301
|
+
};
|
|
302
|
+
persistState(storage, state);
|
|
303
|
+
return state;
|
|
304
|
+
};
|
|
305
|
+
const createCollectPayload = (eventName, options2 = {}) => {
|
|
306
|
+
const nextState = touchSession(currentUnixTime(browser));
|
|
307
|
+
return {
|
|
308
|
+
write_key: config.writeKey,
|
|
309
|
+
events: [
|
|
310
|
+
buildTrackingEventPayload({
|
|
311
|
+
browser,
|
|
312
|
+
state: nextState,
|
|
313
|
+
eventName,
|
|
314
|
+
options: options2
|
|
315
|
+
})
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
const autoTrackCurrentPage = async () => {
|
|
320
|
+
const currentUrl = normalizeOptionalString(browser.location?.href);
|
|
321
|
+
if (!currentUrl || currentUrl === routeRuntime.lastAutoPageUrl) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
routeRuntime.lastAutoPageUrl = currentUrl;
|
|
325
|
+
return dispatchCollect(browser, config.endpoint, createCollectPayload("$page_view"));
|
|
326
|
+
};
|
|
327
|
+
const installRouteListeners = () => {
|
|
328
|
+
const addEventListener = browser.addEventListener?.bind(globalThis);
|
|
329
|
+
const removeEventListener = browser.removeEventListener?.bind(globalThis);
|
|
330
|
+
const pushState = browser.history?.pushState;
|
|
331
|
+
const replaceState = browser.history?.replaceState;
|
|
332
|
+
if (!addEventListener || !removeEventListener || !pushState || !replaceState) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const scheduleRouteTracking = () => {
|
|
336
|
+
if (routeRuntime.timer) {
|
|
337
|
+
const clearTimeoutFn = browser.clearTimeout ?? clearTimeout;
|
|
338
|
+
clearTimeoutFn(routeRuntime.timer);
|
|
339
|
+
}
|
|
340
|
+
const setTimeoutFn = browser.setTimeout ?? setTimeout;
|
|
341
|
+
routeRuntime.timer = setTimeoutFn(() => {
|
|
342
|
+
routeRuntime.timer = null;
|
|
343
|
+
autoTrackCurrentPage();
|
|
344
|
+
}, 0);
|
|
345
|
+
};
|
|
346
|
+
const patchHistoryMethod = (method) => (...args) => {
|
|
347
|
+
const result = method.apply(browser.history, args);
|
|
348
|
+
scheduleRouteTracking();
|
|
349
|
+
return result;
|
|
350
|
+
};
|
|
351
|
+
browser.history.pushState = patchHistoryMethod(pushState);
|
|
352
|
+
browser.history.replaceState = patchHistoryMethod(replaceState);
|
|
353
|
+
addEventListener("popstate", scheduleRouteTracking);
|
|
354
|
+
addEventListener("hashchange", scheduleRouteTracking);
|
|
355
|
+
return () => {
|
|
356
|
+
browser.history.pushState = pushState;
|
|
357
|
+
browser.history.replaceState = replaceState;
|
|
358
|
+
removeEventListener("popstate", scheduleRouteTracking);
|
|
359
|
+
removeEventListener("hashchange", scheduleRouteTracking);
|
|
360
|
+
if (routeRuntime.timer) {
|
|
361
|
+
const clearTimeoutFn = browser.clearTimeout ?? clearTimeout;
|
|
362
|
+
clearTimeoutFn(routeRuntime.timer);
|
|
363
|
+
routeRuntime.timer = null;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
init: async () => {
|
|
369
|
+
if (!config.autoPageview) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
await dispatchCollect(browser, config.endpoint, createCollectPayload("$page_view"));
|
|
373
|
+
routeRuntime.lastAutoPageUrl = normalizeOptionalString(browser.location?.href);
|
|
374
|
+
routeListenerCleanup = installRouteListeners();
|
|
375
|
+
},
|
|
376
|
+
track: async (eventName, options2 = {}) => dispatchCollect(browser, config.endpoint, createCollectPayload(eventName, options2)),
|
|
377
|
+
identify: (userId) => {
|
|
378
|
+
state = {
|
|
379
|
+
...state,
|
|
380
|
+
userId: normalizeOptionalString(userId)
|
|
381
|
+
};
|
|
382
|
+
persistState(storage, state);
|
|
383
|
+
},
|
|
384
|
+
page: async (options2 = {}) => dispatchCollect(browser, config.endpoint, createCollectPayload("$page_view", {
|
|
385
|
+
properties: options2.properties,
|
|
386
|
+
page: options2
|
|
387
|
+
})),
|
|
388
|
+
reset: () => {
|
|
389
|
+
clearState(storage);
|
|
390
|
+
state = createFreshState(browser);
|
|
391
|
+
persistState(storage, state);
|
|
392
|
+
return { ...state };
|
|
393
|
+
},
|
|
394
|
+
getState: () => ({ ...state }),
|
|
395
|
+
destroy: () => {
|
|
396
|
+
routeListenerCleanup?.();
|
|
397
|
+
routeListenerCleanup = null;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/sdk.ts
|
|
403
|
+
var trackingClient = null;
|
|
404
|
+
var init = async (options) => {
|
|
405
|
+
trackingClient?.destroy();
|
|
406
|
+
trackingClient = createTrackingClient(options);
|
|
407
|
+
await trackingClient.init();
|
|
408
|
+
};
|
|
409
|
+
var track = async (eventName, options = {}) => getClient().track(eventName, options);
|
|
410
|
+
var identify = (userId) => {
|
|
411
|
+
getClient().identify(userId);
|
|
412
|
+
};
|
|
413
|
+
var page = async (options = {}) => getClient().page(options);
|
|
414
|
+
var reset = () => getClient().reset();
|
|
415
|
+
var getDebugState = () => getClient().getState();
|
|
416
|
+
var getClient = () => {
|
|
417
|
+
if (!trackingClient) {
|
|
418
|
+
throw new Error("Tracking SDK has not been initialized");
|
|
419
|
+
}
|
|
420
|
+
return trackingClient;
|
|
421
|
+
};
|
|
348
422
|
// src/browser.ts
|
|
349
423
|
var TrackingAnalyticsSDK = {
|
|
350
424
|
init,
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/sdk.ts
|
|
1
|
+
// src/sdk-core.ts
|
|
2
2
|
var DEFAULT_ENDPOINT = "/track/collect";
|
|
3
3
|
var DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
|
|
4
4
|
var DEFAULT_STORAGE_PREFIX = "rs_tracking_sdk";
|
|
@@ -8,128 +8,24 @@ var STORAGE_KEYS = {
|
|
|
8
8
|
lastActivityAt: "last_activity_at",
|
|
9
9
|
userId: "user_id"
|
|
10
10
|
};
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
var track = async (eventName, options = {}) => getClient().track(eventName, options);
|
|
17
|
-
var identify = (userId) => {
|
|
18
|
-
getClient().identify(userId);
|
|
19
|
-
};
|
|
20
|
-
var page = async (options = {}) => getClient().page(options);
|
|
21
|
-
var reset = () => getClient().reset();
|
|
22
|
-
var getDebugState = () => getClient().getState();
|
|
23
|
-
var createTrackingClient = (options) => {
|
|
24
|
-
const browser = resolveBrowser();
|
|
25
|
-
const config = normalizeConfig(options);
|
|
26
|
-
const storage = createScopedStorage(browser, config.storagePrefix);
|
|
27
|
-
let state = loadState(browser, storage);
|
|
28
|
-
const touchSession = (now) => {
|
|
29
|
-
const timeoutExceeded = now - state.lastActivityAt >= config.sessionTimeoutMs / 1000;
|
|
30
|
-
if (timeoutExceeded) {
|
|
31
|
-
state = {
|
|
32
|
-
...state,
|
|
33
|
-
sessionId: generateId(browser)
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
state = {
|
|
37
|
-
...state,
|
|
38
|
-
lastActivityAt: now
|
|
39
|
-
};
|
|
40
|
-
persistState(storage, state);
|
|
41
|
-
return state;
|
|
42
|
-
};
|
|
43
|
-
const buildEventPayload = (eventName, options2 = {}) => {
|
|
44
|
-
const now = currentUnixTime(browser);
|
|
45
|
-
const nextState = touchSession(now);
|
|
46
|
-
const pageContext = resolvePageContext(browser, options2.page);
|
|
47
|
-
const utm = extractUtm(pageContext.url);
|
|
48
|
-
return {
|
|
49
|
-
event_name: eventName.trim(),
|
|
50
|
-
event_time: options2.eventTime,
|
|
51
|
-
anonymous_id: nextState.anonymousId,
|
|
52
|
-
session_id: nextState.sessionId,
|
|
53
|
-
user_id: nextState.userId ?? undefined,
|
|
54
|
-
page_url: pageContext.url,
|
|
55
|
-
page_path: pageContext.path,
|
|
56
|
-
page_title: pageContext.title,
|
|
57
|
-
referrer: pageContext.referrer,
|
|
58
|
-
utm_source: utm.utm_source,
|
|
59
|
-
utm_medium: utm.utm_medium,
|
|
60
|
-
utm_campaign: utm.utm_campaign,
|
|
61
|
-
properties: sanitizeProperties(options2.properties)
|
|
62
|
-
};
|
|
63
|
-
};
|
|
64
|
-
const dispatch = async (payload) => {
|
|
65
|
-
const body = JSON.stringify(payload);
|
|
66
|
-
if (canUseBeacon(browser)) {
|
|
67
|
-
try {
|
|
68
|
-
const blob = createJsonBlob(browser, body);
|
|
69
|
-
const sent = browser.navigator?.sendBeacon?.(config.endpoint, blob);
|
|
70
|
-
if (sent) {
|
|
71
|
-
return { transport: "beacon" };
|
|
72
|
-
}
|
|
73
|
-
} catch {
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const response = await postWithFetch(browser, config.endpoint, body);
|
|
77
|
-
return {
|
|
78
|
-
transport: "fetch",
|
|
79
|
-
response
|
|
80
|
-
};
|
|
81
|
-
};
|
|
11
|
+
var normalizeConfig = (options) => {
|
|
12
|
+
const writeKey = options.writeKey.trim();
|
|
13
|
+
if (!writeKey) {
|
|
14
|
+
throw new Error("Tracking writeKey is required");
|
|
15
|
+
}
|
|
82
16
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
track: async (eventName, options2 = {}) => {
|
|
92
|
-
const payload = {
|
|
93
|
-
write_key: config.writeKey,
|
|
94
|
-
events: [buildEventPayload(eventName, options2)]
|
|
95
|
-
};
|
|
96
|
-
return dispatch(payload);
|
|
97
|
-
},
|
|
98
|
-
identify: (userId) => {
|
|
99
|
-
state = {
|
|
100
|
-
...state,
|
|
101
|
-
userId: normalizeOptionalString(userId)
|
|
102
|
-
};
|
|
103
|
-
persistState(storage, state);
|
|
104
|
-
},
|
|
105
|
-
page: async (options2 = {}) => dispatch({
|
|
106
|
-
write_key: config.writeKey,
|
|
107
|
-
events: [
|
|
108
|
-
buildEventPayload("$page_view", {
|
|
109
|
-
properties: options2.properties,
|
|
110
|
-
page: options2
|
|
111
|
-
})
|
|
112
|
-
]
|
|
113
|
-
}),
|
|
114
|
-
reset: () => {
|
|
115
|
-
clearState(storage);
|
|
116
|
-
state = createFreshState(browser);
|
|
117
|
-
persistState(storage, state);
|
|
118
|
-
return { ...state };
|
|
119
|
-
},
|
|
120
|
-
getState: () => ({ ...state })
|
|
17
|
+
writeKey,
|
|
18
|
+
endpoint: normalizeOptionalString(options.endpoint) ?? DEFAULT_ENDPOINT,
|
|
19
|
+
autoPageview: options.autoPageview ?? true,
|
|
20
|
+
sessionTimeoutMs: Math.max(options.sessionTimeoutMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES, 1) * 60000,
|
|
21
|
+
storagePrefix: normalizeOptionalString(options.storagePrefix) ?? DEFAULT_STORAGE_PREFIX
|
|
121
22
|
};
|
|
122
23
|
};
|
|
123
|
-
var getClient = () => {
|
|
124
|
-
if (!trackingClient) {
|
|
125
|
-
throw new Error("Tracking SDK has not been initialized");
|
|
126
|
-
}
|
|
127
|
-
return trackingClient;
|
|
128
|
-
};
|
|
129
24
|
var resolveBrowser = () => {
|
|
130
25
|
const candidate = globalThis;
|
|
131
26
|
return {
|
|
132
27
|
document: candidate.document,
|
|
28
|
+
history: candidate.history,
|
|
133
29
|
location: candidate.location,
|
|
134
30
|
navigator: candidate.navigator,
|
|
135
31
|
screen: candidate.screen,
|
|
@@ -137,22 +33,99 @@ var resolveBrowser = () => {
|
|
|
137
33
|
fetch: candidate.fetch?.bind(globalThis),
|
|
138
34
|
crypto: candidate.crypto,
|
|
139
35
|
Blob: candidate.Blob,
|
|
140
|
-
Date
|
|
36
|
+
Date,
|
|
37
|
+
addEventListener: candidate.addEventListener?.bind(globalThis),
|
|
38
|
+
removeEventListener: candidate.removeEventListener?.bind(globalThis),
|
|
39
|
+
setTimeout: candidate.setTimeout?.bind(globalThis),
|
|
40
|
+
clearTimeout: candidate.clearTimeout?.bind(globalThis)
|
|
141
41
|
};
|
|
142
42
|
};
|
|
143
|
-
var
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
43
|
+
var currentUnixTime = (browser) => Math.floor((browser.Date ?? Date).now() / 1000);
|
|
44
|
+
var generateId = (browser) => {
|
|
45
|
+
if (browser.crypto?.randomUUID) {
|
|
46
|
+
return browser.crypto.randomUUID();
|
|
47
|
+
}
|
|
48
|
+
const bytes = new Uint8Array(16);
|
|
49
|
+
if (browser.crypto?.getRandomValues) {
|
|
50
|
+
browser.crypto.getRandomValues(bytes);
|
|
51
|
+
} else {
|
|
52
|
+
for (let index = 0;index < bytes.length; index += 1) {
|
|
53
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
57
|
+
};
|
|
58
|
+
var normalizeOptionalString = (value) => {
|
|
59
|
+
const trimmed = value?.trim();
|
|
60
|
+
return trimmed ? trimmed : null;
|
|
61
|
+
};
|
|
62
|
+
var undefinedIfNull = (value) => value ?? undefined;
|
|
63
|
+
var parseInteger = (value) => {
|
|
64
|
+
if (!value) {
|
|
65
|
+
return null;
|
|
147
66
|
}
|
|
67
|
+
const parsed = Number.parseInt(value, 10);
|
|
68
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/sdk-payload.ts
|
|
72
|
+
var buildTrackingEventPayload = (input) => {
|
|
73
|
+
const pageContext = resolvePageContext(input.browser, input.options?.page);
|
|
74
|
+
const utm = extractUtm(pageContext.url);
|
|
148
75
|
return {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
76
|
+
event_name: input.eventName.trim(),
|
|
77
|
+
event_time: input.options?.eventTime,
|
|
78
|
+
anonymous_id: input.state.anonymousId,
|
|
79
|
+
session_id: input.state.sessionId,
|
|
80
|
+
user_id: input.state.userId ?? undefined,
|
|
81
|
+
page_url: pageContext.url,
|
|
82
|
+
page_path: pageContext.path,
|
|
83
|
+
page_title: pageContext.title,
|
|
84
|
+
referrer: pageContext.referrer,
|
|
85
|
+
utm_source: utm.utm_source,
|
|
86
|
+
utm_medium: utm.utm_medium,
|
|
87
|
+
utm_campaign: utm.utm_campaign,
|
|
88
|
+
properties: sanitizeProperties(input.options?.properties)
|
|
154
89
|
};
|
|
155
90
|
};
|
|
91
|
+
var resolvePageContext = (browser, page = {}) => ({
|
|
92
|
+
url: normalizeOptionalString(page.url) ?? undefinedIfNull(normalizeOptionalString(browser.location?.href)),
|
|
93
|
+
path: normalizeOptionalString(page.path) ?? undefinedIfNull(normalizeOptionalString(browser.location?.pathname)),
|
|
94
|
+
title: normalizeOptionalString(page.title) ?? undefinedIfNull(normalizeOptionalString(browser.document?.title)),
|
|
95
|
+
referrer: normalizeOptionalString(page.referrer) ?? undefinedIfNull(normalizeOptionalString(browser.document?.referrer))
|
|
96
|
+
});
|
|
97
|
+
var extractUtm = (url) => {
|
|
98
|
+
if (!url) {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const parsed = new URL(url, "https://sdk.local");
|
|
103
|
+
return {
|
|
104
|
+
utm_source: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_source"))),
|
|
105
|
+
utm_medium: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_medium"))),
|
|
106
|
+
utm_campaign: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_campaign")))
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var sanitizeProperties = (properties) => {
|
|
113
|
+
if (!properties) {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
return Object.entries(properties).reduce((accumulator, [key, value]) => {
|
|
117
|
+
const normalizedKey = key.trim();
|
|
118
|
+
if (!normalizedKey) {
|
|
119
|
+
return accumulator;
|
|
120
|
+
}
|
|
121
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
122
|
+
accumulator[normalizedKey] = value;
|
|
123
|
+
}
|
|
124
|
+
return accumulator;
|
|
125
|
+
}, {});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/sdk-state.ts
|
|
156
129
|
var createScopedStorage = (browser, prefix) => {
|
|
157
130
|
const nativeStorage = browser.localStorage;
|
|
158
131
|
if (!nativeStorage) {
|
|
@@ -219,68 +192,25 @@ var clearState = (storage) => {
|
|
|
219
192
|
storage.removeItem(STORAGE_KEYS.lastActivityAt);
|
|
220
193
|
storage.removeItem(STORAGE_KEYS.userId);
|
|
221
194
|
};
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
236
|
-
};
|
|
237
|
-
var resolvePageContext = (browser, page2 = {}) => ({
|
|
238
|
-
url: normalizeOptionalString(page2.url) ?? undefinedIfNull(normalizeOptionalString(browser.location?.href)),
|
|
239
|
-
path: normalizeOptionalString(page2.path) ?? undefinedIfNull(normalizeOptionalString(browser.location?.pathname)),
|
|
240
|
-
title: normalizeOptionalString(page2.title) ?? undefinedIfNull(normalizeOptionalString(browser.document?.title)),
|
|
241
|
-
referrer: normalizeOptionalString(page2.referrer) ?? undefinedIfNull(normalizeOptionalString(browser.document?.referrer))
|
|
242
|
-
});
|
|
243
|
-
var extractUtm = (url) => {
|
|
244
|
-
if (!url) {
|
|
245
|
-
return {};
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
const parsed = new URL(url, "https://sdk.local");
|
|
249
|
-
return {
|
|
250
|
-
utm_source: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_source"))),
|
|
251
|
-
utm_medium: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_medium"))),
|
|
252
|
-
utm_campaign: undefinedIfNull(normalizeOptionalString(parsed.searchParams.get("utm_campaign")))
|
|
253
|
-
};
|
|
254
|
-
} catch {
|
|
255
|
-
return {};
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
var sanitizeProperties = (properties) => {
|
|
259
|
-
if (!properties) {
|
|
260
|
-
return {};
|
|
261
|
-
}
|
|
262
|
-
return Object.entries(properties).reduce((accumulator, [key, value]) => {
|
|
263
|
-
const normalizedKey = key.trim();
|
|
264
|
-
if (!normalizedKey) {
|
|
265
|
-
return accumulator;
|
|
266
|
-
}
|
|
267
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
268
|
-
accumulator[normalizedKey] = value;
|
|
195
|
+
|
|
196
|
+
// src/sdk-transport.ts
|
|
197
|
+
var dispatchCollect = async (browser, endpoint, payload) => {
|
|
198
|
+
const body = JSON.stringify(payload);
|
|
199
|
+
if (canUseBeacon(browser)) {
|
|
200
|
+
try {
|
|
201
|
+
const blob = createJsonBlob(browser, body);
|
|
202
|
+
const sent = browser.navigator?.sendBeacon?.(endpoint, blob);
|
|
203
|
+
if (sent) {
|
|
204
|
+
return { transport: "beacon" };
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
269
207
|
}
|
|
270
|
-
return accumulator;
|
|
271
|
-
}, {});
|
|
272
|
-
};
|
|
273
|
-
var normalizeOptionalString = (value) => {
|
|
274
|
-
const trimmed = value?.trim();
|
|
275
|
-
return trimmed ? trimmed : null;
|
|
276
|
-
};
|
|
277
|
-
var undefinedIfNull = (value) => value ?? undefined;
|
|
278
|
-
var parseInteger = (value) => {
|
|
279
|
-
if (!value) {
|
|
280
|
-
return null;
|
|
281
208
|
}
|
|
282
|
-
const
|
|
283
|
-
return
|
|
209
|
+
const response = await postWithFetch(browser, endpoint, body);
|
|
210
|
+
return {
|
|
211
|
+
transport: "fetch",
|
|
212
|
+
response
|
|
213
|
+
};
|
|
284
214
|
};
|
|
285
215
|
var canUseBeacon = (browser) => typeof browser.navigator?.sendBeacon === "function";
|
|
286
216
|
var createJsonBlob = (browser, body) => {
|
|
@@ -310,6 +240,150 @@ var postWithFetch = async (browser, endpoint, body) => {
|
|
|
310
240
|
}
|
|
311
241
|
return await response.json();
|
|
312
242
|
};
|
|
243
|
+
|
|
244
|
+
// src/sdk-client.ts
|
|
245
|
+
var createTrackingClient = (options) => {
|
|
246
|
+
const browser = resolveBrowser();
|
|
247
|
+
const config = normalizeConfig(options);
|
|
248
|
+
const storage = createScopedStorage(browser, config.storagePrefix);
|
|
249
|
+
let state = loadState(browser, storage);
|
|
250
|
+
let routeListenerCleanup = null;
|
|
251
|
+
const routeRuntime = {
|
|
252
|
+
timer: null,
|
|
253
|
+
lastAutoPageUrl: normalizeOptionalString(browser.location?.href)
|
|
254
|
+
};
|
|
255
|
+
const touchSession = (now) => {
|
|
256
|
+
const timeoutExceeded = now - state.lastActivityAt >= config.sessionTimeoutMs / 1000;
|
|
257
|
+
if (timeoutExceeded) {
|
|
258
|
+
state = {
|
|
259
|
+
...state,
|
|
260
|
+
sessionId: generateId(browser)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
state = {
|
|
264
|
+
...state,
|
|
265
|
+
lastActivityAt: now
|
|
266
|
+
};
|
|
267
|
+
persistState(storage, state);
|
|
268
|
+
return state;
|
|
269
|
+
};
|
|
270
|
+
const createCollectPayload = (eventName, options2 = {}) => {
|
|
271
|
+
const nextState = touchSession(currentUnixTime(browser));
|
|
272
|
+
return {
|
|
273
|
+
write_key: config.writeKey,
|
|
274
|
+
events: [
|
|
275
|
+
buildTrackingEventPayload({
|
|
276
|
+
browser,
|
|
277
|
+
state: nextState,
|
|
278
|
+
eventName,
|
|
279
|
+
options: options2
|
|
280
|
+
})
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const autoTrackCurrentPage = async () => {
|
|
285
|
+
const currentUrl = normalizeOptionalString(browser.location?.href);
|
|
286
|
+
if (!currentUrl || currentUrl === routeRuntime.lastAutoPageUrl) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
routeRuntime.lastAutoPageUrl = currentUrl;
|
|
290
|
+
return dispatchCollect(browser, config.endpoint, createCollectPayload("$page_view"));
|
|
291
|
+
};
|
|
292
|
+
const installRouteListeners = () => {
|
|
293
|
+
const addEventListener = browser.addEventListener?.bind(globalThis);
|
|
294
|
+
const removeEventListener = browser.removeEventListener?.bind(globalThis);
|
|
295
|
+
const pushState = browser.history?.pushState;
|
|
296
|
+
const replaceState = browser.history?.replaceState;
|
|
297
|
+
if (!addEventListener || !removeEventListener || !pushState || !replaceState) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const scheduleRouteTracking = () => {
|
|
301
|
+
if (routeRuntime.timer) {
|
|
302
|
+
const clearTimeoutFn = browser.clearTimeout ?? clearTimeout;
|
|
303
|
+
clearTimeoutFn(routeRuntime.timer);
|
|
304
|
+
}
|
|
305
|
+
const setTimeoutFn = browser.setTimeout ?? setTimeout;
|
|
306
|
+
routeRuntime.timer = setTimeoutFn(() => {
|
|
307
|
+
routeRuntime.timer = null;
|
|
308
|
+
autoTrackCurrentPage();
|
|
309
|
+
}, 0);
|
|
310
|
+
};
|
|
311
|
+
const patchHistoryMethod = (method) => (...args) => {
|
|
312
|
+
const result = method.apply(browser.history, args);
|
|
313
|
+
scheduleRouteTracking();
|
|
314
|
+
return result;
|
|
315
|
+
};
|
|
316
|
+
browser.history.pushState = patchHistoryMethod(pushState);
|
|
317
|
+
browser.history.replaceState = patchHistoryMethod(replaceState);
|
|
318
|
+
addEventListener("popstate", scheduleRouteTracking);
|
|
319
|
+
addEventListener("hashchange", scheduleRouteTracking);
|
|
320
|
+
return () => {
|
|
321
|
+
browser.history.pushState = pushState;
|
|
322
|
+
browser.history.replaceState = replaceState;
|
|
323
|
+
removeEventListener("popstate", scheduleRouteTracking);
|
|
324
|
+
removeEventListener("hashchange", scheduleRouteTracking);
|
|
325
|
+
if (routeRuntime.timer) {
|
|
326
|
+
const clearTimeoutFn = browser.clearTimeout ?? clearTimeout;
|
|
327
|
+
clearTimeoutFn(routeRuntime.timer);
|
|
328
|
+
routeRuntime.timer = null;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
init: async () => {
|
|
334
|
+
if (!config.autoPageview) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
await dispatchCollect(browser, config.endpoint, createCollectPayload("$page_view"));
|
|
338
|
+
routeRuntime.lastAutoPageUrl = normalizeOptionalString(browser.location?.href);
|
|
339
|
+
routeListenerCleanup = installRouteListeners();
|
|
340
|
+
},
|
|
341
|
+
track: async (eventName, options2 = {}) => dispatchCollect(browser, config.endpoint, createCollectPayload(eventName, options2)),
|
|
342
|
+
identify: (userId) => {
|
|
343
|
+
state = {
|
|
344
|
+
...state,
|
|
345
|
+
userId: normalizeOptionalString(userId)
|
|
346
|
+
};
|
|
347
|
+
persistState(storage, state);
|
|
348
|
+
},
|
|
349
|
+
page: async (options2 = {}) => dispatchCollect(browser, config.endpoint, createCollectPayload("$page_view", {
|
|
350
|
+
properties: options2.properties,
|
|
351
|
+
page: options2
|
|
352
|
+
})),
|
|
353
|
+
reset: () => {
|
|
354
|
+
clearState(storage);
|
|
355
|
+
state = createFreshState(browser);
|
|
356
|
+
persistState(storage, state);
|
|
357
|
+
return { ...state };
|
|
358
|
+
},
|
|
359
|
+
getState: () => ({ ...state }),
|
|
360
|
+
destroy: () => {
|
|
361
|
+
routeListenerCleanup?.();
|
|
362
|
+
routeListenerCleanup = null;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/sdk.ts
|
|
368
|
+
var trackingClient = null;
|
|
369
|
+
var init = async (options) => {
|
|
370
|
+
trackingClient?.destroy();
|
|
371
|
+
trackingClient = createTrackingClient(options);
|
|
372
|
+
await trackingClient.init();
|
|
373
|
+
};
|
|
374
|
+
var track = async (eventName, options = {}) => getClient().track(eventName, options);
|
|
375
|
+
var identify = (userId) => {
|
|
376
|
+
getClient().identify(userId);
|
|
377
|
+
};
|
|
378
|
+
var page = async (options = {}) => getClient().page(options);
|
|
379
|
+
var reset = () => getClient().reset();
|
|
380
|
+
var getDebugState = () => getClient().getState();
|
|
381
|
+
var getClient = () => {
|
|
382
|
+
if (!trackingClient) {
|
|
383
|
+
throw new Error("Tracking SDK has not been initialized");
|
|
384
|
+
}
|
|
385
|
+
return trackingClient;
|
|
386
|
+
};
|
|
313
387
|
export {
|
|
314
388
|
track,
|
|
315
389
|
reset,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TrackingInitOptions } from "./types";
|
|
2
|
+
import { type TrackingClient } from "./sdk-core";
|
|
3
|
+
/**
|
|
4
|
+
* 创建一个完整的追踪客户端实例。
|
|
5
|
+
*
|
|
6
|
+
* @param options - 初始化参数
|
|
7
|
+
* @returns 返回可管理生命周期的客户端
|
|
8
|
+
*/
|
|
9
|
+
export declare const createTrackingClient: (options: TrackingInitOptions) => TrackingClient;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { PageOptions, TrackOptions, TrackingDispatchResult, TrackingInitOptions, TrackingStateSnapshot } from "./types";
|
|
2
|
+
export declare const DEFAULT_ENDPOINT = "/track/collect";
|
|
3
|
+
export declare const DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
|
|
4
|
+
export declare const DEFAULT_STORAGE_PREFIX = "rs_tracking_sdk";
|
|
5
|
+
export declare const STORAGE_KEYS: {
|
|
6
|
+
readonly anonymousId: "anonymous_id";
|
|
7
|
+
readonly sessionId: "session_id";
|
|
8
|
+
readonly lastActivityAt: "last_activity_at";
|
|
9
|
+
readonly userId: "user_id";
|
|
10
|
+
};
|
|
11
|
+
export interface BrowserLike {
|
|
12
|
+
document?: {
|
|
13
|
+
referrer?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
};
|
|
16
|
+
history?: {
|
|
17
|
+
pushState?: History["pushState"];
|
|
18
|
+
replaceState?: History["replaceState"];
|
|
19
|
+
};
|
|
20
|
+
location?: {
|
|
21
|
+
href?: string;
|
|
22
|
+
pathname?: string;
|
|
23
|
+
search?: string;
|
|
24
|
+
};
|
|
25
|
+
navigator?: {
|
|
26
|
+
language?: string;
|
|
27
|
+
sendBeacon?: (url: string, data?: BodyInit | null) => boolean;
|
|
28
|
+
};
|
|
29
|
+
screen?: {
|
|
30
|
+
width?: number;
|
|
31
|
+
height?: number;
|
|
32
|
+
};
|
|
33
|
+
localStorage?: StorageLike;
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
crypto?: {
|
|
36
|
+
randomUUID?: () => string;
|
|
37
|
+
getRandomValues?: (array: Uint8Array) => Uint8Array;
|
|
38
|
+
};
|
|
39
|
+
Blob?: typeof Blob;
|
|
40
|
+
Date?: DateConstructor;
|
|
41
|
+
addEventListener?: typeof globalThis.addEventListener;
|
|
42
|
+
removeEventListener?: typeof globalThis.removeEventListener;
|
|
43
|
+
setTimeout?: typeof globalThis.setTimeout;
|
|
44
|
+
clearTimeout?: typeof globalThis.clearTimeout;
|
|
45
|
+
}
|
|
46
|
+
export interface StorageLike {
|
|
47
|
+
getItem: (key: string) => string | null;
|
|
48
|
+
setItem: (key: string, value: string) => void;
|
|
49
|
+
removeItem: (key: string) => void;
|
|
50
|
+
}
|
|
51
|
+
export interface NormalizedConfig {
|
|
52
|
+
writeKey: string;
|
|
53
|
+
endpoint: string;
|
|
54
|
+
autoPageview: boolean;
|
|
55
|
+
sessionTimeoutMs: number;
|
|
56
|
+
storagePrefix: string;
|
|
57
|
+
}
|
|
58
|
+
export interface TrackingClient {
|
|
59
|
+
init: () => Promise<void>;
|
|
60
|
+
track: (eventName: string, options?: TrackOptions) => Promise<TrackingDispatchResult>;
|
|
61
|
+
identify: (userId: string | null) => void;
|
|
62
|
+
page: (options?: PageOptions) => Promise<TrackingDispatchResult>;
|
|
63
|
+
reset: () => TrackingStateSnapshot;
|
|
64
|
+
getState: () => TrackingStateSnapshot;
|
|
65
|
+
destroy: () => void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 归一化初始化配置,并补齐默认值。
|
|
69
|
+
*
|
|
70
|
+
* @param options - SDK 初始化参数
|
|
71
|
+
* @returns 返回内部使用的标准配置
|
|
72
|
+
* @throws 当 writeKey 为空时抛错
|
|
73
|
+
*/
|
|
74
|
+
export declare const normalizeConfig: (options: TrackingInitOptions) => NormalizedConfig;
|
|
75
|
+
/**
|
|
76
|
+
* 解析当前运行时的浏览器能力。
|
|
77
|
+
*
|
|
78
|
+
* @returns 返回 SDK 需要访问的浏览器能力快照
|
|
79
|
+
*/
|
|
80
|
+
export declare const resolveBrowser: () => BrowserLike;
|
|
81
|
+
/**
|
|
82
|
+
* 返回当前 Unix 秒级时间戳。
|
|
83
|
+
*
|
|
84
|
+
* @param browser - 浏览器能力对象
|
|
85
|
+
* @returns 当前秒级时间戳
|
|
86
|
+
*/
|
|
87
|
+
export declare const currentUnixTime: (browser: BrowserLike) => number;
|
|
88
|
+
/**
|
|
89
|
+
* 生成匿名访客或会话 ID。
|
|
90
|
+
*
|
|
91
|
+
* @param browser - 浏览器能力对象
|
|
92
|
+
* @returns 返回随机 ID
|
|
93
|
+
*/
|
|
94
|
+
export declare const generateId: (browser: BrowserLike) => string;
|
|
95
|
+
/**
|
|
96
|
+
* 归一化可选字符串,空白视为 null。
|
|
97
|
+
*
|
|
98
|
+
* @param value - 输入字符串
|
|
99
|
+
* @returns 返回 trim 后的值或 null
|
|
100
|
+
*/
|
|
101
|
+
export declare const normalizeOptionalString: (value: string | null | undefined) => string | null;
|
|
102
|
+
/**
|
|
103
|
+
* 将 null 映射为 undefined,便于序列化时省略字段。
|
|
104
|
+
*
|
|
105
|
+
* @param value - 任意可空值
|
|
106
|
+
* @returns 返回 undefined 或原值
|
|
107
|
+
*/
|
|
108
|
+
export declare const undefinedIfNull: <T>(value: T | null) => T | undefined;
|
|
109
|
+
/**
|
|
110
|
+
* 解析整数文本,失败时返回 null。
|
|
111
|
+
*
|
|
112
|
+
* @param value - 待解析文本
|
|
113
|
+
* @returns 返回整数或 null
|
|
114
|
+
*/
|
|
115
|
+
export declare const parseInteger: (value: string | null) => number | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PageOptions, TrackOptions, TrackingEventPayload, TrackingProperties, TrackingStateSnapshot } from "./types";
|
|
2
|
+
import { type BrowserLike } from "./sdk-core";
|
|
3
|
+
interface BuildEventPayloadOptions {
|
|
4
|
+
browser: BrowserLike;
|
|
5
|
+
state: TrackingStateSnapshot;
|
|
6
|
+
eventName: string;
|
|
7
|
+
options?: TrackOptions;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 构建符合后端契约的事件 payload。
|
|
11
|
+
*
|
|
12
|
+
* @param input - 事件构建参数
|
|
13
|
+
* @returns 返回标准化事件对象
|
|
14
|
+
*/
|
|
15
|
+
export declare const buildTrackingEventPayload: (input: BuildEventPayloadOptions) => TrackingEventPayload;
|
|
16
|
+
/**
|
|
17
|
+
* 解析页面上下文字段,优先使用显式传入值。
|
|
18
|
+
*
|
|
19
|
+
* @param browser - 浏览器能力对象
|
|
20
|
+
* @param page - 页面参数
|
|
21
|
+
* @returns 返回页面上下文
|
|
22
|
+
*/
|
|
23
|
+
export declare const resolvePageContext: (browser: BrowserLike, page?: PageOptions) => {
|
|
24
|
+
url?: string;
|
|
25
|
+
path?: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
referrer?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* 从 URL 中提取 UTM 参数。
|
|
31
|
+
*
|
|
32
|
+
* @param url - 页面 URL
|
|
33
|
+
* @returns 返回可选的 UTM 字段集合
|
|
34
|
+
*/
|
|
35
|
+
export declare const extractUtm: (url?: string) => Partial<Record<"utm_source" | "utm_medium" | "utm_campaign", string>>;
|
|
36
|
+
/**
|
|
37
|
+
* 清洗自定义属性,仅保留后端允许的标量值。
|
|
38
|
+
*
|
|
39
|
+
* @param properties - 业务属性
|
|
40
|
+
* @returns 返回清洗后的属性对象
|
|
41
|
+
*/
|
|
42
|
+
export declare const sanitizeProperties: (properties?: TrackingProperties) => TrackingProperties;
|
|
43
|
+
/**
|
|
44
|
+
* 计算当前事件应使用的会话活跃时间。
|
|
45
|
+
*
|
|
46
|
+
* @param browser - 浏览器能力对象
|
|
47
|
+
* @returns 返回当前秒级时间戳
|
|
48
|
+
*/
|
|
49
|
+
export declare const getEventTimestamp: (browser: BrowserLike) => number;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TrackingStateSnapshot } from "./types";
|
|
2
|
+
import { type BrowserLike, type StorageLike } from "./sdk-core";
|
|
3
|
+
/**
|
|
4
|
+
* 创建带命名空间前缀的存储访问器;无 localStorage 时回退到内存。
|
|
5
|
+
*
|
|
6
|
+
* @param browser - 浏览器能力对象
|
|
7
|
+
* @param prefix - 存储前缀
|
|
8
|
+
* @returns 返回作用域隔离后的存储实现
|
|
9
|
+
*/
|
|
10
|
+
export declare const createScopedStorage: (browser: BrowserLike, prefix: string) => StorageLike;
|
|
11
|
+
/**
|
|
12
|
+
* 从持久化介质恢复访客态,缺失时自动生成新状态。
|
|
13
|
+
*
|
|
14
|
+
* @param browser - 浏览器能力对象
|
|
15
|
+
* @param storage - 存储实现
|
|
16
|
+
* @returns 返回当前有效状态
|
|
17
|
+
*/
|
|
18
|
+
export declare const loadState: (browser: BrowserLike, storage: StorageLike) => TrackingStateSnapshot;
|
|
19
|
+
/**
|
|
20
|
+
* 创建全新的匿名访客与会话状态。
|
|
21
|
+
*
|
|
22
|
+
* @param browser - 浏览器能力对象
|
|
23
|
+
* @returns 返回新的状态快照
|
|
24
|
+
*/
|
|
25
|
+
export declare const createFreshState: (browser: BrowserLike) => TrackingStateSnapshot;
|
|
26
|
+
/**
|
|
27
|
+
* 持久化当前访客状态。
|
|
28
|
+
*
|
|
29
|
+
* @param storage - 存储实现
|
|
30
|
+
* @param state - 状态快照
|
|
31
|
+
*/
|
|
32
|
+
export declare const persistState: (storage: StorageLike, state: TrackingStateSnapshot) => void;
|
|
33
|
+
/**
|
|
34
|
+
* 清空持久化中的访客状态。
|
|
35
|
+
*
|
|
36
|
+
* @param storage - 存储实现
|
|
37
|
+
*/
|
|
38
|
+
export declare const clearState: (storage: StorageLike) => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TrackingCollectPayload, TrackingDispatchResult, TrackingResponse } from "./types";
|
|
2
|
+
import type { BrowserLike } from "./sdk-core";
|
|
3
|
+
/**
|
|
4
|
+
* 发送采集请求,优先使用 beacon,失败后回退到 fetch。
|
|
5
|
+
*
|
|
6
|
+
* @param browser - 浏览器能力对象
|
|
7
|
+
* @param endpoint - 上报地址
|
|
8
|
+
* @param payload - 请求体
|
|
9
|
+
* @returns 返回本次传输结果
|
|
10
|
+
*/
|
|
11
|
+
export declare const dispatchCollect: (browser: BrowserLike, endpoint: string, payload: TrackingCollectPayload) => Promise<TrackingDispatchResult>;
|
|
12
|
+
/**
|
|
13
|
+
* 判断当前环境是否支持 sendBeacon。
|
|
14
|
+
*
|
|
15
|
+
* @param browser - 浏览器能力对象
|
|
16
|
+
* @returns 返回是否可用
|
|
17
|
+
*/
|
|
18
|
+
export declare const canUseBeacon: (browser: BrowserLike) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 创建 JSON Blob,缺失 Blob 构造器时直接返回字符串。
|
|
21
|
+
*
|
|
22
|
+
* @param browser - 浏览器能力对象
|
|
23
|
+
* @param body - JSON 字符串
|
|
24
|
+
* @returns 返回 Blob 或字符串
|
|
25
|
+
*/
|
|
26
|
+
export declare const createJsonBlob: (browser: BrowserLike, body: string) => Blob | string;
|
|
27
|
+
/**
|
|
28
|
+
* 使用 fetch 发送采集请求,并在响应为 JSON 时返回解析结果。
|
|
29
|
+
*
|
|
30
|
+
* @param browser - 浏览器能力对象
|
|
31
|
+
* @param endpoint - 上报地址
|
|
32
|
+
* @param body - JSON 字符串
|
|
33
|
+
* @returns 返回可选响应体
|
|
34
|
+
* @throws 当当前环境不可用 fetch 或 HTTP 状态非成功时抛错
|
|
35
|
+
*/
|
|
36
|
+
export declare const postWithFetch: (browser: BrowserLike, endpoint: string, body: string) => Promise<TrackingResponse | undefined>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rsclick-log-sdk-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Lightweight Web tracking SDK for rs-click-log.",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"build": "bun run build:esm && bun run build:browser && bun run build:types",
|
|
30
30
|
"build:esm": "bun build --outdir=./dist --format=esm --target=browser ./src/index.ts",
|
|
31
31
|
"build:browser": "bun build --outfile=./dist/index.global.js --format=iife --target=browser ./src/browser.ts",
|
|
32
|
-
"build:types": "
|
|
32
|
+
"build:types": "npm exec --package=typescript tsc -- --project tsconfig.build.json",
|
|
33
33
|
"test": "bun test",
|
|
34
34
|
"typecheck": "npx -p typescript tsc --noEmit -p tsconfig.json"
|
|
35
35
|
}
|
|
36
|
-
}
|
|
36
|
+
}
|