@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 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>