@cnv-vn/track 0.2.0-alpha.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 +279 -0
- package/dist/cnv-track.esm.js +6 -0
- package/dist/cnv-track.min.js +6 -0
- package/dist/cnv-track.umd.cjs +6 -0
- package/dist/types/index.d.ts +449 -0
- package/package.json +115 -0
- package/snippet/README.md +106 -0
- package/snippet/snippet.html +28 -0
- package/snippet/snippet.min.html +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# cnv-track.js
|
|
2
|
+
|
|
3
|
+
> Lightweight, async-first web analytics tracker — bundle size ≤ 20 KB gzipped.
|
|
4
|
+
|
|
5
|
+
`cnv-track.js` là Web JS SDK của hệ thống tracking CNV, có vai trò tương đương
|
|
6
|
+
Google Analytics / Facebook Pixel: nhúng một dòng `<script>` vào website
|
|
7
|
+
khách hàng, tự động thu thập ngữ cảnh và bắn sự kiện tùy chỉnh về Tracking
|
|
8
|
+
Gateway nội bộ.
|
|
9
|
+
|
|
10
|
+
## Trạng thái hiện tại
|
|
11
|
+
|
|
12
|
+
| Task | Hạng mục | Trạng thái |
|
|
13
|
+
| ---- | ----------------------------------------------------- | ----------------------- |
|
|
14
|
+
| 1 | Setup môi trường & thiết kế snippet | ✅ Hoàn thành |
|
|
15
|
+
| 2.1 | Identity: Anonymous Client ID (cookie + localStorage) | ✅ Hoàn thành |
|
|
16
|
+
| 2.2 | Session ID (inactivity 30 phút, cookie + sessionStg) | ✅ Hoàn thành |
|
|
17
|
+
| 2.3 | Context Collector (page / screen / UTM / click-id) | ✅ Hoàn thành |
|
|
18
|
+
| 3 | Public API: `init`, `pageview`, `track`, `identify` | ✅ Hoàn thành |
|
|
19
|
+
| 3.5 | Queue Processor (drain `cnvQ`, sync `.push` proxy) | ✅ Hoàn thành |
|
|
20
|
+
| 4.1 | Transport: `sendBeacon` (ưu tiên) | ✅ Hoàn thành |
|
|
21
|
+
| 4.2 | Transport: Fetch / XHR fallback | ✅ fetch+keepalive |
|
|
22
|
+
| 4.3 | Content-Type `text/plain` → bỏ CORS preflight | ✅ Hoàn thành |
|
|
23
|
+
| 4.4 | Offline retry queue (`localStorage` + `online`) | ✅ Hoàn thành |
|
|
24
|
+
| 5 | QA, performance audit, cross-browser | ✅ 259 test / 5.5KB |
|
|
25
|
+
| 6 | Build, CDN deploy, Integration guide | ✅ docs-site (30 trang) |
|
|
26
|
+
|
|
27
|
+
Tham khảo [kế hoạch tổng thể](./ke_hoach_phat_trien_cnv_track_js.md).
|
|
28
|
+
|
|
29
|
+
### Endpoint contract
|
|
30
|
+
|
|
31
|
+
| Endpoint | Khi nào |
|
|
32
|
+
| -------------------------------------------- | ------------------------------------ |
|
|
33
|
+
| `POST https://cnvcdp.net/api/segment/batch` | Mặc định — flush nhiều event / 1 req |
|
|
34
|
+
| `POST https://cnvcdp.net/api/segment/{type}` | `useBatch: false` — 1 event / req |
|
|
35
|
+
|
|
36
|
+
Schema mirror nguyên `com.cnv.tracking.dto.SegmentMessage` Java DTO
|
|
37
|
+
(camelCase Segment Spec). Chi tiết: [docs/SCHEMA.md](./docs/SCHEMA.md).
|
|
38
|
+
|
|
39
|
+
> **CORS (Sub-task 4.3):** SDK gửi `Content-Type: text/plain` (mặc định) —
|
|
40
|
+
> giá trị CORS-safelisted nên **không phát** `OPTIONS` preflight. Body vẫn là
|
|
41
|
+
> JSON nguyên vẹn → **backend phải parse `text/plain` thành JSON**. Đặt
|
|
42
|
+
> `contentType: 'application/json'` nếu backend bắt buộc strict (khi đó backend
|
|
43
|
+
> phải allow `OPTIONS /segment/*` từ origin khách). SDK luôn gửi
|
|
44
|
+
> `credentials: 'omit'` nên server có thể trả `Access-Control-Allow-Origin: *`.
|
|
45
|
+
|
|
46
|
+
## Cấu trúc dự án
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
.
|
|
50
|
+
├── src/
|
|
51
|
+
│ ├── index.ts # Public entry — installs queue + exports default
|
|
52
|
+
│ ├── types.ts # Segment Spec wire schema (camelCase)
|
|
53
|
+
│ ├── global.d.ts # window.cnvQ + build-time constants
|
|
54
|
+
│ ├── utils/
|
|
55
|
+
│ │ ├── uuid.ts # RFC-4122 v4 generator + validator
|
|
56
|
+
│ │ └── storage.ts # Cookie + LS + sessionStorage helpers
|
|
57
|
+
│ └── core/
|
|
58
|
+
│ ├── identity.ts # Anonymous Client ID resolver
|
|
59
|
+
│ ├── userIdentity.ts # Authenticated userId + traits (Sub-task 3.4)
|
|
60
|
+
│ ├── session.ts # Session ID + inactivity timeout
|
|
61
|
+
│ ├── context.ts # Context Collector (page/screen/UTM/library)
|
|
62
|
+
│ ├── messageBuilder.ts # SegmentMessage factory (track/page/identify/…)
|
|
63
|
+
│ ├── transport.ts # Batch buffer + beacon/fetch flush → /segment/batch
|
|
64
|
+
│ ├── retryQueue.ts # Offline retry queue (localStorage + online replay)
|
|
65
|
+
│ ├── queue.ts # Queue Processor (drain cnvQ + sync .push proxy)
|
|
66
|
+
│ └── runtime.ts # SDK state holder + Public API dispatcher
|
|
67
|
+
├── tests/
|
|
68
|
+
│ ├── uuid.test.ts # Vitest — UUID v4
|
|
69
|
+
│ ├── storage.test.ts # Vitest — cookie + LS + SS helpers
|
|
70
|
+
│ ├── identity.test.ts # Vitest — Client ID lifecycle
|
|
71
|
+
│ ├── userIdentity.test.ts # Vitest — userId / traits lifecycle
|
|
72
|
+
│ ├── session.test.ts # Vitest — Session lifecycle + timeout
|
|
73
|
+
│ ├── context.test.ts # Vitest — page/screen/UTM/library
|
|
74
|
+
│ ├── messageBuilder.test.ts# Vitest — SegmentMessage factory
|
|
75
|
+
│ ├── transport.test.ts # Vitest — batch buffer, beacon/fetch, content-type
|
|
76
|
+
│ ├── retryQueue.test.ts # Vitest — offline stash + online replay
|
|
77
|
+
│ ├── queue.test.ts # Vitest — Queue Processor
|
|
78
|
+
│ ├── runtime.test.ts # Vitest — end-to-end Public API
|
|
79
|
+
│ └── silentFail.test.ts # Vitest — crash-proof / silent-fail contract
|
|
80
|
+
├── snippet/
|
|
81
|
+
│ ├── snippet.html # Bản có chú thích (onboarding)
|
|
82
|
+
│ ├── snippet.min.html # Bản nén (production)
|
|
83
|
+
│ └── README.md # Cách nhúng + biến thể (GTM, SPA, consent)
|
|
84
|
+
├── docs/
|
|
85
|
+
│ ├── SCHEMA.md # Data Schema Payload (human-readable)
|
|
86
|
+
│ ├── IDENTITY.md # Client ID / cookie / LS rules theo RFC 4122 + 6265bis
|
|
87
|
+
│ ├── CONTEXT.md # Context Collector — UTM, click-id, page/screen
|
|
88
|
+
│ ├── TRANSPORT.md # Transport layer — beacon/fetch/retry
|
|
89
|
+
│ └── QA.md # Task 5 — unit test, crash-proof, perf + cross-browser
|
|
90
|
+
├── rollup.config.mjs # Build matrix: IIFE + UMD + ESM + .d.ts
|
|
91
|
+
├── vitest.config.ts # Test config (jsdom env + build-const define)
|
|
92
|
+
├── tsconfig.json # TypeScript strict mode, target ES2015
|
|
93
|
+
├── eslint.config.mjs # Flat config (ESLint v9)
|
|
94
|
+
└── package.json # Scripts + size-limit budgets
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Build pipeline (Sub-task 1.1)
|
|
98
|
+
|
|
99
|
+
Đầu ra cho mỗi lần `npm run build`:
|
|
100
|
+
|
|
101
|
+
| File | Format | Mục đích |
|
|
102
|
+
| ------------------------ | :----: | ----------------------------------------- |
|
|
103
|
+
| `dist/cnv-track.js` | IIFE | Debug build, có sourcemap |
|
|
104
|
+
| `dist/cnv-track.min.js` | IIFE | **CDN target** — minified, < 20KB gzipped |
|
|
105
|
+
| `dist/cnv-track.esm.js` | ESM | Cho Webpack / Vite / Rollup consumers |
|
|
106
|
+
| `dist/cnv-track.umd.cjs` | UMD | Legacy CommonJS / AMD / RequireJS |
|
|
107
|
+
| `dist/types/index.d.ts` | .d.ts | Bundled type declarations |
|
|
108
|
+
|
|
109
|
+
**Tại sao multi-format?** Người dùng cuối có ba loại:
|
|
110
|
+
(a) chèn `<script>` trực tiếp → IIFE,
|
|
111
|
+
(b) `import` qua bundler hiện đại → ESM (tree-shakable),
|
|
112
|
+
(c) Node-style require / RequireJS / NoModule → UMD. Đây là pattern của Segment Analytics, Mixpanel, Amplitude.
|
|
113
|
+
|
|
114
|
+
## Quick start (dev workflow)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm install # Cài deps lần đầu
|
|
118
|
+
npm run build # Build full matrix → dist/
|
|
119
|
+
npm run build:watch # Watch mode trong khi viết code
|
|
120
|
+
npm run build:analyze # Sinh dist/stats.html (rollup-plugin-visualizer)
|
|
121
|
+
npm run type-check # tsc --noEmit
|
|
122
|
+
npm run lint # ESLint + typescript-eslint
|
|
123
|
+
npm run format # Prettier
|
|
124
|
+
npm run size # size-limit — fail nếu vượt 20KB
|
|
125
|
+
npm run verify # type-check + lint + format:check + build + size
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Decisions & rationale
|
|
129
|
+
|
|
130
|
+
| Quyết định | Lý do |
|
|
131
|
+
| -------------------------------- | -------------------------------------------------------------------- |
|
|
132
|
+
| **TypeScript** thay vì JS thuần | Bắt bug ở compile time; types là docs sống cho payload schema |
|
|
133
|
+
| **Target ES2015** | Cover 97%+ trình duyệt (caniuse 2026); IE11 đã EOL — bỏ Babel |
|
|
134
|
+
| **Rollup** thay vì Webpack | Tree-shaking tốt hơn, format-flexible, chuẩn cho thư viện |
|
|
135
|
+
| **Terser** với 3 passes + mangle | Ép kích thước xuống — tối thiểu hóa cost-to-load cho khách hàng |
|
|
136
|
+
| **`sideEffects: false`** | Bật full tree-shaking cho ESM consumers |
|
|
137
|
+
| **size-limit** trong CI gate | Bundle bloat sẽ fail PR trước khi merge — kỷ luật cứng |
|
|
138
|
+
| **Snippet protocol versioning** | `cnvQ.v` cho phép SDK mới detect snippet cũ và migrate |
|
|
139
|
+
| **`crossOrigin="anonymous"`** | Bật CORS để bắt lỗi qua `window.onerror` — cần cho TASK 5 telemetry |
|
|
140
|
+
| **Schema follows Segment Spec** | Cho phép swap downstream pipeline (Segment → Snowplow → self-hosted) |
|
|
141
|
+
|
|
142
|
+
## Snippet (Sub-task 1.2)
|
|
143
|
+
|
|
144
|
+
361 bytes raw (≈ 240B gzipped). Đặt vào `<head>`:
|
|
145
|
+
|
|
146
|
+
<!-- prettier-ignore -->
|
|
147
|
+
```html
|
|
148
|
+
<script>!function(w,d,s,q,u){if(w[q]&&w[q].__loaded)return;var Q=w[q]=w[q]||[];Q.t=+new Date;Q.v="1.0";Q.push(["init",u]);var f=d.getElementsByTagName(s)[0],j=d.createElement(s);j.async=1;j.defer=1;j.crossOrigin="anonymous";j.src="https://cdn.cnv.vn/cnv-track.min.js";f.parentNode.insertBefore(j,f)}(window,document,"script","cnvQ","YOUR_PROJECT_ID");</script>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Chi tiết: [snippet/README.md](./snippet/README.md).
|
|
152
|
+
|
|
153
|
+
## Schema (Sub-task 1.3)
|
|
154
|
+
|
|
155
|
+
Payload v1.0 theo Segment Spec, snake_case keys, flat `properties`,
|
|
156
|
+
ISO-8601 timestamp. Đầy đủ field reference: [docs/SCHEMA.md](./docs/SCHEMA.md).
|
|
157
|
+
|
|
158
|
+
## Identity & Storage (Sub-task 2.1)
|
|
159
|
+
|
|
160
|
+
Anonymous Client ID (UUID v4 theo RFC 4122 §4.4), lưu kép trên first-party
|
|
161
|
+
cookie (RFC 6265bis, `SameSite=Lax`, TTL 730 ngày) và localStorage để bền
|
|
162
|
+
vững qua ITP và cookie-banner wipe. Đầy đủ default values, validation rules,
|
|
163
|
+
browser compat matrix, và tham chiếu pháp lý (GDPR / ePrivacy / CCPA / PDPD):
|
|
164
|
+
[docs/IDENTITY.md](./docs/IDENTITY.md).
|
|
165
|
+
|
|
166
|
+
## Public API (Task 3)
|
|
167
|
+
|
|
168
|
+
Sau khi snippet load `cnv-track.min.js`, `window.cnvTrack` và
|
|
169
|
+
`window.cnvQ.push([...])` đều dispatch về cùng runtime.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// 1. Khởi tạo (snippet đã làm sẵn)
|
|
173
|
+
cnvTrack.init('PRJ_XYZ', {
|
|
174
|
+
endpoint: 'https://cnvcdp.net/api', // default
|
|
175
|
+
flushAt: 20, // batch size
|
|
176
|
+
flushInterval: 5000, // ms
|
|
177
|
+
autoPageview: true, // auto fire page on init
|
|
178
|
+
contentType: 'text/plain', // default — skip CORS preflight | 'application/json'
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 2. Track sự kiện
|
|
182
|
+
cnvTrack.track('AddToCart', { sku: 'A-1', qty: 2 });
|
|
183
|
+
|
|
184
|
+
// 3. Page / Screen
|
|
185
|
+
cnvTrack.pageview(); // mặc định name = document.title
|
|
186
|
+
cnvTrack.page('Cart', { ref: 'email' });
|
|
187
|
+
cnvTrack.screen('CartScreen', { items: 2 });
|
|
188
|
+
|
|
189
|
+
// 4. Identify user
|
|
190
|
+
cnvTrack.identify('u_42', { email: 'a@b.vn', name: 'Ngọc' });
|
|
191
|
+
|
|
192
|
+
// 5. Alias (gộp anonymous → authenticated)
|
|
193
|
+
cnvTrack.alias('u_42'); // previousId mặc định = anonymousId
|
|
194
|
+
|
|
195
|
+
// 6. Group (B2B)
|
|
196
|
+
cnvTrack.group('org_acme', { plan: 'enterprise' });
|
|
197
|
+
|
|
198
|
+
// 7. Consent (GDPR / ePrivacy)
|
|
199
|
+
cnvTrack.consent({ marketing: false });
|
|
200
|
+
|
|
201
|
+
// 8. Reset (sau logout)
|
|
202
|
+
cnvTrack.reset();
|
|
203
|
+
|
|
204
|
+
// 9. Manual flush (trước khi unload)
|
|
205
|
+
cnvTrack.flush();
|
|
206
|
+
|
|
207
|
+
// 10. Hot-patch options
|
|
208
|
+
cnvTrack.set({ flushAt: 1 });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Mỗi method tương ứng 1 verb trong `QueuedCommand` — `cnvQ.push(['track', 'A'])`
|
|
212
|
+
và `cnvTrack.track('A')` là tương đương 1:1.
|
|
213
|
+
|
|
214
|
+
## Context Collector (Sub-task 2.3)
|
|
215
|
+
|
|
216
|
+
`collectContext()` snapshot `context.page`, `context.screen`,
|
|
217
|
+
`context.locale`, `context.timezone`, `context.user_agent`, `context.library`
|
|
218
|
+
và — khi URL chứa UTM/click-id — `context.campaign`. Mọi browser-API access
|
|
219
|
+
đều bọc `try/catch` (silent-fail) nên không bao giờ crash host page.
|
|
220
|
+
|
|
221
|
+
| Sub-collector | Nguồn |
|
|
222
|
+
| ------------------- | ------------------------------------------------------ |
|
|
223
|
+
| `collectPage()` | `location.href`, `document.title`, `document.referrer` |
|
|
224
|
+
| `collectScreen()` | `screen.{width,height}`, `devicePixelRatio` |
|
|
225
|
+
| `collectCampaign()` | `URLSearchParams` over `location.search` |
|
|
226
|
+
| `collectContext()` | composes the above + `navigator.language` + IANA tz |
|
|
227
|
+
|
|
228
|
+
Đầy đủ: nguồn từng field, UTM key map, click-id whitelist, library
|
|
229
|
+
versioning, browser compat: [docs/CONTEXT.md](./docs/CONTEXT.md).
|
|
230
|
+
|
|
231
|
+
## Transport (Sub-task 4.1 – 4.4)
|
|
232
|
+
|
|
233
|
+
`Transport` buffer event trong bộ nhớ rồi flush theo 4 trigger: đủ `flushAt`,
|
|
234
|
+
quá `flushInterval`, `pagehide` / `visibilitychange→hidden`, hoặc `flush()` thủ
|
|
235
|
+
công. Mỗi request đi qua hai tầng:
|
|
236
|
+
|
|
237
|
+
| Thứ tự | Phương thức | Khi nào dùng |
|
|
238
|
+
| ------ | ---------------------- | -------------------------------------- |
|
|
239
|
+
| 1 | `navigator.sendBeacon` | Mặc định — body là `Blob` `text/plain` |
|
|
240
|
+
| 2 | `fetch` + `keepalive` | Fallback khi beacon miss |
|
|
241
|
+
|
|
242
|
+
Beacon "miss" và rơi xuống fetch khi: trình duyệt không có `sendBeacon`,
|
|
243
|
+
payload > 64KB (vượt cap của beacon), beacon queue đầy (`sendBeacon` trả
|
|
244
|
+
`false`), hoặc lời gọi ném lỗi (vd. CSP chặn). Vì sao beacon là chính: trình
|
|
245
|
+
duyệt cam kết gửi nền ngay cả khi tab đóng, không cần `setTimeout` giữ trang.
|
|
246
|
+
|
|
247
|
+
**Content-Type (Sub-task 4.3).** Mặc định `text/plain` — nằm trong CORS-safelist
|
|
248
|
+
nên trình duyệt bỏ qua `OPTIONS` preflight (beacon vì thế gửi gọn lúc unload).
|
|
249
|
+
Body vẫn là JSON, chỉ header khác → **backend phải parse `text/plain` thành
|
|
250
|
+
JSON**. Đổi `contentType: 'application/json'` nếu backend bắt buộc strict (khi
|
|
251
|
+
đó phải allow preflight). Áp dụng cho cả Blob của beacon lẫn header của fetch.
|
|
252
|
+
|
|
253
|
+
**Offline retry queue (Sub-task 4.4).** Khi mất mạng — `navigator.onLine` là
|
|
254
|
+
`false`, hoặc `fetch` keepalive reject — batch lỗi được stash vào `localStorage`
|
|
255
|
+
(key `_cnv_rq`) thay vì bị bỏ, rồi replay theo FIFO khi có event `online` hoặc
|
|
256
|
+
khi SDK boot ở lần tải trang sau. Mỗi event giữ nguyên `originalTimestamp`, chỉ
|
|
257
|
+
đóng dấu lại `sentAt`. **Không** retry với HTTP error status (4xx/5xx — là
|
|
258
|
+
response, sẽ loop) hay payload > 64KB (không transport nào gửi được). Hàng đợi
|
|
259
|
+
cap 100 event, evict batch cũ nhất khi đầy; mọi thao tác storage đều silent-fail.
|
|
260
|
+
|
|
261
|
+
## QA & Performance (Task 5)
|
|
262
|
+
|
|
263
|
+
| Hạng mục | Kết quả |
|
|
264
|
+
| ------------------------ | -------------------------------------------------------------------- |
|
|
265
|
+
| Unit test (Sub-task 5.1) | **259 test / 12 file** (Vitest) — coverage 93.85% stmt · 98.78% line |
|
|
266
|
+
| Crash-proof (5.2) | Silent-fail xuyên suốt + `silentFail.test.ts` (môi trường thù địch) |
|
|
267
|
+
| Bundle (5.3) | IIFE **5.52 KB gzipped** / 20 KB budget (`size-limit` CI gate) |
|
|
268
|
+
| Async load (5.3) | Snippet `async`+`defer`+`insertBefore` → zero-blocking, CLS = 0 |
|
|
269
|
+
| Cross-browser (5.3) | Chrome / Edge / Firefox / Safari (ITP) — đủ chuỗi fallback |
|
|
270
|
+
|
|
271
|
+
Mock trong test tuân thủ **contract thật** (cap 64KB beacon/keepalive được mô
|
|
272
|
+
hình đúng) nên green chứng minh hành vi thật. Audit còn phát hiện + sửa một lỗi
|
|
273
|
+
silent-fail thật: throw đồng bộ trong flush bất đồng bộ (timer/`pagehide`) nay
|
|
274
|
+
được `Transport.send()` chặn. Đầy đủ: [docs/QA.md](./docs/QA.md).
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
UNLICENSED — internal CNV project. Liên hệ team Data Platform trước khi
|
|
279
|
+
re-distribute.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @cnv-vn/track v0.2.0-alpha.2
|
|
3
|
+
* (c) 2026 CNV
|
|
4
|
+
* Released under the UNLICENSED license.
|
|
5
|
+
*/
|
|
6
|
+
const t=["pdf","doc","docx","xls","xlsx","ppt","pptx","txt","csv","rtf","zip","rar","7z","tar","gz","mp3","mp4","mov","wav","avi","mkv","dmg","exe","pkg","apk","msi","svg"];function n(t){return!!t&&"object"==typeof t&&1===t.nodeType&&"A"===t.tagName}function e(t){const n=t.lastIndexOf("/");return-1===n?t:t.slice(n+1)}function r(t){var n,e,r;let o=(null!==(n=t.textContent)&&void 0!==n?n:"").trim();return o||(o=(null!==(e=t.getAttribute("aria-label"))&&void 0!==e?e:"").trim()),o||(o=(null!==(r=t.getAttribute("title"))&&void 0!==r?r:"").trim()),o.length>200?o.slice(0,200):o}const o="__cnvPatched";function i(){try{return"undefined"!=typeof location?location.href:""}catch(t){return""}}const c=["q","s","query","search","keyword"],u={spa:!0,outboundClicks:!0,scroll:!0,downloads:!0,siteSearch:!0},s={spa:!1,outboundClicks:!1,scroll:!1,downloads:!1,siteSearch:!1};function a(t){try{t()}catch(t){}}const l=["gclid","fbclid","ttclid","msclkid","dclid"],d=[["utm_source","source"],["utm_medium","medium"],["utm_campaign","name"],["utm_term","term"],["utm_content","content"],["utm_id","id"]];function f(t,n){try{return t()}catch(t){return n}}function h(){const t=f(()=>window.location.href,"");let n="",e="",r="";if(t)try{const o=new URL(t);n=o.pathname,e=o.search,r=o.hash}catch(t){}return{url:t,path:n,search:e,hash:r,title:f(()=>document.title,""),referrer:f(()=>document.referrer,"")}}function y(t){var n,e;let r="@cnv-vn/track",o="0.0.0-dev";try{r="@cnv-vn/track"}catch(t){}try{o="0.2.0-alpha.2"}catch(t){}return{name:null!==(n=null==t?void 0:t.name)&&void 0!==n?n:r,version:null!==(e=null==t?void 0:t.version)&&void 0!==e?e:o}}const v="0123456789abcdef";function m(t){return v[t>>>4&15]+v[15&t]}function p(){if("undefined"!=typeof crypto){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();if("function"==typeof crypto.getRandomValues){const t=new Uint8Array(16);return crypto.getRandomValues(t),t[6]=15&t[6]|64,t[8]=63&t[8]|128,m(t[0])+m(t[1])+m(t[2])+m(t[3])+"-"+m(t[4])+m(t[5])+"-"+m(t[6])+m(t[7])+"-"+m(t[8])+m(t[9])+"-"+m(t[10])+m(t[11])+m(t[12])+m(t[13])+m(t[14])+m(t[15])}}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const n=16*Math.random()|0;return("x"===t?n:3&n|8).toString(16)})}const b=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function w(t){return"string"==typeof t&&b.test(t)}const g="undefined"!=typeof window&&"undefined"!=typeof document;function x(t){if(!g)return null;try{const n=document.cookie;if(!n)return null;const e=encodeURIComponent(t),r=n.split(";");for(let n=0;n<r.length;n++){const o=r[n].indexOf("=");if(o<0)continue;const i=r[n].slice(0,o).trim();if(i===e||i===t)try{return decodeURIComponent(r[n].slice(o+1))}catch(t){return r[n].slice(o+1)}}return null}catch(t){return null}}function k(t,n,e={}){var r,o,i;if(!g)return!1;try{let c=`${encodeURIComponent(t)}=${encodeURIComponent(n)}`;if(c+=`; path=${null!==(r=e.path)&&void 0!==r?r:"/"}`,e.domain&&(c+=`; domain=${e.domain}`),"number"==typeof e.maxAgeDays){const t=Math.floor(86400*e.maxAgeDays);c+=`; max-age=${t}`,c+=`; expires=${new Date(Date.now()+1e3*t).toUTCString()}`}return c+=`; SameSite=${null!==(o=e.sameSite)&&void 0!==o?o:"Lax"}`,(null!==(i=e.secure)&&void 0!==i?i:"undefined"!=typeof location&&"https:"===location.protocol)&&(c+="; Secure"),document.cookie=c,x(t)===n}catch(t){return!1}}function j(){if(!g)return!1;try{const t="__cnv_probe__";return localStorage.setItem(t,"1"),localStorage.removeItem(t),!0}catch(t){return!1}}function O(t){if(!j())return null;try{return localStorage.getItem(t)}catch(t){return null}}function S(t,n){if(!j())return!1;try{return localStorage.setItem(t,n),!0}catch(t){return!1}}function _(t){if(!j())return!1;try{return localStorage.removeItem(t),!0}catch(t){return!1}}function A(){if(!g)return!1;try{const t="__cnv_probe__";return sessionStorage.setItem(t,"1"),sessionStorage.removeItem(t),!0}catch(t){return!1}}const P="_cnv_cid";function D(t={}){const n=x(P),e=O(P);let r=null;return w(n)?r=n:w(e)&&(r=e),r||(r=p()),I(r,t),r}function I(t,n={}){var e;k(P,t,{domain:n.cookieDomain,maxAgeDays:null!==(e=n.cookieMaxAgeDays)&&void 0!==e?e:730,sameSite:"Lax"}),S(P,t)}const M="1.0";function U(){return(new Date).toISOString()}function C(t,n){var e;const r=null!==(e=n.timestamp)&&void 0!==e?e:U(),o=Object.assign(Object.assign({},n.context),{projectId:n.projectId,schemaVersion:M}),i={type:t,messageId:p(),anonymousId:n.anonymousId,timestamp:r,originalTimestamp:r,context:o};return n.userId&&(i.userId=n.userId),n.sessionId&&(i.sessionId=n.sessionId),n.integrations&&(i.integrations=n.integrations),i}const T="_cnv_sid";function $(t){if(!t)return null;const n=t.lastIndexOf(".");if(n<0)return null;const e=t.slice(0,n),r=Number(t.slice(n+1));return!w(e)||!Number.isFinite(r)||r<=0?null:{id:e,lastActivity:r}}function L(t={}){const n=Date.now(),e=(t=>{var n;return 6e4*(null!==(n=t.timeoutMinutes)&&void 0!==n?n:30)})(t),r=$(x(T)),o=$((t=>{if(!A())return null;try{return sessionStorage.getItem(t)}catch(t){return null}})(T));let i=null;for(const t of[r,o])t&&(n-t.lastActivity>e||(!i||t.lastActivity>i.lastActivity)&&(i=t));const c=i?{id:i.id,lastActivity:n}:{id:p(),lastActivity:n};return R(c,t),c.id}function R(t,n={}){var e;const r=(t=>`${t.id}.${t.lastActivity}`)(t),o=null!==(e=n.timeoutMinutes)&&void 0!==e?e:30;k(T,r,{domain:n.cookieDomain,maxAgeDays:o/1440,sameSite:"Lax"}),((t,n)=>{if(!A())return!1;try{return sessionStorage.setItem(t,n),!0}catch(t){return!1}})(T,r)}class q{constructor(t){var n,e;Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"storageKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"maxMessages",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"flushing",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"onlineHandler",{enumerable:!0,configurable:!0,writable:!0,value:null}),this.storageKey=null!==(n=t.storageKey)&&void 0!==n?n:"_cnv_rq",this.maxMessages=null!==(e=t.maxMessages)&&void 0!==e?e:100,this.installOnlineListener()}stash(t){if(t&&0!==t.length)try{const n=this.read();for(n.push(t);n.length>1&&N(n)>this.maxMessages;)n.shift();this.write(n),this.config.debug&&console.log("[cnv-track] retry queued",t.length,"buffered",N(n))}catch(t){}}flush(){if(this.flushing||"undefined"!=typeof navigator&&!1===navigator.onLine)return Promise.resolve();const t=this.read();if(0===t.length)return Promise.resolve();this.flushing=!0,this.write([]);const n=()=>{this.flushing=!1};return t.reduce((t,n)=>t.then(()=>this.config.resend(n)),Promise.resolve()).then(n,n)}size(){return N(this.read())}clear(){try{_(this.storageKey)}catch(t){}}destroy(){if(this.onlineHandler){try{window.removeEventListener("online",this.onlineHandler)}catch(t){}this.onlineHandler=null}}installOnlineListener(){if("undefined"==typeof window)return;const t=()=>{this.flush()};try{window.addEventListener("online",t),this.onlineHandler=t}catch(t){}}read(){const t=O(this.storageKey);if(!t)return[];try{const n=JSON.parse(t);return Array.isArray(n)?n.filter(t=>Array.isArray(t)&&t.length>0):[]}catch(t){return[]}}write(t){0!==t.length?S(this.storageKey,JSON.stringify(t)):_(this.storageKey)}}function N(t){let n=0;for(const e of t)n+=e.length;return n}class B{constructor(t){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"buffer",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"timer",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"destroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"handlers",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"retryQueue",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.retryQueue=new q({debug:t.debug,resend:t=>this.send(t)}),this.installPageLifecycleHandlers(),this.retryQueue.flush()}setConfig(t){this.config=Object.assign(Object.assign({},this.config),t)}enqueue(t){this.destroyed||(this.buffer.push(t),this.buffer.length>=this.config.flushAt?this.flush():this.timer||(this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.config.flushInterval)))}flush(){if(this.timer&&(clearTimeout(this.timer),this.timer=null),0===this.buffer.length)return Promise.resolve();const t=this.buffer;return this.buffer=[],this.send(t)}bufferedCount(){return this.buffer.length}destroy(){this.destroyed||(this.destroyed=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.uninstallPageLifecycleHandlers(),this.retryQueue.destroy())}send(t){try{return this.config.useBatch?this.sendBatch(t):Promise.all(t.map(t=>this.sendSingle(t))).then(()=>{})}catch(t){return this.config.debug&&console.warn("[cnv-track] send failed synchronously; dropped batch",t),Promise.resolve()}}sendBatch(t){var n;const e=U(),r={batch:t.map(t=>z(t,e)),context:Object.assign(Object.assign({},null!==(n=this.config.batchContext)&&void 0!==n?n:{}),{projectId:this.config.projectId,sentAt:e}),sentAt:e};return this.post(this.batchUrl(),r,t)}sendSingle(t){const n=z(t,U());return this.post(this.singleUrl(t.type),n,[t])}batchUrl(){return this.config.endpoint+this.config.batchPath+"?projectId="+encodeURIComponent(this.config.projectId)}singleUrl(t){const n=this.config.singlePath.replace("{type}",t);return this.config.endpoint+n+"?projectId="+encodeURIComponent(this.config.projectId)}post(t,n,e){const r=JSON.stringify(n),o=((t,n)=>{try{return new Blob([t],{type:n})}catch(t){return null}})(r,this.config.contentType),i=o?o.size:(t=>{try{return new Blob([t]).size}catch(n){return t.length}})(r),c=i>65536?null:e;return c&&"undefined"!=typeof navigator&&!1===navigator.onLine?(this.retryQueue.stash(c),this.config.debug&&console.log("[cnv-track] offline; queued for retry",t),Promise.resolve()):o&&this.sendViaBeacon(t,o)?(this.config.debug&&console.log("[cnv-track] beacon",t),Promise.resolve()):this.sendViaFetch(t,r,c)}sendViaBeacon(t,n){if("undefined"==typeof navigator||"function"!=typeof navigator.sendBeacon)return!1;if(n.size>65536)return!1;try{return navigator.sendBeacon(t,n)}catch(t){return!1}}sendViaFetch(t,n,e){return"function"!=typeof fetch?(this.config.debug&&console.warn("[cnv-track] no transport available; queued for retry"),e&&this.retryQueue.stash(e),Promise.resolve()):fetch(t,{method:"POST",headers:{"Content-Type":this.config.contentType},body:n,keepalive:!0,credentials:"omit",mode:"cors"}).then(n=>{this.config.debug&&console.log("[cnv-track] fetch flushed",t,n.status)}).catch(n=>{this.config.debug&&console.warn("[cnv-track] flush failed; queued for retry",t,n),e&&this.retryQueue.stash(e)})}installPageLifecycleHandlers(){if("undefined"==typeof window||"undefined"==typeof document)return;const t=()=>{this.flush()},n=()=>{"hidden"===document.visibilityState&&this.flush()};try{window.addEventListener("pagehide",t),document.addEventListener("visibilitychange",n),this.handlers={pagehide:t,visibility:n}}catch(t){}}uninstallPageLifecycleHandlers(){if(this.handlers){try{window.removeEventListener("pagehide",this.handlers.pagehide),document.removeEventListener("visibilitychange",this.handlers.visibility)}catch(t){}this.handlers=null}}}function z(t,n){return Object.assign(Object.assign({},t),{sentAt:n})}const J="_cnv_uid",V="_cnv_traits",E={endpoint:"http://localhost:8090",batchPath:"/segment/batch",singlePath:"/segment/{type}",useBatch:!0,contentType:"text/plain",debug:!1,allowAdTracking:!0,cookieMaxAgeDays:730,sessionTimeoutMinutes:30,flushAt:20,flushInterval:5e3,autoPageview:!0,autotrack:!1,integrations:{}},H={analytics:!0,marketing:!0};function F(t,n){if(!Array.isArray(n)||0===n.length)return;const e=n;try{switch(e[0]){case"init":t.init(e[1],e[2]);break;case"pageview":t.pageview(e[1],e[2]);break;case"page":t.page(e[1],e[2]);break;case"screen":t.screen(e[1],e[2]);break;case"track":t.track(e[1],e[2]);break;case"identify":t.identify(e[1],e[2]);break;case"alias":t.alias(e[1],e[2]);break;case"group":t.group(e[1],e[2]);break;case"reset":t.reset();break;case"flush":t.flush();break;case"consent":t.consent(e[1]);break;case"set":t.set(e[1])}}catch(t){}}const G="0.2.0-alpha.2",K="@cnv-vn/track",Q=(()=>{let v=null,m=Object.assign({},E),b=Object.assign({},H),w=null,j=!1,A=null;function P(t){const n=Object.assign({},m);return t?(void 0!==t.endpoint&&(n.endpoint=(e=t.endpoint).endsWith("/")?e.slice(0,-1):e),void 0!==t.batchPath&&(n.batchPath=t.batchPath),void 0!==t.singlePath&&(n.singlePath=t.singlePath),void 0!==t.useBatch&&(n.useBatch=t.useBatch),void 0!==t.contentType&&(n.contentType=t.contentType),void 0!==t.debug&&(n.debug=t.debug),void 0!==t.allowAdTracking&&(n.allowAdTracking=t.allowAdTracking),void 0!==t.cookieDomain&&(n.cookieDomain=t.cookieDomain),void 0!==t.cookieMaxAgeDays&&(n.cookieMaxAgeDays=t.cookieMaxAgeDays),void 0!==t.sessionTimeoutMinutes&&(n.sessionTimeoutMinutes=t.sessionTimeoutMinutes),void 0!==t.flushAt&&(n.flushAt=t.flushAt),void 0!==t.flushInterval&&(n.flushInterval=t.flushInterval),void 0!==t.autoPageview&&(n.autoPageview=t.autoPageview),void 0!==t.autotrack&&(n.autotrack=t.autotrack),void 0!==t.integrations&&(n.integrations=t.integrations),n):n;var e}function M(){return w||(w=new B({endpoint:m.endpoint,batchPath:m.batchPath,singlePath:m.singlePath,useBatch:m.useBatch,contentType:m.contentType,flushAt:m.flushAt,flushInterval:m.flushInterval,projectId:null!=v?v:"",debug:m.debug}),w)}function U(){var t;if(!v)return null;const n=((t={})=>{var n;const e={page:h(),screen:{width:f(()=>window.screen.width,0),height:f(()=>window.screen.height,0),density:f(()=>window.devicePixelRatio||1,1)},locale:f(()=>navigator.language||"",""),timezone:f(()=>Intl.DateTimeFormat().resolvedOptions().timeZone||"",""),userAgent:f(()=>navigator.userAgent||"",""),library:y(t.library)},r=((t,n=!0)=>{const e=f(()=>window.location.search,"");if(!e)return;let r;try{r=new URLSearchParams(e)}catch(t){return}const o={};let i=!1;for(const[t,n]of d){const e=r.get(t);e&&e.length>0&&(o[n]=e,i=!0)}if(n){const t={};for(const n of l){const e=r.get(n);e&&e.length>0&&(t[n]=e)}Object.keys(t).length>0&&(o.clickIds=t,i=!0)}return i?o:void 0})(0,null===(n=t.allowAdTracking)||void 0===n||n);return r&&(e.campaign=r),e})({allowAdTracking:m.allowAdTracking});return b.analytics&&b.marketing||(n.consent=Object.assign({},b)),{anonymousId:D({cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays}),sessionId:L({cookieDomain:m.cookieDomain,timeoutMinutes:m.sessionTimeoutMinutes}),userId:null!==(t=x(J)||O(J))&&void 0!==t?t:void 0,context:n,projectId:v,integrations:Object.keys(m.integrations).length>0?m.integrations:void 0}}function T(t){m.debug&&console.warn(`[cnv-track] ${t}() called before init()`)}function $(t,n){try{const e=U();if(!e)return T("pageview");const r=((t,n,e)=>{const r=C("page",e);return t&&(r.name=t),r.properties=null!=n?n:{},r})(null!=t?t:(()=>{try{return"undefined"!=typeof document&&document.title||void 0}catch(t){return}})(),n,e);M().enqueue(r)}catch(t){}}function q(t,n){$(t,n)}function N(t,n){try{if("string"!=typeof t||0===t.length)return;const e=U();if(!e)return T("track");const r=((t,n,e)=>{const r=C("track",e);return r.event=t,r.properties=null!=n?n:{},r})(t,n,e);M().enqueue(r)}catch(t){}}return{get initialized(){return j},get projectId(){return v},init(l,d){try{if(!l||"string"!=typeof l)return;if(m=P(d),(null==d?void 0:d.consent)&&(b=Object.assign(Object.assign({},b),d.consent)),v=l,w&&w.destroy(),w=null,M(),j=!0,D({cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays}),L({cookieDomain:m.cookieDomain,timeoutMinutes:m.sessionTimeoutMinutes}),m.autoPageview&&$(),A){try{A()}catch(t){}A=null}m.autotrack&&(A=function(l,d){const f=void 0===(h=d)||!1===h?Object.assign(Object.assign({},s),{searchParams:c}):!0===h?Object.assign(Object.assign({},u),{searchParams:c}):{spa:null===(y=h.spa)||void 0===y||y,outboundClicks:null===(v=h.outboundClicks)||void 0===v||v,scroll:null===(m=h.scroll)||void 0===m||m,downloads:null===(p=h.downloads)||void 0===p||p,siteSearch:null===(b=h.siteSearch)||void 0===b||b,downloadExtensions:h.downloadExtensions,searchParams:null!==(w=h.searchParams)&&void 0!==w?w:c};var h,y,v,m,p,b,w;const g=[];let x=null;function k(t){if(f.siteSearch)try{const n=null!=t?t:(()=>{try{return"undefined"!=typeof location?location.href:""}catch(t){return""}})();if(n===x)return;x=n;const e=((t,n=c)=>{try{if(!t){if("undefined"==typeof location)return null;t=location.href}const e=t instanceof URL?t:new URL(t,(()=>{try{return"undefined"!=typeof location?location.href:"http://localhost/"}catch(t){return"http://localhost/"}})());if(!e.search)return null;for(const t of n){const n=e.searchParams.get(t);if(null===n)continue;const r=n.trim();if(0!==r.length)return{query:r,param:t}}return null}catch(t){return null}})(n,f.searchParams);e&&a(()=>l.track("Site Searched",{query:e.query,param:e.param}))}catch(t){}}if(f.outboundClicks||f.downloads){const o=(o=>{var i;if("undefined"==typeof document)return()=>{};const c=new Set((null!==(i=o.downloadExtensions)&&void 0!==i?i:t).map(t=>t.toLowerCase())),u=!!o.onOutbound,s=!!o.onDownload;if(!u&&!s)return()=>{};function a(t){var i,a;try{if(0!==t.button)return;if(t.defaultPrevented)return;const l=(t=>{const e="function"==typeof t.composedPath?t.composedPath():[];if(e.length>0)for(const t of e)if(n(t))return t;let r=t.target;for(;r;){if(n(r))return r;r=r.parentNode}return null})(t);if(!l)return;const d=l.getAttribute("href");if(!d)return;if(/^(javascript:|mailto:|tel:|sms:)/i.test(d))return;if(d.startsWith("#"))return;const f=(t=>{try{return new URL(t,location.href)}catch(t){return null}})(d);if(!f)return;let h=!1;if(s){const t=l.hasAttribute("download"),n=(t=>{const n=e(t),r=n.lastIndexOf(".");return r<=0||r===n.length-1?null:n.slice(r+1).toLowerCase()})(f.pathname);(t||n&&c.has(n))&&(h=!0,null===(i=o.onDownload)||void 0===i||i.call(o,{url:f.href,fileName:e(f.pathname),fileExtension:null!=n?n:"",text:r(l)}))}u&&!h&&f.host!==(()=>{try{return"undefined"!=typeof location?location.hostname:""}catch(t){return""}})()&&f.host&&(null===(a=o.onOutbound)||void 0===a||a.call(o,{url:f.href,host:f.host,text:r(l),target:l.target||"_self"}))}catch(t){}}try{document.addEventListener("click",a,!0)}catch(t){return()=>{}}return()=>{try{document.removeEventListener("click",a,!0)}catch(t){}}})({downloadExtensions:f.downloadExtensions,onOutbound:f.outboundClicks?t=>a(()=>l.track("Outbound Link Clicked",t)):void 0,onDownload:f.downloads?t=>a(()=>l.track("File Downloaded",t)):void 0});g.push(o)}let j=null;if(f.scroll&&(j=(t=>{if("undefined"==typeof window||"undefined"==typeof document)return{destroy(){},reset(){}};let n=!1,e=null,r=!1;function o(){if(e=null,!r&&!n)try{const e=document.documentElement,r=document.body,o=Math.max(e?e.scrollHeight:0,r?r.scrollHeight:0),i=Math.max(e?e.clientHeight:0,window.innerHeight||0);if(o<=i)return n=!0,c(),void t(90);((window.scrollY||(e?e.scrollTop:0)||0)+i)/o>=.9&&(n=!0,c(),t(90))}catch(t){}}function i(){if(null===e)try{e=window.requestAnimationFrame(o)}catch(t){o()}}function c(){try{window.removeEventListener("scroll",i),null!==e&&window.cancelAnimationFrame(e),e=null}catch(t){}}try{window.addEventListener("scroll",i,{passive:!0})}catch(t){try{window.addEventListener("scroll",i)}catch(t){}}try{window.requestAnimationFrame(o)}catch(t){o()}return{destroy(){r=!0,c()},reset(){if(!r){n=!1;try{window.addEventListener("scroll",i,{passive:!0})}catch(t){try{window.addEventListener("scroll",i)}catch(t){}}try{window.requestAnimationFrame(o)}catch(t){o()}}}}})(t=>{a(()=>l.track("Scrolled",{percent:t}))}),g.push(()=>null==j?void 0:j.destroy())),f.spa){const t=function(){if("undefined"==typeof window||"undefined"==typeof history)return()=>{};let t=i();function n(){try{const n=i();if(n===t)return;t=n,(t=>{a(()=>l.page()),null==j||j.reset(),k(t)})(n)}catch(t){}}const e=history.pushState,r=history.replaceState,c=!0===e[o],u=!0===r[o];if(!c){function f(...t){e.apply(this,t),n()}f[o]=!0,history.pushState=f}if(!u){function h(...t){r.apply(this,t),n()}h[o]=!0,history.replaceState=h}const s=()=>n(),d=()=>n();try{window.addEventListener("popstate",s),window.addEventListener("hashchange",d)}catch(y){}return()=>{try{window.removeEventListener("popstate",s),window.removeEventListener("hashchange",d)}catch(t){}try{!c&&history.pushState[o]&&(history.pushState=e),!u&&history.replaceState[o]&&(history.replaceState=r)}catch(t){}}}();g.push(t)}return k(),()=>{for(;g.length>0;){const t=g.pop();try{null==t||t()}catch(t){}}x=null}}({page(t,n){return q(t,n)},track(t,n){return N(t,n)}},m.autotrack))}catch(t){}},pageview:$,page:q,screen(t,n){try{const e=U();if(!e)return T("screen");const r=((t,n,e)=>{const r=C("screen",e);return t&&(r.name=t),r.properties=null!=n?n:{},r})(t,n,e);M().enqueue(r)}catch(t){}},track:N,identify(t,n){try{if("string"!=typeof t||0===t.length)return;((t,n,e={})=>{var r,o;if(t&&(k(J,t,{domain:e.cookieDomain,maxAgeDays:null!==(r=e.cookieMaxAgeDays)&&void 0!==r?r:730,sameSite:"Lax"}),S(J,t),null!=n)){const t=null!==(o=(()=>{const t=O(V);if(!t)return null;try{const n=JSON.parse(t);return n&&"object"==typeof n&&!Array.isArray(n)?n:null}catch(t){return null}})())&&void 0!==o?o:{},e=Object.assign(Object.assign({},t),n);try{S(V,JSON.stringify(e))}catch(t){}}})(t,n,{cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays});const e=U();if(!e)return T("identify");const r=((t,n,e)=>{var r;const o=C("identify",e);return t&&(o.userId=t),o.traits=null!==(r=n)&&void 0!==r?r:{},o})(t,n,e);M().enqueue(r)}catch(t){}},alias(t,n){try{if("string"!=typeof t||0===t.length)return;const e=U();if(!e)return T("alias");const r=((t,n,e)=>{const r=C("alias",e);return t&&(r.userId=t),r.previousId=null!=n?n:e.anonymousId,r})(t,n,e);M().enqueue(r)}catch(t){}},group(t,n){try{if("string"!=typeof t||0===t.length)return;const e=U();if(!e)return T("group");const r=((t,n,e)=>{const r=C("group",e);return r.groupId=t,r.traits=null!=n?n:{},r})(t,n,e);M().enqueue(r)}catch(t){}},reset(){try{((t={})=>{((t,n={})=>{var e;if(!g)return!1;try{let r=`${encodeURIComponent(t)}=`;return r+=`; path=${null!==(e=n.path)&&void 0!==e?e:"/"}`,n.domain&&(r+=`; domain=${n.domain}`),r+="; max-age=0",r+="; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=r,null===x(t)}catch(t){return!1}})(J,{domain:t.cookieDomain}),_(J),_(V)})({cookieDomain:m.cookieDomain}),((t={})=>{I(p(),t)})({cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays}),((t={})=>{R({id:p(),lastActivity:Date.now()},t)})({cookieDomain:m.cookieDomain,timeoutMinutes:m.sessionTimeoutMinutes})}catch(t){}},flush(){try{w&&w.flush()}catch(t){}},consent(t){try{b=Object.assign(Object.assign({},b),t)}catch(t){}},set(t){try{m=P(t),t.consent&&(b=Object.assign(Object.assign({},b),t.consent)),w&&w.setConfig({endpoint:m.endpoint,batchPath:m.batchPath,singlePath:m.singlePath,useBatch:m.useBatch,contentType:m.contentType,flushAt:m.flushAt,flushInterval:m.flushInterval,debug:m.debug})}catch(t){}},destroy(){if(A){try{A()}catch(t){}A=null}w&&w.destroy(),w=null,j=!1,v=null,m=Object.assign({},E),b=Object.assign({},H)}}})();"undefined"!=typeof window&&(t=>{if("undefined"==typeof window)return()=>{};const n=window.cnvQ;if(n&&n.__loaded)return()=>{};const e=window.cnvQ=null!=n?n:[],r=e.splice(0,e.length);e.push.bind(e),e.push=(...n)=>{for(const e of n)F(t,e);return e.length},e.__loaded=!0;for(const n of r)F(t,n)})(Q);const W=Object.assign(Q,{version:G,name:K,schemaVersion:M});export{K as NAME,M as SCHEMA_VERSION,G as VERSION,W as default};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @cnv-vn/track v0.2.0-alpha.2
|
|
3
|
+
* (c) 2026 CNV
|
|
4
|
+
* Released under the UNLICENSED license.
|
|
5
|
+
*/
|
|
6
|
+
!function(t){const n=["pdf","doc","docx","xls","xlsx","ppt","pptx","txt","csv","rtf","zip","rar","7z","tar","gz","mp3","mp4","mov","wav","avi","mkv","dmg","exe","pkg","apk","msi","svg"];function e(t){return!!t&&"object"==typeof t&&1===t.nodeType&&"A"===t.tagName}function r(t){const n=t.lastIndexOf("/");return-1===n?t:t.slice(n+1)}function o(t){var n,e,r;let o=(null!==(n=t.textContent)&&void 0!==n?n:"").trim();return o||(o=(null!==(e=t.getAttribute("aria-label"))&&void 0!==e?e:"").trim()),o||(o=(null!==(r=t.getAttribute("title"))&&void 0!==r?r:"").trim()),o.length>200?o.slice(0,200):o}const i="__cnvPatched";function c(){try{return"undefined"!=typeof location?location.href:""}catch(t){return""}}const u=["q","s","query","search","keyword"],s={spa:!0,outboundClicks:!0,scroll:!0,downloads:!0,siteSearch:!0},a={spa:!1,outboundClicks:!1,scroll:!1,downloads:!1,siteSearch:!1};function l(t){try{t()}catch(t){}}const d=["gclid","fbclid","ttclid","msclkid","dclid"],f=[["utm_source","source"],["utm_medium","medium"],["utm_campaign","name"],["utm_term","term"],["utm_content","content"],["utm_id","id"]];function h(t,n){try{return t()}catch(t){return n}}function y(){const t=h(()=>window.location.href,"");let n="",e="",r="";if(t)try{const o=new URL(t);n=o.pathname,e=o.search,r=o.hash}catch(t){}return{url:t,path:n,search:e,hash:r,title:h(()=>document.title,""),referrer:h(()=>document.referrer,"")}}function v(t){var n,e;let r="@cnv-vn/track",o="0.0.0-dev";try{r="@cnv-vn/track"}catch(t){}try{o="0.2.0-alpha.2"}catch(t){}return{name:null!==(n=null==t?void 0:t.name)&&void 0!==n?n:r,version:null!==(e=null==t?void 0:t.version)&&void 0!==e?e:o}}const m="0123456789abcdef";function p(t){return m[t>>>4&15]+m[15&t]}function b(){if("undefined"!=typeof crypto){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();if("function"==typeof crypto.getRandomValues){const t=new Uint8Array(16);return crypto.getRandomValues(t),t[6]=15&t[6]|64,t[8]=63&t[8]|128,p(t[0])+p(t[1])+p(t[2])+p(t[3])+"-"+p(t[4])+p(t[5])+"-"+p(t[6])+p(t[7])+"-"+p(t[8])+p(t[9])+"-"+p(t[10])+p(t[11])+p(t[12])+p(t[13])+p(t[14])+p(t[15])}}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const n=16*Math.random()|0;return("x"===t?n:3&n|8).toString(16)})}const w=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function g(t){return"string"==typeof t&&w.test(t)}const x="undefined"!=typeof window&&"undefined"!=typeof document;function k(t){if(!x)return null;try{const n=document.cookie;if(!n)return null;const e=encodeURIComponent(t),r=n.split(";");for(let n=0;n<r.length;n++){const o=r[n].indexOf("=");if(o<0)continue;const i=r[n].slice(0,o).trim();if(i===e||i===t)try{return decodeURIComponent(r[n].slice(o+1))}catch(t){return r[n].slice(o+1)}}return null}catch(t){return null}}function j(t,n,e={}){var r,o,i;if(!x)return!1;try{let c=`${encodeURIComponent(t)}=${encodeURIComponent(n)}`;if(c+=`; path=${null!==(r=e.path)&&void 0!==r?r:"/"}`,e.domain&&(c+=`; domain=${e.domain}`),"number"==typeof e.maxAgeDays){const t=Math.floor(86400*e.maxAgeDays);c+=`; max-age=${t}`,c+=`; expires=${new Date(Date.now()+1e3*t).toUTCString()}`}return c+=`; SameSite=${null!==(o=e.sameSite)&&void 0!==o?o:"Lax"}`,(null!==(i=e.secure)&&void 0!==i?i:"undefined"!=typeof location&&"https:"===location.protocol)&&(c+="; Secure"),document.cookie=c,k(t)===n}catch(t){return!1}}function O(){if(!x)return!1;try{const t="__cnv_probe__";return localStorage.setItem(t,"1"),localStorage.removeItem(t),!0}catch(t){return!1}}function _(t){if(!O())return null;try{return localStorage.getItem(t)}catch(t){return null}}function S(t,n){if(!O())return!1;try{return localStorage.setItem(t,n),!0}catch(t){return!1}}function A(t){if(!O())return!1;try{return localStorage.removeItem(t),!0}catch(t){return!1}}function P(){if(!x)return!1;try{const t="__cnv_probe__";return sessionStorage.setItem(t,"1"),sessionStorage.removeItem(t),!0}catch(t){return!1}}const D="_cnv_cid";function I(t={}){const n=k(D),e=_(D);let r=null;return g(n)?r=n:g(e)&&(r=e),r||(r=b()),M(r,t),r}function M(t,n={}){var e;j(D,t,{domain:n.cookieDomain,maxAgeDays:null!==(e=n.cookieMaxAgeDays)&&void 0!==e?e:730,sameSite:"Lax"}),S(D,t)}const U="1.0";function C(){return(new Date).toISOString()}function T(t,n){var e;const r=null!==(e=n.timestamp)&&void 0!==e?e:C(),o=Object.assign(Object.assign({},n.context),{projectId:n.projectId,schemaVersion:U}),i={type:t,messageId:b(),anonymousId:n.anonymousId,timestamp:r,originalTimestamp:r,context:o};return n.userId&&(i.userId=n.userId),n.sessionId&&(i.sessionId=n.sessionId),n.integrations&&(i.integrations=n.integrations),i}const $="_cnv_sid";function L(t){if(!t)return null;const n=t.lastIndexOf(".");if(n<0)return null;const e=t.slice(0,n),r=Number(t.slice(n+1));return!g(e)||!Number.isFinite(r)||r<=0?null:{id:e,lastActivity:r}}function R(t={}){const n=Date.now(),e=(t=>{var n;return 6e4*(null!==(n=t.timeoutMinutes)&&void 0!==n?n:30)})(t),r=L(k($)),o=L((t=>{if(!P())return null;try{return sessionStorage.getItem(t)}catch(t){return null}})($));let i=null;for(const t of[r,o])t&&(n-t.lastActivity>e||(!i||t.lastActivity>i.lastActivity)&&(i=t));const c=i?{id:i.id,lastActivity:n}:{id:b(),lastActivity:n};return q(c,t),c.id}function q(t,n={}){var e;const r=(t=>`${t.id}.${t.lastActivity}`)(t),o=null!==(e=n.timeoutMinutes)&&void 0!==e?e:30;j($,r,{domain:n.cookieDomain,maxAgeDays:o/1440,sameSite:"Lax"}),((t,n)=>{if(!P())return!1;try{return sessionStorage.setItem(t,n),!0}catch(t){return!1}})($,r)}class N{constructor(t){var n,e;Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"storageKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"maxMessages",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"flushing",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"onlineHandler",{enumerable:!0,configurable:!0,writable:!0,value:null}),this.storageKey=null!==(n=t.storageKey)&&void 0!==n?n:"_cnv_rq",this.maxMessages=null!==(e=t.maxMessages)&&void 0!==e?e:100,this.installOnlineListener()}stash(t){if(t&&0!==t.length)try{const n=this.read();for(n.push(t);n.length>1&&B(n)>this.maxMessages;)n.shift();this.write(n),this.config.debug&&console.log("[cnv-track] retry queued",t.length,"buffered",B(n))}catch(t){}}flush(){if(this.flushing||"undefined"!=typeof navigator&&!1===navigator.onLine)return Promise.resolve();const t=this.read();if(0===t.length)return Promise.resolve();this.flushing=!0,this.write([]);const n=()=>{this.flushing=!1};return t.reduce((t,n)=>t.then(()=>this.config.resend(n)),Promise.resolve()).then(n,n)}size(){return B(this.read())}clear(){try{A(this.storageKey)}catch(t){}}destroy(){if(this.onlineHandler){try{window.removeEventListener("online",this.onlineHandler)}catch(t){}this.onlineHandler=null}}installOnlineListener(){if("undefined"==typeof window)return;const t=()=>{this.flush()};try{window.addEventListener("online",t),this.onlineHandler=t}catch(t){}}read(){const t=_(this.storageKey);if(!t)return[];try{const n=JSON.parse(t);return Array.isArray(n)?n.filter(t=>Array.isArray(t)&&t.length>0):[]}catch(t){return[]}}write(t){0!==t.length?S(this.storageKey,JSON.stringify(t)):A(this.storageKey)}}function B(t){let n=0;for(const e of t)n+=e.length;return n}class z{constructor(t){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"buffer",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"timer",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"destroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"handlers",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"retryQueue",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.retryQueue=new N({debug:t.debug,resend:t=>this.send(t)}),this.installPageLifecycleHandlers(),this.retryQueue.flush()}setConfig(t){this.config=Object.assign(Object.assign({},this.config),t)}enqueue(t){this.destroyed||(this.buffer.push(t),this.buffer.length>=this.config.flushAt?this.flush():this.timer||(this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.config.flushInterval)))}flush(){if(this.timer&&(clearTimeout(this.timer),this.timer=null),0===this.buffer.length)return Promise.resolve();const t=this.buffer;return this.buffer=[],this.send(t)}bufferedCount(){return this.buffer.length}destroy(){this.destroyed||(this.destroyed=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.uninstallPageLifecycleHandlers(),this.retryQueue.destroy())}send(t){try{return this.config.useBatch?this.sendBatch(t):Promise.all(t.map(t=>this.sendSingle(t))).then(()=>{})}catch(t){return this.config.debug&&console.warn("[cnv-track] send failed synchronously; dropped batch",t),Promise.resolve()}}sendBatch(t){var n;const e=C(),r={batch:t.map(t=>J(t,e)),context:Object.assign(Object.assign({},null!==(n=this.config.batchContext)&&void 0!==n?n:{}),{projectId:this.config.projectId,sentAt:e}),sentAt:e};return this.post(this.batchUrl(),r,t)}sendSingle(t){const n=J(t,C());return this.post(this.singleUrl(t.type),n,[t])}batchUrl(){return this.config.endpoint+this.config.batchPath+"?projectId="+encodeURIComponent(this.config.projectId)}singleUrl(t){const n=this.config.singlePath.replace("{type}",t);return this.config.endpoint+n+"?projectId="+encodeURIComponent(this.config.projectId)}post(t,n,e){const r=JSON.stringify(n),o=((t,n)=>{try{return new Blob([t],{type:n})}catch(t){return null}})(r,this.config.contentType),i=o?o.size:(t=>{try{return new Blob([t]).size}catch(n){return t.length}})(r),c=i>65536?null:e;return c&&"undefined"!=typeof navigator&&!1===navigator.onLine?(this.retryQueue.stash(c),this.config.debug&&console.log("[cnv-track] offline; queued for retry",t),Promise.resolve()):o&&this.sendViaBeacon(t,o)?(this.config.debug&&console.log("[cnv-track] beacon",t),Promise.resolve()):this.sendViaFetch(t,r,c)}sendViaBeacon(t,n){if("undefined"==typeof navigator||"function"!=typeof navigator.sendBeacon)return!1;if(n.size>65536)return!1;try{return navigator.sendBeacon(t,n)}catch(t){return!1}}sendViaFetch(t,n,e){return"function"!=typeof fetch?(this.config.debug&&console.warn("[cnv-track] no transport available; queued for retry"),e&&this.retryQueue.stash(e),Promise.resolve()):fetch(t,{method:"POST",headers:{"Content-Type":this.config.contentType},body:n,keepalive:!0,credentials:"omit",mode:"cors"}).then(n=>{this.config.debug&&console.log("[cnv-track] fetch flushed",t,n.status)}).catch(n=>{this.config.debug&&console.warn("[cnv-track] flush failed; queued for retry",t,n),e&&this.retryQueue.stash(e)})}installPageLifecycleHandlers(){if("undefined"==typeof window||"undefined"==typeof document)return;const t=()=>{this.flush()},n=()=>{"hidden"===document.visibilityState&&this.flush()};try{window.addEventListener("pagehide",t),document.addEventListener("visibilitychange",n),this.handlers={pagehide:t,visibility:n}}catch(t){}}uninstallPageLifecycleHandlers(){if(this.handlers){try{window.removeEventListener("pagehide",this.handlers.pagehide),document.removeEventListener("visibilitychange",this.handlers.visibility)}catch(t){}this.handlers=null}}}function J(t,n){return Object.assign(Object.assign({},t),{sentAt:n})}const V="_cnv_uid",E="_cnv_traits",H={endpoint:"http://localhost:8090",batchPath:"/segment/batch",singlePath:"/segment/{type}",useBatch:!0,contentType:"text/plain",debug:!1,allowAdTracking:!0,cookieMaxAgeDays:730,sessionTimeoutMinutes:30,flushAt:20,flushInterval:5e3,autoPageview:!0,autotrack:!1,integrations:{}},F={analytics:!0,marketing:!0};function G(t,n){if(!Array.isArray(n)||0===n.length)return;const e=n;try{switch(e[0]){case"init":t.init(e[1],e[2]);break;case"pageview":t.pageview(e[1],e[2]);break;case"page":t.page(e[1],e[2]);break;case"screen":t.screen(e[1],e[2]);break;case"track":t.track(e[1],e[2]);break;case"identify":t.identify(e[1],e[2]);break;case"alias":t.alias(e[1],e[2]);break;case"group":t.group(e[1],e[2]);break;case"reset":t.reset();break;case"flush":t.flush();break;case"consent":t.consent(e[1]);break;case"set":t.set(e[1])}}catch(t){}}const K="0.2.0-alpha.2",Q="@cnv-vn/track",W=(()=>{let t=null,m=Object.assign({},H),p=Object.assign({},F),w=null,g=!1,O=null;function P(t){const n=Object.assign({},m);return t?(void 0!==t.endpoint&&(n.endpoint=(e=t.endpoint).endsWith("/")?e.slice(0,-1):e),void 0!==t.batchPath&&(n.batchPath=t.batchPath),void 0!==t.singlePath&&(n.singlePath=t.singlePath),void 0!==t.useBatch&&(n.useBatch=t.useBatch),void 0!==t.contentType&&(n.contentType=t.contentType),void 0!==t.debug&&(n.debug=t.debug),void 0!==t.allowAdTracking&&(n.allowAdTracking=t.allowAdTracking),void 0!==t.cookieDomain&&(n.cookieDomain=t.cookieDomain),void 0!==t.cookieMaxAgeDays&&(n.cookieMaxAgeDays=t.cookieMaxAgeDays),void 0!==t.sessionTimeoutMinutes&&(n.sessionTimeoutMinutes=t.sessionTimeoutMinutes),void 0!==t.flushAt&&(n.flushAt=t.flushAt),void 0!==t.flushInterval&&(n.flushInterval=t.flushInterval),void 0!==t.autoPageview&&(n.autoPageview=t.autoPageview),void 0!==t.autotrack&&(n.autotrack=t.autotrack),void 0!==t.integrations&&(n.integrations=t.integrations),n):n;var e}function D(){return w||(w=new z({endpoint:m.endpoint,batchPath:m.batchPath,singlePath:m.singlePath,useBatch:m.useBatch,contentType:m.contentType,flushAt:m.flushAt,flushInterval:m.flushInterval,projectId:null!=t?t:"",debug:m.debug}),w)}function U(){var n;if(!t)return null;const e=((t={})=>{var n;const e={page:y(),screen:{width:h(()=>window.screen.width,0),height:h(()=>window.screen.height,0),density:h(()=>window.devicePixelRatio||1,1)},locale:h(()=>navigator.language||"",""),timezone:h(()=>Intl.DateTimeFormat().resolvedOptions().timeZone||"",""),userAgent:h(()=>navigator.userAgent||"",""),library:v(t.library)},r=((t,n=!0)=>{const e=h(()=>window.location.search,"");if(!e)return;let r;try{r=new URLSearchParams(e)}catch(t){return}const o={};let i=!1;for(const[t,n]of f){const e=r.get(t);e&&e.length>0&&(o[n]=e,i=!0)}if(n){const t={};for(const n of d){const e=r.get(n);e&&e.length>0&&(t[n]=e)}Object.keys(t).length>0&&(o.clickIds=t,i=!0)}return i?o:void 0})(0,null===(n=t.allowAdTracking)||void 0===n||n);return r&&(e.campaign=r),e})({allowAdTracking:m.allowAdTracking});return p.analytics&&p.marketing||(e.consent=Object.assign({},p)),{anonymousId:I({cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays}),sessionId:R({cookieDomain:m.cookieDomain,timeoutMinutes:m.sessionTimeoutMinutes}),userId:null!==(n=k(V)||_(V))&&void 0!==n?n:void 0,context:e,projectId:t,integrations:Object.keys(m.integrations).length>0?m.integrations:void 0}}function C(t){m.debug&&console.warn(`[cnv-track] ${t}() called before init()`)}function $(t,n){try{const e=U();if(!e)return C("pageview");const r=((t,n,e)=>{const r=T("page",e);return t&&(r.name=t),r.properties=null!=n?n:{},r})(null!=t?t:(()=>{try{return"undefined"!=typeof document&&document.title||void 0}catch(t){return}})(),n,e);D().enqueue(r)}catch(t){}}function L(t,n){$(t,n)}function N(t,n){try{if("string"!=typeof t||0===t.length)return;const e=U();if(!e)return C("track");const r=((t,n,e)=>{const r=T("track",e);return r.event=t,r.properties=null!=n?n:{},r})(t,n,e);D().enqueue(r)}catch(t){}}return{get initialized(){return g},get projectId(){return t},init(d,f){try{if(!d||"string"!=typeof d)return;if(m=P(f),(null==f?void 0:f.consent)&&(p=Object.assign(Object.assign({},p),f.consent)),t=d,w&&w.destroy(),w=null,D(),g=!0,I({cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays}),R({cookieDomain:m.cookieDomain,timeoutMinutes:m.sessionTimeoutMinutes}),m.autoPageview&&$(),O){try{O()}catch(t){}O=null}m.autotrack&&(O=function(t,d){const f=void 0===(h=d)||!1===h?Object.assign(Object.assign({},a),{searchParams:u}):!0===h?Object.assign(Object.assign({},s),{searchParams:u}):{spa:null===(y=h.spa)||void 0===y||y,outboundClicks:null===(v=h.outboundClicks)||void 0===v||v,scroll:null===(m=h.scroll)||void 0===m||m,downloads:null===(p=h.downloads)||void 0===p||p,siteSearch:null===(b=h.siteSearch)||void 0===b||b,downloadExtensions:h.downloadExtensions,searchParams:null!==(w=h.searchParams)&&void 0!==w?w:u};var h,y,v,m,p,b,w;const g=[];let x=null;function k(n){if(f.siteSearch)try{const e=null!=n?n:(()=>{try{return"undefined"!=typeof location?location.href:""}catch(t){return""}})();if(e===x)return;x=e;const r=((t,n=u)=>{try{if(!t){if("undefined"==typeof location)return null;t=location.href}const e=t instanceof URL?t:new URL(t,(()=>{try{return"undefined"!=typeof location?location.href:"http://localhost/"}catch(t){return"http://localhost/"}})());if(!e.search)return null;for(const t of n){const n=e.searchParams.get(t);if(null===n)continue;const r=n.trim();if(0!==r.length)return{query:r,param:t}}return null}catch(t){return null}})(e,f.searchParams);r&&l(()=>t.track("Site Searched",{query:r.query,param:r.param}))}catch(t){}}if(f.outboundClicks||f.downloads){const i=(t=>{var i;if("undefined"==typeof document)return()=>{};const c=new Set((null!==(i=t.downloadExtensions)&&void 0!==i?i:n).map(t=>t.toLowerCase())),u=!!t.onOutbound,s=!!t.onDownload;if(!u&&!s)return()=>{};function a(n){var i,a;try{if(0!==n.button)return;if(n.defaultPrevented)return;const l=(t=>{const n="function"==typeof t.composedPath?t.composedPath():[];if(n.length>0)for(const t of n)if(e(t))return t;let r=t.target;for(;r;){if(e(r))return r;r=r.parentNode}return null})(n);if(!l)return;const d=l.getAttribute("href");if(!d)return;if(/^(javascript:|mailto:|tel:|sms:)/i.test(d))return;if(d.startsWith("#"))return;const f=(t=>{try{return new URL(t,location.href)}catch(t){return null}})(d);if(!f)return;let h=!1;if(s){const n=l.hasAttribute("download"),e=(t=>{const n=r(t),e=n.lastIndexOf(".");return e<=0||e===n.length-1?null:n.slice(e+1).toLowerCase()})(f.pathname);(n||e&&c.has(e))&&(h=!0,null===(i=t.onDownload)||void 0===i||i.call(t,{url:f.href,fileName:r(f.pathname),fileExtension:null!=e?e:"",text:o(l)}))}u&&!h&&f.host!==(()=>{try{return"undefined"!=typeof location?location.hostname:""}catch(t){return""}})()&&f.host&&(null===(a=t.onOutbound)||void 0===a||a.call(t,{url:f.href,host:f.host,text:o(l),target:l.target||"_self"}))}catch(t){}}try{document.addEventListener("click",a,!0)}catch(t){return()=>{}}return()=>{try{document.removeEventListener("click",a,!0)}catch(t){}}})({downloadExtensions:f.downloadExtensions,onOutbound:f.outboundClicks?n=>l(()=>t.track("Outbound Link Clicked",n)):void 0,onDownload:f.downloads?n=>l(()=>t.track("File Downloaded",n)):void 0});g.push(i)}let j=null;if(f.scroll&&(j=(t=>{if("undefined"==typeof window||"undefined"==typeof document)return{destroy(){},reset(){}};let n=!1,e=null,r=!1;function o(){if(e=null,!r&&!n)try{const e=document.documentElement,r=document.body,o=Math.max(e?e.scrollHeight:0,r?r.scrollHeight:0),i=Math.max(e?e.clientHeight:0,window.innerHeight||0);if(o<=i)return n=!0,c(),void t(90);((window.scrollY||(e?e.scrollTop:0)||0)+i)/o>=.9&&(n=!0,c(),t(90))}catch(t){}}function i(){if(null===e)try{e=window.requestAnimationFrame(o)}catch(t){o()}}function c(){try{window.removeEventListener("scroll",i),null!==e&&window.cancelAnimationFrame(e),e=null}catch(t){}}try{window.addEventListener("scroll",i,{passive:!0})}catch(t){try{window.addEventListener("scroll",i)}catch(t){}}try{window.requestAnimationFrame(o)}catch(t){o()}return{destroy(){r=!0,c()},reset(){if(!r){n=!1;try{window.addEventListener("scroll",i,{passive:!0})}catch(t){try{window.addEventListener("scroll",i)}catch(t){}}try{window.requestAnimationFrame(o)}catch(t){o()}}}}})(n=>{l(()=>t.track("Scrolled",{percent:n}))}),g.push(()=>null==j?void 0:j.destroy())),f.spa){const n=function(){if("undefined"==typeof window||"undefined"==typeof history)return()=>{};let n=c();function e(){try{const e=c();if(e===n)return;n=e,(n=>{l(()=>t.page()),null==j||j.reset(),k(n)})(e)}catch(t){}}const r=history.pushState,o=history.replaceState,u=!0===r[i],s=!0===o[i];if(!u){function f(...t){r.apply(this,t),e()}f[i]=!0,history.pushState=f}if(!s){function h(...t){o.apply(this,t),e()}h[i]=!0,history.replaceState=h}const a=()=>e(),d=()=>e();try{window.addEventListener("popstate",a),window.addEventListener("hashchange",d)}catch(y){}return()=>{try{window.removeEventListener("popstate",a),window.removeEventListener("hashchange",d)}catch(t){}try{!u&&history.pushState[i]&&(history.pushState=r),!s&&history.replaceState[i]&&(history.replaceState=o)}catch(t){}}}();g.push(n)}return k(),()=>{for(;g.length>0;){const t=g.pop();try{null==t||t()}catch(t){}}x=null}}({page(t,n){return L(t,n)},track(t,n){return N(t,n)}},m.autotrack))}catch(t){}},pageview:$,page:L,screen(t,n){try{const e=U();if(!e)return C("screen");const r=((t,n,e)=>{const r=T("screen",e);return t&&(r.name=t),r.properties=null!=n?n:{},r})(t,n,e);D().enqueue(r)}catch(t){}},track:N,identify(t,n){try{if("string"!=typeof t||0===t.length)return;((t,n,e={})=>{var r,o;if(t&&(j(V,t,{domain:e.cookieDomain,maxAgeDays:null!==(r=e.cookieMaxAgeDays)&&void 0!==r?r:730,sameSite:"Lax"}),S(V,t),null!=n)){const t=null!==(o=(()=>{const t=_(E);if(!t)return null;try{const n=JSON.parse(t);return n&&"object"==typeof n&&!Array.isArray(n)?n:null}catch(t){return null}})())&&void 0!==o?o:{},e=Object.assign(Object.assign({},t),n);try{S(E,JSON.stringify(e))}catch(t){}}})(t,n,{cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays});const e=U();if(!e)return C("identify");const r=((t,n,e)=>{var r;const o=T("identify",e);return t&&(o.userId=t),o.traits=null!==(r=n)&&void 0!==r?r:{},o})(t,n,e);D().enqueue(r)}catch(t){}},alias(t,n){try{if("string"!=typeof t||0===t.length)return;const e=U();if(!e)return C("alias");const r=((t,n,e)=>{const r=T("alias",e);return t&&(r.userId=t),r.previousId=null!=n?n:e.anonymousId,r})(t,n,e);D().enqueue(r)}catch(t){}},group(t,n){try{if("string"!=typeof t||0===t.length)return;const e=U();if(!e)return C("group");const r=((t,n,e)=>{const r=T("group",e);return r.groupId=t,r.traits=null!=n?n:{},r})(t,n,e);D().enqueue(r)}catch(t){}},reset(){try{((t={})=>{((t,n={})=>{var e;if(!x)return!1;try{let r=`${encodeURIComponent(t)}=`;return r+=`; path=${null!==(e=n.path)&&void 0!==e?e:"/"}`,n.domain&&(r+=`; domain=${n.domain}`),r+="; max-age=0",r+="; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=r,null===k(t)}catch(t){return!1}})(V,{domain:t.cookieDomain}),A(V),A(E)})({cookieDomain:m.cookieDomain}),((t={})=>{M(b(),t)})({cookieDomain:m.cookieDomain,cookieMaxAgeDays:m.cookieMaxAgeDays}),((t={})=>{q({id:b(),lastActivity:Date.now()},t)})({cookieDomain:m.cookieDomain,timeoutMinutes:m.sessionTimeoutMinutes})}catch(t){}},flush(){try{w&&w.flush()}catch(t){}},consent(t){try{p=Object.assign(Object.assign({},p),t)}catch(t){}},set(t){try{m=P(t),t.consent&&(p=Object.assign(Object.assign({},p),t.consent)),w&&w.setConfig({endpoint:m.endpoint,batchPath:m.batchPath,singlePath:m.singlePath,useBatch:m.useBatch,contentType:m.contentType,flushAt:m.flushAt,flushInterval:m.flushInterval,debug:m.debug})}catch(t){}},destroy(){if(O){try{O()}catch(t){}O=null}w&&w.destroy(),w=null,g=!1,t=null,m=Object.assign({},H),p=Object.assign({},F)}}})();"undefined"!=typeof window&&(t=>{if("undefined"==typeof window)return()=>{};const n=window.cnvQ;if(n&&n.__loaded)return()=>{};const e=window.cnvQ=null!=n?n:[],r=e.splice(0,e.length);e.push.bind(e),e.push=(...n)=>{for(const e of n)G(t,e);return e.length},e.__loaded=!0;for(const n of r)G(t,n)})(W);const X=Object.assign(W,{version:K,name:Q,schemaVersion:U});t.NAME=Q,t.SCHEMA_VERSION=U,t.VERSION=K,t.default=X,Object.defineProperty(t,"t",{value:!0})}(this.cnvTrack=this.cnvTrack||{});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @cnv-vn/track v0.2.0-alpha.2
|
|
3
|
+
* (c) 2026 CNV
|
|
4
|
+
* Released under the UNLICENSED license.
|
|
5
|
+
*/
|
|
6
|
+
var t,n;t=this,n=function(t){const n=["pdf","doc","docx","xls","xlsx","ppt","pptx","txt","csv","rtf","zip","rar","7z","tar","gz","mp3","mp4","mov","wav","avi","mkv","dmg","exe","pkg","apk","msi","svg"];function e(t){return!!t&&"object"==typeof t&&1===t.nodeType&&"A"===t.tagName}function r(t){const n=t.lastIndexOf("/");return-1===n?t:t.slice(n+1)}function o(t){var n,e,r;let o=(null!==(n=t.textContent)&&void 0!==n?n:"").trim();return o||(o=(null!==(e=t.getAttribute("aria-label"))&&void 0!==e?e:"").trim()),o||(o=(null!==(r=t.getAttribute("title"))&&void 0!==r?r:"").trim()),o.length>200?o.slice(0,200):o}const i="__cnvPatched";function c(){try{return"undefined"!=typeof location?location.href:""}catch(t){return""}}const u=["q","s","query","search","keyword"],s={spa:!0,outboundClicks:!0,scroll:!0,downloads:!0,siteSearch:!0},a={spa:!1,outboundClicks:!1,scroll:!1,downloads:!1,siteSearch:!1};function l(t){try{t()}catch(t){}}const d=["gclid","fbclid","ttclid","msclkid","dclid"],f=[["utm_source","source"],["utm_medium","medium"],["utm_campaign","name"],["utm_term","term"],["utm_content","content"],["utm_id","id"]];function h(t,n){try{return t()}catch(t){return n}}function y(){const t=h(()=>window.location.href,"");let n="",e="",r="";if(t)try{const o=new URL(t);n=o.pathname,e=o.search,r=o.hash}catch(t){}return{url:t,path:n,search:e,hash:r,title:h(()=>document.title,""),referrer:h(()=>document.referrer,"")}}function v(t){var n,e;let r="@cnv-vn/track",o="0.0.0-dev";try{r="@cnv-vn/track"}catch(t){}try{o="0.2.0-alpha.2"}catch(t){}return{name:null!==(n=null==t?void 0:t.name)&&void 0!==n?n:r,version:null!==(e=null==t?void 0:t.version)&&void 0!==e?e:o}}const p="0123456789abcdef";function m(t){return p[t>>>4&15]+p[15&t]}function b(){if("undefined"!=typeof crypto){if("function"==typeof crypto.randomUUID)return crypto.randomUUID();if("function"==typeof crypto.getRandomValues){const t=new Uint8Array(16);return crypto.getRandomValues(t),t[6]=15&t[6]|64,t[8]=63&t[8]|128,m(t[0])+m(t[1])+m(t[2])+m(t[3])+"-"+m(t[4])+m(t[5])+"-"+m(t[6])+m(t[7])+"-"+m(t[8])+m(t[9])+"-"+m(t[10])+m(t[11])+m(t[12])+m(t[13])+m(t[14])+m(t[15])}}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const n=16*Math.random()|0;return("x"===t?n:3&n|8).toString(16)})}const w=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function g(t){return"string"==typeof t&&w.test(t)}const x="undefined"!=typeof window&&"undefined"!=typeof document;function k(t){if(!x)return null;try{const n=document.cookie;if(!n)return null;const e=encodeURIComponent(t),r=n.split(";");for(let n=0;n<r.length;n++){const o=r[n].indexOf("=");if(o<0)continue;const i=r[n].slice(0,o).trim();if(i===e||i===t)try{return decodeURIComponent(r[n].slice(o+1))}catch(t){return r[n].slice(o+1)}}return null}catch(t){return null}}function j(t,n,e={}){var r,o,i;if(!x)return!1;try{let c=`${encodeURIComponent(t)}=${encodeURIComponent(n)}`;if(c+=`; path=${null!==(r=e.path)&&void 0!==r?r:"/"}`,e.domain&&(c+=`; domain=${e.domain}`),"number"==typeof e.maxAgeDays){const t=Math.floor(86400*e.maxAgeDays);c+=`; max-age=${t}`,c+=`; expires=${new Date(Date.now()+1e3*t).toUTCString()}`}return c+=`; SameSite=${null!==(o=e.sameSite)&&void 0!==o?o:"Lax"}`,(null!==(i=e.secure)&&void 0!==i?i:"undefined"!=typeof location&&"https:"===location.protocol)&&(c+="; Secure"),document.cookie=c,k(t)===n}catch(t){return!1}}function O(){if(!x)return!1;try{const t="__cnv_probe__";return localStorage.setItem(t,"1"),localStorage.removeItem(t),!0}catch(t){return!1}}function _(t){if(!O())return null;try{return localStorage.getItem(t)}catch(t){return null}}function S(t,n){if(!O())return!1;try{return localStorage.setItem(t,n),!0}catch(t){return!1}}function A(t){if(!O())return!1;try{return localStorage.removeItem(t),!0}catch(t){return!1}}function P(){if(!x)return!1;try{const t="__cnv_probe__";return sessionStorage.setItem(t,"1"),sessionStorage.removeItem(t),!0}catch(t){return!1}}const D="_cnv_cid";function I(t={}){const n=k(D),e=_(D);let r=null;return g(n)?r=n:g(e)&&(r=e),r||(r=b()),M(r,t),r}function M(t,n={}){var e;j(D,t,{domain:n.cookieDomain,maxAgeDays:null!==(e=n.cookieMaxAgeDays)&&void 0!==e?e:730,sameSite:"Lax"}),S(D,t)}const T="1.0";function U(){return(new Date).toISOString()}function C(t,n){var e;const r=null!==(e=n.timestamp)&&void 0!==e?e:U(),o=Object.assign(Object.assign({},n.context),{projectId:n.projectId,schemaVersion:T}),i={type:t,messageId:b(),anonymousId:n.anonymousId,timestamp:r,originalTimestamp:r,context:o};return n.userId&&(i.userId=n.userId),n.sessionId&&(i.sessionId=n.sessionId),n.integrations&&(i.integrations=n.integrations),i}const $="_cnv_sid";function L(t){if(!t)return null;const n=t.lastIndexOf(".");if(n<0)return null;const e=t.slice(0,n),r=Number(t.slice(n+1));return!g(e)||!Number.isFinite(r)||r<=0?null:{id:e,lastActivity:r}}function R(t={}){const n=Date.now(),e=(t=>{var n;return 6e4*(null!==(n=t.timeoutMinutes)&&void 0!==n?n:30)})(t),r=L(k($)),o=L((t=>{if(!P())return null;try{return sessionStorage.getItem(t)}catch(t){return null}})($));let i=null;for(const t of[r,o])t&&(n-t.lastActivity>e||(!i||t.lastActivity>i.lastActivity)&&(i=t));const c=i?{id:i.id,lastActivity:n}:{id:b(),lastActivity:n};return q(c,t),c.id}function q(t,n={}){var e;const r=(t=>`${t.id}.${t.lastActivity}`)(t),o=null!==(e=n.timeoutMinutes)&&void 0!==e?e:30;j($,r,{domain:n.cookieDomain,maxAgeDays:o/1440,sameSite:"Lax"}),((t,n)=>{if(!P())return!1;try{return sessionStorage.setItem(t,n),!0}catch(t){return!1}})($,r)}class N{constructor(t){var n,e;Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"storageKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"maxMessages",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"flushing",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"onlineHandler",{enumerable:!0,configurable:!0,writable:!0,value:null}),this.storageKey=null!==(n=t.storageKey)&&void 0!==n?n:"_cnv_rq",this.maxMessages=null!==(e=t.maxMessages)&&void 0!==e?e:100,this.installOnlineListener()}stash(t){if(t&&0!==t.length)try{const n=this.read();for(n.push(t);n.length>1&&B(n)>this.maxMessages;)n.shift();this.write(n),this.config.debug&&console.log("[cnv-track] retry queued",t.length,"buffered",B(n))}catch(t){}}flush(){if(this.flushing||"undefined"!=typeof navigator&&!1===navigator.onLine)return Promise.resolve();const t=this.read();if(0===t.length)return Promise.resolve();this.flushing=!0,this.write([]);const n=()=>{this.flushing=!1};return t.reduce((t,n)=>t.then(()=>this.config.resend(n)),Promise.resolve()).then(n,n)}size(){return B(this.read())}clear(){try{A(this.storageKey)}catch(t){}}destroy(){if(this.onlineHandler){try{window.removeEventListener("online",this.onlineHandler)}catch(t){}this.onlineHandler=null}}installOnlineListener(){if("undefined"==typeof window)return;const t=()=>{this.flush()};try{window.addEventListener("online",t),this.onlineHandler=t}catch(t){}}read(){const t=_(this.storageKey);if(!t)return[];try{const n=JSON.parse(t);return Array.isArray(n)?n.filter(t=>Array.isArray(t)&&t.length>0):[]}catch(t){return[]}}write(t){0!==t.length?S(this.storageKey,JSON.stringify(t)):A(this.storageKey)}}function B(t){let n=0;for(const e of t)n+=e.length;return n}class z{constructor(t){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:t}),Object.defineProperty(this,"buffer",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"timer",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"destroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"handlers",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"retryQueue",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.retryQueue=new N({debug:t.debug,resend:t=>this.send(t)}),this.installPageLifecycleHandlers(),this.retryQueue.flush()}setConfig(t){this.config=Object.assign(Object.assign({},this.config),t)}enqueue(t){this.destroyed||(this.buffer.push(t),this.buffer.length>=this.config.flushAt?this.flush():this.timer||(this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.config.flushInterval)))}flush(){if(this.timer&&(clearTimeout(this.timer),this.timer=null),0===this.buffer.length)return Promise.resolve();const t=this.buffer;return this.buffer=[],this.send(t)}bufferedCount(){return this.buffer.length}destroy(){this.destroyed||(this.destroyed=!0,this.timer&&(clearTimeout(this.timer),this.timer=null),this.uninstallPageLifecycleHandlers(),this.retryQueue.destroy())}send(t){try{return this.config.useBatch?this.sendBatch(t):Promise.all(t.map(t=>this.sendSingle(t))).then(()=>{})}catch(t){return this.config.debug&&console.warn("[cnv-track] send failed synchronously; dropped batch",t),Promise.resolve()}}sendBatch(t){var n;const e=U(),r={batch:t.map(t=>J(t,e)),context:Object.assign(Object.assign({},null!==(n=this.config.batchContext)&&void 0!==n?n:{}),{projectId:this.config.projectId,sentAt:e}),sentAt:e};return this.post(this.batchUrl(),r,t)}sendSingle(t){const n=J(t,U());return this.post(this.singleUrl(t.type),n,[t])}batchUrl(){return this.config.endpoint+this.config.batchPath+"?projectId="+encodeURIComponent(this.config.projectId)}singleUrl(t){const n=this.config.singlePath.replace("{type}",t);return this.config.endpoint+n+"?projectId="+encodeURIComponent(this.config.projectId)}post(t,n,e){const r=JSON.stringify(n),o=((t,n)=>{try{return new Blob([t],{type:n})}catch(t){return null}})(r,this.config.contentType),i=o?o.size:(t=>{try{return new Blob([t]).size}catch(n){return t.length}})(r),c=i>65536?null:e;return c&&"undefined"!=typeof navigator&&!1===navigator.onLine?(this.retryQueue.stash(c),this.config.debug&&console.log("[cnv-track] offline; queued for retry",t),Promise.resolve()):o&&this.sendViaBeacon(t,o)?(this.config.debug&&console.log("[cnv-track] beacon",t),Promise.resolve()):this.sendViaFetch(t,r,c)}sendViaBeacon(t,n){if("undefined"==typeof navigator||"function"!=typeof navigator.sendBeacon)return!1;if(n.size>65536)return!1;try{return navigator.sendBeacon(t,n)}catch(t){return!1}}sendViaFetch(t,n,e){return"function"!=typeof fetch?(this.config.debug&&console.warn("[cnv-track] no transport available; queued for retry"),e&&this.retryQueue.stash(e),Promise.resolve()):fetch(t,{method:"POST",headers:{"Content-Type":this.config.contentType},body:n,keepalive:!0,credentials:"omit",mode:"cors"}).then(n=>{this.config.debug&&console.log("[cnv-track] fetch flushed",t,n.status)}).catch(n=>{this.config.debug&&console.warn("[cnv-track] flush failed; queued for retry",t,n),e&&this.retryQueue.stash(e)})}installPageLifecycleHandlers(){if("undefined"==typeof window||"undefined"==typeof document)return;const t=()=>{this.flush()},n=()=>{"hidden"===document.visibilityState&&this.flush()};try{window.addEventListener("pagehide",t),document.addEventListener("visibilitychange",n),this.handlers={pagehide:t,visibility:n}}catch(t){}}uninstallPageLifecycleHandlers(){if(this.handlers){try{window.removeEventListener("pagehide",this.handlers.pagehide),document.removeEventListener("visibilitychange",this.handlers.visibility)}catch(t){}this.handlers=null}}}function J(t,n){return Object.assign(Object.assign({},t),{sentAt:n})}const V="_cnv_uid",E="_cnv_traits",H={endpoint:"http://localhost:8090",batchPath:"/segment/batch",singlePath:"/segment/{type}",useBatch:!0,contentType:"text/plain",debug:!1,allowAdTracking:!0,cookieMaxAgeDays:730,sessionTimeoutMinutes:30,flushAt:20,flushInterval:5e3,autoPageview:!0,autotrack:!1,integrations:{}},F={analytics:!0,marketing:!0};function G(t,n){if(!Array.isArray(n)||0===n.length)return;const e=n;try{switch(e[0]){case"init":t.init(e[1],e[2]);break;case"pageview":t.pageview(e[1],e[2]);break;case"page":t.page(e[1],e[2]);break;case"screen":t.screen(e[1],e[2]);break;case"track":t.track(e[1],e[2]);break;case"identify":t.identify(e[1],e[2]);break;case"alias":t.alias(e[1],e[2]);break;case"group":t.group(e[1],e[2]);break;case"reset":t.reset();break;case"flush":t.flush();break;case"consent":t.consent(e[1]);break;case"set":t.set(e[1])}}catch(t){}}const K="0.2.0-alpha.2",Q="@cnv-vn/track",W=(()=>{let t=null,p=Object.assign({},H),m=Object.assign({},F),w=null,g=!1,O=null;function P(t){const n=Object.assign({},p);return t?(void 0!==t.endpoint&&(n.endpoint=(e=t.endpoint).endsWith("/")?e.slice(0,-1):e),void 0!==t.batchPath&&(n.batchPath=t.batchPath),void 0!==t.singlePath&&(n.singlePath=t.singlePath),void 0!==t.useBatch&&(n.useBatch=t.useBatch),void 0!==t.contentType&&(n.contentType=t.contentType),void 0!==t.debug&&(n.debug=t.debug),void 0!==t.allowAdTracking&&(n.allowAdTracking=t.allowAdTracking),void 0!==t.cookieDomain&&(n.cookieDomain=t.cookieDomain),void 0!==t.cookieMaxAgeDays&&(n.cookieMaxAgeDays=t.cookieMaxAgeDays),void 0!==t.sessionTimeoutMinutes&&(n.sessionTimeoutMinutes=t.sessionTimeoutMinutes),void 0!==t.flushAt&&(n.flushAt=t.flushAt),void 0!==t.flushInterval&&(n.flushInterval=t.flushInterval),void 0!==t.autoPageview&&(n.autoPageview=t.autoPageview),void 0!==t.autotrack&&(n.autotrack=t.autotrack),void 0!==t.integrations&&(n.integrations=t.integrations),n):n;var e}function D(){return w||(w=new z({endpoint:p.endpoint,batchPath:p.batchPath,singlePath:p.singlePath,useBatch:p.useBatch,contentType:p.contentType,flushAt:p.flushAt,flushInterval:p.flushInterval,projectId:null!=t?t:"",debug:p.debug}),w)}function T(){var n;if(!t)return null;const e=((t={})=>{var n;const e={page:y(),screen:{width:h(()=>window.screen.width,0),height:h(()=>window.screen.height,0),density:h(()=>window.devicePixelRatio||1,1)},locale:h(()=>navigator.language||"",""),timezone:h(()=>Intl.DateTimeFormat().resolvedOptions().timeZone||"",""),userAgent:h(()=>navigator.userAgent||"",""),library:v(t.library)},r=((t,n=!0)=>{const e=h(()=>window.location.search,"");if(!e)return;let r;try{r=new URLSearchParams(e)}catch(t){return}const o={};let i=!1;for(const[t,n]of f){const e=r.get(t);e&&e.length>0&&(o[n]=e,i=!0)}if(n){const t={};for(const n of d){const e=r.get(n);e&&e.length>0&&(t[n]=e)}Object.keys(t).length>0&&(o.clickIds=t,i=!0)}return i?o:void 0})(0,null===(n=t.allowAdTracking)||void 0===n||n);return r&&(e.campaign=r),e})({allowAdTracking:p.allowAdTracking});return m.analytics&&m.marketing||(e.consent=Object.assign({},m)),{anonymousId:I({cookieDomain:p.cookieDomain,cookieMaxAgeDays:p.cookieMaxAgeDays}),sessionId:R({cookieDomain:p.cookieDomain,timeoutMinutes:p.sessionTimeoutMinutes}),userId:null!==(n=k(V)||_(V))&&void 0!==n?n:void 0,context:e,projectId:t,integrations:Object.keys(p.integrations).length>0?p.integrations:void 0}}function U(t){p.debug&&console.warn(`[cnv-track] ${t}() called before init()`)}function $(t,n){try{const e=T();if(!e)return U("pageview");const r=((t,n,e)=>{const r=C("page",e);return t&&(r.name=t),r.properties=null!=n?n:{},r})(null!=t?t:(()=>{try{return"undefined"!=typeof document&&document.title||void 0}catch(t){return}})(),n,e);D().enqueue(r)}catch(t){}}function L(t,n){$(t,n)}function N(t,n){try{if("string"!=typeof t||0===t.length)return;const e=T();if(!e)return U("track");const r=((t,n,e)=>{const r=C("track",e);return r.event=t,r.properties=null!=n?n:{},r})(t,n,e);D().enqueue(r)}catch(t){}}return{get initialized(){return g},get projectId(){return t},init(d,f){try{if(!d||"string"!=typeof d)return;if(p=P(f),(null==f?void 0:f.consent)&&(m=Object.assign(Object.assign({},m),f.consent)),t=d,w&&w.destroy(),w=null,D(),g=!0,I({cookieDomain:p.cookieDomain,cookieMaxAgeDays:p.cookieMaxAgeDays}),R({cookieDomain:p.cookieDomain,timeoutMinutes:p.sessionTimeoutMinutes}),p.autoPageview&&$(),O){try{O()}catch(t){}O=null}p.autotrack&&(O=function(t,d){const f=void 0===(h=d)||!1===h?Object.assign(Object.assign({},a),{searchParams:u}):!0===h?Object.assign(Object.assign({},s),{searchParams:u}):{spa:null===(y=h.spa)||void 0===y||y,outboundClicks:null===(v=h.outboundClicks)||void 0===v||v,scroll:null===(p=h.scroll)||void 0===p||p,downloads:null===(m=h.downloads)||void 0===m||m,siteSearch:null===(b=h.siteSearch)||void 0===b||b,downloadExtensions:h.downloadExtensions,searchParams:null!==(w=h.searchParams)&&void 0!==w?w:u};var h,y,v,p,m,b,w;const g=[];let x=null;function k(n){if(f.siteSearch)try{const e=null!=n?n:(()=>{try{return"undefined"!=typeof location?location.href:""}catch(t){return""}})();if(e===x)return;x=e;const r=((t,n=u)=>{try{if(!t){if("undefined"==typeof location)return null;t=location.href}const e=t instanceof URL?t:new URL(t,(()=>{try{return"undefined"!=typeof location?location.href:"http://localhost/"}catch(t){return"http://localhost/"}})());if(!e.search)return null;for(const t of n){const n=e.searchParams.get(t);if(null===n)continue;const r=n.trim();if(0!==r.length)return{query:r,param:t}}return null}catch(t){return null}})(e,f.searchParams);r&&l(()=>t.track("Site Searched",{query:r.query,param:r.param}))}catch(t){}}if(f.outboundClicks||f.downloads){const i=(t=>{var i;if("undefined"==typeof document)return()=>{};const c=new Set((null!==(i=t.downloadExtensions)&&void 0!==i?i:n).map(t=>t.toLowerCase())),u=!!t.onOutbound,s=!!t.onDownload;if(!u&&!s)return()=>{};function a(n){var i,a;try{if(0!==n.button)return;if(n.defaultPrevented)return;const l=(t=>{const n="function"==typeof t.composedPath?t.composedPath():[];if(n.length>0)for(const t of n)if(e(t))return t;let r=t.target;for(;r;){if(e(r))return r;r=r.parentNode}return null})(n);if(!l)return;const d=l.getAttribute("href");if(!d)return;if(/^(javascript:|mailto:|tel:|sms:)/i.test(d))return;if(d.startsWith("#"))return;const f=(t=>{try{return new URL(t,location.href)}catch(t){return null}})(d);if(!f)return;let h=!1;if(s){const n=l.hasAttribute("download"),e=(t=>{const n=r(t),e=n.lastIndexOf(".");return e<=0||e===n.length-1?null:n.slice(e+1).toLowerCase()})(f.pathname);(n||e&&c.has(e))&&(h=!0,null===(i=t.onDownload)||void 0===i||i.call(t,{url:f.href,fileName:r(f.pathname),fileExtension:null!=e?e:"",text:o(l)}))}u&&!h&&f.host!==(()=>{try{return"undefined"!=typeof location?location.hostname:""}catch(t){return""}})()&&f.host&&(null===(a=t.onOutbound)||void 0===a||a.call(t,{url:f.href,host:f.host,text:o(l),target:l.target||"_self"}))}catch(t){}}try{document.addEventListener("click",a,!0)}catch(t){return()=>{}}return()=>{try{document.removeEventListener("click",a,!0)}catch(t){}}})({downloadExtensions:f.downloadExtensions,onOutbound:f.outboundClicks?n=>l(()=>t.track("Outbound Link Clicked",n)):void 0,onDownload:f.downloads?n=>l(()=>t.track("File Downloaded",n)):void 0});g.push(i)}let j=null;if(f.scroll&&(j=(t=>{if("undefined"==typeof window||"undefined"==typeof document)return{destroy(){},reset(){}};let n=!1,e=null,r=!1;function o(){if(e=null,!r&&!n)try{const e=document.documentElement,r=document.body,o=Math.max(e?e.scrollHeight:0,r?r.scrollHeight:0),i=Math.max(e?e.clientHeight:0,window.innerHeight||0);if(o<=i)return n=!0,c(),void t(90);((window.scrollY||(e?e.scrollTop:0)||0)+i)/o>=.9&&(n=!0,c(),t(90))}catch(t){}}function i(){if(null===e)try{e=window.requestAnimationFrame(o)}catch(t){o()}}function c(){try{window.removeEventListener("scroll",i),null!==e&&window.cancelAnimationFrame(e),e=null}catch(t){}}try{window.addEventListener("scroll",i,{passive:!0})}catch(t){try{window.addEventListener("scroll",i)}catch(t){}}try{window.requestAnimationFrame(o)}catch(t){o()}return{destroy(){r=!0,c()},reset(){if(!r){n=!1;try{window.addEventListener("scroll",i,{passive:!0})}catch(t){try{window.addEventListener("scroll",i)}catch(t){}}try{window.requestAnimationFrame(o)}catch(t){o()}}}}})(n=>{l(()=>t.track("Scrolled",{percent:n}))}),g.push(()=>null==j?void 0:j.destroy())),f.spa){const n=function(){if("undefined"==typeof window||"undefined"==typeof history)return()=>{};let n=c();function e(){try{const e=c();if(e===n)return;n=e,(n=>{l(()=>t.page()),null==j||j.reset(),k(n)})(e)}catch(t){}}const r=history.pushState,o=history.replaceState,u=!0===r[i],s=!0===o[i];if(!u){function f(...t){r.apply(this,t),e()}f[i]=!0,history.pushState=f}if(!s){function h(...t){o.apply(this,t),e()}h[i]=!0,history.replaceState=h}const a=()=>e(),d=()=>e();try{window.addEventListener("popstate",a),window.addEventListener("hashchange",d)}catch(y){}return()=>{try{window.removeEventListener("popstate",a),window.removeEventListener("hashchange",d)}catch(t){}try{!u&&history.pushState[i]&&(history.pushState=r),!s&&history.replaceState[i]&&(history.replaceState=o)}catch(t){}}}();g.push(n)}return k(),()=>{for(;g.length>0;){const t=g.pop();try{null==t||t()}catch(t){}}x=null}}({page:(t,n)=>L(t,n),track:(t,n)=>N(t,n)},p.autotrack))}catch(t){}},pageview:$,page:L,screen(t,n){try{const e=T();if(!e)return U("screen");const r=((t,n,e)=>{const r=C("screen",e);return t&&(r.name=t),r.properties=null!=n?n:{},r})(t,n,e);D().enqueue(r)}catch(t){}},track:N,identify(t,n){try{if("string"!=typeof t||0===t.length)return;((t,n,e={})=>{var r,o;if(t&&(j(V,t,{domain:e.cookieDomain,maxAgeDays:null!==(r=e.cookieMaxAgeDays)&&void 0!==r?r:730,sameSite:"Lax"}),S(V,t),null!=n)){const t=null!==(o=(()=>{const t=_(E);if(!t)return null;try{const n=JSON.parse(t);return n&&"object"==typeof n&&!Array.isArray(n)?n:null}catch(t){return null}})())&&void 0!==o?o:{},e=Object.assign(Object.assign({},t),n);try{S(E,JSON.stringify(e))}catch(t){}}})(t,n,{cookieDomain:p.cookieDomain,cookieMaxAgeDays:p.cookieMaxAgeDays});const e=T();if(!e)return U("identify");const r=((t,n,e)=>{var r;const o=C("identify",e);return t&&(o.userId=t),o.traits=null!==(r=n)&&void 0!==r?r:{},o})(t,n,e);D().enqueue(r)}catch(t){}},alias(t,n){try{if("string"!=typeof t||0===t.length)return;const e=T();if(!e)return U("alias");const r=((t,n,e)=>{const r=C("alias",e);return t&&(r.userId=t),r.previousId=null!=n?n:e.anonymousId,r})(t,n,e);D().enqueue(r)}catch(t){}},group(t,n){try{if("string"!=typeof t||0===t.length)return;const e=T();if(!e)return U("group");const r=((t,n,e)=>{const r=C("group",e);return r.groupId=t,r.traits=null!=n?n:{},r})(t,n,e);D().enqueue(r)}catch(t){}},reset(){try{((t={})=>{((t,n={})=>{var e;if(!x)return!1;try{let r=`${encodeURIComponent(t)}=`;return r+=`; path=${null!==(e=n.path)&&void 0!==e?e:"/"}`,n.domain&&(r+=`; domain=${n.domain}`),r+="; max-age=0",r+="; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=r,null===k(t)}catch(t){return!1}})(V,{domain:t.cookieDomain}),A(V),A(E)})({cookieDomain:p.cookieDomain}),((t={})=>{M(b(),t)})({cookieDomain:p.cookieDomain,cookieMaxAgeDays:p.cookieMaxAgeDays}),((t={})=>{q({id:b(),lastActivity:Date.now()},t)})({cookieDomain:p.cookieDomain,timeoutMinutes:p.sessionTimeoutMinutes})}catch(t){}},flush(){try{w&&w.flush()}catch(t){}},consent(t){try{m=Object.assign(Object.assign({},m),t)}catch(t){}},set(t){try{p=P(t),t.consent&&(m=Object.assign(Object.assign({},m),t.consent)),w&&w.setConfig({endpoint:p.endpoint,batchPath:p.batchPath,singlePath:p.singlePath,useBatch:p.useBatch,contentType:p.contentType,flushAt:p.flushAt,flushInterval:p.flushInterval,debug:p.debug})}catch(t){}},destroy(){if(O){try{O()}catch(t){}O=null}w&&w.destroy(),w=null,g=!1,t=null,p=Object.assign({},H),m=Object.assign({},F)}}})();"undefined"!=typeof window&&(t=>{if("undefined"==typeof window)return()=>{};const n=window.cnvQ;if(n&&n.__loaded)return()=>{};const e=window.cnvQ=null!=n?n:[],r=e.splice(0,e.length);e.push.bind(e),e.push=(...n)=>{for(const e of n)G(t,e);return e.length},e.__loaded=!0;for(const n of r)G(t,n)})(W);const X=Object.assign(W,{version:K,name:Q,schemaVersion:T});t.NAME=Q,t.SCHEMA_VERSION=T,t.VERSION=K,t.default=X,Object.defineProperty(t,"t",{value:!0})},"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).cnvTrack={});
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* cnv-track.js — Data Schema (Segment Spec compatible)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Wire-format contract with the CNV Tracking Gateway. Mirrors the Segment
|
|
7
|
+
* Spec common envelope (camelCase keys) so payloads decode 1:1 into the
|
|
8
|
+
* server-side `SegmentMessage` / `SegmentBatchRequest` DTOs.
|
|
9
|
+
*
|
|
10
|
+
* Endpoints (relative to `SDKOptions.endpoint`, default `https://cnvcdp.net/api`):
|
|
11
|
+
*
|
|
12
|
+
* POST /segment/batch ← N messages per request (default flush)
|
|
13
|
+
* POST /segment/{type} ← single-message mode (debug / unbatched)
|
|
14
|
+
* where {type} ∈ track | identify | page | screen | group | alias
|
|
15
|
+
*
|
|
16
|
+
* Where `projectId` lives:
|
|
17
|
+
* - URL query param `?projectId=…` (routing / auth at the edge)
|
|
18
|
+
* - `BatchRequest.context.projectId` (so backend can read off the body too)
|
|
19
|
+
* - `SegmentMessage.context.projectId` (per-message, in case messages get
|
|
20
|
+
* forwarded individually downstream)
|
|
21
|
+
*
|
|
22
|
+
* Compatibility policy:
|
|
23
|
+
* - Additive changes (new optional fields) → minor `SCHEMA_VERSION` bump.
|
|
24
|
+
* - Renames / removals / type changes → bump major.
|
|
25
|
+
* ============================================================================
|
|
26
|
+
*/
|
|
27
|
+
/** Wire-format schema version. Stamped into `context.schemaVersion` per message. */
|
|
28
|
+
declare const SCHEMA_VERSION: "1.0";
|
|
29
|
+
/** ISO-8601 timestamp string with millisecond precision and timezone offset. */
|
|
30
|
+
type ISOTimestamp = string;
|
|
31
|
+
/** RFC-4122 v4 UUID string. */
|
|
32
|
+
type UUID = string;
|
|
33
|
+
/**
|
|
34
|
+
* Request `Content-Type`. `text/plain` is CORS-safelisted, so the browser
|
|
35
|
+
* skips the OPTIONS preflight (and `sendBeacon` fires cleanly during unload);
|
|
36
|
+
* `application/json` is stricter on the wire but triggers a preflight. The
|
|
37
|
+
* JSON body itself is identical either way — only the header differs. See
|
|
38
|
+
* Sub-task 4.3.
|
|
39
|
+
*/
|
|
40
|
+
type ContentType = 'text/plain' | 'application/json';
|
|
41
|
+
/** JSON-serializable primitive. */
|
|
42
|
+
type JSONPrimitive = string | number | boolean | null;
|
|
43
|
+
/** Any JSON-serializable value. */
|
|
44
|
+
type JSONValue = JSONPrimitive | JSONValue[] | {
|
|
45
|
+
[key: string]: JSONValue;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The six message types the gateway accepts. Mirrors the regex in
|
|
49
|
+
* `@PostMapping("segment/{type:track|identify|page|screen|group|alias}")`.
|
|
50
|
+
*/
|
|
51
|
+
type MessageType = 'track' | 'identify' | 'page' | 'screen' | 'group' | 'alias';
|
|
52
|
+
interface ContextPage {
|
|
53
|
+
/** Full URL including query string and fragment. */
|
|
54
|
+
url: string;
|
|
55
|
+
/** Pathname only (e.g., `/products/42`). */
|
|
56
|
+
path: string;
|
|
57
|
+
/** Query string including the leading `?`. Empty string if none. */
|
|
58
|
+
search: string;
|
|
59
|
+
/** Hash fragment including the leading `#`. Empty string if none. */
|
|
60
|
+
hash: string;
|
|
61
|
+
/** `document.title` at the time the event was created. */
|
|
62
|
+
title: string;
|
|
63
|
+
/** `document.referrer` — may be empty for direct visits. */
|
|
64
|
+
referrer: string;
|
|
65
|
+
}
|
|
66
|
+
interface ContextScreen {
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
/** `window.devicePixelRatio` — useful for high-DPI segmentation. */
|
|
70
|
+
density: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Marketing attribution block. Field names follow the Segment Spec:
|
|
74
|
+
* utm_campaign → name utm_source → source utm_medium → medium
|
|
75
|
+
* utm_term → term utm_content → content
|
|
76
|
+
*
|
|
77
|
+
* `id` and `clickIds` are CNV extensions (no Segment equivalent) — Spring
|
|
78
|
+
* Jackson will route them through `SegmentMessage.extra` if a strict mapping
|
|
79
|
+
* exists, or via `Map<String, Object>` if `context` stays loosely typed.
|
|
80
|
+
*/
|
|
81
|
+
interface ContextCampaign {
|
|
82
|
+
/** `utm_campaign` — Segment maps to `campaign.name`. */
|
|
83
|
+
name?: string;
|
|
84
|
+
/** `utm_source` */
|
|
85
|
+
source?: string;
|
|
86
|
+
/** `utm_medium` */
|
|
87
|
+
medium?: string;
|
|
88
|
+
/** `utm_term` */
|
|
89
|
+
term?: string;
|
|
90
|
+
/** `utm_content` */
|
|
91
|
+
content?: string;
|
|
92
|
+
/** `utm_id` — CNV extension. */
|
|
93
|
+
id?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Platform click IDs (gclid, fbclid, ttclid, msclkid, dclid, ...).
|
|
96
|
+
* CNV extension — collected verbatim from the query string.
|
|
97
|
+
*/
|
|
98
|
+
clickIds?: Record<string, string>;
|
|
99
|
+
}
|
|
100
|
+
interface ContextLibrary {
|
|
101
|
+
/** Always `@cnv/track`. */
|
|
102
|
+
name: string;
|
|
103
|
+
/** Semver string. Stamped at build time. */
|
|
104
|
+
version: string;
|
|
105
|
+
}
|
|
106
|
+
interface ContextConsent {
|
|
107
|
+
analytics: boolean;
|
|
108
|
+
marketing: boolean;
|
|
109
|
+
}
|
|
110
|
+
interface PayloadContext {
|
|
111
|
+
page?: ContextPage;
|
|
112
|
+
screen?: ContextScreen;
|
|
113
|
+
/** `navigator.language` (e.g., `vi-VN`). */
|
|
114
|
+
locale?: string;
|
|
115
|
+
/** IANA timezone (e.g., `Asia/Ho_Chi_Minh`). */
|
|
116
|
+
timezone?: string;
|
|
117
|
+
/** Verbatim `navigator.userAgent`. Gateway is expected to parse + drop. */
|
|
118
|
+
userAgent?: string;
|
|
119
|
+
library?: ContextLibrary;
|
|
120
|
+
campaign?: ContextCampaign;
|
|
121
|
+
/**
|
|
122
|
+
* Pre-consent flag. Gateway may downgrade storage or skip forwarding to
|
|
123
|
+
* marketing destinations when `marketing === false`.
|
|
124
|
+
*/
|
|
125
|
+
consent?: ContextConsent;
|
|
126
|
+
/**
|
|
127
|
+
* CNV extension — projectId is duplicated here so backend code that
|
|
128
|
+
* forwards individual messages still has access to the source project
|
|
129
|
+
* without having to thread the batch-level context through.
|
|
130
|
+
*/
|
|
131
|
+
projectId?: string;
|
|
132
|
+
/** SDK wire-format version. Echoed for forward-compat triage. */
|
|
133
|
+
schemaVersion?: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* One event in the wire format.
|
|
137
|
+
*
|
|
138
|
+
* Field name and types deliberately mirror `com.cnv.tracking.dto.SegmentMessage`
|
|
139
|
+
* so Jackson decodes the payload into typed fields rather than the `extra`
|
|
140
|
+
* map. Any field NOT listed here will land in `extra` on the server side.
|
|
141
|
+
*
|
|
142
|
+
* type — Discriminator. Required.
|
|
143
|
+
* messageId — UUID v4 per event. Server uses for idempotent dedupe.
|
|
144
|
+
* anonymousId — Device-level id from cookie + localStorage.
|
|
145
|
+
* userId — Authenticated user id (only after `identify()`).
|
|
146
|
+
* sessionId — Rotates after 30' inactivity. Java accepts `session_id` alias.
|
|
147
|
+
* timestamp — When the event was created on the client.
|
|
148
|
+
* originalTimestamp — Stamped once on the client; never overridden during
|
|
149
|
+
* offline retries. Equal to `timestamp` on first send.
|
|
150
|
+
* sentAt — Stamped just before the request leaves the browser.
|
|
151
|
+
*
|
|
152
|
+
* event — Custom event name. ONLY for type=track.
|
|
153
|
+
* name — Page / screen name. For type=page or type=screen.
|
|
154
|
+
* category — Optional grouping. For type=page or type=screen.
|
|
155
|
+
* properties — For track / page / screen.
|
|
156
|
+
* traits — For identify / group.
|
|
157
|
+
*
|
|
158
|
+
* groupId — Required for type=group.
|
|
159
|
+
* previousId — Required for type=alias (the *old* id being aliased away).
|
|
160
|
+
*
|
|
161
|
+
* context — Auto-collected environment (see {@link PayloadContext}).
|
|
162
|
+
* integrations— Per-destination on/off toggles (Segment convention).
|
|
163
|
+
*/
|
|
164
|
+
interface SegmentMessage {
|
|
165
|
+
type: MessageType;
|
|
166
|
+
messageId: UUID;
|
|
167
|
+
anonymousId: UUID;
|
|
168
|
+
userId?: string;
|
|
169
|
+
sessionId?: string;
|
|
170
|
+
timestamp: ISOTimestamp;
|
|
171
|
+
originalTimestamp: ISOTimestamp;
|
|
172
|
+
sentAt?: ISOTimestamp;
|
|
173
|
+
event?: string;
|
|
174
|
+
name?: string;
|
|
175
|
+
category?: string;
|
|
176
|
+
properties?: Record<string, JSONValue>;
|
|
177
|
+
traits?: Record<string, JSONValue>;
|
|
178
|
+
context?: PayloadContext;
|
|
179
|
+
groupId?: string;
|
|
180
|
+
previousId?: string;
|
|
181
|
+
integrations?: Record<string, JSONValue>;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Wrapper sent to `POST /segment/batch`. Shape matches
|
|
185
|
+
* `com.cnv.tracking.dto.SegmentBatchRequest`:
|
|
186
|
+
*
|
|
187
|
+
* batch — N SegmentMessage records.
|
|
188
|
+
* context — Batch-level context. We use it to hoist `projectId` once
|
|
189
|
+
* instead of repeating per message. (Per-message context
|
|
190
|
+
* still carries page / screen / campaign as those vary
|
|
191
|
+
* between events in the same batch — SPA navigation, etc.)
|
|
192
|
+
* integrations — Batch-level destination toggles.
|
|
193
|
+
*/
|
|
194
|
+
interface SegmentBatchRequest {
|
|
195
|
+
batch: SegmentMessage[];
|
|
196
|
+
context?: Record<string, JSONValue>;
|
|
197
|
+
integrations?: Record<string, JSONValue>;
|
|
198
|
+
sentAt?: ISOTimestamp;
|
|
199
|
+
}
|
|
200
|
+
/** Argument shape for `identify(userId, traits)`. */
|
|
201
|
+
interface UserTraits {
|
|
202
|
+
email?: string;
|
|
203
|
+
phone?: string;
|
|
204
|
+
name?: string;
|
|
205
|
+
firstName?: string;
|
|
206
|
+
lastName?: string;
|
|
207
|
+
createdAt?: ISOTimestamp;
|
|
208
|
+
[key: string]: JSONValue | undefined;
|
|
209
|
+
}
|
|
210
|
+
interface SDKOptions {
|
|
211
|
+
/** Tracking Gateway base URL. Default: `https://cnvcdp.net/api`. */
|
|
212
|
+
endpoint?: string;
|
|
213
|
+
/** Path appended for batch flush. Default: `/segment/batch`. */
|
|
214
|
+
batchPath?: string;
|
|
215
|
+
/** Per-type single-message path template. `{type}` is interpolated. */
|
|
216
|
+
singlePath?: string;
|
|
217
|
+
/** Use batch endpoint (true, default) or one request per event. */
|
|
218
|
+
useBatch?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Request `Content-Type`. Default `text/plain` — CORS-safelisted, so no
|
|
221
|
+
* OPTIONS preflight. Switch to `application/json` only if the gateway
|
|
222
|
+
* rejects text bodies (then it must allow the preflight). See Sub-task 4.3.
|
|
223
|
+
*/
|
|
224
|
+
contentType?: ContentType;
|
|
225
|
+
/** Verbose console output. */
|
|
226
|
+
debug?: boolean;
|
|
227
|
+
/** Disable marketing-related identifiers and click-id capture. */
|
|
228
|
+
allowAdTracking?: boolean;
|
|
229
|
+
/** Cookie domain (e.g., `.cnv.vn` for cross-subdomain stitching). */
|
|
230
|
+
cookieDomain?: string;
|
|
231
|
+
/** Cookie lifetime in days. Default 730 (≈ 2 years, ITP-safe upper bound). */
|
|
232
|
+
cookieMaxAgeDays?: number;
|
|
233
|
+
/** Session inactivity timeout in minutes. Default 30. */
|
|
234
|
+
sessionTimeoutMinutes?: number;
|
|
235
|
+
/** Buffered events before forced flush. Default 20. */
|
|
236
|
+
flushAt?: number;
|
|
237
|
+
/** Max ms between flushes. Default 5000. */
|
|
238
|
+
flushInterval?: number;
|
|
239
|
+
/** Fire a `pageview` automatically on `init()`. Default true. */
|
|
240
|
+
autoPageview?: boolean;
|
|
241
|
+
/**
|
|
242
|
+
* Enhanced Measurement — opt-in automatic instrumentation. Mirrors the
|
|
243
|
+
* features GA4's gtag enables by default. Pass `true` to enable every
|
|
244
|
+
* feature with sensible defaults, `false` to disable, or a granular object
|
|
245
|
+
* to flip individual features. See {@link AutotrackOptions}.
|
|
246
|
+
*
|
|
247
|
+
* Default: `false` (off, for backward compatibility).
|
|
248
|
+
*/
|
|
249
|
+
autotrack?: boolean | AutotrackOptions;
|
|
250
|
+
/** Initial consent state. Default `{ analytics: true, marketing: true }`. */
|
|
251
|
+
consent?: Partial<ContextConsent>;
|
|
252
|
+
/** Per-call integrations override. */
|
|
253
|
+
integrations?: Record<string, JSONValue>;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Granular toggles for the built-in autotrack instrumentation. Every feature
|
|
257
|
+
* defaults to `true` when `autotrack: true`, or can be individually flipped
|
|
258
|
+
* when the user passes an object.
|
|
259
|
+
*
|
|
260
|
+
* Event names follow Segment Spec conventions where one exists, with CNV
|
|
261
|
+
* extensions for the rest:
|
|
262
|
+
* - SPA pageview → emits `type:"page"` (same as manual `page()`)
|
|
263
|
+
* - Outbound link click → `track('Outbound Link Clicked', { url, … })`
|
|
264
|
+
* - 90% scroll depth → `track('Scrolled', { percent: 90 })`
|
|
265
|
+
* - File download click → `track('File Downloaded', { url, extension, … })`
|
|
266
|
+
* - Site search (?q= …) → `track('Site Searched', { query })`
|
|
267
|
+
*/
|
|
268
|
+
interface AutotrackOptions {
|
|
269
|
+
/**
|
|
270
|
+
* Monkey-patch `history.pushState` / `history.replaceState` and listen for
|
|
271
|
+
* `popstate` + `hashchange` so SPA route changes (React Router, Vue Router,
|
|
272
|
+
* Next.js App Router, …) fire a `page()` automatically. Default `true`.
|
|
273
|
+
*/
|
|
274
|
+
spa?: boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Document-level click delegation. Fires `track('Outbound Link Clicked')`
|
|
277
|
+
* when the user clicks an `<a>` whose host differs from `location.host`.
|
|
278
|
+
* Default `true`.
|
|
279
|
+
*/
|
|
280
|
+
outboundClicks?: boolean;
|
|
281
|
+
/**
|
|
282
|
+
* Passive scroll listener that fires `track('Scrolled', { percent: 90 })`
|
|
283
|
+
* exactly once per page when the user reaches ≥ 90% of the document height.
|
|
284
|
+
* Re-armed on SPA route change. Default `true`.
|
|
285
|
+
*/
|
|
286
|
+
scroll?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Same click delegation pipeline as `outboundClicks`. Fires
|
|
289
|
+
* `track('File Downloaded')` when an `<a href>` points at a recognized
|
|
290
|
+
* file extension (pdf, doc/docx, xls/xlsx, ppt/pptx, zip, rar, 7z, csv,
|
|
291
|
+
* txt, mp3, mp4, mov, wav, dmg, exe, pkg, gz). Default `true`.
|
|
292
|
+
*/
|
|
293
|
+
downloads?: boolean;
|
|
294
|
+
/**
|
|
295
|
+
* Inspect the URL query string for common search parameter names
|
|
296
|
+
* (`q`, `s`, `query`, `search`, `keyword`) and fire
|
|
297
|
+
* `track('Site Searched', { query })` whenever a non-empty value is found
|
|
298
|
+
* at pageview time. Default `true`.
|
|
299
|
+
*/
|
|
300
|
+
siteSearch?: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Additional file extensions to treat as downloads (merged with defaults).
|
|
303
|
+
* Lower-case, no leading dot.
|
|
304
|
+
*/
|
|
305
|
+
downloadExtensions?: ReadonlyArray<string>;
|
|
306
|
+
/**
|
|
307
|
+
* Override the list of query parameter names checked by `siteSearch`.
|
|
308
|
+
* Order matters — first non-empty match wins.
|
|
309
|
+
*/
|
|
310
|
+
searchParams?: ReadonlyArray<string>;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Each entry in `window.cnvQ` is a tuple `[methodName, ...args]`.
|
|
314
|
+
* The async snippet seeds the queue as a plain Array; once the SDK loads,
|
|
315
|
+
* it drains the queue and replaces `.push` with a synchronous dispatcher.
|
|
316
|
+
*
|
|
317
|
+
* Discriminated by tuple index 0 for exhaustive type-narrowing.
|
|
318
|
+
*/
|
|
319
|
+
type QueuedCommand = readonly ['init', string, SDKOptions?] | readonly ['pageview', string?, Record<string, JSONValue>?] | readonly ['page', string?, Record<string, JSONValue>?] | readonly ['screen', string?, Record<string, JSONValue>?] | readonly ['track', string, Record<string, JSONValue>?] | readonly ['identify', string, UserTraits?] | readonly ['alias', string, string?] | readonly ['group', string, Record<string, JSONValue>?] | readonly ['reset'] | readonly ['flush'] | readonly ['consent', Partial<ContextConsent>] | readonly ['set', Partial<SDKOptions>];
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* ============================================================================
|
|
323
|
+
* queue — Queue Processor (Sub-task 3.5)
|
|
324
|
+
* ============================================================================
|
|
325
|
+
*
|
|
326
|
+
* The async snippet seeds `window.cnvQ = []` and pushes `[methodName, ...args]`
|
|
327
|
+
* tuples while the SDK is still loading. Once this file runs, we:
|
|
328
|
+
*
|
|
329
|
+
* 1. Snapshot whatever the snippet pushed (the pending FIFO backlog).
|
|
330
|
+
* 2. Replace `cnvQ.push` with a proxy that *synchronously* dispatches.
|
|
331
|
+
* 3. Drain the snapshot through the dispatcher in original order.
|
|
332
|
+
* 4. Mark `cnvQ.__loaded = true` so duplicate snippet includes no-op.
|
|
333
|
+
*
|
|
334
|
+
* Ordering note: step 2 happens BEFORE step 3 on purpose. If a dispatched
|
|
335
|
+
* command synchronously triggers another `cnvQ.push` (e.g., a user's init
|
|
336
|
+
* options has an `onReady` callback that fires a `track`), the new push
|
|
337
|
+
* also goes through the proxy and stays in FIFO order with the backlog
|
|
338
|
+
* rather than racing the drain loop.
|
|
339
|
+
*
|
|
340
|
+
* Every dispatch is wrapped in try/catch (Sub-task 5.2 — silent-fail
|
|
341
|
+
* contract). A malformed command MUST NOT crash the host page or stop
|
|
342
|
+
* the rest of the queue from draining.
|
|
343
|
+
* ============================================================================
|
|
344
|
+
*/
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Surface the queue forwards inbound tuples to. Each method matches one
|
|
348
|
+
* `QueuedCommand` discriminator. The runtime (in `index.ts`) implements this.
|
|
349
|
+
*/
|
|
350
|
+
interface Dispatcher {
|
|
351
|
+
init(projectId: string, options?: SDKOptions): void;
|
|
352
|
+
pageview(name?: string, properties?: Record<string, JSONValue>): void;
|
|
353
|
+
page(name?: string, properties?: Record<string, JSONValue>): void;
|
|
354
|
+
screen(name?: string, properties?: Record<string, JSONValue>): void;
|
|
355
|
+
track(event: string, properties?: Record<string, JSONValue>): void;
|
|
356
|
+
identify(userId: string, traits?: UserTraits): void;
|
|
357
|
+
alias(userId: string, previousId?: string): void;
|
|
358
|
+
group(groupId: string, traits?: Record<string, JSONValue>): void;
|
|
359
|
+
reset(): void;
|
|
360
|
+
flush(): void;
|
|
361
|
+
consent(state: Partial<ContextConsent>): void;
|
|
362
|
+
set(options: Partial<SDKOptions>): void;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* ============================================================================
|
|
367
|
+
* runtime — SDK state holder + public API (Sub-tasks 3.1 / 3.2 / 3.3 / 3.4)
|
|
368
|
+
* ============================================================================
|
|
369
|
+
*
|
|
370
|
+
* Implements the {@link Dispatcher} surface consumed by the Queue Processor.
|
|
371
|
+
* Module-scoped state (project id, resolved options, transport instance,
|
|
372
|
+
* consent) lives here as a singleton. `createRuntime()` exists so tests can
|
|
373
|
+
* spin up an isolated copy without leaking state between cases.
|
|
374
|
+
*
|
|
375
|
+
* Public API and what each method does:
|
|
376
|
+
* init(projectId, options?) → set up runtime, build transport,
|
|
377
|
+
* optionally fire an automatic pageview
|
|
378
|
+
* pageview(name?, properties?) → emit `type:"page"`
|
|
379
|
+
* page(name?, properties?) → emit `type:"page"` (alias of pageview)
|
|
380
|
+
* screen(name?, properties?) → emit `type:"screen"`
|
|
381
|
+
* track(event, properties?) → emit `type:"track"`
|
|
382
|
+
* identify(userId, traits?) → emit `type:"identify"`, persist uid
|
|
383
|
+
* alias(userId, previousId?) → emit `type:"alias"`
|
|
384
|
+
* group(groupId, traits?) → emit `type:"group"`
|
|
385
|
+
* reset() → rotate anonymousId, drop userId
|
|
386
|
+
* flush() → force-flush the transport buffer
|
|
387
|
+
* consent(state) → merge consent flags
|
|
388
|
+
* set(options) → patch SDK options at runtime
|
|
389
|
+
*
|
|
390
|
+
* Init order matters:
|
|
391
|
+
* 1. Cookie / LS reads happen on demand (lazy) — every event resolves
|
|
392
|
+
* its own anonymousId + sessionId at message-build time so a re-init
|
|
393
|
+
* with new cookieDomain takes effect on the very next event.
|
|
394
|
+
* 2. Transport is recreated whenever endpoint / batch settings change.
|
|
395
|
+
*
|
|
396
|
+
* Silent-fail (Sub-task 5.2):
|
|
397
|
+
* Every public method is wrapped in try/catch. A bug inside the SDK
|
|
398
|
+
* MUST NEVER throw out into the host page.
|
|
399
|
+
* ============================================================================
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
/** Public-facing handle that exposes the SDK methods + a few internals. */
|
|
403
|
+
interface Runtime extends Dispatcher {
|
|
404
|
+
readonly initialized: boolean;
|
|
405
|
+
readonly projectId: string | null;
|
|
406
|
+
/** Tear down timers + listeners. Test-only in production. */
|
|
407
|
+
destroy(): void;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* ============================================================================
|
|
412
|
+
* cnv-track.js — public entry point
|
|
413
|
+
* ============================================================================
|
|
414
|
+
*
|
|
415
|
+
* Wires the runtime (`./core/runtime.ts`) into the Queue Processor
|
|
416
|
+
* (`./core/queue.ts`) and exposes the public API surface.
|
|
417
|
+
*
|
|
418
|
+
* Two consumer paths:
|
|
419
|
+
*
|
|
420
|
+
* 1. Script-tag (IIFE) via snippet:
|
|
421
|
+
* Snippet seeds `window.cnvQ`, this module auto-installs the Queue
|
|
422
|
+
* Processor on import, drains the backlog, and starts dispatching.
|
|
423
|
+
*
|
|
424
|
+
* 2. ES module:
|
|
425
|
+
* `import cnvTrack from '@cnv/track'`
|
|
426
|
+
* `cnvTrack.init('PRJ_X')`
|
|
427
|
+
* Side-effectful auto-install still runs (a no-op if `window.cnvQ`
|
|
428
|
+
* is empty); consumers can drive the SDK directly via the default
|
|
429
|
+
* export or by pushing tuples through `window.cnvQ`.
|
|
430
|
+
* ============================================================================
|
|
431
|
+
*/
|
|
432
|
+
|
|
433
|
+
/** Library version. Replaced at build time by @rollup/plugin-replace. */
|
|
434
|
+
declare const VERSION: string;
|
|
435
|
+
/** Library name. Replaced at build time by @rollup/plugin-replace. */
|
|
436
|
+
declare const NAME: string;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Public SDK handle. Methods correspond 1:1 with `QueuedCommand` verbs so
|
|
440
|
+
* `window.cnvQ.push(['track', 'X'])` and `cnvTrack.track('X')` are interchangeable.
|
|
441
|
+
*/
|
|
442
|
+
declare const cnvTrack: Runtime & {
|
|
443
|
+
version: string;
|
|
444
|
+
name: string;
|
|
445
|
+
schemaVersion: "1.0";
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
export { NAME, SCHEMA_VERSION, VERSION, cnvTrack as default };
|
|
449
|
+
export type { AutotrackOptions, ContextCampaign, ContextConsent, ContextLibrary, ContextPage, ContextScreen, JSONValue, MessageType, PayloadContext, QueuedCommand, SDKOptions, SegmentBatchRequest, SegmentMessage, UserTraits };
|
package/package.json
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cnv-vn/track",
|
|
3
|
+
"version": "0.2.0-alpha.2",
|
|
4
|
+
"description": "CNV Tracking SDK — lightweight, async-first web analytics tracker (<20KB gzipped).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"analytics",
|
|
7
|
+
"tracking",
|
|
8
|
+
"tag",
|
|
9
|
+
"pixel",
|
|
10
|
+
"sdk",
|
|
11
|
+
"telemetry",
|
|
12
|
+
"events",
|
|
13
|
+
"cnv"
|
|
14
|
+
],
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"author": "CNV <dev@cnv.vn>",
|
|
17
|
+
"homepage": "https://internal.192168173.com/cnv/common/cnv-tracking",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://internal.192168173.com/cnv/common/cnv-tracking.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://internal.192168173.com/cnv/common/cnv-tracking/-/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/cnv-track.umd.cjs",
|
|
27
|
+
"module": "./dist/cnv-track.esm.js",
|
|
28
|
+
"unpkg": "./dist/cnv-track.min.js",
|
|
29
|
+
"jsdelivr": "./dist/cnv-track.min.js",
|
|
30
|
+
"browser": "./dist/cnv-track.min.js",
|
|
31
|
+
"types": "./dist/types/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/types/index.d.ts",
|
|
35
|
+
"import": "./dist/cnv-track.esm.js",
|
|
36
|
+
"require": "./dist/cnv-track.umd.cjs",
|
|
37
|
+
"default": "./dist/cnv-track.umd.cjs"
|
|
38
|
+
},
|
|
39
|
+
"./snippet": "./snippet/snippet.html",
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist/cnv-track.min.js",
|
|
44
|
+
"dist/cnv-track.esm.js",
|
|
45
|
+
"dist/cnv-track.umd.cjs",
|
|
46
|
+
"dist/types/",
|
|
47
|
+
"snippet/",
|
|
48
|
+
"README.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"sideEffects": false,
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "rollup -c",
|
|
57
|
+
"build:watch": "rollup -c -w",
|
|
58
|
+
"build:analyze": "cross-env ANALYZE=true rollup -c",
|
|
59
|
+
"dev": "node scripts/dev-server.mjs",
|
|
60
|
+
"clean": "rimraf dist",
|
|
61
|
+
"prebuild": "npm run clean",
|
|
62
|
+
"size": "size-limit",
|
|
63
|
+
"size:snippet": "node -e \"const s=require('fs').statSync('snippet/snippet.min.html').size;const max=500;if(s>max){console.error('snippet/snippet.min.html ('+s+'B) exceeds budget '+max+'B');process.exit(1)}else console.log('snippet/snippet.min.html: '+s+'B / '+max+'B budget — OK')\"",
|
|
64
|
+
"type-check": "tsc --noEmit",
|
|
65
|
+
"type-check:tests": "tsc --noEmit -p tsconfig.test.json",
|
|
66
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
67
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
68
|
+
"format": "prettier --write \"src/**/*.{ts,js,json,md}\" \"*.{json,md,mjs}\"",
|
|
69
|
+
"format:check": "prettier --check \"src/**/*.{ts,js,json,md}\" \"*.{json,md,mjs}\"",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"test:watch": "vitest",
|
|
72
|
+
"test:coverage": "vitest run --coverage",
|
|
73
|
+
"smoke": "node scripts/smoke-dist.mjs",
|
|
74
|
+
"verify": "npm run type-check && npm run type-check:tests && npm run lint && npm run format:check && npm run test && npm run build && npm run size && npm run size:snippet && npm run smoke"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@rollup/plugin-commonjs": "^28.0.2",
|
|
78
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
79
|
+
"@rollup/plugin-replace": "^6.0.2",
|
|
80
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
81
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
82
|
+
"@size-limit/preset-small-lib": "^11.1.6",
|
|
83
|
+
"@types/node": "^25.9.1",
|
|
84
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
85
|
+
"cross-env": "^7.0.3",
|
|
86
|
+
"eslint": "^9.17.0",
|
|
87
|
+
"eslint-config-prettier": "^9.1.0",
|
|
88
|
+
"jsdom": "^29.1.1",
|
|
89
|
+
"prettier": "^3.4.2",
|
|
90
|
+
"rimraf": "^6.0.1",
|
|
91
|
+
"rollup": "^4.28.1",
|
|
92
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
93
|
+
"rollup-plugin-filesize": "^10.0.0",
|
|
94
|
+
"rollup-plugin-visualizer": "^5.13.1",
|
|
95
|
+
"size-limit": "^11.1.6",
|
|
96
|
+
"tslib": "^2.8.1",
|
|
97
|
+
"typescript": "^5.7.2",
|
|
98
|
+
"typescript-eslint": "^8.18.0",
|
|
99
|
+
"vitest": "^4.1.7"
|
|
100
|
+
},
|
|
101
|
+
"size-limit": [
|
|
102
|
+
{
|
|
103
|
+
"name": "IIFE (minified + gzipped)",
|
|
104
|
+
"path": "dist/cnv-track.min.js",
|
|
105
|
+
"limit": "20 KB",
|
|
106
|
+
"gzip": true
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "ESM tree-shaken (gzipped)",
|
|
110
|
+
"path": "dist/cnv-track.esm.js",
|
|
111
|
+
"limit": "20 KB",
|
|
112
|
+
"gzip": true
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# cnv-track.js — Async Snippet
|
|
2
|
+
|
|
3
|
+
Đoạn mã loader bất đồng bộ nhúng vào website khách hàng. Hai biến thể:
|
|
4
|
+
|
|
5
|
+
| File | Mục đích | Kích thước |
|
|
6
|
+
|---------------------|-------------------------------------------|-----------:|
|
|
7
|
+
| `snippet.html` | Bản có chú thích — dùng khi onboarding | ~1.2 KB |
|
|
8
|
+
| `snippet.min.html` | Bản nén — paste vào production | 361 B |
|
|
9
|
+
|
|
10
|
+
## 1. Cách nhúng
|
|
11
|
+
|
|
12
|
+
Chèn vào trong thẻ `<head>`, **trước** mọi tracking pixel khác (GTM, Meta, …):
|
|
13
|
+
|
|
14
|
+
<!-- prettier-ignore -->
|
|
15
|
+
```html
|
|
16
|
+
<!DOCTYPE html>
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<meta charset="utf-8" />
|
|
20
|
+
<title>My Shop</title>
|
|
21
|
+
|
|
22
|
+
<!-- cnv-track.js -->
|
|
23
|
+
<script>!function(w,d,s,q,u){if(w[q]&&w[q].__loaded)return;var Q=w[q]=w[q]||[];Q.t=+new Date;Q.v="1.0";Q.push(["init",u]);var f=d.getElementsByTagName(s)[0],j=d.createElement(s);j.async=1;j.defer=1;j.crossOrigin="anonymous";j.src="https://cdn.cnv.vn/cnv-track.min.js";f.parentNode.insertBefore(j,f)}(window,document,"script","cnvQ","YOUR_PROJECT_ID");</script>
|
|
24
|
+
<!-- /cnv-track.js -->
|
|
25
|
+
</head>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Đổi `YOUR_PROJECT_ID` thành mã dự án trong dashboard CNV.
|
|
29
|
+
|
|
30
|
+
## 2. Gọi sự kiện trước khi SDK tải xong
|
|
31
|
+
|
|
32
|
+
Snippet đã tạo sẵn hàng đợi `window.cnvQ`. Bạn có thể `push` ngay sau khi
|
|
33
|
+
snippet chạy — sự kiện sẽ được "phát lại" khi `cnv-track.min.js` tải xong.
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<button id="cta">Thêm vào giỏ</button>
|
|
37
|
+
<script>
|
|
38
|
+
document.getElementById('cta').addEventListener('click', function () {
|
|
39
|
+
cnvQ.push(['track', 'AddToCart', { product_id: 'SKU-42', price: 199000 }]);
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 3. Các API mà queue chấp nhận
|
|
45
|
+
|
|
46
|
+
| Tuple | Ý nghĩa |
|
|
47
|
+
|------------------------------------------|----------------------------------------------------|
|
|
48
|
+
| `['init', projectId, options?]` | (Snippet tự gọi) khởi tạo SDK |
|
|
49
|
+
| `['pageview', props?]` | Bắn pageview thủ công (cần thiết cho SPA) |
|
|
50
|
+
| `['track', eventName, props?]` | Sự kiện tùy chỉnh |
|
|
51
|
+
| `['identify', userId, traits?]` | Định danh user sau khi login |
|
|
52
|
+
| `['alias', newId, prevId?]` | Gộp danh tính ẩn danh ↔ đã định danh |
|
|
53
|
+
| `['group', groupId, traits?]` | Gắn user vào nhóm (B2B) |
|
|
54
|
+
| `['reset']` | Xóa client/session id (sau logout) |
|
|
55
|
+
| `['consent', { analytics, marketing }]` | Cập nhật trạng thái đồng ý cookie |
|
|
56
|
+
| `['set', options]` | Thay đổi SDKOptions runtime |
|
|
57
|
+
|
|
58
|
+
## 4. Đặc tính kỹ thuật của snippet
|
|
59
|
+
|
|
60
|
+
- **Async + defer**: trình duyệt không bao giờ chặn render vì script này.
|
|
61
|
+
- **`crossOrigin="anonymous"`**: bật CORS để bắt được lỗi qua `window.onerror`.
|
|
62
|
+
- **Insert-before-first-script**: kỹ thuật DOM an toàn nhất — luôn có một
|
|
63
|
+
`<script>` tham chiếu hợp lệ kể cả khi `<head>` rỗng.
|
|
64
|
+
- **Double-include guard** (`__loaded`): paste 2 lần không gây hỏng SDK.
|
|
65
|
+
- **Boot timing** (`cnvQ.t`): SDK so sánh với `performance.now()` để báo cáo
|
|
66
|
+
thời gian khởi động lên gateway.
|
|
67
|
+
- **Snippet protocol version** (`cnvQ.v = "1.0"`): cho phép SDK upgrade
|
|
68
|
+
ngược tương thích với snippet cũ.
|
|
69
|
+
|
|
70
|
+
## 5. Các biến thể tích hợp
|
|
71
|
+
|
|
72
|
+
### 5.1. Google Tag Manager
|
|
73
|
+
|
|
74
|
+
Tạo **Custom HTML Tag**, dán nội dung `snippet.min.html`, trigger `Initialization
|
|
75
|
+
- All Pages`. **Không** dùng trigger `Page View` (quá muộn).
|
|
76
|
+
|
|
77
|
+
### 5.2. Single Page Applications (React / Vue / Next.js)
|
|
78
|
+
|
|
79
|
+
Vẫn dùng cùng snippet ở `<head>`. Mỗi lần route thay đổi, gọi:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
window.cnvQ.push(['pageview']);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> SDK tự thu thập `window.location.href`, `document.title` tại thời điểm
|
|
86
|
+
> push — không cần truyền tay.
|
|
87
|
+
|
|
88
|
+
### 5.3. Có cookie consent banner
|
|
89
|
+
|
|
90
|
+
Đặt snippet ở `<head>` (vô hại — chưa gửi gì). Sau khi user đồng ý:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
window.cnvQ.push(['consent', { analytics: true, marketing: true }]);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
SDK chỉ bắt đầu gửi sự kiện sau khi nhận lệnh `consent`.
|
|
97
|
+
|
|
98
|
+
## 6. Kiểm tra snippet hoạt động
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
// DevTools console
|
|
102
|
+
window.cnvQ; // → Array có boot time và lệnh 'init'
|
|
103
|
+
window.cnvQ.__loaded; // → true sau khi cnv-track.min.js tải xong
|
|
104
|
+
performance.getEntriesByName('https://cdn.cnv.vn/cnv-track.min.js')[0];
|
|
105
|
+
// → kiểm tra đã async load, không chặn LCP/TBT
|
|
106
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- ============================================================ -->
|
|
2
|
+
<!-- cnv-track.js — async loader snippet (snippet protocol v1.0) -->
|
|
3
|
+
<!-- Drop into <head>, BEFORE any other tracking / pixel script. -->
|
|
4
|
+
<!-- -->
|
|
5
|
+
<!-- Steps before going live: -->
|
|
6
|
+
<!-- 1. Replace YOUR_PROJECT_ID with the id from CNV dashboard. -->
|
|
7
|
+
<!-- 2. (Optional) Replace cdn.cnv.vn if you self-host the SDK. -->
|
|
8
|
+
<!-- 3. Verify in DevTools → Network: cnv-track.min.js is async. -->
|
|
9
|
+
<!-- ============================================================ -->
|
|
10
|
+
<script>
|
|
11
|
+
(function (w, d, s, q, u) {
|
|
12
|
+
// Double-include guard — second snippet wins nothing.
|
|
13
|
+
if (w[q] && w[q].__loaded) return;
|
|
14
|
+
var Q = (w[q] = w[q] || []);
|
|
15
|
+
Q.t = +new Date(); // boot timestamp — used by perf audit + telemetry
|
|
16
|
+
Q.v = '1.0'; // snippet protocol version
|
|
17
|
+
Q.push(['init', u]);
|
|
18
|
+
|
|
19
|
+
var f = d.getElementsByTagName(s)[0];
|
|
20
|
+
var j = d.createElement(s);
|
|
21
|
+
j.async = true;
|
|
22
|
+
j.defer = true;
|
|
23
|
+
j.crossOrigin = 'anonymous';
|
|
24
|
+
j.src = 'https://cdn.cnv.vn/cnv-track.min.js';
|
|
25
|
+
f.parentNode.insertBefore(j, f);
|
|
26
|
+
})(window, document, 'script', 'cnvQ', 'YOUR_PROJECT_ID');
|
|
27
|
+
</script>
|
|
28
|
+
<!-- End cnv-track.js -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<script>!function(w,d,s,q,u){if(w[q]&&w[q].__loaded)return;var Q=w[q]=w[q]||[];Q.t=+new Date;Q.v="1.0";Q.push(["init",u]);var f=d.getElementsByTagName(s)[0],j=d.createElement(s);j.async=1;j.defer=1;j.crossOrigin="anonymous";j.src="https://cdn.cnv.vn/cnv-track.min.js";f.parentNode.insertBefore(j,f)}(window,document,"script","cnvQ","YOUR_PROJECT_ID");</script>
|