@visitor-analytics-sdk/core 1.0.0 → 1.0.1
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 +93 -0
- package/dist/index.d.ts +51 -18
- package/dist/index.js +208 -72
- package/package.json +25 -2
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @visitor-analytics-sdk/core
|
|
2
|
+
|
|
3
|
+
Privacy-preserving, framework-agnostic, zero-dependency analytics SDK for web applications. Drop it in any stack and start collecting anonymous visitor data — browser, device, performance, features, environment, and interaction.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @visitor-analytics-sdk/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createAnalytics } from "@visitor-analytics-sdk/core";
|
|
11
|
+
|
|
12
|
+
const analytics = createAnalytics({
|
|
13
|
+
endpoint: "/api/analytics",
|
|
14
|
+
storage: "indexeddb",
|
|
15
|
+
autoStart: true,
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Zero runtime dependencies** — no external packages, tiny bundle impact
|
|
22
|
+
- **Privacy-first** — never collects PII; all data anonymous and aggregate-only
|
|
23
|
+
- **Plugin-based** — modular collectors, storage adapters, and plugins
|
|
24
|
+
- **Full TypeScript** — strict types, no `any`, auto-complete ready
|
|
25
|
+
- **Tree-shakable** — ESM-first, import only what you need
|
|
26
|
+
- **Offline-ready** — queues records offline, auto-syncs when online
|
|
27
|
+
- **Framework-agnostic** — works with React, Vue, Svelte, Solid, Astro, vanilla JS
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
createAnalytics({
|
|
33
|
+
endpoint: string;
|
|
34
|
+
storage: "memory" | "localstorage" | "indexeddb" | StorageAdapter;
|
|
35
|
+
autoStart?: boolean;
|
|
36
|
+
batchSize?: number; // default: 50
|
|
37
|
+
flushInterval?: number; // default: 30000 (30s)
|
|
38
|
+
maxRetries?: number; // default: 5
|
|
39
|
+
compressionEnabled?: boolean;
|
|
40
|
+
deduplicationEnabled?: boolean;
|
|
41
|
+
sessionTimeout?: number; // default: 1800000 (30min)
|
|
42
|
+
includeUserAgent?: boolean;
|
|
43
|
+
includeReferrer?: boolean;
|
|
44
|
+
customData?: Record<string, string | number | boolean>;
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Instance methods
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
analytics.start()
|
|
52
|
+
analytics.stop()
|
|
53
|
+
analytics.flush()
|
|
54
|
+
analytics.sync()
|
|
55
|
+
analytics.retryFailed()
|
|
56
|
+
analytics.use(plugin)
|
|
57
|
+
analytics.addCollector(collector)
|
|
58
|
+
analytics.removeCollector(name)
|
|
59
|
+
analytics.query(filters)
|
|
60
|
+
analytics.getCollectedData()
|
|
61
|
+
analytics.export()
|
|
62
|
+
analytics.on(event, handler)
|
|
63
|
+
analytics.off(event, handler)
|
|
64
|
+
analytics.destroy()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Events
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
analytics.on("start", () => {});
|
|
71
|
+
analytics.on("stop", () => {});
|
|
72
|
+
analytics.on("flush", () => {});
|
|
73
|
+
analytics.on("record-collected", (record) => {});
|
|
74
|
+
analytics.on("batch-uploaded", (batchId) => {});
|
|
75
|
+
analytics.on("batch-failed", (batchId, error) => {});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Collectors
|
|
79
|
+
|
|
80
|
+
Six built-in collectors activate automatically:
|
|
81
|
+
|
|
82
|
+
| Collector | Data |
|
|
83
|
+
|---|---|
|
|
84
|
+
| Browser | name, version, engine, language |
|
|
85
|
+
| Device | OS, screen, touch, form factor |
|
|
86
|
+
| Performance | CLS, LCP, INP, navigation timing |
|
|
87
|
+
| Environment | timezone, language, color scheme |
|
|
88
|
+
| Features | WebGL, WASM, WebGPU, WebRTC |
|
|
89
|
+
| Interaction | session, clicks, scroll, routes |
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
type AnalyticsEvent = "start" | "stop" | "flush" | "sync" | "record-collected" | "batch-uploaded" | "batch-failed" | "error" | "collector-registered" | "collector-removed" | "plugin-installed" | "plugin-uninstalled" | "storage-saved" | "storage-loaded" | "config-changed";
|
|
2
|
+
type StorageType = "memory" | "localstorage" | "indexeddb";
|
|
3
|
+
|
|
4
|
+
type Handler = (...args: readonly unknown[]) => void;
|
|
5
|
+
declare class EventBus {
|
|
6
|
+
private readonly listeners;
|
|
7
|
+
on(event: AnalyticsEvent, handler: Handler): void;
|
|
8
|
+
off(event: AnalyticsEvent, handler: Handler): void;
|
|
9
|
+
emit(event: AnalyticsEvent, ...args: readonly unknown[]): void;
|
|
10
|
+
removeAllListeners(event?: AnalyticsEvent): void;
|
|
11
|
+
listenerCount(event: AnalyticsEvent): number;
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
interface AnalyticsRecord {
|
|
2
15
|
readonly id: string;
|
|
3
16
|
readonly timestamp: number;
|
|
@@ -267,7 +280,6 @@ interface PermissionsLike {
|
|
|
267
280
|
interface PermissionDesc {
|
|
268
281
|
name: string;
|
|
269
282
|
}
|
|
270
|
-
type StorageType = "memory" | "localstorage" | "indexeddb" | "custom";
|
|
271
283
|
interface StorageAdapter {
|
|
272
284
|
save(record: AnalyticsRecord): Promise<void>;
|
|
273
285
|
saveBatch(records: readonly AnalyticsRecord[]): Promise<void>;
|
|
@@ -304,15 +316,35 @@ interface UploadResult {
|
|
|
304
316
|
readonly retryable: boolean;
|
|
305
317
|
}
|
|
306
318
|
type UploadEventHandler = (event: UploadEvent) => void;
|
|
307
|
-
|
|
308
|
-
readonly type: "batch-sent"
|
|
319
|
+
type UploadEvent = {
|
|
320
|
+
readonly type: "batch-sent";
|
|
321
|
+
readonly batchId: string;
|
|
322
|
+
readonly recordCount: number;
|
|
323
|
+
readonly timestamp: number;
|
|
324
|
+
} | {
|
|
325
|
+
readonly type: "batch-success";
|
|
326
|
+
readonly batchId: string;
|
|
327
|
+
readonly recordCount: number;
|
|
328
|
+
readonly timestamp: number;
|
|
329
|
+
} | {
|
|
330
|
+
readonly type: "batch-failed";
|
|
309
331
|
readonly batchId: string;
|
|
310
332
|
readonly recordCount: number;
|
|
311
333
|
readonly timestamp: number;
|
|
312
334
|
readonly error?: string;
|
|
313
|
-
|
|
314
|
-
readonly
|
|
315
|
-
|
|
335
|
+
} | {
|
|
336
|
+
readonly type: "retry-scheduled";
|
|
337
|
+
readonly batchId: string;
|
|
338
|
+
readonly recordCount: number;
|
|
339
|
+
readonly timestamp: number;
|
|
340
|
+
readonly retryCount: number;
|
|
341
|
+
readonly nextRetryAt: number;
|
|
342
|
+
} | {
|
|
343
|
+
readonly type: "queue-full";
|
|
344
|
+
readonly batchId: string;
|
|
345
|
+
readonly recordCount: number;
|
|
346
|
+
readonly timestamp: number;
|
|
347
|
+
};
|
|
316
348
|
interface Plugin {
|
|
317
349
|
readonly name: string;
|
|
318
350
|
readonly version: string;
|
|
@@ -327,7 +359,6 @@ interface PluginContext {
|
|
|
327
359
|
off(event: AnalyticsEvent, handler: (...args: readonly unknown[]) => void): void;
|
|
328
360
|
getConfig(): AnalyticsConfig;
|
|
329
361
|
}
|
|
330
|
-
type AnalyticsEvent = "start" | "stop" | "flush" | "sync" | "record-collected" | "batch-uploaded" | "batch-failed" | "error" | "collector-registered" | "collector-removed" | "plugin-installed" | "plugin-uninstalled" | "storage-saved" | "storage-loaded" | "config-changed";
|
|
331
362
|
interface AnalyticsConfig {
|
|
332
363
|
readonly endpoint: string;
|
|
333
364
|
readonly storage: StorageType | StorageAdapter;
|
|
@@ -366,11 +397,21 @@ interface VisitorAnalyticsInstance {
|
|
|
366
397
|
addCollector(collector: Collector): void;
|
|
367
398
|
removeCollector(name: string): void;
|
|
368
399
|
getCollectedData(): Promise<readonly AnalyticsRecord[]>;
|
|
400
|
+
query(query: AnalyticsQuery): Promise<readonly AnalyticsRecord[]>;
|
|
369
401
|
export(): Promise<string>;
|
|
370
402
|
on(event: AnalyticsEvent, handler: (...args: readonly unknown[]) => void): void;
|
|
371
403
|
off(event: AnalyticsEvent, handler: (...args: readonly unknown[]) => void): void;
|
|
404
|
+
trackRouteChange(url: string): void;
|
|
372
405
|
destroy(): Promise<void>;
|
|
373
406
|
}
|
|
407
|
+
interface AnalyticsQuery {
|
|
408
|
+
readonly since?: number;
|
|
409
|
+
readonly until?: number;
|
|
410
|
+
readonly pagePath?: string;
|
|
411
|
+
readonly sessionId?: string;
|
|
412
|
+
readonly limit?: number;
|
|
413
|
+
readonly offset?: number;
|
|
414
|
+
}
|
|
374
415
|
|
|
375
416
|
declare class VisitorAnalytics implements VisitorAnalyticsInstance {
|
|
376
417
|
private config;
|
|
@@ -395,7 +436,9 @@ declare class VisitorAnalytics implements VisitorAnalyticsInstance {
|
|
|
395
436
|
addCollector(collector: Collector): void;
|
|
396
437
|
removeCollector(name: string): void;
|
|
397
438
|
getCollectedData(): Promise<readonly AnalyticsRecord[]>;
|
|
439
|
+
query(query: AnalyticsQuery): Promise<readonly AnalyticsRecord[]>;
|
|
398
440
|
export(): Promise<string>;
|
|
441
|
+
trackRouteChange(url: string): void;
|
|
399
442
|
on(event: AnalyticsEvent, handler: (...args: readonly unknown[]) => void): void;
|
|
400
443
|
off(event: AnalyticsEvent, handler: (...args: readonly unknown[]) => void): void;
|
|
401
444
|
destroy(): Promise<void>;
|
|
@@ -405,14 +448,4 @@ declare class VisitorAnalytics implements VisitorAnalyticsInstance {
|
|
|
405
448
|
}
|
|
406
449
|
declare function createAnalytics(config?: AnalyticsConfigPartial): VisitorAnalyticsInstance;
|
|
407
450
|
|
|
408
|
-
type
|
|
409
|
-
declare class EventBus {
|
|
410
|
-
private readonly listeners;
|
|
411
|
-
on(event: AnalyticsEvent, handler: Handler): void;
|
|
412
|
-
off(event: AnalyticsEvent, handler: Handler): void;
|
|
413
|
-
emit(event: AnalyticsEvent, ...args: readonly unknown[]): void;
|
|
414
|
-
removeAllListeners(event?: AnalyticsEvent): void;
|
|
415
|
-
listenerCount(event: AnalyticsEvent): number;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
export { type AnalyticsConfig, type AnalyticsConfigPartial, type AnalyticsEvent, type AnalyticsRecord, type BrowserData, type ClipboardLike, type Collector, type CollectorCategory, type CollectorContext, type DeviceData, type DeviceFormFactor, type DocumentLike, type EnvironmentData, EventBus, type FeatureData, type HistoryLike, type InteractionData, type LocationLike, type MetadataData, type NavigationTiming, type NavigatorLike, type NetworkInformationLike, type PaintTiming, type PerformanceData, type PerformanceLike, type PermissionDesc, type PermissionsLike, type Plugin, type PluginContext, type ScreenLike, type ScreenOrientationLike, type StorageAdapter, type StorageManagerLike, type StorageType, type TouchSupport, type UploadEvent, type UploadEventHandler, type UploadPayload, type UploadResult, type UploaderConfig, VisitorAnalytics, type VisitorAnalyticsFactory, type VisitorAnalyticsInstance, type WindowLike, createAnalytics };
|
|
451
|
+
export { type AnalyticsConfig, type AnalyticsConfigPartial, type AnalyticsEvent, type AnalyticsQuery, type AnalyticsRecord, type BrowserData, type ClipboardLike, type Collector, type CollectorCategory, type CollectorContext, type DeviceData, type DeviceFormFactor, type DocumentLike, type EnvironmentData, EventBus, type FeatureData, type HistoryLike, type InteractionData, type LocationLike, type MetadataData, type NavigationTiming, type NavigatorLike, type NetworkInformationLike, type PaintTiming, type PerformanceData, type PerformanceLike, type PermissionDesc, type PermissionsLike, type Plugin, type PluginContext, type ScreenLike, type ScreenOrientationLike, type StorageAdapter, type StorageManagerLike, type StorageType, type TouchSupport, type UploadEvent, type UploadEventHandler, type UploadPayload, type UploadResult, type UploaderConfig, VisitorAnalytics, type VisitorAnalyticsFactory, type VisitorAnalyticsInstance, type WindowLike, createAnalytics };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// ../utils/src/index.ts
|
|
2
|
+
var SDK_VERSION = "1.0.0";
|
|
3
|
+
function generateId() {
|
|
4
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
8
|
+
const r = Math.random() * 16 | 0;
|
|
9
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
10
|
+
return v.toString(16);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function generateShortId() {
|
|
14
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
15
|
+
const bytes = new Uint8Array(8);
|
|
16
|
+
crypto.getRandomValues(bytes);
|
|
17
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
18
|
+
}
|
|
19
|
+
return generateId().replace(/-/g, "").slice(0, 16);
|
|
20
|
+
}
|
|
21
|
+
function safeCall(fn, fallback) {
|
|
22
|
+
try {
|
|
23
|
+
return fn();
|
|
24
|
+
} catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function requestIdle(callback, timeout = 5e3) {
|
|
29
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
30
|
+
window.requestIdleCallback(callback, { timeout });
|
|
31
|
+
} else {
|
|
32
|
+
setTimeout(callback, 0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function deepFreeze(obj) {
|
|
36
|
+
Object.freeze(obj);
|
|
37
|
+
for (const key of Object.keys(obj)) {
|
|
38
|
+
const val = obj[key];
|
|
39
|
+
if (val !== null && typeof val === "object" && !Object.isFrozen(val)) {
|
|
40
|
+
deepFreeze(val);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
function isBrowser() {
|
|
46
|
+
return typeof window !== "undefined" && typeof document !== "undefined" && typeof navigator !== "undefined";
|
|
47
|
+
}
|
|
2
48
|
var EventBus = class {
|
|
3
49
|
listeners = /* @__PURE__ */ new Map();
|
|
4
50
|
on(event, handler) {
|
|
@@ -33,6 +79,23 @@ var EventBus = class {
|
|
|
33
79
|
return this.listeners.get(event)?.size ?? 0;
|
|
34
80
|
}
|
|
35
81
|
};
|
|
82
|
+
function detectBestStorage() {
|
|
83
|
+
if (typeof window === "undefined") return "memory";
|
|
84
|
+
try {
|
|
85
|
+
if (typeof indexedDB !== "undefined") return "indexeddb";
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
if (typeof localStorage !== "undefined") {
|
|
90
|
+
const testKey = "__va_storage_test__";
|
|
91
|
+
localStorage.setItem(testKey, "1");
|
|
92
|
+
localStorage.removeItem(testKey);
|
|
93
|
+
return "localstorage";
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
return "memory";
|
|
98
|
+
}
|
|
36
99
|
|
|
37
100
|
// ../plugins/src/plugin-manager.ts
|
|
38
101
|
var PluginManager = class {
|
|
@@ -99,57 +162,6 @@ function createPluginContext(addCollector, removeCollector, eventBus, getConfig)
|
|
|
99
162
|
};
|
|
100
163
|
}
|
|
101
164
|
|
|
102
|
-
// ../utils/src/index.ts
|
|
103
|
-
var SDK_VERSION = "1.0.0";
|
|
104
|
-
function generateId() {
|
|
105
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
106
|
-
return crypto.randomUUID();
|
|
107
|
-
}
|
|
108
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
109
|
-
const r = Math.random() * 16 | 0;
|
|
110
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
111
|
-
return v.toString(16);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
function generateShortId() {
|
|
115
|
-
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
116
|
-
const bytes = new Uint8Array(8);
|
|
117
|
-
crypto.getRandomValues(bytes);
|
|
118
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
119
|
-
}
|
|
120
|
-
return generateId().replace(/-/g, "").slice(0, 16);
|
|
121
|
-
}
|
|
122
|
-
function safeCall(fn, fallback) {
|
|
123
|
-
try {
|
|
124
|
-
return fn();
|
|
125
|
-
} catch {
|
|
126
|
-
return fallback;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function requestIdle(callback, timeout = 5e3) {
|
|
130
|
-
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
131
|
-
window.requestIdleCallback(callback, { timeout });
|
|
132
|
-
} else {
|
|
133
|
-
setTimeout(callback, 0);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function deepFreeze(obj) {
|
|
137
|
-
Object.freeze(obj);
|
|
138
|
-
for (const key of Object.keys(obj)) {
|
|
139
|
-
const val = obj[key];
|
|
140
|
-
if (val !== null && typeof val === "object" && !Object.isFrozen(val)) {
|
|
141
|
-
deepFreeze(val);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return obj;
|
|
145
|
-
}
|
|
146
|
-
function cloneRecord(record) {
|
|
147
|
-
return JSON.parse(JSON.stringify(record));
|
|
148
|
-
}
|
|
149
|
-
function isBrowser() {
|
|
150
|
-
return typeof window !== "undefined" && typeof document !== "undefined" && typeof navigator !== "undefined";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
165
|
// ../uploader/src/uploader.ts
|
|
154
166
|
var DEFAULT_CONFIG = {
|
|
155
167
|
endpoint: "",
|
|
@@ -173,6 +185,7 @@ var Uploader = class {
|
|
|
173
185
|
seenBatchIds = /* @__PURE__ */ new Set();
|
|
174
186
|
isUploading = false;
|
|
175
187
|
isStopped = false;
|
|
188
|
+
beforeUnloadHandler = null;
|
|
176
189
|
constructor(storage, config) {
|
|
177
190
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
178
191
|
this.storage = storage;
|
|
@@ -184,6 +197,12 @@ var Uploader = class {
|
|
|
184
197
|
requestIdle(() => this.flush());
|
|
185
198
|
}, this.config.flushInterval);
|
|
186
199
|
}
|
|
200
|
+
if (typeof window !== "undefined") {
|
|
201
|
+
this.beforeUnloadHandler = () => {
|
|
202
|
+
this.flushViaSendBeacon();
|
|
203
|
+
};
|
|
204
|
+
window.addEventListener("beforeunload", this.beforeUnloadHandler);
|
|
205
|
+
}
|
|
187
206
|
}
|
|
188
207
|
stop() {
|
|
189
208
|
this.isStopped = true;
|
|
@@ -195,6 +214,10 @@ var Uploader = class {
|
|
|
195
214
|
clearTimeout(this.retryTimer);
|
|
196
215
|
this.retryTimer = null;
|
|
197
216
|
}
|
|
217
|
+
if (this.beforeUnloadHandler && typeof window !== "undefined") {
|
|
218
|
+
window.removeEventListener("beforeunload", this.beforeUnloadHandler);
|
|
219
|
+
this.beforeUnloadHandler = null;
|
|
220
|
+
}
|
|
198
221
|
this.retryQueue.clear();
|
|
199
222
|
}
|
|
200
223
|
onEvent(handler) {
|
|
@@ -267,6 +290,28 @@ var Uploader = class {
|
|
|
267
290
|
this.isUploading = false;
|
|
268
291
|
}
|
|
269
292
|
}
|
|
293
|
+
// C3: sendBeacon fallback for page unload
|
|
294
|
+
flushViaSendBeacon() {
|
|
295
|
+
if (this.isStopped) return;
|
|
296
|
+
try {
|
|
297
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem("va_records") : null;
|
|
298
|
+
if (!raw) return;
|
|
299
|
+
const records = JSON.parse(raw);
|
|
300
|
+
if (!Array.isArray(records) || records.length === 0) return;
|
|
301
|
+
const batchId = generateShortId();
|
|
302
|
+
const payload = {
|
|
303
|
+
records,
|
|
304
|
+
batchId,
|
|
305
|
+
timestamp: Date.now(),
|
|
306
|
+
sdkVersion: SDK_VERSION
|
|
307
|
+
};
|
|
308
|
+
const blob = new Blob([JSON.stringify(payload)], { type: "application/json" });
|
|
309
|
+
if (this.config.endpoint && typeof navigator !== "undefined" && "sendBeacon" in navigator) {
|
|
310
|
+
navigator.sendBeacon(this.config.endpoint, blob);
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
270
315
|
async sendBatch(payload) {
|
|
271
316
|
if (!this.config.endpoint) {
|
|
272
317
|
return { success: false, batchId: payload.batchId, error: "No endpoint configured", retryable: false };
|
|
@@ -319,8 +364,7 @@ var Uploader = class {
|
|
|
319
364
|
batchId: payload.batchId,
|
|
320
365
|
recordCount: payload.records.length,
|
|
321
366
|
timestamp: Date.now(),
|
|
322
|
-
error: "Max retries exceeded"
|
|
323
|
-
retryCount
|
|
367
|
+
error: "Max retries exceeded"
|
|
324
368
|
});
|
|
325
369
|
return;
|
|
326
370
|
}
|
|
@@ -381,6 +425,7 @@ var Uploader = class {
|
|
|
381
425
|
async sync() {
|
|
382
426
|
await this.flush();
|
|
383
427
|
}
|
|
428
|
+
// M1: Document compression browser support
|
|
384
429
|
async compress(data) {
|
|
385
430
|
if (typeof CompressionStream === "undefined") {
|
|
386
431
|
return data;
|
|
@@ -404,15 +449,15 @@ var Uploader = class {
|
|
|
404
449
|
var MemoryStorage = class {
|
|
405
450
|
records = [];
|
|
406
451
|
async save(record) {
|
|
407
|
-
this.records.push(
|
|
452
|
+
this.records.push(record);
|
|
408
453
|
}
|
|
409
454
|
async saveBatch(records) {
|
|
410
455
|
for (const record of records) {
|
|
411
|
-
this.records.push(
|
|
456
|
+
this.records.push(record);
|
|
412
457
|
}
|
|
413
458
|
}
|
|
414
459
|
async load() {
|
|
415
|
-
return this.records
|
|
460
|
+
return this.records;
|
|
416
461
|
}
|
|
417
462
|
async loadBatch(limit) {
|
|
418
463
|
return this.records.splice(0, limit);
|
|
@@ -441,13 +486,13 @@ var LocalStorageAdapter = class {
|
|
|
441
486
|
}
|
|
442
487
|
async save(record) {
|
|
443
488
|
const records = this.readAll();
|
|
444
|
-
records.push(
|
|
489
|
+
records.push(record);
|
|
445
490
|
this.writeAll(records);
|
|
446
491
|
}
|
|
447
492
|
async saveBatch(records) {
|
|
448
493
|
const existing = this.readAll();
|
|
449
494
|
for (const record of records) {
|
|
450
|
-
existing.push(
|
|
495
|
+
existing.push(record);
|
|
451
496
|
}
|
|
452
497
|
this.writeAll(existing);
|
|
453
498
|
}
|
|
@@ -543,7 +588,7 @@ var IndexedDBAdapter = class {
|
|
|
543
588
|
return new Promise((resolve, reject) => {
|
|
544
589
|
const tx = db.transaction(this.storeName, "readwrite");
|
|
545
590
|
const store = tx.objectStore(this.storeName);
|
|
546
|
-
const request = store.put(
|
|
591
|
+
const request = store.put(record);
|
|
547
592
|
request.onsuccess = () => resolve();
|
|
548
593
|
request.onerror = () => reject(request.error);
|
|
549
594
|
});
|
|
@@ -554,7 +599,7 @@ var IndexedDBAdapter = class {
|
|
|
554
599
|
const tx = db.transaction(this.storeName, "readwrite");
|
|
555
600
|
const store = tx.objectStore(this.storeName);
|
|
556
601
|
for (const record of records) {
|
|
557
|
-
store.put(
|
|
602
|
+
store.put(record);
|
|
558
603
|
}
|
|
559
604
|
tx.oncomplete = () => resolve();
|
|
560
605
|
tx.onerror = () => reject(tx.error);
|
|
@@ -684,11 +729,13 @@ var BrowserCollector = class {
|
|
|
684
729
|
}
|
|
685
730
|
};
|
|
686
731
|
}
|
|
732
|
+
// H1: Improved UA parser with proper detection order
|
|
687
733
|
parseUA(ua) {
|
|
688
734
|
let name = "unknown";
|
|
689
735
|
let version = "0";
|
|
690
736
|
let engine = "unknown";
|
|
691
737
|
let engineVersion = "0";
|
|
738
|
+
const isHeadless = ua.includes("HeadlessChrome") || ua.includes("Headless");
|
|
692
739
|
if (ua.includes("Firefox/") && !ua.includes("Seamonkey")) {
|
|
693
740
|
name = "Firefox";
|
|
694
741
|
const match = ua.match(/Firefox\/([\d.]+)/);
|
|
@@ -704,7 +751,27 @@ var BrowserCollector = class {
|
|
|
704
751
|
const match = ua.match(/(?:OPR|Opera)\/([\d.]+)/);
|
|
705
752
|
version = match?.[1] ?? "0";
|
|
706
753
|
engine = "Blink";
|
|
707
|
-
} else if (ua.includes("
|
|
754
|
+
} else if (ua.includes("SamsungBrowser/")) {
|
|
755
|
+
name = "Samsung Internet";
|
|
756
|
+
const match = ua.match(/SamsungBrowser\/([\d.]+)/);
|
|
757
|
+
version = match?.[1] ?? "0";
|
|
758
|
+
engine = "Blink";
|
|
759
|
+
} else if (ua.includes("UCBrowser/") || ua.includes("UCWEB")) {
|
|
760
|
+
name = "UC Browser";
|
|
761
|
+
const match = ua.match(/(?:UCBrowser|UCWEB)\/([\d.]+)/);
|
|
762
|
+
version = match?.[1] ?? "0";
|
|
763
|
+
engine = "Blink";
|
|
764
|
+
} else if (ua.includes("Opera Mini/")) {
|
|
765
|
+
name = "Opera Mini";
|
|
766
|
+
const match = ua.match(/Opera Mini\/([\d.]+)/);
|
|
767
|
+
version = match?.[1] ?? "0";
|
|
768
|
+
engine = "Presto";
|
|
769
|
+
} else if (ua.includes("CriOS/")) {
|
|
770
|
+
name = "Chrome";
|
|
771
|
+
const match = ua.match(/CriOS\/([\d.]+)/);
|
|
772
|
+
version = match?.[1] ?? "0";
|
|
773
|
+
engine = "WebKit";
|
|
774
|
+
} else if (ua.includes("Chrome/") && !ua.includes("Edg/") && !ua.includes("OPR/")) {
|
|
708
775
|
name = "Chrome";
|
|
709
776
|
const match = ua.match(/Chrome\/([\d.]+)/);
|
|
710
777
|
version = match?.[1] ?? "0";
|
|
@@ -720,6 +787,9 @@ var BrowserCollector = class {
|
|
|
720
787
|
version = match?.[1] ?? "0";
|
|
721
788
|
engine = "Trident";
|
|
722
789
|
}
|
|
790
|
+
if (isHeadless && name !== "unknown") {
|
|
791
|
+
name += " (headless)";
|
|
792
|
+
}
|
|
723
793
|
if (engine === "Blink") {
|
|
724
794
|
const match = ua.match(/AppleWebKit\/([\d.]+)/);
|
|
725
795
|
engineVersion = match?.[1] ?? "0";
|
|
@@ -1177,6 +1247,8 @@ var InteractionCollector = class {
|
|
|
1177
1247
|
cleanupFns = [];
|
|
1178
1248
|
sessionTimeout = null;
|
|
1179
1249
|
sessionTimeoutMs;
|
|
1250
|
+
originalPushState = null;
|
|
1251
|
+
originalReplaceState = null;
|
|
1180
1252
|
constructor(options) {
|
|
1181
1253
|
this.sessionTimeoutMs = options?.sessionTimeout ?? 30 * 60 * 1e3;
|
|
1182
1254
|
this.state = this.createInitialState();
|
|
@@ -1271,8 +1343,28 @@ var InteractionCollector = class {
|
|
|
1271
1343
|
this.cleanupFns.push(() => {
|
|
1272
1344
|
win.removeEventListener("popstate", onPopState);
|
|
1273
1345
|
});
|
|
1346
|
+
const histWin = win;
|
|
1347
|
+
this.originalPushState = histWin.history.pushState.bind(histWin.history);
|
|
1348
|
+
this.originalReplaceState = histWin.history.replaceState.bind(histWin.history);
|
|
1349
|
+
const state = this.state;
|
|
1350
|
+
const origPush = this.originalPushState;
|
|
1351
|
+
const origReplace = this.originalReplaceState;
|
|
1352
|
+
histWin.history.pushState = function(data, unused, url) {
|
|
1353
|
+
origPush.call(this, data, unused, url);
|
|
1354
|
+
state.routeChanges++;
|
|
1355
|
+
state.lastPage = location.href;
|
|
1356
|
+
};
|
|
1357
|
+
histWin.history.replaceState = function(data, unused, url) {
|
|
1358
|
+
origReplace.call(this, data, unused, url);
|
|
1359
|
+
state.lastPage = location.href;
|
|
1360
|
+
};
|
|
1274
1361
|
this.startSessionTimeout();
|
|
1275
1362
|
}
|
|
1363
|
+
// H3: Public method for framework integrations to manually track route changes
|
|
1364
|
+
trackRouteChange(url) {
|
|
1365
|
+
this.state.routeChanges++;
|
|
1366
|
+
this.state.lastPage = url;
|
|
1367
|
+
}
|
|
1276
1368
|
startSessionTimeout() {
|
|
1277
1369
|
this.sessionTimeout = setTimeout(() => {
|
|
1278
1370
|
this.state.sessionStart = Date.now();
|
|
@@ -1316,6 +1408,12 @@ var InteractionCollector = class {
|
|
|
1316
1408
|
if (this.sessionTimeout !== null) {
|
|
1317
1409
|
clearTimeout(this.sessionTimeout);
|
|
1318
1410
|
}
|
|
1411
|
+
if (this.originalPushState && typeof history !== "undefined") {
|
|
1412
|
+
history.pushState = this.originalPushState;
|
|
1413
|
+
}
|
|
1414
|
+
if (this.originalReplaceState && typeof history !== "undefined") {
|
|
1415
|
+
history.replaceState = this.originalReplaceState;
|
|
1416
|
+
}
|
|
1319
1417
|
for (const fn of this.cleanupFns) {
|
|
1320
1418
|
fn();
|
|
1321
1419
|
}
|
|
@@ -1326,7 +1424,8 @@ var InteractionCollector = class {
|
|
|
1326
1424
|
// src/visitor-analytics.ts
|
|
1327
1425
|
var DEFAULT_CONFIG2 = {
|
|
1328
1426
|
endpoint: "",
|
|
1329
|
-
|
|
1427
|
+
// H5: Auto-detect best storage instead of defaulting to memory
|
|
1428
|
+
storage: "indexeddb",
|
|
1330
1429
|
autoStart: true,
|
|
1331
1430
|
batchSize: 50,
|
|
1332
1431
|
flushInterval: 3e4,
|
|
@@ -1367,6 +1466,9 @@ var VisitorAnalytics = class {
|
|
|
1367
1466
|
if (config.headers) {
|
|
1368
1467
|
merged.headers = { ...DEFAULT_CONFIG2.headers, ...config.headers };
|
|
1369
1468
|
}
|
|
1469
|
+
if (!config.storage) {
|
|
1470
|
+
merged.storage = detectBestStorage();
|
|
1471
|
+
}
|
|
1370
1472
|
this.config = merged;
|
|
1371
1473
|
this.eventBus = new EventBus();
|
|
1372
1474
|
this.sessionId = generateId();
|
|
@@ -1415,7 +1517,9 @@ var VisitorAnalytics = class {
|
|
|
1415
1517
|
);
|
|
1416
1518
|
}
|
|
1417
1519
|
}
|
|
1520
|
+
// C1: Guard buildCollectorContext with isBrowser() - returns null on server
|
|
1418
1521
|
buildCollectorContext() {
|
|
1522
|
+
if (!isBrowser()) return null;
|
|
1419
1523
|
if (this.collectorContext) return this.collectorContext;
|
|
1420
1524
|
const ctx = {
|
|
1421
1525
|
document,
|
|
@@ -1431,13 +1535,16 @@ var VisitorAnalytics = class {
|
|
|
1431
1535
|
}
|
|
1432
1536
|
start() {
|
|
1433
1537
|
if (this.isRunning) return;
|
|
1538
|
+
if (!isBrowser()) return;
|
|
1434
1539
|
this.isRunning = true;
|
|
1435
1540
|
this.uploader.start();
|
|
1436
1541
|
this.eventBus.emit("start");
|
|
1437
1542
|
for (const collector of this.collectors) {
|
|
1438
|
-
if (collector.init
|
|
1543
|
+
if (collector.init) {
|
|
1544
|
+
const ctx = this.buildCollectorContext();
|
|
1545
|
+
if (!ctx) continue;
|
|
1439
1546
|
requestIdle(() => {
|
|
1440
|
-
collector.init(
|
|
1547
|
+
collector.init(ctx).catch((err) => {
|
|
1441
1548
|
console.debug("[VisitorAnalytics] Collector init failed:", collector.name, err);
|
|
1442
1549
|
});
|
|
1443
1550
|
});
|
|
@@ -1457,13 +1564,13 @@ var VisitorAnalytics = class {
|
|
|
1457
1564
|
}
|
|
1458
1565
|
this.eventBus.emit("stop");
|
|
1459
1566
|
}
|
|
1567
|
+
// C2: flush() should only upload what's already stored, not collect again
|
|
1460
1568
|
async flush() {
|
|
1461
|
-
await this.collectAndStore();
|
|
1462
1569
|
await this.uploader.flush();
|
|
1463
1570
|
this.eventBus.emit("flush");
|
|
1464
1571
|
}
|
|
1572
|
+
// C2: sync() same - only upload, don't re-collect
|
|
1465
1573
|
async sync() {
|
|
1466
|
-
await this.collectAndStore();
|
|
1467
1574
|
await this.uploader.sync();
|
|
1468
1575
|
this.eventBus.emit("sync");
|
|
1469
1576
|
}
|
|
@@ -1497,9 +1604,40 @@ var VisitorAnalytics = class {
|
|
|
1497
1604
|
async getCollectedData() {
|
|
1498
1605
|
return this.storage.load();
|
|
1499
1606
|
}
|
|
1607
|
+
// M2: Query API for filtering records
|
|
1608
|
+
async query(query) {
|
|
1609
|
+
const allRecords = await this.storage.load();
|
|
1610
|
+
let result = [...allRecords];
|
|
1611
|
+
if (query.since !== void 0) {
|
|
1612
|
+
result = result.filter((r) => r.timestamp >= query.since);
|
|
1613
|
+
}
|
|
1614
|
+
if (query.until !== void 0) {
|
|
1615
|
+
result = result.filter((r) => r.timestamp <= query.until);
|
|
1616
|
+
}
|
|
1617
|
+
if (query.pagePath !== void 0) {
|
|
1618
|
+
result = result.filter((r) => r.pagePath === query.pagePath);
|
|
1619
|
+
}
|
|
1620
|
+
if (query.sessionId !== void 0) {
|
|
1621
|
+
result = result.filter((r) => r.sessionId === query.sessionId);
|
|
1622
|
+
}
|
|
1623
|
+
if (query.offset !== void 0) {
|
|
1624
|
+
result = result.slice(query.offset);
|
|
1625
|
+
}
|
|
1626
|
+
if (query.limit !== void 0) {
|
|
1627
|
+
result = result.slice(0, query.limit);
|
|
1628
|
+
}
|
|
1629
|
+
return result;
|
|
1630
|
+
}
|
|
1500
1631
|
async export() {
|
|
1501
1632
|
return this.storage.export();
|
|
1502
1633
|
}
|
|
1634
|
+
// H3: Expose trackRouteChange for SPA framework integrations
|
|
1635
|
+
trackRouteChange(url) {
|
|
1636
|
+
const interactionCollector = this.collectors.find((c) => c.name === "interaction");
|
|
1637
|
+
if (interactionCollector) {
|
|
1638
|
+
interactionCollector.trackRouteChange(url);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1503
1641
|
on(event, handler) {
|
|
1504
1642
|
this.eventBus.on(event, handler);
|
|
1505
1643
|
}
|
|
@@ -1521,6 +1659,7 @@ var VisitorAnalytics = class {
|
|
|
1521
1659
|
async collectAndStore() {
|
|
1522
1660
|
if (!isBrowser()) return;
|
|
1523
1661
|
const context = this.buildCollectorContext();
|
|
1662
|
+
if (!context) return;
|
|
1524
1663
|
const record = await this.collectRecord(context);
|
|
1525
1664
|
if (record) {
|
|
1526
1665
|
await this.storage.save(record);
|
|
@@ -1675,8 +1814,5 @@ var VisitorAnalytics = class {
|
|
|
1675
1814
|
function createAnalytics(config) {
|
|
1676
1815
|
return new VisitorAnalytics(config);
|
|
1677
1816
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
VisitorAnalytics,
|
|
1681
|
-
createAnalytics
|
|
1682
|
-
};
|
|
1817
|
+
|
|
1818
|
+
export { EventBus, VisitorAnalytics, createAnalytics };
|
package/package.json
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visitor-analytics-sdk/core",
|
|
3
|
-
"
|
|
3
|
+
"description": "Privacy-preserving, framework-agnostic, zero-dependency analytics SDK for web applications. Core orchestrator with event bus, type-safe API, and plugin system.",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"visitor-analytics",
|
|
7
|
+
"analytics",
|
|
8
|
+
"privacy",
|
|
9
|
+
"tracking",
|
|
10
|
+
"sdk",
|
|
11
|
+
"typescript",
|
|
12
|
+
"web-analytics",
|
|
13
|
+
"zero-deps"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/instax-dutta/visitor-analytics.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/instax-dutta/visitor-analytics#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/instax-dutta/visitor-analytics/issues"
|
|
22
|
+
},
|
|
23
|
+
"version": "1.0.1",
|
|
4
24
|
"type": "module",
|
|
5
25
|
"main": "dist/index.js",
|
|
6
26
|
"module": "dist/index.js",
|
|
7
27
|
"types": "dist/index.d.ts",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
8
31
|
"exports": {
|
|
9
32
|
".": {
|
|
10
33
|
"import": "./dist/index.js",
|
|
@@ -19,7 +42,7 @@
|
|
|
19
42
|
"tsup": "^8.0.0"
|
|
20
43
|
},
|
|
21
44
|
"scripts": {
|
|
22
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
45
|
+
"build": "tsup src/index.ts --format esm --dts --treeshake",
|
|
23
46
|
"typecheck": "tsc --noEmit"
|
|
24
47
|
}
|
|
25
48
|
}
|