You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@unomi.apache.org by sh...@apache.org on 2026/10/06 13:58:31 UTC
(unomi-tracker) branch main updated (bb019ed -> c7d48c6)
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/unomi-tracker.git
from bb019ed Align AGENTS.md and SECURITY.md with repo doc conventions. (#25)
new bc2c34d chore: ignore local .env.local files
new 54a94ef chore: refresh CI and capture tracker bug-reproduction tests
new c7d48c6 Merge remote-tracking branch 'origin/main'
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.github/workflows/on-code-change.yml | 4 +-
.gitignore | 5 +-
JIRA_TICKETS.md | 311 +++++++++++++
PROJECT_PLAN.md | 850 +++++++++++++++++++++++++++++++++++
test/bugs/bug-demonstration.test.js | 359 +++++++++++++++
5 files changed, 1526 insertions(+), 3 deletions(-)
create mode 100644 JIRA_TICKETS.md
create mode 100644 PROJECT_PLAN.md
create mode 100644 test/bugs/bug-demonstration.test.js
(unomi-tracker) 01/03: chore: ignore local .env.local files
Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/unomi-tracker.git
commit bc2c34da286f9806082dec8fe8e8aa06927311cc
Author: Serge Huber <sh...@apache.org>
AuthorDate: Fri Jul 17 17:01:01 2026 +0200
chore: ignore local .env.local files
---
.gitignore | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index ee8ecb9..918460a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -267,4 +267,7 @@ storybook-static/
yalc.lock
.yalc
-*.iml
\ No newline at end of file
+*.iml
+# Local env (secrets)
+.env.local
+.env*.local
(unomi-tracker) 02/03: chore: refresh CI and capture tracker bug-reproduction tests
Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/unomi-tracker.git
commit 54a94efcc1fc02625e38e58f9c08776698a0b6cf
Author: Serge Huber <sh...@apache.org>
AuthorDate: Tue Oct 6 15:56:01 2026 +0200
chore: refresh CI and capture tracker bug-reproduction tests
---
.github/workflows/on-code-change.yml | 4 +-
JIRA_TICKETS.md | 311 +++++++++++++
PROJECT_PLAN.md | 850 +++++++++++++++++++++++++++++++++++
test/bugs/bug-demonstration.test.js | 359 +++++++++++++++
4 files changed, 1522 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/on-code-change.yml b/.github/workflows/on-code-change.yml
index aa86768..7833335 100644
--- a/.github/workflows/on-code-change.yml
+++ b/.github/workflows/on-code-change.yml
@@ -8,7 +8,7 @@ jobs:
lint-tests:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Install dependencies
shell: bash
run: yarn install --frozen-lockfile
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
needs: lint-tests
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Install dependencies
shell: bash
run: yarn install --frozen-lockfile
diff --git a/JIRA_TICKETS.md b/JIRA_TICKETS.md
new file mode 100644
index 0000000..b1f3d8f
--- /dev/null
+++ b/JIRA_TICKETS.md
@@ -0,0 +1,311 @@
+# JIRA Tickets - Apache Unomi Tracker
+
+**Project:** UNOMI-TRACKER
+**Version:** 1.5.0 → 2.0.0 (Modernization)
+**Timeline:** 6 months (26 weeks, phased approach)
+**Server Compatibility:** Unomi V2 + Unomi V4.0 (multi-tenancy)
+
+**PR Strategy:** Each ticket = 1 phase = 1 stacked PR. Strict linear dependency chain.
+
+```
+PR 1 → PR 2 → PR 3 → PR 4 → PR 5 → PR 6 → PR 7 → PR 8
+```
+
+---
+
+## UNOMI-TRACKER-001: Phase 0 - Testing infrastructure ⭐ START HERE
+
+**PR 1** | **Depends on:** nothing
+**Issue Type:** Epic
+**Priority:** High (P1)
+**Component:** Testing Infrastructure
+**Labels:** testing, vitest, msw, quality-assurance
+
+**Description:**
+Establish comprehensive testing infrastructure with server mocks before any code changes begin. This is the foundation that guarantees backward compatibility and quality throughout the entire modernization process.
+
+**What will be done:**
+Set up Vitest test framework with jsdom, configure MSW for mocking Unomi server endpoints (both V2 and V4.0 modes), create bug verification tests proving the two critical bugs exist, create unit tests for all utility functions, create integration tests for all tracker flows, and create backward compatibility tests covering 100% of the public API. Achieve 80%+ code coverage.
+
+**Acceptance Criteria:**
+- [ ] Vitest configured with jsdom and coverage reporting
+- [ ] MSW set up with Unomi server mocks (V2 + V4.0 modes)
+- [ ] Bug verification tests created (proving bugs exist)
+- [ ] 80%+ code coverage achieved
+- [ ] All utility functions tested
+- [ ] All integration scenarios tested
+- [ ] 100% backward compatibility test coverage
+- [ ] CI/CD pipeline updated
+- [ ] Coverage reports generated
+
+**Technical Details:**
+- **Files:** `vitest.config.ts`, `test/setup.ts`, `test/mocks/`, test files
+- **Dependencies:** `vitest`, `@vitest/ui`, `@vitest/coverage-v8`, `msw`, `jsdom`
+- **Estimated Effort:** 40-60 hours
+
+---
+
+## UNOMI-TRACKER-002: Phase 1 - Fix critical bugs and add Unomi V4.0 compatibility
+
+**PR 2** | **Depends on:** PR 1 (UNOMI-TRACKER-001)
+**Issue Type:** Epic
+**Priority:** Critical (P0)
+**Component:** Core Functionality
+**Labels:** bug, critical, event-tracking, id-resolution, v3-compatibility
+
+**Description:**
+Fix two critical bugs and add Unomi V4.0 multi-tenancy support. These are all targeted changes to the existing codebase that should be completed before larger refactoring begins.
+
+**Problem (Bugs):**
+1. **Bug #1 (Line 448)**: `splice(eventIndex, 0)` doesn't actually remove elements from the `eventsPrevented` array. The code removes 0 elements instead of 1, causing incorrect event handling behavior.
+
+2. **Bug #2 (Lines 1420-1428)**: The `_resolveId` function has multiple logic errors:
+ - Uses array result from `filter()` as a string in `replace()` operation
+ - Incorrect index check (`> 0` instead of `!== -1`) misses matches at position 0
+ - Function is marked with `@ts-expect-error` acknowledging it's broken
+
+**What will be done:**
+Fix both bugs: correct the splice operation and rewrite `_resolveId`. Add Unomi V4.0 support: introduce optional `apiKey`/`tenantId` config fields and inject `X-Unomi-Api-Key`/`X-Unomi-Tenant-Id` headers in the `ajax()` method when configured. The tracker will work with both V2 (no API key) and V4.0 (with API key) servers, maintaining full backward compatibility.
+
+**Acceptance Criteria:**
+- [ ] Bug #1 fixed: `splice` correctly removes elements
+- [ ] Bug #2 fixed: `_resolveId` function rewritten with correct logic
+- [ ] Bug verification tests pass
+- [ ] V4.0 support: `apiKey` and `tenantId` optional config fields added
+- [ ] V4.0 support: `X-Unomi-Api-Key` header injected when `apiKey` is set
+- [ ] Tests for V2 mode (no API key) and V4.0 mode (with API key)
+- [ ] Tracker works against V2 servers with API key configured (header ignored)
+- [ ] Types updated in `types.ts`
+- [ ] No regression in existing functionality
+
+**Technical Details:**
+- **Files:** `src/apache-unomi-tracker.js`, `src/types.ts`
+- **V4.0 change scope:** ~20-30 lines of implementation code (additive only)
+- **Estimated Effort:** 8-12 hours
+
+---
+
+## UNOMI-TRACKER-003: Phase 2 - Modernize build toolchain and config cleanup
+
+**PR 3** | **Depends on:** PR 2 (UNOMI-TRACKER-002)
+**Issue Type:** Epic
+**Priority:** Medium (P2)
+**Component:** Build System, Dependencies, Configuration
+**Labels:** modernization, build, dependencies, configuration
+
+**Description:**
+Modernize the build toolchain and upgrade all dependencies to 2026 standards. Also fix configuration issues (browserslist, XDomainRequest type) that are prerequisites for TypeScript migration.
+
+**What will be done:**
+Upgrade Rollup to 4.x, replace Babel with esbuild (removing `babel.config.json` entirely), upgrade runtime and dev dependencies, add modern `browserslist` configuration, fix incorrect `XDomainRequest` type definition, and configure TypeScript for incremental migration (`allowJs`, `checkJs`).
+
+**Acceptance Criteria:**
+- [ ] Rollup 4.x installed and configured
+- [ ] Babel removed, esbuild integrated
+- [ ] All dependencies upgraded (`es6-crawler-detect` v4, ESLint v9, TS ESLint plugins)
+- [ ] `browserslist` configuration added (deprecated Babel `browsers` field removed)
+- [ ] `XDomainRequest` type definition fixed in `global.d.ts`
+- [ ] TypeScript configured for incremental migration
+- [ ] All builds (CJS, ESM, UMD) produce correct output
+- [ ] Build time improved (target: 50% faster)
+- [ ] All tests passing
+
+**Technical Details:**
+- **Files:** `package.json`, `rollup.config.js`, `babel.config.json` (removed), `tsconfig.json`, `.eslintrc.json`, `src/global.d.ts`, `.browserslistrc`
+- **Estimated Effort:** 12-20 hours
+
+---
+
+## UNOMI-TRACKER-004: Phase 3 - Extract and convert utility functions to TypeScript
+
+**PR 4** | **Depends on:** PR 3 (UNOMI-TRACKER-003)
+**Issue Type:** Epic
+**Priority:** Medium (P2)
+**Component:** TypeScript Migration
+**Labels:** modernization, typescript, utilities
+
+**Description:**
+Extract leaf-level utility functions from the monolithic main file and convert them to TypeScript modules. First step of the "edge → trunk" migration. Requires the build system from Phase 2 to support TypeScript compilation.
+
+**What will be done:**
+Extract and convert all utility functions to TypeScript modules: cookie utilities, event builders, AJAX utility, form utilities, and URL utilities. Update main file to import from new modules.
+
+**Acceptance Criteria:**
+- [ ] All utility functions extracted to `src/utils/*.ts`
+- [ ] All utilities converted to TypeScript with proper types
+- [ ] Unit tests pass for all utilities
+- [ ] Main file updated to import from new modules
+- [ ] Functionality verified unchanged
+- [ ] All tests passing
+
+**Technical Details:**
+- **Files:** `src/utils/cookies.ts`, `src/utils/events.ts`, `src/utils/ajax.ts`, `src/utils/form.ts`, `src/utils/url.ts`, `src/apache-unomi-tracker.js`
+- **Estimated Effort:** 15-21 hours
+
+---
+
+## UNOMI-TRACKER-005: Phase 4 - Extract and convert feature modules to TypeScript
+
+**PR 5** | **Depends on:** PR 4 (UNOMI-TRACKER-004)
+**Issue Type:** Epic
+**Priority:** Medium (P2)
+**Component:** TypeScript Migration
+**Labels:** modernization, typescript, features
+
+**Description:**
+Extract higher-level feature modules from the main file and convert to TypeScript. These modules depend on the utility modules extracted in Phase 3, so they import from `src/utils/`.
+
+**What will be done:**
+Extract and convert all feature logic to TypeScript modules: personalization, optimization/A/B testing, and event tracking. Feature modules import from `src/utils/` established in the previous phase. Update main file to import from new modules.
+
+**Acceptance Criteria:**
+- [ ] All feature modules extracted to `src/features/*.ts`
+- [ ] All features converted to TypeScript with proper types
+- [ ] Features import from `src/utils/` (no duplication)
+- [ ] Integration tests pass
+- [ ] Main file updated to import from new modules
+- [ ] All tests passing
+
+**Technical Details:**
+- **Files:** `src/features/personalization.ts`, `src/features/optimization.ts`, `src/features/tracking.ts`, `src/apache-unomi-tracker.js`
+- **Estimated Effort:** 16-22 hours
+
+---
+
+## UNOMI-TRACKER-006: Phase 5 - Convert core tracker to TypeScript class with facade layer
+
+**PR 6** | **Depends on:** PR 5 (UNOMI-TRACKER-005)
+**Issue Type:** Epic
+**Priority:** Medium (P2)
+**Component:** TypeScript Migration, Extensibility
+**Labels:** modernization, typescript, core, backward-compatibility, facade
+
+**Description:**
+Convert the now-slimmed-down main tracker to a TypeScript class with the facade layer for backward compatibility. Requires both utilities (Phase 3) and features (Phase 4) to be extracted first so the core only contains orchestration logic.
+
+**What will be done:**
+Create `UnomiTracker` class with proper encapsulation. Build a facade layer so `useTracker()` still returns a plain mutable `wem` object with the exact same shape -- delegating to the internal class. This ensures consumers who override methods, access state directly, or add custom properties are not broken. Add deprecation warnings on `_` prefixed methods. Define formal public API contract.
+
+**Acceptance Criteria:**
+- [ ] Core tracker converted to TypeScript class
+- [ ] Facade returns `wem` object with exact same shape as current API
+- [ ] Monkey-patching still works (e.g., `wem.ajax = customFn`)
+- [ ] Direct state access still works (e.g., `wem.cxs`, `wem.sessionID`)
+- [ ] `_` prefixed methods log deprecation warnings
+- [ ] Public API contract defined as TypeScript interface
+- [ ] `@ts-expect-error` comments reduced by 50%+
+- [ ] 80%+ TypeScript coverage achieved
+- [ ] All tests passing
+
+**Technical Details:**
+- **Files:** `src/core/tracker.ts`, `src/facade/wem.ts`, `src/types/public-api.ts`, `src/index.ts`
+- **Estimated Effort:** 16-22 hours
+
+---
+
+## UNOMI-TRACKER-007: Phase 6 - Extension hooks + code quality
+
+**PR 7** | **Depends on:** PR 6 (UNOMI-TRACKER-006)
+**Issue Type:** Epic
+**Priority:** Medium (P2)
+**Component:** Code Quality, Extensibility
+**Labels:** modernization, refactoring, architecture, code-quality, extensibility
+
+**Description:**
+Implement the extension hooks API and address remaining code quality issues. Requires the facade/class architecture from Phase 5 so hooks can be wired into the tracker internals.
+
+**What will be done:**
+Build the extension hooks system (`beforeRequest`, `afterRequest`, `onContextLoaded`, `beforeEvent`, `afterEvent`) providing a clean alternative to monkey-patching. Replace deprecated event creation APIs with modern alternatives. Address remaining `@ts-expect-error` comments. Implement code splitting and lazy loading for optional features.
+
+**Acceptance Criteria:**
+- [ ] Extension hooks API implemented and documented
+- [ ] Hooks provide clean alternative for all common monkey-patching patterns
+- [ ] All deprecated event APIs replaced (`CustomEvent()`, `Event()` constructors)
+- [ ] Remaining `@ts-expect-error` comments addressed
+- [ ] Code splitting implemented for optional features
+- [ ] Final modular directory structure in place
+- [ ] All tests passing
+
+**Technical Details:**
+- **Files:** `src/core/hooks.ts`, `src/core/tracker.ts`, `src/facade/wem.ts`, `src/types/`, `src/index.ts`
+- **Estimated Effort:** 20-28 hours
+
+---
+
+## UNOMI-TRACKER-008: Phase 7 - Documentation + DX + final polish
+
+**PR 8** | **Depends on:** PR 7 (UNOMI-TRACKER-007)
+**Issue Type:** Epic
+**Priority:** Low (P3)
+**Component:** Documentation, Developer Experience
+**Labels:** documentation, dx, tooling, refactoring
+
+**Description:**
+Final improvements to documentation, code quality, and developer experience. Done last so docs reflect the final API surface including hooks, facade, V4.0 support, and public API contract.
+
+**What will be done:**
+Expand README with comprehensive documentation (installation, usage, API reference, V4.0 config, hooks API, migration guide for consumers). Refactor duplicated code patterns, standardize function styles. Set up ESLint + Prettier with pre-commit hooks (Husky, lint-staged). Update CI/CD pipeline.
+
+**Acceptance Criteria:**
+- [ ] README expanded with comprehensive documentation
+- [ ] V4.0 configuration documented with examples
+- [ ] Extension hooks API documented with migration examples
+- [ ] Public API contract documented
+- [ ] Duplicated code refactored (DRY)
+- [ ] Function styles standardized (KISS)
+- [ ] ESLint + Prettier configured with pre-commit hooks
+- [ ] CI/CD pipeline updated with type checking, linting, coverage
+- [ ] All checks passing
+
+**Technical Details:**
+- **Files:** `README.md`, `.eslintrc.json`, `.prettierrc`, `.husky/`, `.github/workflows/ci.yml`, source files
+- **Estimated Effort:** 12-20 hours
+
+---
+
+## 📊 Summary
+
+### Dependency Chain (Stacked PRs)
+
+```
+PR 1 UNOMI-TRACKER-001 Phase 0: Testing Infrastructure ⭐ START
+ ↓
+PR 2 UNOMI-TRACKER-002 Phase 1: Bug Fixes + V4.0 Compatibility
+ ↓
+PR 3 UNOMI-TRACKER-003 Phase 2: Build Modernization + Config
+ ↓
+PR 4 UNOMI-TRACKER-004 Phase 3: TypeScript - Utilities
+ ↓
+PR 5 UNOMI-TRACKER-005 Phase 4: TypeScript - Features
+ ↓
+PR 6 UNOMI-TRACKER-006 Phase 5: TypeScript - Core + Facade
+ ↓
+PR 7 UNOMI-TRACKER-007 Phase 6: Extension Hooks + Code Quality
+ ↓
+PR 8 UNOMI-TRACKER-008 Phase 7: Documentation + DX + Polish
+```
+
+### Ticket Statistics
+- **Total Tickets:** 8
+- **Critical (P0):** 1 (Bug fixes + V4.0)
+- **High (P1):** 1 (Testing infrastructure)
+- **Medium (P2):** 5 (Build, TypeScript phases, Code Quality)
+- **Low (P3):** 1 (Documentation + Polish)
+
+### Estimated Effort
+- **Total:** 139-205 hours (~18-26 working days)
+- **Timeline:** 26 weeks (phased approach)
+
+### Key Principles
+
+- ✅ **DRY / KISS / SOLID**: All implementations follow these engineering principles
+- ✅ **Testing First**: Complete testing infrastructure before any code changes
+- ✅ **Backward Compatible**: Facade preserves exact API shape for consumers who extend/embed
+- ✅ **Extensible**: Official hooks API replaces need for monkey-patching
+- ✅ **Dual Server Support**: Works with both Unomi V2 and V4.0 (multi-tenancy)
+- ✅ **Incremental**: Gradual migration with deprecation warnings, not big bang
+- ✅ **Stacked PRs**: Each phase = 1 PR in a strict linear chain; every PR is reviewable and mergeable independently
+
+---
+
+*Note: Ticket numbers (UNOMI-TRACKER-XXX) should be replaced with actual JIRA ticket numbers when creating tickets.*
diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md
new file mode 100644
index 0000000..9d4092f
--- /dev/null
+++ b/PROJECT_PLAN.md
@@ -0,0 +1,850 @@
+# Apache Unomi Tracker - Project Analysis & Modernization Plan
+
+**Version:** 1.0
+**Date:** 2026
+**Status:** Planning Phase
+
+---
+
+## Executive Summary
+
+This document provides a comprehensive analysis of the Apache Unomi Tracker project, identifies critical bugs, documents all issues, and outlines a modernization strategy for 2026. The plan prioritizes **testing first** to ensure backward compatibility throughout the modernization process, and includes support for **Unomi V4.0 multi-tenancy**.
+
+**Key Findings:**
+- 🔴 **2 Critical Bugs** requiring immediate fixes
+- ⚠️ **13 Issues** across documentation, code quality, and compatibility
+- 🚀 **Modernization Opportunity** to improve maintainability and type safety
+- 🔄 **V4.0 Compatibility** needed to support Unomi multi-tenancy alongside V2
+
+**Recommended Approach (8 phases, 8 stacked PRs):**
+1. **Phase 0**: Establish comprehensive test suite with server mocks (4 weeks)
+2. **Phase 1**: Fix critical bugs + add Unomi V4.0 compatibility (2 weeks)
+3. **Phase 2**: Modernize build toolchain + config cleanup (4 weeks)
+4. **Phase 3**: TypeScript migration -- extract utilities (2 weeks)
+5. **Phase 4**: TypeScript migration -- extract features (2 weeks)
+6. **Phase 5**: TypeScript migration -- core class + facade (4 weeks)
+7. **Phase 6**: Extension hooks + code quality (4 weeks)
+8. **Phase 7**: Documentation + DX + final polish (4 weeks)
+
+**Total Timeline:** 6 months (26 weeks)
+**Risk Level:** Low-Medium (incremental, backward compatible)
+**PR Strategy:** Each phase = 1 stacked PR. Strict linear dependency chain: PR N builds on PR N-1.
+
+---
+
+## Part 1: Current State Analysis
+
+### Codebase Structure
+- **Main file**: `src/apache-unomi-tracker.js` (~1,500 lines) - monolithic structure
+- **Type definitions**: `src/types.ts` (exists, well-structured)
+- **Build system**: Rollup 2.x + Babel 7.x
+- **Testing**: Minimal (basic existence check only)
+- **Module format**: Supports ESM, CJS, and UMD outputs ✅
+
+### Dependencies Status (2026)
+| Package | Current | Latest (2026) | Status | Action |
+|---------|---------|---------------|--------|--------|
+| Rollup | 2.79.0 | 4.x | ⚠️ Outdated | Upgrade |
+| Babel | 7.18.x | 8.x | ⚠️ Outdated | Replace with esbuild |
+| TypeScript | 5.8.2 | 5.7.x+ | ✅ Current | Keep |
+| ESLint | 8.23.0 | 9.x | ⚠️ Outdated | Upgrade |
+| es6-crawler-detect | 3.3.0 | 4.x | ⚠️ Outdated | Upgrade (has types in v4) |
+
+---
+
+## Part 2: Critical Bugs (Must Fix Immediately)
+
+### 🔴 Bug #1: Incorrect Array Splice Operation
+
+**Location:** `src/apache-unomi-tracker.js:448`
+**Severity:** Critical - Logic Bug
+**Status:** Verified ✅
+
+**Current Code:**
+```javascript
+var eventIndex = wem.eventsPrevented.indexOf(targetId);
+if (eventIndex !== -1) {
+ wem.eventsPrevented.splice(eventIndex, 0); // ❌ BUG: Removes 0 elements
+}
+```
+
+**The Problem:**
+- `splice(index, 0)` removes **0 elements** - it does nothing!
+- Array element is never removed
+- Causes incorrect event handling behavior
+
+**Verification:**
+```javascript
+const arr = ['a', 'b', 'c'];
+arr.splice(1, 0); // Removes 0 elements
+console.log(arr); // ['a', 'b', 'c'] - unchanged!
+```
+
+**Fix:**
+```javascript
+if (eventIndex !== -1) {
+ wem.eventsPrevented.splice(eventIndex, 1); // ✅ Remove 1 element
+}
+```
+
+**Impact:** Event prevention logic doesn't work correctly, potential memory issues.
+
+---
+
+### 🔴 Bug #2: Broken `_resolveId` Function
+
+**Location:** `src/apache-unomi-tracker.js:1420-1428`
+**Severity:** Critical - Multiple Logic Errors
+**Status:** Verified ✅ (marked with `@ts-expect-error` in code)
+
+**Current Code:**
+```javascript
+_resolveId: function (id) {
+ if (wem.digitalData.sourceLocalIdentifierMap) {
+ var source = Object.keys(wem.digitalData.sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0; // ❌ BUG 1: > 0 misses index 0
+ });
+ // @ts-expect-error This code is broken, source is an array
+ return source ? id.replace(source, wem.digitalData.sourceLocalIdentifierMap[source]) : id;
+ // ❌ BUG 2: source is an array, not a string
+ // ❌ BUG 3: Can't use array in replace()
+ }
+ return id;
+}
+```
+
+**The Problems:**
+1. **`filter()` returns array, not string** - but used as string in `replace()`
+2. **`indexOf(source) > 0` misses matches at position 0** - should be `!== -1`
+3. **Type error** - array used where string expected
+
+**Verification:**
+```javascript
+// Issue 1: filter returns array
+const source = Object.keys(map).filter(...);
+console.log(Array.isArray(source)); // true - but used as string!
+
+// Issue 2: > 0 misses index 0
+const id = 'prefix-item'; // Match at index 0
+id.indexOf('prefix') > 0; // false - should be true!
+```
+
+**Fix:**
+```javascript
+_resolveId: function (id) {
+ if (wem.digitalData.sourceLocalIdentifierMap) {
+ var sources = Object.keys(wem.digitalData.sourceLocalIdentifierMap);
+ for (var i = 0; i < sources.length; i++) {
+ var source = sources[i];
+ if (id.indexOf(source) !== -1) { // ✅ Fix: !== -1 finds all matches
+ return id.replace(source, wem.digitalData.sourceLocalIdentifierMap[source]);
+ }
+ }
+ }
+ return id;
+}
+```
+
+**Impact:** ID mapping fails, tracker can't find elements on page (forms, videos, clicks).
+
+**Test File:** See `test/bugs/bug-demonstration.test.js` for comprehensive bug verification tests.
+
+---
+
+## Part 3: Issues Found
+
+### 📚 Documentation Issues
+
+#### 1. Minimal README.md (High Priority)
+- ❌ No installation instructions
+- ❌ No usage examples
+- ❌ No API documentation
+- ❌ No configuration guide
+
+**Fix:** Expand README with installation, usage, API reference, examples.
+
+---
+
+### ⚠️ Code Quality Issues
+
+#### 2. Excessive Type Suppression (Medium Priority)
+- 14 `@ts-expect-error` comments throughout codebase
+- Masks potential runtime errors
+- Reduces code reliability
+
+**Fix:** Address type issues properly, add type guards, update type definitions.
+
+#### 3. Deprecated Event Creation APIs (Medium Priority)
+- Uses `document.createEvent()` and `initCustomEvent()` (deprecated)
+- Should use modern `CustomEvent()` and `Event()` constructors
+
+**Fix:** Replace with modern APIs (lines 795-796, 1320-1322).
+
+#### 4. Inconsistent Function Styles (Low Priority)
+- Mixes arrow functions and function expressions
+- Should standardize on one style
+
+#### 5. Incomplete Null Checking (Low Priority)
+- `_isObject` function could be more explicit about null checking
+
+---
+
+### 🔧 Compatibility Issues
+
+#### 6. Deprecated Babel Configuration (High Priority)
+- Uses deprecated `browsers` field
+- Should use modern `browserslist` format
+
+**Fix:** Add `.browserslistrc` or update `package.json`, remove deprecated field.
+
+#### 7. Incorrect TypeScript Type Definition (High Priority)
+- `XDomainRequest` incorrectly typed as `XMLHttpRequest` in `global.d.ts`
+- Should have proper interface definition
+
+**Fix:** Define proper `XDomainRequest` interface.
+
+---
+
+### 🧪 Testing Issues
+
+#### 8. Minimal Test Coverage (High Priority)
+- Only basic existence check
+- No functional tests
+- No integration tests
+- No edge case coverage
+
+**Fix:** Establish comprehensive test suite (see Phase 0 below).
+
+---
+
+## Part 4: Unomi V4.0 Compatibility
+
+### Context
+
+Apache Unomi V4.0 introduces **multi-tenancy**: all data (profiles, events, segments, rules, schemas) is now tenant-scoped. The main impact on the tracker is a new **authentication model** for API endpoints.
+
+### What Changed in V4.0
+
+| Aspect | Unomi V2 | Unomi V4.0 |
+|--------|----------|------------|
+| **Authentication** | Cookies + optional `X-Unomi-Peer` for protected events | `X-Unomi-Api-Key` header (public API key) |
+| **Multi-tenancy** | None (single tenant) | Full tenant isolation, per-tenant API keys |
+| **Endpoint paths** | `/context.json`, `/eventcollector` | **Same** -- no change |
+| **Request/response format** | `ContextRequest` / `ContextResponse` | **Same** -- no change |
+| **Protected events** | IP + `X-Unomi-Peer` header | API key scopes (or V2 compat mode) |
+
+### V2 Compatibility Mode (Server-Side)
+
+Unomi V4.0 includes a built-in V2 compatibility mode (`UNOMI-904`) that allows V2 clients to work without changes:
+- Public endpoints (`/context.json`) accept requests without authentication
+- A default tenant is used for all operations
+- Protected events still use IP + `X-Unomi-Peer`
+- This is a **transitional feature**, not a permanent solution
+
+### Tracker Compatibility Strategy
+
+**Approach: Configuration-driven authentication** -- the tracker adds API key headers when configured, and works as-is (V2 mode) when not.
+
+#### Configuration Changes
+
+Two new optional fields in `wemInitConfig`:
+
+```typescript
+wemInitConfig: {
+ contextServerUrl: string;
+ // ... existing fields ...
+
+ // NEW: Unomi V4.0 multi-tenancy support
+ apiKey?: string; // Public API key for V4.0 authentication
+ tenantId?: string; // Tenant ID (for admin/private operations, optional)
+}
+```
+
+#### Implementation Changes
+
+The `ajax()` method conditionally injects the `X-Unomi-Api-Key` header:
+
+```javascript
+// In the ajax() function, after existing header setup:
+if (wem.apiKey) {
+ xhr.setRequestHeader('X-Unomi-Api-Key', wem.apiKey);
+}
+if (wem.tenantId) {
+ xhr.setRequestHeader('X-Unomi-Tenant-Id', wem.tenantId);
+}
+```
+
+#### CORS Preflight Consideration
+
+The current tracker uses `Content-Type: text/plain;charset=UTF-8` to **avoid CORS preflight**. Adding custom headers like `X-Unomi-Api-Key` **will trigger a CORS preflight** (`OPTIONS` request).
+
+**Impact:** One extra round-trip per unique URL (browser caches preflight via `Access-Control-Max-Age`).
+
+**Why this is acceptable:**
+- The Unomi V4.0 server already supports CORS for `X-Unomi-Api-Key`
+- Preflight is cached by browsers, so the overhead is minimal
+- This only affects V4.0 mode; V2 mode remains unchanged (no preflight)
+- The security benefit of proper API key authentication outweighs the minor overhead
+
+#### Compatibility Matrix
+
+| Tracker Config | Unomi V2 Server | V4.0 (V2 compat mode) | V4.0 (native) |
+|----------------|-----------------|------------------------|----------------|
+| No API key | Works | Works | 401 Unauthorized |
+| With API key | Header ignored* | Works | Works |
+
+\* V2 servers don't check for `X-Unomi-Api-Key`, so the extra header is silently ignored -- making the tracker safely backward compatible in both directions.
+
+---
+
+## Part 5: Modernization Strategy
+
+### Stacked PR Model
+
+Each phase produces exactly one PR. Each PR depends only on the previous one, forming a strict linear chain suitable for stacked PR workflows.
+
+```
+PR 1 (Phase 0): Testing infrastructure
+ ↓
+PR 2 (Phase 1): Bug fixes + V4.0 support
+ ↓
+PR 3 (Phase 2): Build modernization + config cleanup
+ ↓
+PR 4 (Phase 3): Extract utilities → TypeScript
+ ↓
+PR 5 (Phase 4): Extract features → TypeScript
+ ↓
+PR 6 (Phase 5): Core TypeScript class + facade
+ ↓
+PR 7 (Phase 6): Extension hooks + code quality
+ ↓
+PR 8 (Phase 7): Documentation + DX + final polish
+```
+
+---
+
+### Phase 0: Testing Infrastructure (Weeks 1-4) ⭐ START HERE
+
+**PR 1** | Depends on: nothing | Ticket: UNOMI-TRACKER-001
+
+**Critical:** Must be completed before any code changes.
+
+#### Goals
+- Establish comprehensive test suite with server mocks
+- Achieve 80%+ code coverage
+- Create bug verification tests
+- Ensure backward compatibility tests
+
+#### Implementation
+
+**1. Set up Vitest + MSW**
+```json
+{
+ "devDependencies": {
+ "vitest": "^2.0.0",
+ "@vitest/ui": "^2.0.0",
+ "@vitest/coverage-v8": "^2.0.0",
+ "msw": "^2.0.0",
+ "jsdom": "^25.0.0"
+ }
+}
+```
+
+**2. Mock Unomi Server Endpoints**
+- `/context.json` (POST) - context loading
+- `/eventcollector` (POST) - event collection
+- Support success, error, timeout, network failure scenarios
+- Both V2 (no auth) and V4.0 (API key) mock handlers
+
+**3. Test Coverage**
+- Unit tests: utilities, core logic
+- Integration tests: tracker initialization, context loading, event collection
+- Backward compatibility: 100% API coverage
+- Bug verification: prove bugs exist
+
+**Deliverables:**
+- ✅ Vitest configured
+- ✅ MSW with Unomi mocks (V2 + V4.0)
+- ✅ 80%+ code coverage
+- ✅ Bug verification tests
+- ✅ All tests passing
+
+---
+
+### Phase 1: Fix Critical Bugs + Unomi V4.0 Compatibility (Weeks 5-6)
+
+**PR 2** | Depends on: PR 1 (Phase 0) | Ticket: UNOMI-TRACKER-002
+
+1. Fix Bug #1: Change `splice(eventIndex, 0)` to `splice(eventIndex, 1)`
+2. Fix Bug #2: Rewrite `_resolveId` function with correct logic
+3. Add Unomi V4.0 multi-tenancy support:
+ - Add optional `apiKey` and `tenantId` to `wemInitConfig` and types
+ - Modify `ajax()` to inject `X-Unomi-Api-Key` / `X-Unomi-Tenant-Id` headers when configured
+ - Add tests for both V2 (no API key) and V4.0 (with API key) modes
+
+**Verification:** Bug verification tests now pass. V4.0 authentication tests pass.
+
+**Deliverables:**
+- ✅ Both critical bugs fixed
+- ✅ V4.0 API key support added (~20-30 lines, additive only)
+- ✅ All tests passing
+
+---
+
+### Phase 2: Modernize Build Toolchain + Config Cleanup (Weeks 7-10)
+
+**PR 3** | Depends on: PR 2 (Phase 1) | Ticket: UNOMI-TRACKER-003
+
+#### 2.1 Upgrade Rollup to 4.x
+- Better performance, improved TypeScript support, better tree-shaking
+
+#### 2.2 Replace Babel with esbuild
+- 10-100x faster builds, native TypeScript support, smaller bundles
+- Remove `babel.config.json` entirely
+
+#### 2.3 Upgrade Dependencies
+- `es6-crawler-detect`: 3.3.0 → 4.0.0 (has TypeScript types)
+- `eslint`: 8.23.0 → 9.0.0
+- Add TypeScript ESLint plugins
+
+#### 2.4 Fix Configuration Issues
+- Add modern `browserslist` format (replaces deprecated Babel `browsers` field)
+- Fix `XDomainRequest` type definition in `global.d.ts`
+- Configure TypeScript with `allowJs` and `checkJs` for incremental migration
+
+**Deliverables:**
+- ✅ Rollup 4.x + esbuild (Babel removed)
+- ✅ All dependencies upgraded
+- ✅ Config issues fixed (browserslist, XDomainRequest type)
+- ✅ Build produces same output, faster
+
+---
+
+### Phase 3: TypeScript Migration -- Utilities (Weeks 11-12)
+
+**PR 4** | Depends on: PR 3 (Phase 2) | Ticket: UNOMI-TRACKER-004
+
+First step of the incremental "edge → trunk" migration strategy. Extract leaf-level utility functions that have no dependencies on other tracker code.
+
+- Create `src/utils/` directory
+- Extract cookie utilities (`setCookie`, `getCookie`, `removeCookie`) → `src/utils/cookies.ts`
+- Extract event builders (`buildEvent`, `buildTarget`, `buildSource`, etc.) → `src/utils/events.ts`
+- Extract AJAX utility → `src/utils/ajax.ts`
+- Extract form utilities (`_extractFormData`) → `src/utils/form.ts`
+- Extract URL utilities (`getUrlParameter`, `convertUrlParametersToObj`) → `src/utils/url.ts`
+- Update main file to import from new modules
+
+**Deliverables:**
+- ✅ All utility functions extracted and converted to TypeScript
+- ✅ Unit tests for all utilities
+- ✅ Main file imports from new modules
+- ✅ All tests passing
+
+---
+
+### Phase 4: TypeScript Migration -- Features (Weeks 13-14)
+
+**PR 5** | Depends on: PR 4 (Phase 3) | Ticket: UNOMI-TRACKER-005
+
+Extract higher-level feature modules. These depend on the utility modules extracted in Phase 3.
+
+- Create `src/features/` directory
+- Extract personalization logic → `src/features/personalization.ts`
+- Extract optimization/A/B testing logic → `src/features/optimization.ts`
+- Extract event tracking logic → `src/features/tracking.ts`
+- Feature modules import from `src/utils/` (established in Phase 3)
+- Update main file to import from new modules
+
+**Deliverables:**
+- ✅ All feature modules extracted and converted to TypeScript
+- ✅ Integration tests for all features
+- ✅ All tests passing
+
+---
+
+### Phase 5: TypeScript Migration -- Core Class + Facade (Weeks 15-18)
+
+**PR 6** | Depends on: PR 5 (Phase 4) | Ticket: UNOMI-TRACKER-006
+
+Convert the now-slimmed-down main tracker to a TypeScript class. Build the facade layer for backward compatibility. This requires both utilities (Phase 3) and features (Phase 4) to be extracted first so the core only contains orchestration logic.
+
+- Convert tracker object to `UnomiTracker` TypeScript class
+- Build facade: `useTracker()` returns plain `wem` object that delegates to the class
+- Facade exposes same public methods, same state access (via getters/setters), same mutability
+- Add deprecation warnings on `_` prefixed methods (not removed until v2.0)
+- Define formal public API contract as TypeScript interface
+- Remove `@ts-expect-error` comments (target: 50% reduction)
+
+**Deliverables:**
+- ✅ Core tracker in TypeScript with clean encapsulation
+- ✅ Facade preserves exact `wem` object shape
+- ✅ Public API contract defined and tested
+- ✅ 80%+ TypeScript coverage
+- ✅ All tests passing
+
+---
+
+### Phase 6: Extension Hooks + Code Quality (Weeks 19-22)
+
+**PR 7** | Depends on: PR 6 (Phase 5) | Ticket: UNOMI-TRACKER-007
+
+Requires the facade/class architecture from Phase 5 to be in place so hooks can be wired into the tracker internals.
+
+#### 6.1 Extension Hooks API
+- Implement official hooks system (`beforeRequest`, `afterRequest`, `onContextLoaded`, `beforeEvent`, `afterEvent`)
+- Provides clean alternative to monkey-patching
+- Follows Open/Closed Principle
+
+#### 6.2 Code Quality
+- Replace deprecated `document.createEvent()` APIs with `CustomEvent()` and `Event()` constructors
+- Address remaining `@ts-expect-error` comments
+- Implement lazy loading for optional features (personalization, optimization)
+
+#### 6.3 Final Directory Structure
+```
+src/
+ ├── index.ts # Main entry point (useTracker facade)
+ ├── core/
+ │ ├── tracker.ts # UnomiTracker class (internal)
+ │ ├── context.ts # Context loading
+ │ ├── callbacks.ts # Callback management
+ │ └── hooks.ts # Extension hooks system
+ ├── facade/
+ │ └── wem.ts # Facade builder (plain object wrapping class)
+ ├── utils/ # Utilities (converted in Phase 3)
+ ├── features/ # Features (converted in Phase 4)
+ └── types/
+ ├── index.ts # Type exports
+ └── public-api.ts # Public API contract (interface)
+```
+
+**Deliverables:**
+- ✅ Extension hooks API implemented and tested
+- ✅ Deprecated APIs replaced
+- ✅ Code splitting implemented
+- ✅ Final modular structure in place
+- ✅ All tests passing
+
+---
+
+### Phase 7: Documentation + DX + Final Polish (Weeks 23-26)
+
+**PR 8** | Depends on: PR 7 (Phase 6) | Ticket: UNOMI-TRACKER-008
+
+Done last so documentation reflects the final API surface, including hooks, facade, and V4.0 support.
+
+#### 7.1 Documentation
+- Expand README with installation, usage, API reference, examples
+- Document V4.0 configuration (`apiKey`, `tenantId`)
+- Document extension hooks API with migration examples
+- Document public API contract and migration guide for consumers
+
+#### 7.2 Code Cleanup
+- Refactor duplicated code patterns (DRY)
+- Standardize function declaration styles (KISS)
+- Improve `_isObject` and other utility functions
+
+#### 7.3 Developer Experience
+- ESLint + Prettier with pre-commit hooks (Husky, lint-staged)
+- CI/CD pipeline with type checking, linting, coverage reporting
+
+**Deliverables:**
+- ✅ Comprehensive documentation (including V4.0 + hooks)
+- ✅ Duplicated code refactored
+- ✅ ESLint + Prettier + pre-commit hooks configured
+- ✅ CI/CD pipeline updated
+- ✅ All checks passing
+
+---
+
+## Part 6: Implementation Guidelines
+
+### Engineering Principles
+
+All implementation work must follow these principles:
+
+**DRY (Don't Repeat Yourself)**
+- Extract shared logic into reusable utilities and functions
+- Eliminate duplicated patterns (e.g., duplicated context loading checks, repeated null guards)
+- Use parameterized functions instead of copy-paste variants
+- Single source of truth for configuration, constants, and type definitions
+
+**KISS (Keep It Simple, Stupid)**
+- Prefer simple, readable solutions over clever abstractions
+- Don't over-engineer the plugin/extension system -- start minimal, expand as needed
+- Each function should do one thing and be easy to understand
+- Avoid premature optimization -- measure first, optimize second
+
+**SOLID Principles**
+- **Single Responsibility**: Each module/file handles one concern (e.g., `cookies.ts` only manages cookies, `ajax.ts` only handles HTTP)
+- **Open/Closed**: The tracker is open for extension (hooks, plugins, overrides) but closed for modification (stable public API contract)
+- **Liskov Substitution**: Any wrapped or extended tracker instance must work wherever the base tracker is expected
+- **Interface Segregation**: Consumers import only what they need; modular exports prevent forced dependency on unused features
+- **Dependency Inversion**: Internal modules depend on abstractions (interfaces/types), not concrete implementations; the `ajax` function should be injectable/overridable
+
+### Testing First Approach
+
+**Critical Principle:** All code changes must have tests first.
+
+1. Write tests demonstrating current behavior (including bugs)
+2. Write tests for desired behavior
+3. Make code changes
+4. Verify tests pass
+5. Ensure no regressions
+
+### Backward Compatibility & Extensibility
+
+#### The Problem
+
+The current library exports a factory function `useTracker()` that returns a **plain mutable object** (`wem`) with ~50 methods and directly accessible state. There is no encapsulation -- all methods and properties (including those prefixed with `_`) are publicly accessible and mutable.
+
+Consumers who embed/extend the library may rely on any of these patterns:
+
+| Extension Pattern | Example | Risk Level |
+|-------------------|---------|------------|
+| **Monkey-patching methods** | `wem.ajax = myCustomAjax` | High |
+| **Adding custom methods** | `wem.myCustomTrack = function() {...}` | Medium |
+| **Direct state access** | `wem.cxs`, `wem.sessionID`, `wem.digitalData` | High |
+| **Overriding internals** | `wem._onSuccess = myHandler` | High |
+| **Wrapping the factory** | `const wem = useTracker(); wem.extra = ...` | Medium |
+| **DOM event listeners** | `document.addEventListener('wem.event', ...)` | Low |
+| **Callback system** | `digitalData.loadCallbacks`, `personalizationCallback` | Low |
+
+If we convert to a TypeScript class with private members and split code into modules, **all high-risk patterns break**.
+
+#### The Solution: Facade + Hooks Architecture
+
+The cleanest approach follows the **Open/Closed Principle**: keep the existing API surface stable while building clean internals underneath.
+
+**Layer 1: Internal Modules (new, clean)**
+```
+src/core/ → Clean TypeScript classes and functions
+src/utils/ → Pure utility functions
+src/features/ → Feature modules
+src/types/ → Type definitions and interfaces
+```
+
+**Layer 2: Facade Object (backward compatible)**
+```typescript
+// src/index.ts
+export const useTracker = () => {
+ const tracker = new UnomiTracker(); // Clean internal implementation
+
+ // The wem facade: a plain object that delegates to the internal tracker
+ // This preserves the exact same shape consumers expect
+ const wem = {
+ // --- Public state (directly accessible, as before) ---
+ get cxs() { return tracker.cxs; },
+ set cxs(v) { tracker.cxs = v; },
+ get digitalData() { return tracker.digitalData; },
+ get sessionID() { return tracker.sessionID; },
+ // ...
+
+ // --- Public methods (same signatures as before) ---
+ initTracker: (d) => tracker.initTracker(d),
+ startTracker: (o) => tracker.startTracker(o),
+ loadContext: (...args) => tracker.loadContext(...args),
+ collectEvent: (...args) => tracker.collectEvent(...args),
+ buildEvent: (...args) => tracker.buildEvent(...args),
+ ajax: (opts) => tracker.ajax(opts),
+ // ... all ~25 public methods ...
+
+ // --- "Private" methods (deprecated, with warnings) ---
+ _onSuccess: (...args) => {
+ console.warn('[WEM] _onSuccess is internal and may change. Use loadCallbacks instead.');
+ return tracker._onSuccess(...args);
+ },
+ // ... other _ methods with deprecation warnings ...
+ };
+
+ return wem;
+};
+```
+
+**Key properties of this approach:**
+- The returned `wem` object has the **exact same shape** as today
+- Methods can still be monkey-patched (the object is mutable)
+- State is still directly accessible via getters/setters
+- Internal implementation is clean TypeScript with proper encapsulation
+- `_` prefixed methods log deprecation warnings, guiding consumers to official APIs
+- Follows **Liskov Substitution**: the facade is a drop-in replacement
+
+**Layer 3: Extension Hooks (new, opt-in)**
+
+For consumers who need to extend behavior, provide official hooks instead of requiring monkey-patching:
+
+```typescript
+// Official extension points (Open/Closed Principle)
+wem.hooks = {
+ // Middleware for ajax requests (replaces monkey-patching wem.ajax)
+ beforeRequest: (callback) => tracker.addHook('beforeRequest', callback),
+ afterRequest: (callback) => tracker.addHook('afterRequest', callback),
+
+ // Context lifecycle hooks (replaces overriding _onSuccess)
+ onContextLoaded: (callback) => tracker.addHook('contextLoaded', callback),
+ onContextError: (callback) => tracker.addHook('contextError', callback),
+
+ // Event hooks (replaces overriding collectEvent)
+ beforeEvent: (callback) => tracker.addHook('beforeEvent', callback),
+ afterEvent: (callback) => tracker.addHook('afterEvent', callback),
+};
+```
+
+**Usage example for consumers who currently monkey-patch:**
+```javascript
+// BEFORE (fragile monkey-patching):
+const { wem } = useTracker();
+const originalAjax = wem.ajax;
+wem.ajax = function(options) {
+ options.headers = { ...options.headers, 'X-Custom': 'value' };
+ return originalAjax.call(this, options);
+};
+
+// AFTER (official hooks API):
+const { wem } = useTracker();
+wem.hooks.beforeRequest((options) => {
+ options.headers = { ...options.headers, 'X-Custom': 'value' };
+ return options;
+});
+```
+
+#### Migration Path for Consumers
+
+| Version | What Changes | Consumer Impact |
+|---------|-------------|-----------------|
+| **v1.5.x** (current) | Bug fixes + V4.0 support | None -- additive only |
+| **v1.6.x** | Internal refactoring behind facade | None -- same API shape |
+| **v1.7.x** | `_` methods log deprecation warnings | Warnings in console for internal method usage |
+| **v1.8.x** | Hooks API available | Consumers can migrate off monkey-patching |
+| **v2.0.0** | `_` methods removed, class-only export option | Breaking for consumers using `_` methods |
+
+This gives consumers **at least 3 minor versions** of warning before anything breaks.
+
+#### Formal Public API Contract
+
+To make this explicit, the public API surface is defined as:
+
+**Stable (guaranteed across v1.x):**
+- `useTracker()` factory function
+- All methods without `_` prefix (~25 methods)
+- State properties: `cxs`, `digitalData`, `sessionID`, `fallback`, `DOMLoaded`
+- `wemInitConfig` shape (additive changes only)
+- DOM events dispatched by `dispatchJSEvent`
+- Callback system: `loadCallbacks`, `personalizationCallback`
+
+**Internal (may change with deprecation notice):**
+- All `_` prefixed methods (~15 methods)
+- Internal state: `contextLoaded`, `eventsPrevented`, `formNamesToWatch`
+
+### Versioning Strategy
+
+- **v1.5.x**: Current version + bug fixes + V4.0 support
+- **v1.6.x-v1.8.x**: Internal modernization behind facade, hooks API
+- **v2.0.0**: Future major version (can remove deprecated `_` methods, class-only export)
+
+---
+
+## Part 7: Success Metrics
+
+### Code Quality
+- [ ] All code follows DRY, KISS, and SOLID principles
+- [ ] 80%+ TypeScript coverage
+- [ ] 0 `@ts-expect-error` comments (or documented)
+- [ ] 80%+ test coverage
+- [ ] All ESLint rules passing
+- [ ] No duplicated logic patterns
+
+### Performance
+- [ ] Bundle size maintained or reduced
+- [ ] Build time improved (target: 50% faster)
+- [ ] Runtime performance maintained
+
+### Compatibility
+- [ ] All existing tests pass
+- [ ] No breaking changes in public API
+- [ ] All output formats work (CJS, ESM, UMD)
+- [ ] Tracker works with Unomi V2 (no API key configured)
+- [ ] Tracker works with Unomi V4.0 (API key configured)
+- [ ] Tracker works with Unomi V4.0 in V2 compatibility mode
+
+### Extensibility
+- [ ] Facade preserves exact `wem` object shape for consumers who extend/embed
+- [ ] Monkey-patching still works on the facade object
+- [ ] Direct state access (`wem.cxs`, `wem.sessionID`) still works
+- [ ] Hooks API available for clean extensibility
+- [ ] `_` prefixed methods log deprecation warnings (not removed until v2.0)
+- [ ] Public API contract documented and tested
+
+---
+
+## Part 8: Quick Wins (Can Start Immediately)
+
+1. **Set up Vitest and MSW** ⭐ START HERE
+2. **Create bug verification tests**
+3. **Fix critical bugs** (after tests are in place)
+4. **Upgrade es6-crawler-detect to v4** (has types)
+5. **Set up ESLint for TypeScript**
+
+---
+
+## Appendix: Test Examples
+
+### Bug Verification Test
+See `test/bugs/bug-demonstration.test.js` for comprehensive bug verification.
+
+### Server Mock Examples
+
+#### V2 Mode (no authentication)
+```typescript
+// test/mocks/handlers-v2.ts
+import { http, HttpResponse } from 'msw';
+
+export const v2Handlers = [
+ http.post('*/context.json', () => {
+ return HttpResponse.json({
+ profileId: 'test-profile-123',
+ sessionId: 'test-session-456',
+ });
+ }),
+ http.post('*/eventcollector', () => {
+ return HttpResponse.json({ eventsProcessed: 1 });
+ })
+];
+```
+
+#### V4.0 Mode (API key authentication)
+```typescript
+// test/mocks/handlers-v3.ts
+import { http, HttpResponse } from 'msw';
+
+const VALID_API_KEY = 'test-public-api-key-123';
+
+export const v3Handlers = [
+ http.post('*/context.json', ({ request }) => {
+ const apiKey = request.headers.get('X-Unomi-Api-Key');
+ if (!apiKey || apiKey !== VALID_API_KEY) {
+ return new HttpResponse('Unauthorized', { status: 401 });
+ }
+ return HttpResponse.json({
+ profileId: 'test-profile-123',
+ sessionId: 'test-session-456',
+ });
+ }),
+ http.post('*/eventcollector', ({ request }) => {
+ const apiKey = request.headers.get('X-Unomi-Api-Key');
+ if (!apiKey || apiKey !== VALID_API_KEY) {
+ return new HttpResponse('Unauthorized', { status: 401 });
+ }
+ return HttpResponse.json({ eventsProcessed: 1 });
+ })
+];
+```
+
+---
+
+**Document Version:** 1.3
+**Last Updated:** January 2026
+**Changelog:**
+- v1.3 - Restructured into 8 linear phases for stacked PR workflow; aligned phase/ticket numbering
+- v1.2 - Added extensibility/compatibility layer strategy (facade + hooks), DRY/KISS/SOLID principles, public API contract
+- v1.1 - Added Unomi V4.0 multi-tenancy compatibility analysis and plan
diff --git a/test/bugs/bug-demonstration.test.js b/test/bugs/bug-demonstration.test.js
new file mode 100644
index 0000000..0e600c7
--- /dev/null
+++ b/test/bugs/bug-demonstration.test.js
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Bug Demonstration Tests
+ *
+ * These tests demonstrate the bugs found in the codebase.
+ * They are expected to FAIL, proving the bugs exist.
+ * Once the bugs are fixed, these tests should be updated to verify the fixes.
+ */
+
+/**
+ * IMPORTANT: These tests are designed to DEMONSTRATE bugs exist.
+ * Some tests will show incorrect behavior (proving bugs).
+ * After bugs are fixed, update these tests to verify the fixes work.
+ */
+
+// Note: Using require for now since we're testing current JS implementation
+// In full test suite, we'll use ES modules
+const { describe, it, expect } = require('vitest');
+
+describe('Bug Demonstrations - These tests prove bugs exist', () => {
+
+ describe('BUG #1: Incorrect splice operation in sendClickEvent (Line 448)', () => {
+ /**
+ * BUG DESCRIPTION:
+ * In sendClickEvent, when an event is already in eventsPrevented array,
+ * the code uses splice(eventIndex, 0) which does NOT remove the element.
+ * This causes the array to grow indefinitely and prevents proper event handling.
+ *
+ * EXPECTED BEHAVIOR:
+ * - If event is already prevented, remove it from array (to allow it again)
+ * - OR: If event is already prevented, skip adding it again
+ *
+ * ACTUAL BEHAVIOR:
+ * - splice(eventIndex, 0) removes 0 elements, so element stays in array
+ * - Array grows indefinitely on repeated clicks
+ */
+
+ it('should demonstrate that splice(eventIndex, 0) does not remove elements', () => {
+ // Simulate the buggy code
+ const eventsPrevented = ['button1', 'button2'];
+ const eventIndex = eventsPrevented.indexOf('button1');
+
+ expect(eventIndex).toBe(0); // button1 is at index 0
+
+ // This is what the current code does (WRONG)
+ eventsPrevented.splice(eventIndex, 0);
+
+ // The element is still there! (BUG)
+ expect(eventsPrevented.length).toBe(2); // Should be 1 if we wanted to remove it
+ expect(eventsPrevented).toContain('button1'); // Still present
+
+ // What it SHOULD do:
+ // eventsPrevented.splice(eventIndex, 1); // Remove 1 element
+ // expect(eventsPrevented.length).toBe(1);
+ // expect(eventsPrevented).not.toContain('button1');
+ });
+
+ it('should demonstrate eventsPrevented array grows indefinitely', () => {
+ // Simulate multiple clicks on the same button
+ const eventsPrevented = [];
+ const targetId = 'my-button';
+
+ // First click - adds to array
+ if (eventsPrevented.indexOf(targetId) === -1) {
+ eventsPrevented.push(targetId);
+ }
+ expect(eventsPrevented.length).toBe(1);
+
+ // Second click - should remove or skip, but current code does nothing
+ const eventIndex = eventsPrevented.indexOf(targetId);
+ if (eventIndex !== -1) {
+ // BUGGY CODE: This doesn't remove anything
+ eventsPrevented.splice(eventIndex, 0);
+ } else {
+ eventsPrevented.push(targetId);
+ }
+
+ // Array still has the element (BUG)
+ expect(eventsPrevented.length).toBe(1); // Actually this test shows the logic issue
+ // But if the code path is wrong, it might add again:
+
+ // Third click - might add again because splice didn't remove it
+ const eventIndex2 = eventsPrevented.indexOf(targetId);
+ if (eventIndex2 !== -1) {
+ eventsPrevented.splice(eventIndex2, 0); // Still doesn't remove
+ } else {
+ eventsPrevented.push(targetId); // This won't execute
+ }
+
+ // The real issue: if the logic intended to remove, it doesn't work
+ // If the logic intended to skip, the array management is broken
+ });
+
+ it('should show the correct behavior after fix', () => {
+ // This test shows what SHOULD happen after the bug is fixed
+ const eventsPrevented = ['button1'];
+ const eventIndex = eventsPrevented.indexOf('button1');
+
+ // CORRECT CODE: Remove 1 element
+ eventsPrevented.splice(eventIndex, 1);
+
+ expect(eventsPrevented.length).toBe(0);
+ expect(eventsPrevented).not.toContain('button1');
+ });
+ });
+
+ describe('BUG #2: Broken _resolveId function (Lines 1420-1428)', () => {
+ /**
+ * BUG DESCRIPTION:
+ * The _resolveId function has multiple issues:
+ * 1. Uses filter() which returns an array, but treats result as a string
+ * 2. Uses indexOf(source) > 0 instead of >= 0 or !== -1 (misses matches at position 0)
+ * 3. Tries to use array as string in replace() operation
+ *
+ * EXPECTED BEHAVIOR:
+ * - Should find source patterns in the ID
+ * - Should replace matched patterns with mapped values
+ * - Should work for matches at any position (including index 0)
+ *
+ * ACTUAL BEHAVIOR:
+ * - Filter returns array, not string
+ * - replace() fails because source is an array
+ * - Misses matches at the beginning of the string
+ */
+
+ it('should demonstrate that filter returns an array, not a string', () => {
+ // Simulate the buggy code
+ const sourceLocalIdentifierMap = {
+ 'old-prefix': 'new-prefix',
+ 'another-old': 'another-new'
+ };
+
+ const id = 'old-prefix-item-123';
+
+ // This is what the current code does (WRONG)
+ const source = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0; // BUG: > 0 misses index 0
+ });
+
+ // source is an ARRAY, not a string!
+ expect(Array.isArray(source)).toBe(true);
+ expect(typeof source).toBe('object');
+ expect(source).not.toBe('old-prefix'); // It's an array containing 'old-prefix'
+
+ // The code then tries to do: id.replace(source, ...)
+ // This will fail because replace() expects a string, not an array
+ expect(() => {
+ id.replace(source, sourceLocalIdentifierMap[source]); // This would fail
+ }).toThrow(); // Or return wrong result
+ });
+
+ it('should demonstrate that indexOf > 0 misses matches at position 0', () => {
+ const sourceLocalIdentifierMap = {
+ 'prefix': 'new-prefix'
+ };
+
+ const id = 'prefix-item-123'; // Match at index 0
+ const id2 = 'some-prefix-item-123'; // Match at index 5
+
+ // Current buggy code
+ const source1 = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0; // BUG: > 0
+ });
+
+ const source2 = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id2.indexOf(source) > 0;
+ });
+
+ // First ID has match at index 0, so > 0 is false, filter returns empty array
+ expect(source1.length).toBe(0); // BUG: Should find 'prefix'
+
+ // Second ID has match at index 5, so > 0 is true, filter returns array with 'prefix'
+ expect(source2.length).toBe(1); // This one works (by accident)
+
+ // CORRECT CODE should use !== -1 or >= 0
+ const correctSource1 = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) !== -1; // CORRECT
+ });
+ expect(correctSource1.length).toBe(1); // Now it finds it
+ });
+
+ it('should demonstrate the complete broken behavior', () => {
+ const sourceLocalIdentifierMap = {
+ 'old': 'new'
+ };
+
+ const id = 'old-item'; // Match at index 0
+
+ // Simulate the complete buggy function
+ function buggy_resolveId(id) {
+ if (sourceLocalIdentifierMap) {
+ var source = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0; // BUG 1: > 0 misses index 0
+ });
+ // BUG 2: source is an array, not a string
+ // BUG 3: Can't use array in replace()
+ return source ? id.replace(source, sourceLocalIdentifierMap[source]) : id;
+ }
+ return id;
+ }
+
+ // This will either throw an error or return wrong result
+ const result = buggy_resolveId(id);
+
+ // The function likely returns the original ID unchanged (because source is empty array, falsy)
+ // OR it throws an error when trying to use array in replace()
+ expect(result).toBe(id); // Returns unchanged (wrong behavior)
+ // OR expect(() => buggy_resolveId(id)).toThrow(); // If replace() throws
+ });
+
+ it('should show the correct behavior after fix', () => {
+ const sourceLocalIdentifierMap = {
+ 'old': 'new',
+ 'prefix': 'new-prefix'
+ };
+
+ // CORRECT implementation
+ function correct_resolveId(id) {
+ if (sourceLocalIdentifierMap) {
+ var sources = Object.keys(sourceLocalIdentifierMap);
+ for (var i = 0; i < sources.length; i++) {
+ var source = sources[i];
+ if (id.indexOf(source) !== -1) { // CORRECT: !== -1 finds all matches
+ return id.replace(source, sourceLocalIdentifierMap[source]);
+ }
+ }
+ }
+ return id;
+ }
+
+ // Test with match at index 0
+ expect(correct_resolveId('old-item')).toBe('new-item');
+
+ // Test with match at index > 0
+ expect(correct_resolveId('some-old-item')).toBe('some-new-item');
+
+ // Test with no match
+ expect(correct_resolveId('other-item')).toBe('other-item');
+ });
+
+ it('should demonstrate real-world impact: ID mapping fails', () => {
+ // Real-world scenario: Unomi backend uses IDs like "site:page:123"
+ // Frontend uses IDs like "page-123"
+ // sourceLocalIdentifierMap should map between them
+
+ const sourceLocalIdentifierMap = {
+ 'site:page:': 'page-',
+ 'site:video:': 'video-'
+ };
+
+ // Backend ID from Unomi
+ const backendId = 'site:page:123';
+
+ // Current buggy code
+ function buggy_resolveId(id) {
+ if (sourceLocalIdentifierMap) {
+ var source = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0; // BUG: 'site:page:' is at index 0, so > 0 is false
+ });
+ return source ? id.replace(source, sourceLocalIdentifierMap[source]) : id;
+ }
+ return id;
+ }
+
+ const result = buggy_resolveId(backendId);
+
+ // Should be 'page-123' but buggy code returns unchanged
+ expect(result).toBe(backendId); // BUG: Returns 'site:page:123' instead of 'page-123'
+
+ // This means the tracker can't find elements on the page!
+ // document.getElementById('page-123') won't find element with id 'site:page:123'
+ });
+ });
+
+ describe('Integration: How bugs affect real functionality', () => {
+ it('should demonstrate Bug #1 prevents proper click event handling', () => {
+ // Scenario: User clicks a button multiple times
+ // After first click, event should be prevented and tracked
+ // On subsequent clicks, the behavior should be consistent
+
+ const eventsPrevented = [];
+ const targetId = 'submit-button';
+
+ // First click
+ let eventIndex = eventsPrevented.indexOf(targetId);
+ if (eventIndex === -1) {
+ eventsPrevented.push(targetId);
+ // Event is prevented and sent to Unomi
+ }
+ expect(eventsPrevented.length).toBe(1);
+
+ // Second click - current buggy code
+ eventIndex = eventsPrevented.indexOf(targetId);
+ if (eventIndex !== -1) {
+ eventsPrevented.splice(eventIndex, 0); // BUG: Doesn't remove
+ }
+
+ // Array still has the element
+ expect(eventsPrevented.length).toBe(1);
+ expect(eventsPrevented).toContain(targetId);
+
+ // The logic is unclear:
+ // - If it should remove: bug prevents removal
+ // - If it should skip: why check and do nothing?
+ // Either way, the behavior is incorrect
+ });
+
+ it('should demonstrate Bug #2 prevents element lookup', () => {
+ // Scenario: Unomi returns tracked condition with ID "site:page:contact-form"
+ // Frontend HTML has id="contact-form"
+ // sourceLocalIdentifierMap should map "site:page:" to ""
+
+ const sourceLocalIdentifierMap = {
+ 'site:page:': '',
+ 'site:video:': ''
+ };
+
+ const unomiId = 'site:page:contact-form';
+ const expectedFrontendId = 'contact-form';
+
+ // Current buggy code
+ function buggy_resolveId(id) {
+ if (sourceLocalIdentifierMap) {
+ var source = Object.keys(sourceLocalIdentifierMap).filter(function (source) {
+ return id.indexOf(source) > 0; // BUG: 'site:page:' at index 0
+ });
+ return source ? id.replace(source, sourceLocalIdentifierMap[source]) : id;
+ }
+ return id;
+ }
+
+ const resolvedId = buggy_resolveId(unomiId);
+
+ // Should resolve to 'contact-form' but doesn't
+ expect(resolvedId).toBe(unomiId); // BUG: Returns 'site:page:contact-form'
+
+ // Now try to find element:
+ // document.getElementById('contact-form') - won't find it
+ // document.getElementById('site:page:contact-form') - also won't find it (wrong ID)
+ // Result: Tracker can't attach listeners to form!
+ });
+ });
+});
(unomi-tracker) 03/03: Merge remote-tracking branch 'origin/main'
Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
sergehuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/unomi-tracker.git
commit c7d48c662246c015069c63a92afb422f1d5438d4
Merge: 54a94ef bb019ed
Author: Serge Huber <sh...@apache.org>
AuthorDate: Tue Oct 6 15:56:30 2026 +0200
Merge remote-tracking branch 'origin/main'
.asf.yaml | 14 ++++++++++++++
AGENTS.md | 30 ++++++++++++++++++++++++++++++
SECURITY.md | 30 ++++++++++++++++++++++++++++++
3 files changed, 74 insertions(+)