From 3681e4bcccb908eb83d19f149f74f4305e490583 Mon Sep 17 00:00:00 2001 From: Patricio Date: Mon, 1 Sep 2025 12:30:11 +0200 Subject: [PATCH] claude mds instrumentation --- CLAUDE.md | 554 +++++++++++++++++++++++ libraries/browser-tracker-core/CLAUDE.md | 452 ++++++++++++++++++ libraries/tracker-core/CLAUDE.md | 384 ++++++++++++++++ plugins/CLAUDE.md | 413 +++++++++++++++++ trackers/CLAUDE.md | 474 +++++++++++++++++++ 5 files changed, 2277 insertions(+) create mode 100644 CLAUDE.md create mode 100644 libraries/browser-tracker-core/CLAUDE.md create mode 100644 libraries/tracker-core/CLAUDE.md create mode 100644 plugins/CLAUDE.md create mode 100644 trackers/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..a04310f6b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,554 @@ +# Snowplow JavaScript Tracker - CLAUDE.md + +## Project Overview + +The Snowplow JavaScript Tracker is a comprehensive analytics tracking library for web, browser, and React Native applications. It provides event tracking capabilities for sending data to Snowplow collectors. The project uses a monorepo structure managed by Rush.js with TypeScript as the primary language. + +### Key Technologies +- **Language**: TypeScript/JavaScript +- **Build System**: Rush.js (monorepo) + Rollup +- **Package Manager**: pnpm (v9.7.1) +- **Testing**: Jest with jsdom +- **Target Environments**: Browser, Node.js, React Native + +## Development Commands + +```bash +# Install dependencies and build all packages +rush install +rush build + +# Run tests +rush test # Run all tests +rush test:unit # Unit tests only +rush test:e2e # E2E tests + +# Build specific package +cd trackers/browser-tracker && rushx build + +# Quality checks +rush lint # ESLint +rush format # Prettier +``` + +## Architecture + +### System Design +The tracker follows a **layered plugin architecture** with core libraries providing fundamental tracking capabilities and plugins extending functionality for specific use cases. + +``` +┌─────────────────────────────────────────┐ +│ Trackers Layer │ +│ (browser, javascript, node, react-native) │ +├─────────────────────────────────────────┤ +│ Plugins Layer │ +│ (30+ browser plugins for features) │ +├─────────────────────────────────────────┤ +│ Core Libraries Layer │ +│ (tracker-core, browser-tracker-core) │ +└─────────────────────────────────────────┘ +``` + +### Module Organization +- **libraries/**: Core tracking functionality + - `tracker-core`: Platform-agnostic tracking logic + - `browser-tracker-core`: Browser-specific core features +- **trackers/**: Platform-specific tracker implementations +- **plugins/**: Feature-specific plugins (ad tracking, media, ecommerce, etc.) +- **common/**: Shared Rush.js configuration and scripts + +## Core Architectural Principles + +### 1. Plugin-Based Architecture +```typescript +// ✅ Correct: Plugin pattern with activation lifecycle +export function MyPlugin(): BrowserPlugin { + return { + activateBrowserPlugin: (tracker) => { + _trackers[tracker.id] = tracker; + } + }; +} + +// ❌ Wrong: Direct tracker manipulation +export function myFeature(tracker) { + tracker.someMethod(); // Breaks encapsulation +} +``` + +### 2. Event Builder Pattern +```typescript +// ✅ Correct: Use builder functions for events +import { buildSelfDescribingEvent } from '@snowplow/tracker-core'; +const event = buildSelfDescribingEvent({ schema, data }); + +// ❌ Wrong: Manual event construction +const event = { e: 'ue', ue_pr: {...} }; // Don't construct manually +``` + +### 3. Type-Safe Self-Describing JSON +```typescript +// ✅ Correct: Typed SelfDescribingJson +export type MyEventData = { + field: string; + value: number; +}; +const event: SelfDescribingJson = { + schema: 'iglu:com.example/event/jsonschema/1-0-0', + data: { field: 'test', value: 42 } +}; + +// ❌ Wrong: Untyped data objects +const event = { schema: '...', data: someData }; +``` + +### 4. Workspace Dependencies +```typescript +// ✅ Correct: Use workspace protocol for internal deps +"dependencies": { + "@snowplow/tracker-core": "workspace:*" +} + +// ❌ Wrong: Version-specific internal dependencies +"dependencies": { + "@snowplow/tracker-core": "^3.0.0" +} +``` + +## Layer Organization & Responsibilities + +### Core Libraries (`libraries/`) +- **tracker-core**: Event building, payload construction, emitter logic +- **browser-tracker-core**: Browser APIs, storage, cookies, DOM helpers + +### Trackers (`trackers/`) +- **browser-tracker**: Modern browser tracking API +- **javascript-tracker**: Legacy SP.js compatible tracker +- **node-tracker**: Server-side Node.js tracking +- **react-native-tracker**: Mobile app tracking + +### Plugins (`plugins/`) +Each plugin follows the standard structure: +``` +browser-plugin-*/ +├── src/ +│ ├── index.ts # Plugin export & registration +│ ├── api.ts # Public API functions +│ ├── types.ts # TypeScript definitions +│ └── schemata.ts # Event schemas +├── test/ +├── package.json +└── rollup.config.js +``` + +## Critical Import Patterns + +### 1. Core Imports +```typescript +// ✅ Correct: Import from package roots +import { BrowserPlugin } from '@snowplow/browser-tracker-core'; +import { buildSelfDescribingEvent } from '@snowplow/tracker-core'; + +// ❌ Wrong: Deep imports into src +import { something } from '@snowplow/browser-tracker-core/src/helpers'; +``` + +### 2. Plugin Registration +```typescript +// ✅ Correct: Plugin initialization pattern +import { AdTrackingPlugin } from '@snowplow/browser-plugin-ad-tracking'; + +newTracker('sp', 'collector.example.com', { + plugins: [AdTrackingPlugin()] +}); + +// ❌ Wrong: Direct plugin function calls +AdTrackingPlugin.trackAdClick(); // Plugins need registration +``` + +### 3. Type Exports +```typescript +// ✅ Correct: Re-export types with functions +export { AdClickEvent, trackAdClick } from './api'; + +// ❌ Wrong: Forget to export event types +export { trackAdClick } from './api'; // Missing type export +``` + +## Essential Library Patterns + +### Event Tracking Pattern +```typescript +// Standard event tracking with context +export function trackMyEvent( + event: MyEvent & CommonEventProperties, + trackers: Array = Object.keys(_trackers) +) { + dispatchToTrackersInCollection(trackers, _trackers, (t) => { + t.core.track(buildMyEvent(event), event.context, event.timestamp); + }); +} +``` + +### Plugin State Management +```typescript +// Plugin-scoped tracker registry +const _trackers: Record = {}; + +export function MyPlugin(): BrowserPlugin { + return { + activateBrowserPlugin: (tracker) => { + _trackers[tracker.id] = tracker; + } + }; +} +``` + +### Schema Definition Pattern +```typescript +// schemata.ts - Centralized schema constants +export const MY_SCHEMA = 'iglu:com.snowplowanalytics.snowplow/my_event/jsonschema/1-0-0'; + +// Usage in event builders +const event: SelfDescribingJson = { + schema: MY_SCHEMA, + data: eventData +}; +``` + +## Model Organization Pattern + +### Event Type Definitions +```typescript +// types.ts - Event data interfaces +export interface MediaPlayerEvent { + currentTime: number; + duration?: number; + ended: boolean; + loop: boolean; + // ... other fields +} + +// Composite types for API +export type MediaTrackEvent = MediaPlayerEvent & { + eventType: MediaEventType; + label?: string; +}; +``` + +### Context Builders +```typescript +// Context creation pattern +export function buildMediaContext(player: MediaPlayer): SelfDescribingJson { + return { + schema: MEDIA_PLAYER_SCHEMA, + data: { + currentTime: player.currentTime, + duration: player.duration, + // ... map player state + } + }; +} +``` + +## Common Pitfalls & Solutions + +### 1. Forgetting Plugin Registration +```typescript +// ❌ Wrong: Using plugin API without registration +import { trackAdClick } from '@snowplow/browser-plugin-ad-tracking'; +trackAdClick(event); // Error: _trackers is empty + +// ✅ Correct: Register plugin first +import { AdTrackingPlugin, trackAdClick } from '@snowplow/browser-plugin-ad-tracking'; +newTracker('sp', 'collector.example.com', { + plugins: [AdTrackingPlugin()] +}); +trackAdClick(event); // Works correctly +``` + +### 2. Incorrect Timestamp Types +```typescript +// ❌ Wrong: String timestamps +event.timestamp = '2024-01-01T00:00:00Z'; + +// ✅ Correct: Use Timestamp ADT +event.timestamp = { type: 'ttm', value: Date.now() }; +// or just number for device timestamp +event.timestamp = Date.now(); +``` + +### 3. Manual Payload Construction +```typescript +// ❌ Wrong: Building payloads manually +const payload = { + e: 'se', + se_ca: 'category', + se_ac: 'action' +}; + +// ✅ Correct: Use builder functions +const payload = buildStructEvent({ + category: 'category', + action: 'action' +}); +``` + +## File Structure Template + +### New Plugin Structure +``` +plugins/browser-plugin-[feature]/ +├── src/ +│ ├── index.ts # Plugin export +│ ├── api.ts # Public tracking functions +│ ├── types.ts # TypeScript interfaces +│ ├── schemata.ts # Schema constants +│ └── contexts.ts # Context builders +├── test/ +│ └── [feature].test.ts # Jest tests +├── package.json # Workspace package +├── tsconfig.json # TypeScript config +├── rollup.config.js # Build configuration +└── README.md # Documentation +``` + +## Testing Patterns + +### Test Directory Structure +Each package maintains its own `test/` directory with TypeScript test files: +``` +package/ +├── test/ +│ ├── *.test.ts # Unit tests +│ ├── integration/ # Integration tests +│ ├── functional/ # E2E/browser tests +│ └── __snapshots__/ # Jest snapshots +``` + +### Test File Naming Conventions +```typescript +// ✅ Correct: Descriptive test file names +events.test.ts // Testing event functions +contexts.test.ts // Testing context builders +browser_props.test.ts // Testing browser properties + +// ❌ Wrong: Generic or unclear names +test.test.ts // Too generic +unit.test.ts // Not descriptive +``` + +### Test Suite Structure +```typescript +// ✅ Correct: Nested describe blocks with clear naming +describe('AdTrackingPlugin', () => { + describe('#trackAdClick', () => { + it('tracks click event with correct schema', () => {}); + it('includes required fields in payload', () => {}); + }); +}); + +// ❌ Wrong: Flat structure without context +describe('tests', () => { + it('test1', () => {}); + it('test2', () => {}); +}); +``` + +### Common Test Utilities +```typescript +// Using lodash/fp for test data extraction +import F from 'lodash/fp'; + +const getUEEvents = F.compose(F.filter(F.compose(F.eq('ue'), F.get('e')))); +const extractEventProperties = F.map(F.compose( + F.get('data'), + (cx: string) => JSON.parse(cx), + F.get('ue_pr') +)); +``` + +### Mock Setup Pattern +```typescript +// Standard mock configuration in tests +beforeAll(() => { + jest.spyOn(document, 'title', 'get').mockReturnValue('Test Title'); +}); + +beforeEach(() => { + jest.clearAllMocks(); + // Reset tracker state between tests +}); +``` + +### Snapshot Testing +```typescript +// ✅ Correct: Snapshot for complex objects +expect(buildMediaContext(player)).toMatchSnapshot(); + +// ❌ Wrong: Snapshot for simple values +expect(event.id).toMatchSnapshot(); // Use toBe() instead +``` + +### Test Categories & Best Practices + +#### 1. Unit Tests +Test individual functions and modules in isolation: +```typescript +// ✅ Correct: Testing pure functions +it('makeDimension correctly floors dimension type values', () => { + expect(makeDimension(800.5)).toBe(800); + expect(makeDimension('not-a-number')).toBe(0); +}); +``` + +#### 2. Integration Tests +Test multi-component interactions: +```typescript +// ✅ Correct: Testing plugin integration with tracker +it('registers plugin and tracks events', () => { + const tracker = createTracker({ plugins: [MyPlugin()] }); + trackMyEvent({ data: 'test' }); + expect(eventStore.getEvents()).toHaveLength(1); +}); +``` + +#### 3. Functional/E2E Tests +Browser automation tests for full tracker behavior: +```typescript +// Located in test/functional/ +it('persists session across page reloads', async () => { + await browser.url('/test-page'); + const sessionId = await browser.execute(() => tracker.getSessionId()); + await browser.refresh(); + const newSessionId = await browser.execute(() => tracker.getSessionId()); + expect(sessionId).toBe(newSessionId); +}); +``` + +### Testing Configuration + +#### Jest Configuration Pattern +```javascript +// jest.config.js - Standard config for packages +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['../../setupTestGlobals.ts'], + reporters: ['jest-standard-reporter'] +}; +``` + +#### Global Test Setup +```typescript +// setupTestGlobals.ts - Shared test utilities +import 'whatwg-fetch'; +global.crypto = { + getRandomValues: (buffer) => nodeCrypto.randomFillSync(buffer) +}; +``` + +### Event Store Testing Pattern +```typescript +// ✅ Correct: Using in-memory event store for tests +import { newInMemoryEventStore } from '@snowplow/tracker-core'; + +const eventStore = newInMemoryEventStore(); +const tracker = createTracker({ eventStore }); +// Verify events: eventStore.getEvents() +``` + +### Testing Async Behavior +```typescript +// ✅ Correct: Testing timer-based functionality +it('sends ping event after interval', async () => { + jest.useFakeTimers(); + startPingInterval(player, 30); + jest.advanceTimersByTime(30000); + await Promise.resolve(); // Allow async operations + expect(trackMediaPing).toHaveBeenCalled(); + jest.useRealTimers(); +}); +``` + +## Quick Reference + +### Import Checklist +- [ ] Import from package root, not `/src` +- [ ] Include type exports with functions +- [ ] Use `workspace:*` for internal dependencies +- [ ] Import builders from `@snowplow/tracker-core` +- [ ] Import browser utils from `@snowplow/browser-tracker-core` + +### Common Tasks + +```typescript +// Create new tracker +import { newTracker } from '@snowplow/browser-tracker'; +newTracker('sp', 'collector.example.com', { + appId: 'my-app', + plugins: [/* plugins */] +}); + +// Track custom event +import { trackSelfDescribingEvent } from '@snowplow/browser-tracker'; +trackSelfDescribingEvent({ + event: { + schema: 'iglu:com.example/event/jsonschema/1-0-0', + data: { /* event data */ } + } +}); + +// Add global context +import { addGlobalContexts } from '@snowplow/browser-tracker'; +addGlobalContexts([{ + schema: 'iglu:com.example/context/jsonschema/1-0-0', + data: { /* context data */ } +}]); + +// Test a tracker with in-memory event store +import { createTracker } from '../helpers'; +import { newInMemoryEventStore } from '@snowplow/tracker-core'; +const eventStore = newInMemoryEventStore(); +const tracker = createTracker({ eventStore }); +``` + +## Contributing to CLAUDE.md + +When adding or updating content in this document, please follow these guidelines: + +### File Size Limit +- **CLAUDE.md must not exceed 40KB** (currently ~11KB) +- Check file size after updates: `wc -c CLAUDE.md` +- Remove outdated content if approaching the limit + +### Code Examples +- Keep all code examples **4 lines or fewer** +- Focus on the essential pattern, not complete implementations +- Use `// ❌` and `// ✅` to clearly show wrong vs right approaches + +### Content Organization +- Add new patterns to existing sections when possible +- Create new sections sparingly to maintain structure +- Update the architectural principles section for major changes +- Ensure examples follow current codebase conventions + +### Quality Standards +- Test any new patterns in actual code before documenting +- Verify imports and syntax are correct for the codebase +- Keep language concise and actionable +- Focus on "what" and "how", minimize "why" explanations + +### Multiple CLAUDE.md Files +- **Directory-specific CLAUDE.md files** can be created for specialized modules +- Follow the same structure and guidelines as this root CLAUDE.md +- Keep them focused on directory-specific patterns and conventions +- Maximum 20KB per directory-specific CLAUDE.md file + +### Instructions for LLMs +When editing files in this repository, **always check for CLAUDE.md guidance**: + +1. **Look for CLAUDE.md in the same directory** as the file being edited +2. **If not found, check parent directories** recursively up to project root +3. **Follow the patterns and conventions** described in the applicable CLAUDE.md +4. **Prioritize directory-specific guidance** over root-level guidance when conflicts exist \ No newline at end of file diff --git a/libraries/browser-tracker-core/CLAUDE.md b/libraries/browser-tracker-core/CLAUDE.md new file mode 100644 index 000000000..d77dbccd0 --- /dev/null +++ b/libraries/browser-tracker-core/CLAUDE.md @@ -0,0 +1,452 @@ +# Browser Tracker Core Library - CLAUDE.md + +## Module Overview + +The `@snowplow/browser-tracker-core` library provides browser-specific tracking functionality built on top of tracker-core. It handles DOM interactions, browser storage, cookies, and browser-specific event tracking features. + +## Core Responsibilities + +- **Browser Detection**: User agent, viewport, screen detection +- **Storage Management**: Cookies, localStorage, sessionStorage +- **DOM Helpers**: Cross-domain linking, form tracking utilities +- **Session Management**: Session tracking and storage +- **Page View Tracking**: Page view IDs and activity tracking +- **Browser Plugins**: Plugin system for browser features + +## Architecture Patterns + +### Browser Plugin Pattern +```typescript +// ✅ Correct: Typed plugin interface +export interface BrowserPlugin { + activateBrowserPlugin?: (tracker: BrowserTracker) => void; + contexts?: () => Array; + logger?: (logger: Logger) => void; + beforeTrack?: (payloadBuilder: PayloadBuilder) => void; + afterTrack?: (payload: Payload) => void; +} + +// ❌ Wrong: Untyped plugin +const plugin = { activate: (t: any) => {} }; +``` + +### Tracker State Management +```typescript +// ✅ Correct: Shared state pattern +export interface SharedState { + bufferFlushers: Array<(sync?: boolean) => void>; + hasLoaded: boolean; + registeredOnLoadHandlers: Array; + contextProviders: Array; +} + +// ❌ Wrong: Global variables +let trackerState = {}; // Avoid global state +``` + +### Storage Abstraction Pattern +```typescript +// ✅ Correct: Storage interface +export interface Storage { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; +} + +// ❌ Wrong: Direct localStorage access +localStorage.setItem('key', 'value'); // Use abstraction +``` + +## Critical Import Patterns + +### Browser-Specific Exports +```typescript +// index.ts - Browser features +export * from './tracker/types'; +export * from './helpers'; +export * from './detectors'; +export * from './proxies'; +export * from './plugins'; +export * from './state'; +export { Tracker } from './tracker'; +``` + +### Helper Utilities +```typescript +// ✅ Correct: Import from helpers +import { getCookieValue, setCookie } from './helpers'; + +// ❌ Wrong: Reimplementing browser utils +function getCookie(name: string) { /* custom */ } +``` + +## Cookie Management + +### Cookie Storage Pattern +```typescript +// ✅ Correct: Use cookie storage abstraction +export class CookieStorage implements Storage { + constructor( + private cookieName: string, + private cookieDomain?: string, + private cookieLifetime?: number + ) {} + + setItem(key: string, value: string): void { + setCookie(this.cookieName, value, this.cookieLifetime); + } +} + +// ❌ Wrong: Direct document.cookie manipulation +document.cookie = `name=value; domain=.example.com`; +``` + +### ID Cookie Pattern +```typescript +// Standard ID cookie structure +interface IdCookie { + userId: string; // Domain user ID + sessionId: string; // Session ID + sessionIndex: number; // Session count + eventIndex: number; // Event count in session + lastActivity: number; // Last activity timestamp +} +``` + +## Session Management + +### Session Storage Pattern +```typescript +// ✅ Correct: Session data structure +export interface SessionData { + sessionId: string; + previousSessionId?: string; + sessionIndex: number; + userId: string; + firstEventId?: string; + firstEventTimestamp?: number; +} + +// ❌ Wrong: Unstructured session data +const session = { id: '123', data: {} }; +``` + +### Activity Tracking +```typescript +// Activity callback configuration +export interface ActivityTrackingConfiguration { + minimumVisitLength: number; + heartbeatDelay: number; + callback?: ActivityCallback; +} +``` + +## DOM Interaction Patterns + +### Cross-Domain Linking +```typescript +// ✅ Correct: Decorator pattern +export function decorateQuerystring( + url: string, + name: string, + value: string +): string { + const [urlPath, anchor] = url.split('#'); + const [basePath, queries] = urlPath.split('?'); + // ... decoration logic + return decorated; +} + +// ❌ Wrong: String concatenation +url += '?_sp=' + value; // Fragile +``` + +### Form Element Helpers +```typescript +// Get form element values safely +export function getElementValue(element: HTMLElement): string | null { + if (element instanceof HTMLInputElement) { + return element.value; + } + if (element instanceof HTMLTextAreaElement) { + return element.value; + } + return null; +} +``` + +## Browser Detection + +### Detector Functions +```typescript +// ✅ Correct: Feature detection +export function hasLocalStorage(): boolean { + try { + const test = 'localStorage'; + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch (e) { + return false; + } +} + +// ❌ Wrong: Assume availability +if (localStorage) { /* use it */ } // May throw +``` + +### Browser Properties +```typescript +// Standard browser context +export interface BrowserProperties { + viewport: [number, number]; + documentSize: [number, number]; + resolution: [number, number]; + colorDepth: number; + devicePixelRatio: number; + cookieEnabled: boolean; + online: boolean; + browserLanguage: string; + documentLanguage: string; + webdriver: boolean; +} +``` + +## Event Queue Management + +### Out Queue Pattern +```typescript +// ✅ Correct: Managed queue with retry +export class OutQueue { + constructor( + private readonly maxQueueSize: number, + private readonly eventStore: EventStore + ) {} + + enqueue(event: Payload): void { + if (this.queue.length >= this.maxQueueSize) { + this.flush(); + } + this.queue.push(event); + } +} + +// ❌ Wrong: Unbounded array +const events = []; // No size management +``` + +## Common Patterns + +### Page View ID Generation +```typescript +// ✅ Correct: UUID per page view +import { v4 as uuid } from 'uuid'; +export function generatePageViewId(): string { + return uuid(); +} + +// ❌ Wrong: Sequential IDs +let pvId = 0; +function getPageViewId() { return ++pvId; } +``` + +### Local Storage Event Store +```typescript +// Event persistence pattern +export class LocalStorageEventStore implements EventStore { + constructor(private readonly key: string) {} + + async add(event: EventStorePayload): Promise { + const events = this.getAll(); + events.push(event); + localStorage.setItem(this.key, JSON.stringify(events)); + return events.length; + } +} +``` + +## Testing Patterns + +### Test Directory Structure +``` +test/ +├── tracker/ # Tracker-specific tests +│ ├── cookie_storage.test.ts +│ ├── page_view.test.ts +│ └── session_data.test.ts +├── helpers/ # Helper function tests +├── detectors.test.ts # Browser detection tests +└── __snapshots__/ # Jest snapshots +``` + +### Mock Browser APIs +```typescript +// Mock storage for tests +class MockStorage implements Storage { + private store: Record = {}; + + getItem(key: string): string | null { + return this.store[key] || null; + } + + setItem(key: string, value: string): void { + this.store[key] = value; + } + + clear(): void { + this.store = {}; + } +} + +// Mock document properties +jest.spyOn(document, 'title', 'get').mockReturnValue('Test Page'); +jest.spyOn(document, 'referrer', 'get').mockReturnValue('https://referrer.com'); +``` + +### DOM Testing +```typescript +// ✅ Correct: Setup and teardown DOM +beforeEach(() => { + document.body.innerHTML = ` +
+ + +
+ `; +}); + +afterEach(() => { + document.body.innerHTML = ''; + jest.clearAllMocks(); +}); + +// ❌ Wrong: Leave DOM dirty +beforeEach(() => { + document.body.innerHTML += '
test
'; // Accumulates +}); +``` + +### Testing Browser Detection +```typescript +// ✅ Correct: Mock browser features +describe('Browser detection', () => { + it('detects localStorage support', () => { + const mockSetItem = jest.fn(); + Object.defineProperty(window, 'localStorage', { + value: { setItem: mockSetItem, removeItem: jest.fn() }, + writable: true + }); + + expect(hasLocalStorage()).toBe(true); + }); + + it('handles localStorage errors', () => { + Object.defineProperty(window, 'localStorage', { + value: { + setItem: () => { throw new Error('Quota exceeded'); } + }, + writable: true + }); + + expect(hasLocalStorage()).toBe(false); + }); +}); +``` + +### Cookie Testing +```typescript +// ✅ Correct: Test cookie operations +describe('Cookie management', () => { + beforeEach(() => { + // Clear cookies + document.cookie.split(';').forEach(c => { + document.cookie = c.replace(/^ +/, '') + .replace(/=.*/, '=;expires=' + new Date().toUTCString()); + }); + }); + + it('sets and retrieves cookies', () => { + setCookie('test', 'value', 86400); + expect(getCookieValue('test')).toBe('value'); + }); +}); +``` + +### Testing Helper Pattern +```typescript +// Common test helper in test/helpers.ts +export function createTracker(config?: Partial) { + const eventStore = newInMemoryEventStore(); + return addTracker('sp', 'sp', 'js-test', '', new SharedState(), { + eventStore, + ...config + }); +} + +// Usage in tests +it('tracks page view', () => { + const tracker = createTracker(); + tracker.trackPageView(); + // Assert... +}); +``` + +## File Organization + +``` +browser-tracker-core/ +├── src/ +│ ├── index.ts # Public exports +│ ├── snowplow.ts # Main tracker factory +│ ├── state.ts # Shared state +│ ├── plugins.ts # Plugin system +│ ├── proxies.ts # Proxy tracking +│ ├── detectors.ts # Browser detection +│ ├── helpers/ +│ │ ├── index.ts # Helper exports +│ │ ├── browser_props.ts +│ │ ├── cross_domain.ts +│ │ └── storage.ts +│ └── tracker/ +│ ├── index.ts # Tracker implementation +│ ├── types.ts # TypeScript types +│ ├── cookie_storage.ts +│ ├── id_cookie.ts +│ ├── out_queue.ts +│ └── local_storage_event_store.ts +└── test/ + └── [module].test.ts +``` + +## Quick Reference + +### Essential Imports +```typescript +import { + BrowserTracker, + BrowserPlugin, + SharedState, + ActivityTrackingConfiguration, + PageViewEvent, + getCookieValue, + decorateQuerystring, + hasLocalStorage +} from '@snowplow/browser-tracker-core'; +``` + +### Plugin Development Checklist +- [ ] Implement `BrowserPlugin` interface +- [ ] Store tracker reference in `activateBrowserPlugin` +- [ ] Clean up resources in cleanup hooks +- [ ] Handle browser API failures gracefully +- [ ] Test with different storage configurations + +## Contributing to CLAUDE.md + +When modifying browser-tracker-core: + +1. **Test browser compatibility** across major browsers +2. **Handle storage failures** gracefully +3. **Respect privacy settings** (cookies, storage) +4. **Maintain IE11 compatibility** if required +5. **Document browser-specific behavior** \ No newline at end of file diff --git a/libraries/tracker-core/CLAUDE.md b/libraries/tracker-core/CLAUDE.md new file mode 100644 index 000000000..02de6dc77 --- /dev/null +++ b/libraries/tracker-core/CLAUDE.md @@ -0,0 +1,384 @@ +# Tracker Core Library - CLAUDE.md + +## Module Overview + +The `@snowplow/tracker-core` library provides platform-agnostic event tracking functionality. It contains the fundamental building blocks for constructing Snowplow events, managing contexts, and handling payloads. This is the foundation that all platform-specific trackers build upon. + +## Core Responsibilities + +- **Event Builders**: Functions to construct typed Snowplow events +- **Payload Management**: JSON and form-encoded payload construction +- **Context Handling**: Global and conditional context providers +- **Plugin System**: Core plugin architecture and lifecycle +- **Emitter Logic**: Event batching and request management + +## Architecture Patterns + +### Event Builder Pattern +All events follow a consistent builder pattern that returns a `PayloadBuilder`: + +```typescript +// ✅ Correct: Use typed builders +export function buildPageView(event: PageViewEvent): PayloadBuilder { + const { pageUrl, pageTitle, referrer } = event; + return payloadBuilder() + .add('e', 'pv') + .add('url', pageUrl) + .add('page', pageTitle) + .add('refr', referrer); +} + +// ❌ Wrong: Manual payload construction +function makePageView(url: string) { + return { e: 'pv', url }; // Don't build manually +} +``` + +### Self-Describing JSON Pattern +```typescript +// ✅ Correct: Strongly typed SDJ +export type SelfDescribingJson> = { + schema: string; + data: T extends any[] ? never : T; +}; + +// ❌ Wrong: Loose typing +type Event = { schema: any; data: any }; +``` + +### Timestamp ADT Pattern +```typescript +// ✅ Correct: Use discriminated union +export type Timestamp = TrueTimestamp | DeviceTimestamp | number; +export interface TrueTimestamp { + readonly type: 'ttm'; + readonly value: number; +} + +// ❌ Wrong: Ambiguous timestamp types +type Timestamp = number | { value: number }; +``` + +## Critical Import Patterns + +### Public API Exports +```typescript +// index.ts - Centralized exports +export * from './core'; +export * from './contexts'; +export * from './payload'; +export * from './emitter'; +export * from './plugins'; +export { buildPageView, buildStructEvent } from './events'; +``` + +### Internal Module Structure +```typescript +// ✅ Correct: Import from sibling modules +import { PayloadBuilder } from './payload'; +import { SelfDescribingJson } from './core'; + +// ❌ Wrong: Circular dependencies +import { something } from '../index'; // Avoid +``` + +## Event Building Standards + +### Standard Event Properties +```typescript +// Every event builder should accept CommonEventProperties +export interface CommonEventProperties> { + context?: Array> | null; + timestamp?: Timestamp | null; +} +``` + +### Event Factory Pattern +```typescript +// Standard event builder structure +export function buildCustomEvent( + event: CustomEventData +): PayloadBuilder { + return payloadBuilder() + .add('e', 'ue') // Event type + .addJson('ue_px', 'ue_pr', { + schema: EVENT_SCHEMA, + data: processEventData(event) + }); +} +``` + +## Context Management + +### Global Context Pattern +```typescript +// ✅ Correct: Conditional context provider +export type ConditionalContextProvider = + [ContextFilter, ContextPrimitive | ContextGenerator]; + +const globalContext: ConditionalContextProvider = [ + (event) => event.eventType === 'pv', // Filter + { schema: '...', data: {...} } // Context +]; + +// ❌ Wrong: Unfiltered global contexts +const context = { schema: '...', data: {...} }; // No filtering +``` + +### Plugin Context Integration +```typescript +// Plugin contexts are merged with global contexts +export function pluginContexts(): PluginContexts { + return { + addPluginContexts: (contexts: Array) => { + // Contexts added by plugins + } + }; +} +``` + +## Emitter Architecture + +### Event Store Pattern +```typescript +// Event storage abstraction +export interface EventStore { + add(payload: EventStorePayload): Promise; + count(): Promise; + getAll(): Promise; + removeHead(count: number): Promise; +} +``` + +### Request Building +```typescript +// Batch request construction +export class EmitterRequest { + constructor( + events: EmitterEvent[], + byteLimit: number + ) { + this.batch = this.buildBatch(events, byteLimit); + } +} +``` + +## Common Patterns + +### UUID Generation +```typescript +// ✅ Correct: Use uuid v4 +import { v4 as uuid } from 'uuid'; +const eventId = uuid(); + +// ❌ Wrong: Custom ID generation +const id = Math.random().toString(36); +``` + +### Base64 Encoding +```typescript +// Use the provided base64 utilities +import { base64urlencode } from './base64'; +const encoded = base64urlencode(jsonString); +``` + +### Logger Pattern +```typescript +// Conditional logging +import { LOG } from './logger'; +LOG.error('Error message', error); +LOG.warn('Warning message'); +``` + +## Testing Patterns + +### Mock Payload Builders +```typescript +// Test helper pattern +function createMockPayloadBuilder(): PayloadBuilder { + return payloadBuilder() + .add('aid', 'test-app') + .add('p', 'web'); +} +``` + +### Event Validation +```typescript +// Validate event structure in tests +expect(event.build()).toMatchObject({ + e: 'ue', + ue_pr: expect.stringContaining('schema') +}); +``` + +## Testing Patterns + +### Test Directory Structure +``` +test/ +├── core.test.ts # Core tracker tests +├── payload.test.ts # Payload builder tests +├── contexts.test.ts # Context management tests +├── emitter/ # Emitter-specific tests +└── __snapshots__/ # Jest snapshots +``` + +### In-Memory Event Store Pattern +```typescript +// ✅ Correct: Use in-memory store for testing +import { newInMemoryEventStore } from '@snowplow/tracker-core'; + +describe('Event tracking', () => { + let eventStore: EventStore; + + beforeEach(() => { + eventStore = newInMemoryEventStore(); + }); + + it('stores events correctly', () => { + const tracker = new TrackerCore({ eventStore }); + tracker.track(buildPageView({ pageUrl: 'test' })); + + const events = eventStore.getEvents(); + expect(events).toHaveLength(1); + expect(events[0].e).toBe('pv'); + }); +}); +``` + +### Payload Builder Testing +```typescript +// ✅ Correct: Test payload construction +describe('PayloadBuilder', () => { + it('builds valid Snowplow payload', () => { + const payload = new PayloadBuilder() + .add('e', 'pv') + .add('url', 'http://example.com') + .add('page', 'Home') + .build(); + + expect(payload).toEqual({ + e: 'pv', + url: 'http://example.com', + page: 'Home' + }); + }); + + it('encodes base64 for self-describing events', () => { + const payload = buildSelfDescribingEvent({ + event: { + schema: 'iglu:com.example/test/jsonschema/1-0-0', + data: { key: 'value' } + } + }); + + const built = payload.build(); + expect(built.ue_pr).toBeDefined(); + const decoded = JSON.parse(Buffer.from(built.ue_pr, 'base64').toString()); + expect(decoded.data.schema).toBe('iglu:com.example/test/jsonschema/1-0-0'); + }); +}); +``` + +### Context Testing +```typescript +// ✅ Correct: Test context providers +describe('Context management', () => { + it('adds global contexts to events', () => { + const contextProvider = () => [{ + schema: 'iglu:com.example/context/jsonschema/1-0-0', + data: { contextKey: 'contextValue' } + }]; + + const tracker = new TrackerCore({ + contextProviders: [contextProvider] + }); + + const payload = tracker.track(buildPageView({ pageUrl: 'test' })); + const contexts = JSON.parse(Buffer.from(payload.co, 'base64').toString()); + + expect(contexts.data).toContainEqual( + expect.objectContaining({ + data: { contextKey: 'contextValue' } + }) + ); + }); +}); +``` + +### Event Builder Testing Pattern +```typescript +// ✅ Correct: Test custom event builders +function buildCustomEvent(data: CustomEventData): PayloadBuilder { + return new PayloadBuilder() + .add('e', 'ue') + .add('ue_pr', buildSelfDescribingJson({ + schema: CUSTOM_EVENT_SCHEMA, + data + })); +} + +describe('Custom event builder', () => { + it('creates valid self-describing event', () => { + const event = buildCustomEvent({ field: 'value' }); + const payload = event.build(); + + expect(payload.e).toBe('ue'); + expect(JSON.parse(Buffer.from(payload.ue_pr, 'base64').toString())) + .toMatchSnapshot(); + }); +}); +``` + +## File Organization + +``` +tracker-core/ +├── src/ +│ ├── index.ts # Public API exports +│ ├── core.ts # Core types and tracker +│ ├── payload.ts # Payload builder +│ ├── contexts.ts # Context management +│ ├── plugins.ts # Plugin system +│ ├── base64.ts # Encoding utilities +│ ├── logger.ts # Logging +│ ├── schemata.ts # Schema constants +│ └── emitter/ +│ ├── index.ts # Emitter exports +│ ├── emitter_event.ts +│ └── emitter_request.ts +└── test/ + └── [module].test.ts +``` + +## Quick Reference + +### Essential Imports +```typescript +import { + PayloadBuilder, + SelfDescribingJson, + CommonEventProperties, + buildSelfDescribingEvent, + buildPageView, + buildStructEvent, + TrackerCore +} from '@snowplow/tracker-core'; +``` + +### Event Builder Checklist +- [ ] Accept event-specific data interface +- [ ] Return `PayloadBuilder` +- [ ] Add event type with `.add('e', 'type')` +- [ ] Handle optional CommonEventProperties +- [ ] Export from index.ts + +## Contributing to CLAUDE.md + +When modifying tracker-core patterns: + +1. **Maintain backward compatibility** - This is a core library +2. **Update type exports** when adding new interfaces +3. **Follow builder pattern** for new event types +4. **Test payload structure** thoroughly +5. **Document schema changes** in schemata.ts \ No newline at end of file diff --git a/plugins/CLAUDE.md b/plugins/CLAUDE.md new file mode 100644 index 000000000..a78aba399 --- /dev/null +++ b/plugins/CLAUDE.md @@ -0,0 +1,413 @@ +# Browser Plugins - CLAUDE.md + +## Plugin Architecture Overview + +Browser plugins extend the core tracking functionality with feature-specific capabilities. Each plugin follows a consistent pattern for registration, state management, and API exposure. Plugins are self-contained modules that integrate seamlessly with the tracker lifecycle. + +## Core Plugin Pattern + +### Plugin Structure Template +```typescript +// ✅ Correct: Standard plugin structure +import { BrowserPlugin, BrowserTracker } from '@snowplow/browser-tracker-core'; + +const _trackers: Record = {}; + +export function MyPlugin(): BrowserPlugin { + return { + activateBrowserPlugin: (tracker) => { + _trackers[tracker.id] = tracker; + } + }; +} + +// ❌ Wrong: Global state without cleanup +let tracker: BrowserTracker; // Global leak +``` + +### API Function Pattern +```typescript +// ✅ Correct: Dispatch to registered trackers +export function trackMyEvent( + event: MyEvent & CommonEventProperties, + trackers: Array = Object.keys(_trackers) +) { + dispatchToTrackersInCollection(trackers, _trackers, (t) => { + t.core.track(buildMyEvent(event), event.context, event.timestamp); + }); +} + +// ❌ Wrong: Single tracker assumption +export function trackEvent(event: MyEvent) { + tracker.track(event); // Which tracker? +} +``` + +## Plugin Categories + +### 1. Event Tracking Plugins +Plugins that add new event types: +- `browser-plugin-ad-tracking`: Ad impressions, clicks, conversions +- `browser-plugin-media-tracking`: Video/audio player events +- `browser-plugin-ecommerce`: Transaction and item tracking +- `browser-plugin-error-tracking`: JavaScript error capture + +### 2. Context Enhancement Plugins +Plugins that add contextual data: +- `browser-plugin-client-hints`: Browser client hints +- `browser-plugin-timezone`: Timezone information +- `browser-plugin-geolocation`: Location data +- `browser-plugin-ga-cookies`: Google Analytics cookies + +### 3. Behavior Tracking Plugins +Plugins that track user interactions: +- `browser-plugin-form-tracking`: Form interactions +- `browser-plugin-link-click-tracking`: Link clicks +- `browser-plugin-button-click-tracking`: Button clicks +- `browser-plugin-element-tracking`: General element visibility + +### 4. Performance Plugins +Plugins for performance monitoring: +- `browser-plugin-performance-timing`: Page load performance +- `browser-plugin-web-vitals`: Core Web Vitals +- `browser-plugin-performance-navigation-timing`: Navigation timing + +## Common Plugin Patterns + +### Schema Definition +```typescript +// schemata.ts - Centralized schemas +export const AD_CLICK_SCHEMA = + 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0'; +export const AD_IMPRESSION_SCHEMA = + 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0'; +``` + +### Event Builder Integration +```typescript +// ✅ Correct: Use core builders +import { buildSelfDescribingEvent } from '@snowplow/tracker-core'; + +function buildAdClick(event: AdClickEvent): PayloadBuilder { + return buildSelfDescribingEvent({ + event: { + schema: AD_CLICK_SCHEMA, + data: cleanAdClickData(event) + } + }); +} + +// ❌ Wrong: Custom payload construction +function buildAdClick(event: AdClickEvent) { + return { e: 'ue', data: event }; // Non-standard +} +``` + +### Configuration Options +```typescript +// ✅ Correct: Typed configuration +export interface MediaTrackingConfiguration { + captureEvents?: MediaEventType[]; + boundaries?: number[]; + volumeChangeTrackingInterval?: number; +} + +export function MediaTrackingPlugin( + config: MediaTrackingConfiguration = {} +): BrowserPlugin { + const settings = { ...defaultConfig, ...config }; + // Plugin implementation +} + +// ❌ Wrong: Untyped config +export function Plugin(config: any) { } +``` + +### DOM Observer Pattern +```typescript +// ✅ Correct: Managed observers +export function FormTrackingPlugin(): BrowserPlugin { + let observer: MutationObserver | null = null; + + return { + activateBrowserPlugin: (tracker) => { + observer = new MutationObserver(handleMutations); + observer.observe(document, { childList: true }); + }, + contexts: () => { + // Cleanup on context request if needed + observer?.disconnect(); + return []; + } + }; +} + +// ❌ Wrong: Unmanaged observers +new MutationObserver(callback).observe(document, {}); // Leak +``` + +## Plugin State Management + +### Tracker Registry +```typescript +// ✅ Correct: Scoped tracker storage +const _trackers: Record = {}; +const _configurations: WeakMap = new WeakMap(); + +// ❌ Wrong: Global configuration +let globalConfig: Config; // Affects all trackers +``` + +### Element Tracking State +```typescript +// ✅ Correct: WeakMap for DOM references +const elementStates = new WeakMap(); + +// ❌ Wrong: Memory leak potential +const elementStates: Map = new Map(); +``` + +## Event API Standards + +### Standard Event Interface +```typescript +// ✅ Correct: Consistent event structure +export interface MediaPlayerEvent { + currentTime: number; + duration?: number; + ended: boolean; + loop: boolean; + muted: boolean; + paused: boolean; + playbackRate: number; + volume: number; +} + +// ❌ Wrong: Inconsistent naming +export interface VideoData { + time: number; // Should be currentTime + length?: number; // Should be duration +} +``` + +### Optional Properties Pattern +```typescript +// ✅ Correct: Clear optionality +export interface AdClickEvent { + targetUrl: string; + clickId?: string; + costModel?: 'cpa' | 'cpc' | 'cpm'; + cost?: number; + bannerId?: string; + zoneId?: string; + impressionId?: string; + advertiserId?: string; + campaignId?: string; +} + +// ❌ Wrong: Everything optional +export interface AdEvent { + targetUrl?: string; // Required field as optional +} +``` + +## Context Building + +### Dynamic Context Pattern +```typescript +// ✅ Correct: Context from current state +export function getMediaContext(player: HTMLMediaElement): SelfDescribingJson { + return { + schema: MEDIA_PLAYER_SCHEMA, + data: { + currentTime: player.currentTime, + duration: player.duration || null, + ended: player.ended, + paused: player.paused + } + }; +} + +// ❌ Wrong: Stale context +const context = { /* captured once */ }; +``` + +## Testing Plugin Patterns + +### Test File Organization +``` +plugin/test/ +├── events.test.ts # Event tracking tests +├── contexts.test.ts # Context builder tests +├── api.test.ts # Public API tests +└── __snapshots__/ # Jest snapshot files +``` + +### Mock Tracker Setup +```typescript +// Standard test setup with event store +import { addTracker, SharedState } from '@snowplow/browser-tracker-core'; +import { newInMemoryEventStore } from '@snowplow/tracker-core'; + +describe('MyPlugin', () => { + let eventStore = newInMemoryEventStore(); + + beforeEach(() => { + SharedState.clear(); + eventStore = newInMemoryEventStore(); + addTracker('sp', 'sp', 'js-test', '', new SharedState(), { + eventStore, + plugins: [MyPlugin()] + }); + }); +}); +``` + +### Event Extraction Utilities +```typescript +// Common lodash/fp patterns for test assertions +import F from 'lodash/fp'; + +const getUEEvents = F.compose( + F.filter(F.compose(F.eq('ue'), F.get('e'))) +); + +const extractEventProperties = F.map( + F.compose( + F.get('data'), + (cx: string) => JSON.parse(cx), + F.get('ue_pr') + ) +); + +// Extract specific schema events +const extractUeEvent = (schema: string) => ({ + from: F.compose( + F.first, + F.filter(F.compose(F.eq(schema), F.get('schema'))), + F.flatten, + extractEventProperties, + getUEEvents + ) +}); +``` + +### Event Verification Patterns +```typescript +// ✅ Correct: Verify event schema and data +it('tracks ad click with correct schema', () => { + trackAdClick({ targetUrl: 'https://example.com' }); + + const events = eventStore.getEvents(); + const adClickEvent = extractUeEvent(AD_CLICK_SCHEMA).from(events); + + expect(adClickEvent).toMatchObject({ + targetUrl: 'https://example.com' + }); +}); + +// ❌ Wrong: Testing implementation details +it('calls internal function', () => { + expect(internalFunction).toHaveBeenCalled(); // Don't test internals +}); +``` + +### Context Testing +```typescript +// ✅ Correct: Test context generation +it('generates media player context', () => { + const player = document.createElement('video'); + player.currentTime = 30; + player.duration = 100; + + const context = buildMediaContext(player); + expect(context.data).toMatchObject({ + currentTime: 30, + duration: 100 + }); +}); +``` + +### Snapshot Testing for Complex Objects +```typescript +// ✅ Correct: Snapshot for schema validation +it('builds correct event structure', () => { + const event = buildAdClickEvent({ + targetUrl: 'https://example.com', + clickId: '123' + }); + expect(event).toMatchSnapshot(); +}); +``` + +## Plugin Development Checklist + +- [ ] Implement `BrowserPlugin` interface +- [ ] Create tracker registry with `_trackers` +- [ ] Define TypeScript interfaces for events +- [ ] Add schema constants to `schemata.ts` +- [ ] Implement tracking functions with `dispatchToTrackersInCollection` +- [ ] Export both plugin and API functions from index.ts +- [ ] Handle configuration options with defaults +- [ ] Clean up resources (observers, timers) properly +- [ ] Add comprehensive Jest tests +- [ ] Document usage in README.md + +## Common Pitfalls + +### 1. Forgetting Tracker Registration +```typescript +// ❌ Wrong: API without plugin +import { trackAdClick } from '@snowplow/browser-plugin-ad-tracking'; +trackAdClick(event); // Error: _trackers empty + +// ✅ Correct: Register plugin first +import { AdTrackingPlugin } from '@snowplow/browser-plugin-ad-tracking'; +newTracker('sp', 'collector', { plugins: [AdTrackingPlugin()] }); +``` + +### 2. Memory Leaks +```typescript +// ❌ Wrong: Unremoved listeners +element.addEventListener('click', handler); + +// ✅ Correct: Cleanup in plugin lifecycle +return { + activateBrowserPlugin: (tracker) => { + element.addEventListener('click', handler); + }, + contexts: () => { + element.removeEventListener('click', handler); + return []; + } +}; +``` + +## Quick Reference + +### Plugin File Structure +``` +browser-plugin-[name]/ +├── src/ +│ ├── index.ts # Plugin & API exports +│ ├── api.ts # Tracking functions +│ ├── types.ts # TypeScript interfaces +│ ├── schemata.ts # Schema constants +│ └── contexts.ts # Context builders +├── test/ +│ └── [name].test.ts # Jest tests +├── package.json +├── tsconfig.json +├── rollup.config.js +└── README.md +``` + +## Contributing to CLAUDE.md + +When creating or modifying plugins: + +1. **Follow the standard plugin pattern** for consistency +2. **Export all public types** from index.ts +3. **Use workspace protocol** for dependencies +4. **Test with multiple trackers** to ensure isolation +5. **Document configuration options** clearly \ No newline at end of file diff --git a/trackers/CLAUDE.md b/trackers/CLAUDE.md new file mode 100644 index 000000000..90ce98560 --- /dev/null +++ b/trackers/CLAUDE.md @@ -0,0 +1,474 @@ +# Platform Trackers - CLAUDE.md + +## Tracker Implementations Overview + +The `trackers/` directory contains platform-specific implementations that build upon the core libraries. Each tracker is optimized for its target environment while maintaining API consistency across platforms. + +## Tracker Types + +### 1. Browser Tracker (`browser-tracker`) +Modern browser tracking with full TypeScript support and tree-shaking: +```typescript +// ✅ Correct: Modern import syntax +import { newTracker, trackPageView } from '@snowplow/browser-tracker'; + +// ❌ Wrong: Legacy global access +window.snowplow('newTracker', ...); // Use imports +``` + +### 2. JavaScript Tracker (`javascript-tracker`) +Legacy SP.js compatible tracker for backward compatibility: +```typescript +// Legacy API maintained for compatibility +window.snowplow('newTracker', 'sp', 'collector.example.com'); +window.snowplow('trackPageView'); +``` + +### 3. Node Tracker (`node-tracker`) +Server-side tracking for Node.js applications: +```typescript +// ✅ Correct: Node-specific implementation +import { tracker } from '@snowplow/node-tracker'; +const t = tracker(emitters, 'namespace', 'appId'); + +// ❌ Wrong: Browser APIs in Node +window.snowplow(...); // Not available in Node +``` + +### 4. React Native Tracker (`react-native-tracker`) +Mobile app tracking with React Native specific features: +```typescript +// ✅ Correct: React Native patterns +import { createTracker } from '@snowplow/react-native-tracker'; +const t = createTracker('namespace', { + endpoint: 'collector.example.com', + method: 'post' +}); +``` + +## Browser Tracker Patterns + +### Initialization Pattern +```typescript +// ✅ Correct: Typed configuration +import { newTracker, BrowserTracker } from '@snowplow/browser-tracker'; + +newTracker('sp1', 'collector.example.com', { + appId: 'my-app', + platform: 'web', + cookieDomain: '.example.com', + discoverRootDomain: true, + plugins: [/* plugins */] +}); + +// ❌ Wrong: Untyped config +newTracker('sp1', 'collector.example.com', { + someOption: 'value' // Type error +}); +``` + +### Plugin Integration +```typescript +// ✅ Correct: Plugin array in config +import { PerformanceTimingPlugin } from '@snowplow/browser-plugin-performance-timing'; +import { ErrorTrackingPlugin } from '@snowplow/browser-plugin-error-tracking'; + +newTracker('sp', 'collector.example.com', { + plugins: [ + PerformanceTimingPlugin(), + ErrorTrackingPlugin() + ] +}); + +// ❌ Wrong: Adding plugins after initialization +const t = newTracker('sp', 'collector.example.com'); +t.addPlugin(plugin); // Not supported +``` + +### Multi-Tracker Pattern +```typescript +// ✅ Correct: Named trackers with specific targeting +newTracker('marketing', 'marketing.collector.com'); +newTracker('product', 'product.collector.com'); + +trackPageView({}, ['marketing']); // Only marketing +trackPageView({}, ['product']); // Only product +trackPageView({}); // All trackers + +// ❌ Wrong: Assuming single tracker +trackPageView(); // Which tracker? +``` + +## JavaScript Tracker (Legacy) + +### Global Queue Pattern +```typescript +// ✅ Correct: Queue-based API +window.snowplow = window.snowplow || []; +window.snowplow.push(['newTracker', 'sp', 'collector.example.com']); +window.snowplow.push(['trackPageView']); + +// ❌ Wrong: Direct function calls before load +window.snowplow('trackPageView'); // May not be ready +``` + +### Async Loading Pattern +```typescript +// ✅ Correct: Async script loading +(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; +p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments)}; +p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; +n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,'script','//cdn.jsdelivr.net/npm/@snowplow/javascript-tracker@latest/dist/sp.js','snowplow')); +``` + +### Bundle Size Optimization +```typescript +// sp.js - Full bundle with common plugins +// sp.lite.js - Minimal bundle, add plugins as needed + + +``` + +## Node Tracker Patterns + +### Emitter Configuration +```typescript +// ✅ Correct: Node-specific emitter +import { gotEmitter, tracker } from '@snowplow/node-tracker'; + +const emitter = gotEmitter({ + endpoint: 'collector.example.com', + protocol: 'https', + bufferSize: 5, + retries: 3 +}); + +const t = tracker(emitter, 'namespace', 'appId'); + +// ❌ Wrong: Browser emitter in Node +import { Emitter } from '@snowplow/browser-tracker-core'; +``` + +### Server-Side Events +```typescript +// ✅ Correct: Server context +t.track(buildPageView({ + pageUrl: req.url, + referrer: req.headers.referer, + userAgent: req.headers['user-agent'] +}), [{ + schema: 'iglu:com.example/server_context/jsonschema/1-0-0', + data: { + requestId: req.id, + serverTime: Date.now() + } +}]); + +// ❌ Wrong: Browser APIs +t.track(buildPageView({ + pageUrl: window.location.href // Not available +})); +``` + +## React Native Tracker Patterns + +### Platform-Specific Plugins +```typescript +// ✅ Correct: Mobile-specific plugins +import { + InstallTracker, + ScreenViewTracker, + AppLifecycleTracker +} from '@snowplow/react-native-tracker'; + +const tracker = createTracker('namespace', config, { + plugins: [ + InstallTracker(), + ScreenViewTracker(), + AppLifecycleTracker() + ] +}); + +// ❌ Wrong: Browser plugins in React Native +import { LinkClickTracking } from '@snowplow/browser-plugin-link-click-tracking'; +``` + +### Native Bridge Pattern +```typescript +// ✅ Correct: Platform detection +import { Platform } from 'react-native'; + +const config = { + endpoint: 'collector.example.com', + method: Platform.OS === 'ios' ? 'post' : 'get' +}; + +// ❌ Wrong: Assuming platform +const config = { method: 'post' }; // What about Android? +``` + +### Async Storage Integration +```typescript +// ✅ Correct: React Native storage +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const tracker = createTracker('namespace', { + endpoint: 'collector.example.com', + sessionContext: true, + applicationContext: true, + platformContext: true +}); + +// ❌ Wrong: Browser storage +localStorage.setItem(...); // Not available +``` + +## Common Tracker Patterns + +### Event Context Pattern +```typescript +// ✅ Correct: Structured contexts +trackStructEvent({ + category: 'video', + action: 'play', + label: 'tutorial', + property: 'intro', + value: 1.5 +}, [{ + schema: 'iglu:com.example/video_context/jsonschema/1-0-0', + data: { videoId: '123', duration: 120 } +}]); + +// ❌ Wrong: Unstructured data +trackEvent('video', 'play', { videoId: '123' }); +``` + +### Batch Configuration +```typescript +// ✅ Correct: Platform-appropriate batching +// Browser - smaller batches, more frequent +{ bufferSize: 1, maxPostBytes: 40000 } + +// Node - larger batches, less frequent +{ bufferSize: 10, maxPostBytes: 100000 } + +// React Native - mobile-optimized +{ bufferSize: 5, maxPostBytes: 50000 } +``` + +## Testing Patterns + +### Test Organization by Tracker Type +``` +trackers/ +├── browser-tracker/test/ +│ ├── unit/ # Unit tests +│ └── integration/ # Integration tests +├── javascript-tracker/test/ +│ ├── integration/ # Snowplow Micro tests +│ └── functional/ # Browser automation tests +├── node-tracker/test/ +│ └── *.test.ts # Node-specific tests +└── react-native-tracker/test/ + └── *.test.ts # RN-specific tests +``` + +### Browser Tracker Tests +```typescript +// ✅ Correct: Complete browser environment mock +import { JSDOM } from 'jsdom'; + +const dom = new JSDOM('', { + url: 'http://localhost', + referrer: 'http://example.com', + contentType: 'text/html' +}); + +global.window = dom.window; +global.document = window.document; +global.navigator = window.navigator; + +// ❌ Wrong: Partial mocks +global.window = { location: {} }; // Missing properties +``` + +### Integration Testing with Snowplow Micro +```typescript +// ✅ Correct: Test against local collector +describe('Snowplow Micro integration', () => { + const microUrl = 'http://localhost:9090'; + + beforeAll(async () => { + // Ensure Micro is running + await fetch(`${microUrl}/micro/all`); + }); + + it('sends events to collector', async () => { + newTracker('sp', `${microUrl}`, { bufferSize: 1 }); + trackPageView(); + + // Wait for event to be sent + await new Promise(r => setTimeout(r, 100)); + + // Verify in Micro + const response = await fetch(`${microUrl}/micro/all`); + const data = await response.json(); + expect(data.total).toBeGreaterThan(0); + }); +}); +``` + +### Functional/E2E Browser Tests +```typescript +// ✅ Correct: WebdriverIO test pattern +describe('Activity tracking', () => { + it('tracks time on page', async () => { + await browser.url('/test-page.html'); + + // Enable activity tracking + await browser.execute(() => { + window.snowplow('enableActivityTracking', { + minimumVisitLength: 10, + heartbeatDelay: 10 + }); + }); + + // Wait for activity + await browser.pause(11000); + + // Check for ping event + const events = await browser.execute(() => window.capturedEvents); + const pingEvent = events.find(e => e.e === 'pp'); + expect(pingEvent).toBeDefined(); + }); +}); +``` + +### Node Tracker Tests +```typescript +// ✅ Correct: Mock HTTP with nock +import nock from 'nock'; +import { tracker, gotEmitter } from '@snowplow/node-tracker'; + +describe('Node tracker', () => { + let scope: nock.Scope; + + beforeEach(() => { + scope = nock('https://collector.example.com') + .post('/com.snowplowanalytics.snowplow/tp2') + .reply(200); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it('sends batch of events', async () => { + const emitter = gotEmitter({ + endpoint: 'collector.example.com', + bufferSize: 2 + }); + const t = tracker(emitter, 'namespace', 'appId'); + + t.trackPageView({ pageUrl: 'http://example.com' }); + t.trackPageView({ pageUrl: 'http://example.com/page2' }); + + await t.flush(); + expect(scope.isDone()).toBe(true); + }); +}); +``` + +### React Native Tests +```typescript +// ✅ Correct: Mock RN dependencies +jest.mock('@react-native-async-storage/async-storage', () => ({ + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn() +})); + +jest.mock('react-native', () => ({ + Platform: { + OS: 'ios', + select: jest.fn(obj => obj.ios) + }, + Dimensions: { + get: () => ({ width: 375, height: 812 }) + } +})); + +describe('React Native tracker', () => { + it('uses mobile-specific configuration', () => { + const tracker = createTracker('namespace', { + endpoint: 'collector.example.com' + }); + + expect(tracker.getSubjectData()).toMatchObject({ + platform: 'mob' + }); + }); +}); +``` + +### Performance Testing +```typescript +// ✅ Correct: Measure bundle impact +it('keeps bundle size under limit', () => { + const stats = require('../dist/stats.json'); + const maxSize = 50 * 1024; // 50KB + + expect(stats.bundleSize).toBeLessThan(maxSize); +}); +``` + +## Performance Optimization + +### Tree Shaking (Browser Tracker) +```typescript +// ✅ Correct: Named imports for tree shaking +import { trackPageView, trackStructEvent } from '@snowplow/browser-tracker'; + +// ❌ Wrong: Importing everything +import * as snowplow from '@snowplow/browser-tracker'; +``` + +### Code Splitting (JavaScript Tracker) +```typescript +// ✅ Correct: Load plugins on demand +if (needsMediaTracking) { + import('@snowplow/browser-plugin-media-tracking').then(({ MediaTrackingPlugin }) => { + // Use plugin + }); +} +``` + +## Quick Reference + +### Tracker Initialization +```typescript +// Browser Tracker +import { newTracker } from '@snowplow/browser-tracker'; +newTracker('sp', 'collector.com', config); + +// JavaScript Tracker +window.snowplow('newTracker', 'sp', 'collector.com', config); + +// Node Tracker +import { tracker, gotEmitter } from '@snowplow/node-tracker'; +const t = tracker(gotEmitter(config), 'namespace', 'appId'); + +// React Native Tracker +import { createTracker } from '@snowplow/react-native-tracker'; +const t = createTracker('namespace', config); +``` + +## Contributing to CLAUDE.md + +When working with trackers: + +1. **Maintain platform separation** - Don't mix browser/node APIs +2. **Use appropriate storage** for each platform +3. **Test with platform-specific mocks** +4. **Optimize for platform constraints** (bundle size, network, battery) +5. **Document platform-specific behavior** clearly \ No newline at end of file