diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4a802c4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +# Source code +**/*.ts @Topsort/integrations +**/*.tsx @Topsort/integrations +**/*.js @Topsort/integrations + +# Config +**/*.json @Topsort/integrations + +# CI +.github/ @Topsort/integrations diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43f6590 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,144 @@ +# analytics.js + +Topsort's analytics.js is a browser-side JavaScript library that auto-detects DOM events (impressions, clicks, and purchases) via `data-ts-*` HTML attributes, deduplicates and queues them with retry logic, and sends them to the Topsort Analytics API using `@topsort/sdk`. It is published to npm as `@topsort/analytics.js`. + +## Git Workflow + +- **Never commit directly to `main`.** All changes go through PRs from a dedicated branch. +- Branch names should be descriptive (e.g., `feat/add-google-environment`, `fix/merge-pagination-offset`). +- **Large changes must be broken into stacked PRs** — each PR should be independently reviewable and represent a single logical unit of work (e.g., one PR adds the config, the next adds the validation schema, the next adds tests). Avoid monolithic PRs that touch many unrelated things at once. +- Each PR in a stack should be based on the previous branch, not `main`, so they can be reviewed and merged in order. +- **Admin override** (`gh pr merge --admin`) is only appropriate to bypass the review requirement when all CI checks pass. Never use it to force-merge a PR with failing CI — fix the failures first. Before using `--admin`, check whether the repo allows it (e.g. `gh api repos/{owner}/{repo}` or branch protection settings). If admin override is not permitted or you cannot verify it is, do not merge — ask the user instead. +- Keep branches up to date with `main` before merging — rebase or merge `main` into your branch to resolve conflicts locally, not in the merge commit. +- Use [Conventional Commits](https://www.conventionalcommits.org/) for all commit messages (e.g., `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`). +- Never approve or merge a PR that has unresolved review comments — address or explicitly dismiss each one first. Always check nested/threaded comments (e.g. replies under bot comments) as they may contain substantive issues not visible at the top level. +- Before merging with `--admin`, wait at least **5 minutes** after the PR is opened. This gives Bugbot and other async bots time to post their comments. After the wait, check all PR comments (including nested/threaded replies) for unresolved issues before merging. + +## Tech Stack + +| Layer | Tool | +|---|---| +| Language | TypeScript (strict mode, ES6 target) | +| Runtime | Browser (DOM APIs, `window.TS` global config) | +| Package manager | pnpm (v10.22.0, declared in `packageManager` field) | +| Bundler | Vite (builds UMD, ESM, and IIFE formats) | +| Testing | Vitest with jsdom environment | +| HTTP mocking | MSW (Mock Service Worker) | +| Linting/Formatting | Biome (v2.3.5) | +| Coverage | @vitest/coverage-v8, reported to Codecov | +| SDK dependency | `@topsort/sdk` (the only runtime dependency) | +| Node version | >=20.0.0 | + +## Key Commands + +| Command | Description | +|---|---| +| `pnpm install` | Install dependencies | +| `pnpm run build` | Build UMD + ESM bundles, then IIFE bundle | +| `pnpm run test` | Run unit tests with coverage (Vitest) | +| `pnpm run lint` | Run Biome checks (linting + formatting) | +| `pnpm run lint:fix` | Auto-fix Biome lint issues | +| `pnpm run lint:ci` | Run Biome in CI mode (fails on any issue) | +| `pnpm run format` | Check formatting with Biome | +| `pnpm run format:fix` | Auto-fix formatting with Biome | +| `pnpm run types:check` | Run `tsc --noemit` to type-check without emitting | +| `pnpm run test:e2e` | Build + run E2E test server (Express-based, manual) | + +## Architecture + +### Directory Structure + +``` +src/ + detector.ts # Main entry point: DOM observation, event detection, API dispatch + queue.ts # Persistent event queue with retry + exponential backoff + store.ts # Storage abstraction (LocalStorage with MemoryStore fallback, BidStore for session) + set.ts # Utility to truncate a Set to a max size (keeps newest entries) + index.d.ts # Public type re-exports + *.test.ts # Co-located unit tests for each module +mocks/ + api-server.ts # Express server for manual E2E testing +tests/ + browser-test.ts # Browser-based E2E test runner + components.tsx # React test components (used with react-router-dom) + test.html # HTML harness for E2E tests + real_e2e.html # Manual E2E test page +@types/ + global.d.ts # Global type declarations (window.TS interface) +``` + +### Data Flow + +1. **Initialization** (`detector.ts`): On `DOMContentLoaded` (or immediately if the document is already loaded), the library reads `window.TS` config (token, url, optional getUserId). It scans the existing DOM for elements matching `[data-ts-product]`, `[data-ts-action]`, `[data-ts-items]`, or `[data-ts-resolved-bid]`. + +2. **Detection**: Two mechanisms detect events: + - **IntersectionObserver** (threshold 0.5): Fires `Impression` events when a product element becomes 50% visible. Each element is unobserved after its first impression. + - **MutationObserver**: Watches for new child elements and attribute changes (`data-ts-product`, `data-ts-action`, `data-ts-items`, `data-ts-resolved-bid`) to detect dynamically added or modified products. + - **Click listeners**: Attached to product elements (or their `[data-ts-clickable]` children for granular control). Clicks on banners store the `resolvedBidId` in session storage (`BidStore`) for cross-page attribution via `data-ts-resolved-bid="inherit"`. + - **Purchase events**: Triggered when elements with `data-ts-action="purchase"` are detected; item data is parsed from `data-ts-items` JSON attribute. + +3. **Deduplication**: A `Set` of seen event keys (page + type + product + bid + items) prevents duplicate events. The set is capped at 2,500 entries, dropping oldest first (`truncateSet`). + +4. **Queuing** (`queue.ts`): Events are appended to a `Queue` backed by `LocalStorageStore` (falls back to `MemoryStore` if localStorage is unavailable). The queue: + - Caps at 250 entries (drops oldest on overflow). + - Processes up to 25 events per batch. + - Uses exponential backoff for retries (max 3 retries). + - High-priority events (purchases) are processed immediately; low-priority events are batched with a 250ms delay. + +5. **Dispatch**: The `processor` function creates a `TopsortClient` from `@topsort/sdk` and calls `reportEvent()` for each queued event. On success, the event is removed from the queue. On retryable failure, it is kept for retry. On permanent failure or after max retries, it is dropped. + +6. **User ID**: Managed via a cookie (`tsuid` by default, configurable via `window.TS.cookieName`). Can be overridden by providing `window.TS.getUserId`. The library also exposes `setUserId` and `resetUserId` on `window.TS`. + +### Build Outputs + +- `dist/ts.js` — UMD bundle (default for `require()`) +- `dist/ts.mjs` — ESM bundle (default for `import`) +- `dist/ts.iife.js` — IIFE bundle for direct `