@cnv-vn/track
v0.2.0-alpha.8
Published
CNV Tracking SDK — lightweight, async-first web analytics tracker (<20KB gzipped).
Downloads
410
Maintainers
Readme
cnv-track.js
Lightweight, async-first web analytics tracker — bundle size ≤ 20 KB gzipped.
cnv-track.js là Web JS SDK của hệ thống tracking CNV, có vai trò tương đương
Google Analytics / Facebook Pixel: nhúng một dòng <script> vào website
khách hàng, tự động thu thập ngữ cảnh và bắn sự kiện tùy chỉnh về Tracking
Gateway nội bộ.
Trạng thái hiện tại
| Task | Hạng mục | Trạng thái |
| ---- | ----------------------------------------------------- | ----------------------- |
| 1 | Setup môi trường & thiết kế snippet | ✅ Hoàn thành |
| 2.1 | Identity: Anonymous Client ID (cookie + localStorage) | ✅ Hoàn thành |
| 2.2 | Session ID (inactivity 30 phút, cookie + sessionStg) | ✅ Hoàn thành |
| 2.3 | Context Collector (page / screen / UTM / click-id) | ✅ Hoàn thành |
| 3 | Public API: init, pageview, track, identify | ✅ Hoàn thành |
| 3.5 | Queue Processor (drain cnvQ, sync .push proxy) | ✅ Hoàn thành |
| 4.1 | Transport: sendBeacon (ưu tiên) | ✅ Hoàn thành |
| 4.2 | Transport: Fetch / XHR fallback | ✅ fetch+keepalive |
| 4.3 | Content-Type text/plain → bỏ CORS preflight | ✅ Hoàn thành |
| 4.4 | Offline retry queue (localStorage + online) | ✅ Hoàn thành |
| 5 | QA, performance audit, cross-browser | ✅ 259 test / 5.5KB |
| 6 | Build, CDN deploy, Integration guide | ✅ docs-site (30 trang) |
Tham khảo kế hoạch tổng thể.
Endpoint contract
| Endpoint | Khi nào |
| -------------------------------------------- | ------------------------------------ |
| POST https://cnvcdp.net/api/segment/batch | Mặc định — flush nhiều event / 1 req |
| POST https://cnvcdp.net/api/segment/{type} | useBatch: false — 1 event / req |
Schema mirror nguyên com.cnv.tracking.dto.SegmentMessage Java DTO
(camelCase Segment Spec). Chi tiết: docs/SCHEMA.md.
CORS (Sub-task 4.3): SDK gửi
Content-Type: text/plain(mặc định) — giá trị CORS-safelisted nên không phátOPTIONSpreflight. Body vẫn là JSON nguyên vẹn → backend phải parsetext/plainthành JSON. ĐặtcontentType: 'application/json'nếu backend bắt buộc strict (khi đó backend phải allowOPTIONS /segment/*từ origin khách). SDK luôn gửicredentials: 'omit'nên server có thể trảAccess-Control-Allow-Origin: *.
Cấu trúc dự án
.
├── src/
│ ├── index.ts # Public entry — installs queue + exports default
│ ├── types.ts # Segment Spec wire schema (camelCase)
│ ├── global.d.ts # window.cnvQ + build-time constants
│ ├── utils/
│ │ ├── uuid.ts # RFC-4122 v4 generator + validator
│ │ └── storage.ts # Cookie + LS + sessionStorage helpers
│ └── core/
│ ├── identity.ts # Anonymous Client ID resolver
│ ├── userIdentity.ts # Authenticated userId + traits (Sub-task 3.4)
│ ├── session.ts # Session ID + inactivity timeout
│ ├── context.ts # Context Collector (page/screen/UTM/library)
│ ├── messageBuilder.ts # SegmentMessage factory (track/page/identify/…)
│ ├── transport.ts # Batch buffer + beacon/fetch flush → /segment/batch
│ ├── retryQueue.ts # Offline retry queue (localStorage + online replay)
│ ├── queue.ts # Queue Processor (drain cnvQ + sync .push proxy)
│ └── runtime.ts # SDK state holder + Public API dispatcher
├── tests/
│ ├── uuid.test.ts # Vitest — UUID v4
│ ├── storage.test.ts # Vitest — cookie + LS + SS helpers
│ ├── identity.test.ts # Vitest — Client ID lifecycle
│ ├── userIdentity.test.ts # Vitest — userId / traits lifecycle
│ ├── session.test.ts # Vitest — Session lifecycle + timeout
│ ├── context.test.ts # Vitest — page/screen/UTM/library
│ ├── messageBuilder.test.ts# Vitest — SegmentMessage factory
│ ├── transport.test.ts # Vitest — batch buffer, beacon/fetch, content-type
│ ├── retryQueue.test.ts # Vitest — offline stash + online replay
│ ├── queue.test.ts # Vitest — Queue Processor
│ ├── runtime.test.ts # Vitest — end-to-end Public API
│ └── silentFail.test.ts # Vitest — crash-proof / silent-fail contract
├── snippet/
│ ├── snippet.html # Bản có chú thích (onboarding)
│ ├── snippet.min.html # Bản nén (production)
│ └── README.md # Cách nhúng + biến thể (GTM, SPA, consent)
├── docs/
│ ├── SCHEMA.md # Data Schema Payload (human-readable)
│ ├── IDENTITY.md # Client ID / cookie / LS rules theo RFC 4122 + 6265bis
│ ├── CONTEXT.md # Context Collector — UTM, click-id, page/screen
│ ├── TRANSPORT.md # Transport layer — beacon/fetch/retry
│ └── QA.md # Task 5 — unit test, crash-proof, perf + cross-browser
├── rollup.config.mjs # Build matrix: IIFE + UMD + ESM + .d.ts
├── vitest.config.ts # Test config (jsdom env + build-const define)
├── tsconfig.json # TypeScript strict mode, target ES2015
├── eslint.config.mjs # Flat config (ESLint v9)
└── package.json # Scripts + size-limit budgetsBuild pipeline (Sub-task 1.1)
Đầu ra cho mỗi lần npm run build:
| File | Format | Mục đích |
| ------------------------ | :----: | ----------------------------------------- |
| dist/cnv-track.js | IIFE | Debug build, có sourcemap |
| dist/cnv-track.min.js | IIFE | CDN target — minified, < 20KB gzipped |
| dist/cnv-track.esm.js | ESM | Cho Webpack / Vite / Rollup consumers |
| dist/cnv-track.umd.cjs | UMD | Legacy CommonJS / AMD / RequireJS |
| dist/types/index.d.ts | .d.ts | Bundled type declarations |
Tại sao multi-format? Người dùng cuối có ba loại:
(a) chèn <script> trực tiếp → IIFE,
(b) import qua bundler hiện đại → ESM (tree-shakable),
(c) Node-style require / RequireJS / NoModule → UMD. Đây là pattern của Segment Analytics, Mixpanel, Amplitude.
Quick start (dev workflow)
npm install # Cài deps lần đầu
npm run build # Build full matrix → dist/
npm run build:watch # Watch mode trong khi viết code
npm run build:analyze # Sinh dist/stats.html (rollup-plugin-visualizer)
npm run type-check # tsc --noEmit
npm run lint # ESLint + typescript-eslint
npm run format # Prettier
npm run size # size-limit — fail nếu vượt 20KB
npm run verify # type-check + lint + format:check + build + sizeDecisions & rationale
| Quyết định | Lý do |
| -------------------------------- | -------------------------------------------------------------------- |
| TypeScript thay vì JS thuần | Bắt bug ở compile time; types là docs sống cho payload schema |
| Target ES2015 | Cover 97%+ trình duyệt (caniuse 2026); IE11 đã EOL — bỏ Babel |
| Rollup thay vì Webpack | Tree-shaking tốt hơn, format-flexible, chuẩn cho thư viện |
| 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 |
| sideEffects: false | Bật full tree-shaking cho ESM consumers |
| size-limit trong CI gate | Bundle bloat sẽ fail PR trước khi merge — kỷ luật cứng |
| Snippet protocol versioning | cnvQ.v cho phép SDK mới detect snippet cũ và migrate |
| crossOrigin="anonymous" | Bật CORS để bắt lỗi qua window.onerror — cần cho TASK 5 telemetry |
| Schema follows Segment Spec | Cho phép swap downstream pipeline (Segment → Snowplow → self-hosted) |
Snippet (Sub-task 1.2)
361 bytes raw (≈ 240B gzipped). Đặt vào <head>:
<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>Chi tiết: snippet/README.md.
Schema (Sub-task 1.3)
Payload v1.0 theo Segment Spec, snake_case keys, flat properties,
ISO-8601 timestamp. Đầy đủ field reference: docs/SCHEMA.md.
Identity & Storage (Sub-task 2.1)
Anonymous Client ID (UUID v4 theo RFC 4122 §4.4), lưu kép trên first-party
cookie (RFC 6265bis, SameSite=Lax, TTL 730 ngày) và localStorage để bền
vững qua ITP và cookie-banner wipe. Đầy đủ default values, validation rules,
browser compat matrix, và tham chiếu pháp lý (GDPR / ePrivacy / CCPA / PDPD):
docs/IDENTITY.md.
Public API (Task 3)
Sau khi snippet load cnv-track.min.js, window.cnvTrack và
window.cnvQ.push([...]) đều dispatch về cùng runtime.
// 1. Khởi tạo (snippet đã làm sẵn)
cnvTrack.init('PRJ_XYZ', {
endpoint: 'https://cnvcdp.net/api', // default
flushAt: 20, // batch size
flushInterval: 5000, // ms
autoPageview: true, // auto fire page on init
contentType: 'text/plain', // default — skip CORS preflight | 'application/json'
});
// 2. Track sự kiện
cnvTrack.track('AddToCart', { sku: 'A-1', qty: 2 });
// 3. Page / Screen
cnvTrack.pageview(); // mặc định name = document.title
cnvTrack.page('Cart', { ref: 'email' });
cnvTrack.screen('CartScreen', { items: 2 });
// 4. Identify user
cnvTrack.identify('u_42', { email: '[email protected]', name: 'Ngọc' });
// 5. Alias (gộp anonymous → authenticated)
cnvTrack.alias('u_42'); // previousId mặc định = anonymousId
// 6. Group (B2B)
cnvTrack.group('org_acme', { plan: 'enterprise' });
// 7. Consent (GDPR / ePrivacy)
cnvTrack.consent({ marketing: false });
// 8. Reset (sau logout)
cnvTrack.reset();
// 9. Manual flush (trước khi unload)
cnvTrack.flush();
// 10. Hot-patch options
cnvTrack.set({ flushAt: 1 });Mỗi method tương ứng 1 verb trong QueuedCommand — cnvQ.push(['track', 'A'])
và cnvTrack.track('A') là tương đương 1:1.
Context Collector (Sub-task 2.3)
collectContext() snapshot context.page, context.screen,
context.locale, context.timezone, context.user_agent, context.library
và — khi URL chứa UTM/click-id — context.campaign. Mọi browser-API access
đều bọc try/catch (silent-fail) nên không bao giờ crash host page.
| Sub-collector | Nguồn |
| ------------------- | ------------------------------------------------------ |
| collectPage() | location.href, document.title, document.referrer |
| collectScreen() | screen.{width,height}, devicePixelRatio |
| collectCampaign() | URLSearchParams over location.search |
| collectContext() | composes the above + navigator.language + IANA tz |
Đầy đủ: nguồn từng field, UTM key map, click-id whitelist, library versioning, browser compat: docs/CONTEXT.md.
Transport (Sub-task 4.1 – 4.4)
Transport buffer event trong bộ nhớ rồi flush theo 4 trigger: đủ flushAt,
quá flushInterval, pagehide / visibilitychange→hidden, hoặc flush() thủ
công. Mỗi request đi qua hai tầng:
| Thứ tự | Phương thức | Khi nào dùng |
| ------ | ---------------------- | -------------------------------------- |
| 1 | navigator.sendBeacon | Mặc định — body là Blob text/plain |
| 2 | fetch + keepalive | Fallback khi beacon miss |
Beacon "miss" và rơi xuống fetch khi: trình duyệt không có sendBeacon,
payload > 64KB (vượt cap của beacon), beacon queue đầy (sendBeacon trả
false), hoặc lời gọi ném lỗi (vd. CSP chặn). Vì sao beacon là chính: trình
duyệt cam kết gửi nền ngay cả khi tab đóng, không cần setTimeout giữ trang.
Content-Type (Sub-task 4.3). Mặc định text/plain — nằm trong CORS-safelist
nên trình duyệt bỏ qua OPTIONS preflight (beacon vì thế gửi gọn lúc unload).
Body vẫn là JSON, chỉ header khác → backend phải parse text/plain thành
JSON. Đổi contentType: 'application/json' nếu backend bắt buộc strict (khi
đó phải allow preflight). Áp dụng cho cả Blob của beacon lẫn header của fetch.
Offline retry queue (Sub-task 4.4). Khi mất mạng — navigator.onLine là
false, hoặc fetch keepalive reject — batch lỗi được stash vào localStorage
(key _cnv_rq) thay vì bị bỏ, rồi replay theo FIFO khi có event online hoặc
khi SDK boot ở lần tải trang sau. Mỗi event giữ nguyên originalTimestamp, chỉ
đóng dấu lại sentAt. Không retry với HTTP error status (4xx/5xx — là
response, sẽ loop) hay payload > 64KB (không transport nào gửi được). Hàng đợi
cap 100 event, evict batch cũ nhất khi đầy; mọi thao tác storage đều silent-fail.
QA & Performance (Task 5)
| Hạng mục | Kết quả |
| ------------------------ | -------------------------------------------------------------------- |
| Unit test (Sub-task 5.1) | 259 test / 12 file (Vitest) — coverage 93.85% stmt · 98.78% line |
| Crash-proof (5.2) | Silent-fail xuyên suốt + silentFail.test.ts (môi trường thù địch) |
| Bundle (5.3) | IIFE 5.52 KB gzipped / 20 KB budget (size-limit CI gate) |
| Async load (5.3) | Snippet async+defer+insertBefore → zero-blocking, CLS = 0 |
| Cross-browser (5.3) | Chrome / Edge / Firefox / Safari (ITP) — đủ chuỗi fallback |
Mock trong test tuân thủ contract thật (cap 64KB beacon/keepalive được mô
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
silent-fail thật: throw đồng bộ trong flush bất đồng bộ (timer/pagehide) nay
được Transport.send() chặn. Đầy đủ: docs/QA.md.
License
UNLICENSED — internal CNV project. Liên hệ team Data Platform trước khi re-distribute.
