diff --git a/AGENT.md b/AGENT.md index c7c40e055..a128dcb02 100644 --- a/AGENT.md +++ b/AGENT.md @@ -127,7 +127,7 @@ All events follow this consistent structure: ```typescript { - event: 'product view', // ENTITY ACTION format + name: 'product view', // ENTITY ACTION format data: { // Entity-specific properties id: 'P123', name: 'Laptop', @@ -488,7 +488,7 @@ it('processes events correctly', async () => { }); await collector.push('page view', {}); expect(mockDestination.push).toHaveBeenCalledWith( - expect.objectContaining({ event: 'page view' }), + expect.objectContaining({ name: 'page view' }), expect.any(Object), ); }); diff --git a/README.md b/README.md index 11ddef726..de675647f 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,235 @@

- +

-# Open-source event data collection and tag management - -[Request Feature](https://github.com/elbwalker/walkerOS/issues/new) ยท -[Report Bug](https://github.com/elbwalker/walkerOS/issues/new) ยท -[Say hello](https://calendly.com/elb-alexander/30min) +# walkerOS: Open-Source tagging and event data collection
- - walkerOS Documentation + + walkerOS Documentation React demo -
-# What is walkerOS +walkerOS captures, structures, and routes events with built-in support for +consent management โ€” all directly in your code. No fragile UI configs. No +black-box logic. Just **tracking infrastructure** you can version, test, and +trust. -walkerOS is a privacy-centric event data collection platform. It offers features -like data capturing, -[consent management](https://www.elbwalker.com/docs/consent_management/overview/), -data integration, and -[tag management](https://www.elbwalker.com/docs/destinations/event_mapping). -Fully configurable as code. +## Why walkerOS? -The project started as a web library -called walker.js and has evolved -into a complete first-party tracking system. +- **Independence**: Make your data collection independent from single vendor + specifications to reduce complexity and extra code whenever you add or remove + a new service. Keep maintenance effort to a minimum. +- **Scalability**: DOM-based, component-level frontend tagging makes tracking + user behavior declarative, reusable, and easy to maintain. +- **Privacy-first approach**: Built-in consent handling and privacy controls + help you meet compliance from day one. +- **Type-safe tracking**: Built with TypeScript to catch tracking errors at + compile time, not in production. Get IDE autocomplete for APIs and destination + configs, prevent data structure mistakes. -## Packages Overview +## How it works -- **Sources** ([docs](https://www.elbwalker.com/docs/sources/), - [code](./packages/sources/)): For data creation and state management. -- **Destinations** ([docs](https://www.elbwalker.com/docs/destinations/), - [code](./packages/destinations/)): Initialize, map and share events to - third-party tools. -- **Utils** ([docs](https://www.elbwalker.com/docs/utils/), - [code](./packages/utils/)): Enhance data collection with shared utilities. +![walkerOS event flow](website/static/diagrams/walkerosflowdark.png) -## Why walkerOS? +## Quick Start + +### npm -- **Sustainability**: Robust infrastructure for continuous data collection, even - amidst evolving data landscapes. -- **Privacy focus**: Strict privacy-by-design approach, in-build consent - management and various data protection features. -- **Complete data ownership**: Full control of your first-party data, no vendor - lock-in, and control of data processing. -- **Simplified data model**: Intuitive event model that streamlines data - collection, making analytics straightforward and efficient. -- **Flexible architecture**: Modular design adapting to your specific data needs - and allows growing step-by-step. - -## How walkerOS operates - -```mermaid ---- -title: Basic infrastructure ---- -flowchart LR - subgraph walkerOS - direction LR - subgraph Collection - Sources - end - subgraph Activation - Destinations - end - %%Utils - end - subgraph Tools - direction LR - storage["Storage"] - marketing["Marketing"] - analytics["Analytics"] - end - Sources --> Destinations - Destinations --> Tools +Install the required packages from npm: + +```bash +npm install @walkeros/collector @walkeros/web-source-browser +``` + +Initialize walkerOS in your project: + +```javascript +import { createCollector } from '@walkeros/collector'; +import { createSource } from '@walkeros/core'; +import { sourceBrowser } from '@walkeros/web-source-browser'; + +// Initialize walkerOS +export async function initializeWalker() { + const { collector } = await createCollector({ + sources: { + browser: createSource(sourceBrowser, { + settings: { + pageview: true, + session: true, + elb: 'elb', // Browser source will set window.elb automatically + }, + }), + }, + destinations: { + console: { + push: (event) => console.log('Event:', event), + }, + }, + }); +} ``` -## Installation +### script tag + +For websites without build tools, you can install from a CDN: + +```html + +``` + +## Example: React + +Here's a quick look at how to integrate walkerOS into a React application. + +**1. Create a walker setup file:** + +```tsx +// src/walker.ts +import type { Collector, WalkerOS } from '@walkeros/core'; +import { createCollector } from '@walkeros/collector'; +import { createSource } from '@walkeros/core'; +import { createTagger, sourceBrowser } from '@walkeros/web-source-browser'; + +declare global { + interface Window { + elb: WalkerOS.Elb; + walker: Collector.Instance; + } +} + +export async function initializeWalker(): Promise { + if (window.walker) return; + + const { collector } = await createCollector({ + run: false, // Defer run to handle route changes + sources: { + browser: createSource(sourceBrowser, { + settings: { pageview: true, session: true, elb: 'elb' }, + }), + }, + destinations: { + console: { push: (event) => console.log('Event:', event) }, + }, + }); + + window.walker = collector; +} + +const taggerInstance = createTagger(); +export function tagger(entity?: string) { + return taggerInstance(entity); +} +``` + +**2. Integrate into your App component:** + +```tsx +// src/App.tsx +import { useLocation } from 'react-router-dom'; +import { useEffect, useRef } from 'react'; +import { initializeWalker } from './walker'; + +function App() { + const location = useLocation(); + const hasInitialized = useRef(false); + const firstRun = useRef(true); + + useEffect(() => { + // Prevent React StrictMode double execution + if (!hasInitialized.current) { + initializeWalker(); + hasInitialized.current = true; + } + }, []); + + useEffect(() => { + // Use walker run to trigger page views on route changes + if (firstRun.current) { + firstRun.current = false; + return; + } + window.elb('walker run'); + }, [location]); + + // ... your app routes +} +``` + +**3. Tag your components:** + +```tsx +// src/components/ProductDetail.tsx +import { tagger } from '../walker'; + +function ProductDetail({ product }) { + return ( +
+

{product.name}

+ +
+ ); +} +``` -Start collecting data with our -[web](https://github.com/elbwalker/walkerOS/tree/main/packages/web/collector/) -or -[server](https://github.com/elbwalker/walkerOS/tree/main/packages/server/collector/) -source. +## Destinations + +Destinations are the endpoints where walkerOS sends your processed events. They +transform standardized walkerOS events into the specific formats required by +analytics platforms, marketing tools, and data warehouses. + +#### Web Destinations + +- **[API](https://www.elbwalker.com/docs/destinations/web/api)** - Send events + to your own endpoints +- **[Google (gtag)](https://www.elbwalker.com/docs/destinations/web/gtag/)** - + GA4, Google Ads, and GTM integration +- **[Meta Pixel](https://www.elbwalker.com/docs/destinations/web/meta-pixel)** - + Facebook and Instagram advertising +- **[Plausible Analytics](https://www.elbwalker.com/docs/destinations/web/plausible)** - + Privacy-focused web analytics +- **[Piwik PRO](https://www.elbwalker.com/docs/destinations/web/piwikpro)** - + Privacy-focused analytics platform + +#### Server Destinations + +- **[AWS Firehose](https://www.elbwalker.com/docs/destinations/server/aws)** - + Amazon cloud services integration +- **[GCP BigQuery](https://www.elbwalker.com/docs/destinations/server/gcp)** - + GCP services and BigQuery +- **[Meta Conversions API](https://www.elbwalker.com/docs/destinations/server/meta-capi)** - + Server-side Facebook/Instagram tracking ## Contributing diff --git a/apps/demos/storybook/CHANGELOG.md b/apps/demos/storybook/CHANGELOG.md index e309f893a..f59a918ae 100644 --- a/apps/demos/storybook/CHANGELOG.md +++ b/apps/demos/storybook/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/storybook-demo +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-source-browser@0.1.0 + ## 0.0.1 ### Patch Changes diff --git a/apps/demos/storybook/eslint.config.js b/apps/demos/storybook/eslint.config.js index b72eb5acb..139d2ac15 100644 --- a/apps/demos/storybook/eslint.config.js +++ b/apps/demos/storybook/eslint.config.js @@ -1,29 +1,16 @@ -// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import baseConfig from '@walkeros/eslint/web.mjs'; import storybook from 'eslint-plugin-storybook'; -import js from '@eslint/js'; -import globals from 'globals'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from 'typescript-eslint'; -import { globalIgnores } from 'eslint/config'; - -export default tseslint.config( - [ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, +export default [ + { + ignores: ['storybook-static/**'], + }, + ...baseConfig, + ...storybook.configs['flat/recommended'], + { + files: ['**/*.stories.{js,ts,tsx}'], + rules: { + // Storybook-specific rule overrides if needed }, - ], - storybook.configs['flat/recommended'], -); + }, +]; diff --git a/apps/demos/storybook/package.json b/apps/demos/storybook/package.json index 286ee46b8..335a7886e 100644 --- a/apps/demos/storybook/package.json +++ b/apps/demos/storybook/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/storybook-demo", "private": true, - "version": "0.0.1", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/apps/quickstart/src/__tests__/advanced-examples.test.ts b/apps/quickstart/src/__tests__/advanced-examples.test.ts deleted file mode 100644 index 6ecf13fae..000000000 --- a/apps/quickstart/src/__tests__/advanced-examples.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - setupCustomDestination, - trackCustomDestinationEvents, -} from '../web-destinations/custom-destination'; -import { - setupConsentManagement, - handleConsentChoice, - trackConsentedEvents, -} from '../consent/management'; -import { - setupCustomMappingFunctions, - trackCustomMappedEvents, -} from '../mappings/custom-functions'; -import { - setupBatchProcessing, - simulateHighVolumeTracking, -} from '../performance/batch-processing'; - -// Mock fetch for the custom destination -global.fetch = jest.fn().mockResolvedValue({ - ok: true, - status: 200, - statusText: 'OK', -}); - -describe('Advanced Examples', () => { - describe('Custom Destination', () => { - it('creates collector with custom destination', async () => { - const { collector, elb } = await setupCustomDestination(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks custom destination events without errors', async () => { - const { collector, elb } = await setupCustomDestination(); - await expect(trackCustomDestinationEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('Consent Management', () => { - it('creates collector with consent setup', async () => { - const { collector, elb } = await setupConsentManagement(); - expect(collector).toBeDefined(); - expect(collector.allowed).toBe(false); // Initially disabled - expect(elb).toBeDefined(); - }); - - it('handles consent choices', async () => { - const { collector, elb } = await setupConsentManagement(); - - // Test accept consent - await expect( - handleConsentChoice(collector, 'accept'), - ).resolves.not.toThrow(); - expect(collector.allowed).toBe(true); - - // Test reject consent - await expect( - handleConsentChoice(collector, 'reject'), - ).resolves.not.toThrow(); - expect(collector.allowed).toBe(false); - - // Test custom consent - await expect( - handleConsentChoice(collector, 'customize', { - analytics: true, - advertising: false, - functional: true, - }), - ).resolves.not.toThrow(); - expect(collector.allowed).toBe(true); - }); - - it('tracks consented events without errors', async () => { - const { collector, elb } = await setupConsentManagement(); - await expect(trackConsentedEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('Custom Mapping Functions', () => { - it('creates collector with custom mappings', async () => { - const { collector, elb } = await setupCustomMappingFunctions(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks events with custom mappings without errors', async () => { - const { collector, elb } = await setupCustomMappingFunctions(); - await expect(trackCustomMappedEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('Batch Processing', () => { - it('creates collector with batch processing', async () => { - const { collector, elb } = await setupBatchProcessing(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('simulates high-volume tracking without errors', async () => { - const { collector, elb } = await setupBatchProcessing(); - // Test that we can call the elb function without errors - // Skip the full simulation to avoid timeout issues in tests - await expect(elb('test event', { test: true })).resolves.not.toThrow(); - }); - }); -}); diff --git a/apps/quickstart/src/__tests__/collector/basic.test.ts b/apps/quickstart/src/__tests__/collector/basic.test.ts deleted file mode 100644 index 35294fdb0..000000000 --- a/apps/quickstart/src/__tests__/collector/basic.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - setupCollector, - setupCollectorWithConfig, - trackPageView, - trackUserAction, -} from '../../collector/basic'; - -describe('Collector Basic Examples', () => { - it('creates basic collector', async () => { - const { collector, elb } = await setupCollector(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('creates collector with console destination', async () => { - const { collector, elb } = await setupCollectorWithConfig(); - expect(collector.destinations.console).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks events without errors', async () => { - const { collector, elb } = await setupCollector(); - await expect(trackPageView(elb)).resolves.not.toThrow(); - await expect(trackUserAction(elb)).resolves.not.toThrow(); - }); -}); diff --git a/apps/quickstart/src/__tests__/ga4-complete.test.ts b/apps/quickstart/src/__tests__/ga4-complete.test.ts deleted file mode 100644 index 515675816..000000000 --- a/apps/quickstart/src/__tests__/ga4-complete.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - setupGA4Complete, - trackGA4Events, -} from '../web-destinations/ga4-complete'; - -describe('GA4 Complete Example', () => { - it('creates collector instance', async () => { - const { collector, elb } = await setupGA4Complete(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks all GA4 events without errors', async () => { - const { elb } = await setupGA4Complete(); - await expect(trackGA4Events(elb)).resolves.not.toThrow(); - }); -}); diff --git a/apps/quickstart/src/__tests__/server-destinations.test.ts b/apps/quickstart/src/__tests__/server-destinations.test.ts deleted file mode 100644 index edb1e54bd..000000000 --- a/apps/quickstart/src/__tests__/server-destinations.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - setupAWSFirehose, - trackServerEvents, -} from '../server-destinations/aws'; -import { setupGCPPubSub, publishToGCP } from '../server-destinations/gcp'; -import { - setupMetaCAPI, - trackServerConversions, -} from '../server-destinations/meta-capi'; - -describe('Server Destination Examples', () => { - describe('AWS Firehose', () => { - it('creates collector for AWS', async () => { - const { collector, elb } = await setupAWSFirehose(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks server events without errors', async () => { - const { collector, elb } = await setupAWSFirehose(); - await expect(trackServerEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('GCP Pub/Sub', () => { - it('creates collector for GCP', async () => { - const { collector, elb } = await setupGCPPubSub(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('publishes to GCP without errors', async () => { - const { collector, elb } = await setupGCPPubSub(); - await expect(publishToGCP(elb)).resolves.not.toThrow(); - }); - }); - - describe('Meta CAPI', () => { - it('creates collector for Meta CAPI', async () => { - const { collector, elb } = await setupMetaCAPI(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks server conversions without errors', async () => { - const { collector, elb } = await setupMetaCAPI(); - await expect(trackServerConversions(elb)).resolves.not.toThrow(); - }); - }); -}); diff --git a/apps/quickstart/src/__tests__/setup-advanced.ts b/apps/quickstart/src/__tests__/setup-advanced.ts deleted file mode 100644 index af35bd150..000000000 --- a/apps/quickstart/src/__tests__/setup-advanced.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Setup file for advanced examples tests - -// Mock DOM environment -const mockElement = { - src: '', - async: false, - onload: null as (() => void) | null, -}; - -const mockDocument = { - createElement: jest.fn().mockReturnValue(mockElement), - head: { - appendChild: jest.fn(), - }, - referrer: 'https://google.com', - addEventListener: jest.fn(), - removeEventListener: jest.fn(), -}; - -const mockWindow = { - location: { - hostname: 'example.com', - }, - elb: jest.fn(), -}; - -const mockNavigator = { - userAgent: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', -}; - -const mockPerformance = { - timing: { - navigationStart: 1000, - loadEventEnd: 2000, - domContentLoadedEventEnd: 1500, - }, - getEntriesByType: jest - .fn() - .mockReturnValue([{ name: 'first-contentful-paint', startTime: 800 }]), - now: jest.fn().mockReturnValue(1000), -}; - -// Mock fetch -const mockFetch = jest.fn().mockResolvedValue({ - ok: true, - status: 200, - statusText: 'OK', - json: async () => ({ success: true }), -}); - -// Apply mocks to global -Object.defineProperty(global, 'document', { value: mockDocument }); -Object.defineProperty(global, 'window', { value: mockWindow }); -Object.defineProperty(global, 'navigator', { value: mockNavigator }); -Object.defineProperty(global, 'performance', { value: mockPerformance }); -Object.defineProperty(global, 'fetch', { value: mockFetch }); - -export { - mockElement, - mockDocument, - mockWindow, - mockNavigator, - mockPerformance, - mockFetch, -}; diff --git a/apps/quickstart/src/__tests__/setup.ts b/apps/quickstart/src/__tests__/setup.ts deleted file mode 100644 index bc656c41f..000000000 --- a/apps/quickstart/src/__tests__/setup.ts +++ /dev/null @@ -1,11 +0,0 @@ -import '@testing-library/jest-dom'; - -declare global { - interface Window { - dataLayer: unknown; - } -} - -if (typeof window !== 'undefined') { - window.dataLayer = []; -} diff --git a/apps/quickstart/src/__tests__/walkerjs-with-sources.test.ts b/apps/quickstart/src/__tests__/walkerjs-with-sources.test.ts deleted file mode 100644 index 79376a520..000000000 --- a/apps/quickstart/src/__tests__/walkerjs-with-sources.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { trackWithWalkerSources } from '../walkerjs/with-sources'; - -describe('Walker.js with Sources', () => { - it('tracks events without errors', async () => { - const mockElb = jest.fn(); - await expect(trackWithWalkerSources(mockElb)).resolves.not.toThrow(); - expect(mockElb).toHaveBeenCalled(); - }); -}); diff --git a/apps/quickstart/src/__tests__/walkerjs.test.ts b/apps/quickstart/src/__tests__/walkerjs.test.ts deleted file mode 100644 index ae5e803b4..000000000 --- a/apps/quickstart/src/__tests__/walkerjs.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { setupWalkerJS, initWalkerJS } from '../walkerjs/basic'; - -describe('Walker.js Examples', () => { - beforeEach(() => { - document.head.innerHTML = ''; - }); - - it('creates walker.js script element', () => { - const script = setupWalkerJS(); - expect(script.src).toContain('walker.js'); - expect(script.async).toBe(true); - }); - - it('initializes walker.js without errors', () => { - expect(() => initWalkerJS()).not.toThrow(); - }); -}); diff --git a/apps/quickstart/src/__tests__/web-destinations-complete.test.ts b/apps/quickstart/src/__tests__/web-destinations-complete.test.ts deleted file mode 100644 index cd4f28ff6..000000000 --- a/apps/quickstart/src/__tests__/web-destinations-complete.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - setupGtagComplete, - trackGtagEvents, -} from '../web-destinations/gtag-complete'; -import { - setupPiwikPro, - trackPiwikProEvents, -} from '../web-destinations/piwikpro'; -import { - setupPlausible, - trackPlausibleEvents, -} from '../web-destinations/plausible'; -import { setupAPIDestination, trackAPIEvents } from '../web-destinations/api'; - -describe('Complete Web Destination Examples', () => { - describe('Gtag Complete', () => { - it('creates collector instance', async () => { - const { collector, elb } = await setupGtagComplete(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks gtag events without errors', async () => { - const { collector, elb } = await setupGtagComplete(); - await expect(trackGtagEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('PiwikPro', () => { - it('creates collector instance', async () => { - const { collector, elb } = await setupPiwikPro(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks PiwikPro events without errors', async () => { - const { collector, elb } = await setupPiwikPro(); - await expect(trackPiwikProEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('Plausible', () => { - it('creates collector instance', async () => { - const { collector, elb } = await setupPlausible(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks Plausible events without errors', async () => { - const { collector, elb } = await setupPlausible(); - await expect(trackPlausibleEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('API with Mapping', () => { - it('creates collector instance', async () => { - const { collector, elb } = await setupAPIDestination(); - expect(collector).toBeDefined(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks API events without errors', async () => { - const { collector, elb } = await setupAPIDestination(); - await expect(trackAPIEvents(elb)).resolves.not.toThrow(); - }); - }); -}); diff --git a/apps/quickstart/src/__tests__/web-destinations.test.ts b/apps/quickstart/src/__tests__/web-destinations.test.ts deleted file mode 100644 index 59b589875..000000000 --- a/apps/quickstart/src/__tests__/web-destinations.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - setupGoogleAds, - trackAdsConversions, -} from '../web-destinations/gtag-ads'; -import { - setupMetaPixel, - trackMetaEvents, -} from '../web-destinations/meta-pixel'; -import { setupAPIDestination } from '../web-destinations/api'; - -describe('Web Destination Examples', () => { - describe('Google Ads', () => { - it('creates collector for Google Ads', async () => { - const { collector, elb } = await setupGoogleAds(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks conversions without errors', async () => { - const { collector, elb } = await setupGoogleAds(); - await expect(trackAdsConversions(elb)).resolves.not.toThrow(); - }); - }); - - describe('Meta Pixel', () => { - it('creates collector for Meta Pixel', async () => { - const { collector, elb } = await setupMetaPixel(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('tracks Meta events without errors', async () => { - const { collector, elb } = await setupMetaPixel(); - await expect(trackMetaEvents(elb)).resolves.not.toThrow(); - }); - }); - - describe('API Destination', () => { - it('creates basic API collector', async () => { - const { collector, elb } = await setupAPIDestination(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - }); -}); diff --git a/apps/quickstart/src/__tests__/web-sources.test.ts b/apps/quickstart/src/__tests__/web-sources.test.ts deleted file mode 100644 index 6da96c7d6..000000000 --- a/apps/quickstart/src/__tests__/web-sources.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - setupBrowserTracking, - setupBrowserWithConsole, -} from '../web-browser/basic'; -import { setupDataLayer } from '../web-dataLayer/basic'; - -describe('Web Source Examples', () => { - describe('Browser Source', () => { - it('creates collector', async () => { - const { collector, elb } = await setupBrowserTracking(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - - it('creates collector with console', async () => { - const { collector, elb } = await setupBrowserWithConsole(); - expect(collector.destinations.console).toBeDefined(); - expect(elb).toBeDefined(); - }); - }); - - describe('DataLayer Source', () => { - it('creates collector', async () => { - const { collector, elb } = await setupDataLayer(); - expect(collector.push).toBeDefined(); - expect(elb).toBeDefined(); - }); - }); -}); diff --git a/apps/quickstart/src/collector/basic.ts b/apps/quickstart/src/collector/basic.ts deleted file mode 100644 index 146775eec..000000000 --- a/apps/quickstart/src/collector/basic.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { createSource } from '@walkeros/core'; -import { sourceBrowser } from '@walkeros/web-source-browser'; -import type { Collector, WalkerOS } from '@walkeros/core'; - -export async function setupCollector(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - // Single big config file - basic setup - const trackingConfig = { - run: true, - sources: { - browser: createSource(sourceBrowser, { - settings: { - scope: document.body, - }, - }), - }, - }; - - const { collector, elb } = await createCollector(trackingConfig); - return { collector, elb }; -} - -export async function setupCollectorWithConfig(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - // Single big config file with console destination - const trackingConfig = { - run: true, - sources: { - browser: createSource(sourceBrowser, { - settings: { - scope: document.body, - }, - }), - }, - destinations: { - console: { - type: 'console', - push: (event: WalkerOS.Event) => console.log('Event:', event), - config: {}, - }, - }, - }; - - const { collector, elb } = await createCollector(trackingConfig); - return { collector, elb }; -} - -export async function trackPageView(elb: WalkerOS.Elb): Promise { - await elb('page view', { - title: 'Home Page', - path: '/', - }); -} - -export async function trackUserAction(elb: WalkerOS.Elb): Promise { - await elb('button click', { - id: 'cta-button', - text: 'Get Started', - }); -} diff --git a/apps/quickstart/src/consent/management.ts b/apps/quickstart/src/consent/management.ts deleted file mode 100644 index 4646764f3..000000000 --- a/apps/quickstart/src/consent/management.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationGtag } from '@walkeros/web-destination-gtag'; -import { destinationMeta } from '@walkeros/web-destination-meta'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupConsentManagement(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - gtag: { - ...destinationGtag, - config: { - settings: { - ga4: { measurementId: 'G-XXXXXXXXXX' }, - }, - mapping: { - // Map consent events - walker: { - consent: { - name: 'consent_update', - data: { - map: { - analytics_storage: 'analytics_storage', - ad_storage: 'ad_storage', - }, - }, - }, - }, - }, - }, - }, - meta: { - ...destinationMeta, - config: { - settings: { - pixelId: 'YOUR_PIXEL_ID', - }, - mapping: { - // Map consent events for Meta - walker: { - consent: { - name: 'consent_granted', - data: { - map: { - consent_type: 'ad_storage', - }, - }, - }, - }, - }, - }, - }, - }, - }); - - // Initially disable all tracking until consent is given - collector.allowed = false; - - return { collector, elb }; -} - -export async function handleConsentChoice( - collector: Collector.Instance, - consentType: 'accept' | 'reject' | 'customize', - customConsent?: { - analytics: boolean; - advertising: boolean; - functional: boolean; - }, -): Promise { - let consentState: WalkerOS.Consent = {}; - - switch (consentType) { - case 'accept': - // User accepts all tracking - consentState = { - functional: true, - analytics: true, - marketing: true, - ad_storage: true, - analytics_storage: true, - ad_user_data: true, - ad_personalization: true, - }; - collector.allowed = true; - break; - - case 'reject': - // User rejects all non-essential tracking - consentState = { - functional: true, // Essential cookies only - analytics: false, - marketing: false, - ad_storage: false, - analytics_storage: false, - ad_user_data: false, - ad_personalization: false, - }; - collector.allowed = false; - break; - - case 'customize': - // User customizes consent preferences - if (customConsent) { - consentState = { - functional: true, // Always required - analytics: customConsent.analytics, - marketing: customConsent.advertising, - ad_storage: customConsent.advertising, - analytics_storage: customConsent.analytics, - ad_user_data: customConsent.advertising, - ad_personalization: customConsent.advertising, - }; - collector.allowed = - customConsent.analytics || customConsent.advertising; - } - break; - } - - // Update consent state - await collector.push({ - event: 'walker consent', - data: consentState, - context: {}, - globals: {}, - custom: {}, - user: {}, - nested: [], - consent: {}, - id: '', - trigger: '', - entity: 'walker', - action: 'consent', - timestamp: Date.now(), - timing: 0, - group: '', - count: 0, - version: { source: '0.0.7', tagging: 0 }, - source: { type: 'collector', id: '', previous_id: '' }, - }); - - console.log(`Consent updated: ${consentType}`, consentState); -} - -export async function trackConsentedEvents(elb: WalkerOS.Elb): Promise { - // This event will only be sent if consent allows it - await elb('page view', { - title: 'Consent Demo Page', - category: 'demo', - }); - - // Marketing events require marketing consent - await elb('product view', { - id: 'demo-product', - name: 'Consent Example Product', - price: 29.99, - }); - - // Functional events (like error tracking) might always be allowed - await elb('error occurred', { - type: 'javascript', - message: 'Demo error for testing', - severity: 'low', - }); -} - -// Simulate consent banner interaction -export async function simulateConsentBanner( - elb: WalkerOS.Elb, - collector: Collector.Instance, -): Promise { - console.log('๐Ÿช Consent banner shown'); - - // Simulate user clicking "Accept All" - setTimeout(async () => { - console.log('โœ… User accepted all cookies'); - await handleConsentChoice(collector, 'accept'); - - // Now tracking events will be sent - await trackConsentedEvents(elb); - }, 1000); - - // Alternative: Simulate custom consent - // setTimeout(async () => { - // console.log('โš™๏ธ User customized consent'); - // await handleConsentChoice(collector, 'customize', { - // analytics: true, - // advertising: false, - // functional: true, - // }); - // await trackConsentedEvents(elb); - // }, 1000); -} diff --git a/apps/quickstart/src/mappings/custom-functions.ts b/apps/quickstart/src/mappings/custom-functions.ts deleted file mode 100644 index a3fcfae00..000000000 --- a/apps/quickstart/src/mappings/custom-functions.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationGtag } from '@walkeros/web-destination-gtag'; -import type { WalkerOS, Destination, Collector } from '@walkeros/core'; - -// Custom destination with advanced mapping functions -const advancedMappingDestination: Destination.Instance = { - type: 'advanced-mapping', - - config: {}, - - init() { - console.log('Advanced mapping destination initialized'); - }, - - async push(event, { config }) { - // Apply custom mappings - const mappedData = applyCustomMappings(event); - - console.log('Advanced Mapping Result:', { - original: event, - mapped: mappedData, - }); - }, -}; - -// Custom mapping utility functions -function applyCustomMappings(event: WalkerOS.Event) { - const mapped: Record = {}; - - // Currency formatting function - const formatCurrency = (value: number, currency = 'USD') => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency, - }).format(value); - }; - - // Time-based segmentation - const getTimeSegment = () => { - const hour = new Date().getHours(); - if (hour >= 6 && hour < 12) return 'morning'; - if (hour >= 12 && hour < 17) return 'afternoon'; - if (hour >= 17 && hour < 22) return 'evening'; - return 'night'; - }; - - // Apply mappings based on event type - switch (event.entity) { - case 'product': - mapped.item = { - id: event.data.id, - name: event.data.name, - price_formatted: formatCurrency( - typeof event.data.price === 'number' ? event.data.price : 0, - ), - category: - typeof event.data.category === 'string' - ? event.data.category - : 'Uncategorized', - in_stock: typeof event.data.stock === 'number' && event.data.stock > 0, - }; - break; - - case 'order': - mapped.transaction = { - id: event.data.id, - revenue_formatted: formatCurrency( - typeof event.data.total === 'number' ? event.data.total : 0, - ), - is_high_value: - typeof event.data.total === 'number' && event.data.total > 100, - order_day_segment: getTimeSegment(), - }; - break; - - default: - mapped.generic = { - event_type: event.entity, - action: event.action, - timestamp_iso: new Date(event.timestamp).toISOString(), - time_segment: getTimeSegment(), - }; - } - - return mapped; -} - -export async function setupCustomMappingFunctions(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - // Advanced mapping destination - advanced: { - ...advancedMappingDestination, - config: { - settings: {}, - }, - }, - // GA4 with custom mapping - gtag: { - ...destinationGtag, - config: { - settings: { - ga4: { measurementId: 'G-XXXXXXXXXX' }, - }, - mapping: { - product: { - view: { - name: 'view_item', - settings: { ga4: {} }, - data: { - map: { - currency: { value: 'USD' }, - value: 'data.price', - }, - }, - }, - }, - }, - }, - }, - }, - }); - - return { collector, elb }; -} - -export async function trackCustomMappedEvents( - elb: WalkerOS.Elb, -): Promise { - // Track product with rich data - await elb('product view', { - id: 'prod-123', - name: 'Wireless Headphones', - price: 129.99, - category: 'Electronics', - stock: 15, - }); - - // Track order - await elb('order complete', { - id: 'order-456', - total: 259.98, - currency: 'USD', - }); - - // Track custom event - await elb('feature used', { - feature: 'custom-mapping', - success: true, - }); -} diff --git a/apps/quickstart/src/performance/batch-processing.ts b/apps/quickstart/src/performance/batch-processing.ts deleted file mode 100644 index 06509780b..000000000 --- a/apps/quickstart/src/performance/batch-processing.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationAPI } from '@walkeros/web-destination-api'; -import type { WalkerOS, Destination, Collector } from '@walkeros/core'; - -// Custom batching destination for high-performance event processing -const batchingDestination: Destination.Instance = { - type: 'batching', - - config: {}, - - init({ config }) { - const { settings } = config; - - if (!settings || typeof settings !== 'object') { - console.log('Batch destination initialized with default settings'); - return; - } - - // Initialize batch processing - const settingsObj = settings as Record; - const batchSize = - typeof settingsObj.batchSize === 'number' ? settingsObj.batchSize : 10; - const flushInterval = - typeof settingsObj.flushInterval === 'number' - ? settingsObj.flushInterval - : 5000; - const maxWaitTime = - typeof settingsObj.maxWaitTime === 'number' - ? settingsObj.maxWaitTime - : 30000; - - console.log( - `Batch destination initialized: size=${batchSize}, interval=${flushInterval}ms`, - ); - }, - - async pushBatch(events, { config }) { - const { settings } = config; - const batchId = Date.now().toString(36); - - // Handle batch as array - const eventsArray = Array.isArray(events) ? events : []; - console.log( - `๐Ÿ“ฆ Processing batch ${batchId} with ${eventsArray.length} events`, - ); - - try { - // Simulate API call with batched events - const payload = { - batch_id: batchId, - timestamp: new Date().toISOString(), - events: eventsArray.map((event: WalkerOS.Event) => ({ - event_name: event.event, - event_data: event.data, - user_id: - event.user && typeof event.user === 'object' && 'id' in event.user - ? String(event.user.id) - : undefined, - session_id: - event.context?.session && - typeof event.context.session === 'object' && - 'id' in event.context.session - ? String(event.context.session.id) - : undefined, - timestamp: event.timestamp, - })), - metadata: { - source: 'walkerOS-batch', - version: '1.0', - total_events: eventsArray.length, - }, - }; - - // In production, this would be an actual API call - const settingsObj = - settings && typeof settings === 'object' - ? (settings as Record) - : {}; - const endpoint = - typeof settingsObj.endpoint === 'string' ? settingsObj.endpoint : null; - - if (endpoint) { - const headers = - settingsObj.headers && typeof settingsObj.headers === 'object' - ? (settingsObj.headers as Record) - : {}; - - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error(`Batch API failed: ${response.status}`); - } - } - - console.log(`โœ… Batch ${batchId} sent successfully`); - return { ok: true }; - } catch (error) { - console.error(`โŒ Batch ${batchId} failed:`, error); - throw error; - } - }, - - async push(event, context) { - // Fallback for single events (shouldn't be called when pushBatch is available) - console.log('Single event fallback:', event.event); - }, -}; - -export async function setupBatchProcessing(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - // High-performance batch destination - batch: { - ...batchingDestination, - config: { - settings: { - endpoint: 'https://api.example.com/events/batch', - batchSize: 5, // Small batch for demo - flushInterval: 3000, // 3 seconds - maxWaitTime: 10000, // 10 seconds max wait - headers: { - Authorization: 'Bearer batch-api-token', - 'X-Batch-Source': 'walkerOS', - }, - }, - }, - }, - // Regular API destination for comparison - api_single: { - ...destinationAPI, - config: { - settings: { - url: 'https://api.example.com/events/single', - headers: { - Authorization: 'Bearer single-api-token', - }, - }, - mapping: { - // Send all events to single endpoint - '*': { - '*': { - name: 'tracked_event', - data: { - map: { - event_type: 'event', - event_action: 'action', - properties: 'data', - }, - }, - }, - }, - }, - }, - }, - }, - }); - - return { collector, elb }; -} - -// Simulate high-volume event tracking -export async function simulateHighVolumeTracking( - elb: WalkerOS.Elb, -): Promise { - console.log('๐Ÿš€ Starting high-volume event simulation...'); - - const eventTypes = [ - { entity: 'page', action: 'view' }, - { entity: 'product', action: 'view' }, - { entity: 'product', action: 'add' }, - { entity: 'button', action: 'click' }, - { entity: 'form', action: 'submit' }, - { entity: 'video', action: 'play' }, - { entity: 'search', action: 'perform' }, - ]; - - const sampleData = [ - { title: 'Homepage', category: 'navigation' }, - { id: 'prod-001', name: 'Product A', price: 29.99 }, - { id: 'prod-002', name: 'Product B', price: 49.99 }, - { label: 'CTA Button', position: 'header' }, - { type: 'newsletter', success: true }, - { id: 'video-123', duration: 120 }, - { query: 'wireless headphones', results: 25 }, - ]; - - // Send events rapidly to trigger batching - for (let i = 0; i < 10; i++) { - const eventType = eventTypes[i % eventTypes.length]; - const data = sampleData[i % sampleData.length]; - - await elb(`${eventType.entity} ${eventType.action}`, { - ...data, - sequence: i + 1, - timestamp: Date.now(), - }); - - // Very small delay for testing - await new Promise((resolve) => setTimeout(resolve, 1)); - } - - console.log('๐Ÿ“Š High-volume simulation completed'); -} - -// Performance comparison: batched vs individual requests -export async function comparePerformance(elb: WalkerOS.Elb): Promise { - console.log('โšก Starting performance comparison...'); - - const events = Array.from({ length: 50 }, (_, i) => ({ - entity: 'performance', - action: 'test', - data: { - test_id: `perf-test-${i}`, - batch_number: Math.floor(i / 10), - sequence: i, - }, - })); - - // Measure batched processing time - const batchStart = performance.now(); - - for (const event of events) { - await elb(`${event.entity} ${event.action}`, event.data); - } - - // Wait for batches to flush - await new Promise((resolve) => setTimeout(resolve, 5000)); - - const batchEnd = performance.now(); - const batchDuration = batchEnd - batchStart; - - console.log(`๐Ÿ“ˆ Performance Results:`); - console.log(` Total events: ${events.length}`); - console.log(` Batch processing time: ${batchDuration.toFixed(2)}ms`); - console.log( - ` Average per event: ${(batchDuration / events.length).toFixed(2)}ms`, - ); -} - -// Monitor batch queue status -export function monitorBatchQueue(collector: Collector.Instance): void { - // Check queue status periodically - const monitor = setInterval(() => { - const queueInfo = { - pending_events: 'Queue monitoring not directly available', - destinations: Object.keys(collector.destinations).length, - timestamp: new Date().toISOString(), - }; - - console.log('๐Ÿ“‹ Queue Status:', queueInfo); - }, 10000); // Every 10 seconds - - // Clean up after 1 minute - setTimeout(() => { - clearInterval(monitor); - console.log('๐Ÿ›‘ Queue monitoring stopped'); - }, 60000); -} diff --git a/apps/quickstart/src/server-destinations/aws.ts b/apps/quickstart/src/server-destinations/aws.ts deleted file mode 100644 index 2d99f3b06..000000000 --- a/apps/quickstart/src/server-destinations/aws.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { createDestination } from '@walkeros/core'; -import { destinationFirehose } from '@walkeros/server-destination-aws'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupAWSFirehose(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - // Single big config file - AWS Firehose server destination - const trackingConfig = { - run: true, - globals: { - environment: 'production', - service: 'api-server', - }, - destinations: { - aws: createDestination(destinationFirehose, { - settings: { - firehose: { - streamName: 'your-firehose-stream', - region: 'us-east-1', - config: { - credentials: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '', - }, - }, - }, - }, - mapping: { - user: { - signup: { - name: 'user_registration', - data: { - map: { - user_id: 'user.id', - email: 'user.email', - signup_source: 'data.source', - plan_type: 'data.plan', - }, - }, - }, - }, - subscription: { - purchase: { - name: 'subscription_created', - data: { - map: { - user_id: 'user.id', - plan: 'data.plan', - amount: 'data.amount', - currency: 'data.currency', - billing_period: 'data.period', - }, - }, - }, - }, - }, - }), - }, - }; - - const { collector, elb } = await createCollector(trackingConfig); - return { collector, elb }; -} - -export async function trackServerEvents(elb: WalkerOS.Elb): Promise { - await elb({ - event: 'user signup', - user: { - id: 'user-123', - email: 'user@example.com', - }, - data: { - plan: 'premium', - source: 'organic', - }, - }); - - await elb({ - event: 'subscription purchase', - user: { - id: 'user-123', - }, - data: { - plan: 'premium', - amount: 99.99, - currency: 'USD', - period: 'monthly', - }, - }); -} diff --git a/apps/quickstart/src/server-destinations/gcp.ts b/apps/quickstart/src/server-destinations/gcp.ts deleted file mode 100644 index 6ff305630..000000000 --- a/apps/quickstart/src/server-destinations/gcp.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupGCPPubSub(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector(); - return { collector, elb }; -} - -export const gcpPubSubConfig = { - settings: { - projectId: 'your-gcp-project', - topicName: 'walkerOS-events', - credentials: { - client_email: process.env.GCP_CLIENT_EMAIL || '', - private_key: process.env.GCP_PRIVATE_KEY || '', - }, - }, -}; - -export async function publishToGCP(elb: WalkerOS.Elb): Promise { - await elb('api request', { - endpoint: '/api/v1/users', - method: 'POST', - }); - - await elb('job complete', { - jobId: 'job-789', - type: 'data-processing', - }); -} diff --git a/apps/quickstart/src/server-destinations/meta-capi.ts b/apps/quickstart/src/server-destinations/meta-capi.ts deleted file mode 100644 index 0737d4cd6..000000000 --- a/apps/quickstart/src/server-destinations/meta-capi.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupMetaCAPI(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector(); - return { collector, elb }; -} - -export const metaCAPIConfig = { - settings: { - pixelId: 'YOUR_PIXEL_ID', - accessToken: process.env.META_ACCESS_TOKEN || '', - test_event_code: process.env.META_TEST_EVENT_CODE, - }, -}; - -export async function trackServerConversions(elb: WalkerOS.Elb): Promise { - await elb({ - event: 'order complete', - user: { - id: 'user-456', - email: 'customer@example.com', - }, - data: { - id: 'order-789', - total: 199.99, - currency: 'USD', - }, - }); - - await elb({ - event: 'form submit', - user: { - id: 'user-789', - email: 'lead@example.com', - phone: '+1234567890', - }, - data: { - type: 'contact', - value: 100, - }, - }); -} diff --git a/apps/quickstart/src/walkerjs/basic.ts b/apps/quickstart/src/walkerjs/basic.ts deleted file mode 100644 index 9c4d91645..000000000 --- a/apps/quickstart/src/walkerjs/basic.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function setupWalkerJS(): HTMLScriptElement { - const script = document.createElement('script'); - script.src = - 'https://cdn.jsdelivr.net/npm/@walkeros/walker.js@latest/dist/index.browser.js'; - script.async = true; - document.head.appendChild(script); - return script; -} - -export function initWalkerJS(): void { - const script = setupWalkerJS(); - script.onload = () => { - console.log('Walker.js loaded'); - }; -} diff --git a/apps/quickstart/src/walkerjs/with-sources.ts b/apps/quickstart/src/walkerjs/with-sources.ts deleted file mode 100644 index f9a9f5d4b..000000000 --- a/apps/quickstart/src/walkerjs/with-sources.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Walker.js bundle includes browser source by default -export async function setupWalkerWithSources(): Promise { - // Load walker.js bundle from CDN - await new Promise((resolve) => { - const script = document.createElement('script'); - script.src = - 'https://cdn.jsdelivr.net/npm/@walkeros/walker.js@latest/dist/index.browser.js'; - script.async = true; - script.onload = () => resolve(); - document.head.appendChild(script); - }); - - // Walker.js automatically initializes with browser source - // which tracks DOM elements with data-elb attributes - return (window as Record).elb; -} - -export async function trackWithWalkerSources(elb: unknown): Promise { - // Type the elb function safely - const elbFn = elb as ( - command: string, - config: Record, - ) => Promise; - - // Configure browser source to track specific attributes - await elbFn('walker config', { - source: { - browser: { - // Track elements with data-track attributes - dataLayer: true, - // Enable click tracking - click: true, - // Enable view tracking (intersection observer) - view: true, - }, - }, - }); - - // Add a destination to see the events - await elbFn('walker destination', { - push: (event: Record) => { - console.log('Walker.js Event:', { - event: event.event, - data: event.data, - trigger: event.trigger, - }); - }, - }); - - // Trigger a custom event - await elbFn('button click', { - label: 'Sign Up', - position: 'header', - }); -} diff --git a/apps/quickstart/src/web-browser/basic.ts b/apps/quickstart/src/web-browser/basic.ts deleted file mode 100644 index 108259f40..000000000 --- a/apps/quickstart/src/web-browser/basic.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupBrowserTracking(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector(); - return { collector, elb }; -} - -export async function setupBrowserWithConsole(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - console: { - push: (event) => console.log('Event:', event), - }, - }, - }); - return { collector, elb }; -} diff --git a/apps/quickstart/src/web-dataLayer/basic.ts b/apps/quickstart/src/web-dataLayer/basic.ts deleted file mode 100644 index 81859d1fd..000000000 --- a/apps/quickstart/src/web-dataLayer/basic.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupDataLayer(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector(); - return { collector, elb }; -} diff --git a/apps/quickstart/src/web-destinations/api.ts b/apps/quickstart/src/web-destinations/api.ts deleted file mode 100644 index 96f70cadf..000000000 --- a/apps/quickstart/src/web-destinations/api.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { createSource, createDestination } from '@walkeros/core'; -import { destinationAPI } from '@walkeros/web-destination-api'; -import { sourceBrowser } from '@walkeros/web-source-browser'; -import type { WalkerOS, Collector, Source } from '@walkeros/core'; - -export async function setupAPIDestination(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - // Single big config file - API destination setup - const trackingConfig = { - run: true, - globals: { - environment: 'production', - api_version: 'v1', - }, - sources: { - browser: createSource(sourceBrowser, { - settings: { - scope: document.body, - session: true, - }, - }), - }, - destinations: { - api: createDestination(destinationAPI, { - settings: { - url: 'https://api.example.com/events', - headers: { - 'X-API-Key': 'your-api-key', - 'Content-Type': 'application/json', - }, - }, - mapping: { - page: { - view: { - name: 'pageview', - data: { - map: { - url: 'data.url', - title: 'data.title', - timestamp: 'timestamp', - }, - }, - }, - }, - order: { - complete: { - name: 'purchase', - data: { - map: { - order_id: 'data.id', - revenue: 'data.total', - currency: 'data.currency', - }, - }, - }, - }, - }, - }), - }, - }; - - const { collector, elb } = await createCollector(trackingConfig); - return { collector, elb }; -} - -export async function trackAPIEvents(elb: WalkerOS.Elb): Promise { - await elb('page view', { - url: '/products', - title: 'Products Page', - }); - - await elb('order complete', { - id: 'order-999', - total: 249.99, - }); -} diff --git a/apps/quickstart/src/web-destinations/custom-destination.ts b/apps/quickstart/src/web-destinations/custom-destination.ts deleted file mode 100644 index 17655e4c1..000000000 --- a/apps/quickstart/src/web-destinations/custom-destination.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import type { WalkerOS, Destination, Collector } from '@walkeros/core'; - -// Custom destination that sends events to a webhook -const customWebhookDestination: Destination.Instance = { - type: 'webhook', - - config: {}, - - init({ config }) { - const { settings } = config; - const settingsObj = - settings && typeof settings === 'object' - ? (settings as Record) - : {}; - if (!settingsObj.url || typeof settingsObj.url !== 'string') { - console.warn('Custom webhook destination: URL not configured'); - return false; - } - console.log('Custom webhook destination initialized'); - }, - - async push(event, { config }) { - const { settings } = config; - const settingsObj = - settings && typeof settings === 'object' - ? (settings as Record) - : {}; - - if (!settingsObj.url || typeof settingsObj.url !== 'string') { - console.warn( - 'Custom webhook destination: No URL configured, skipping event', - ); - return; - } - - // Send to webhook - try { - const headers = - settingsObj.headers && typeof settingsObj.headers === 'object' - ? (settingsObj.headers as Record) - : {}; - - const response = await fetch(settingsObj.url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...headers, - }, - body: JSON.stringify({ - timestamp: new Date().toISOString(), - event: event.event, - data: event.data, - user: event.user, - session: event.context?.session, - }), - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - console.log('Event sent to webhook:', event.event); - } catch (error) { - console.error('Failed to send event to webhook:', error); - throw error; - } - }, -}; - -export async function setupCustomDestination(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - webhook: { - ...customWebhookDestination, - config: { - settings: { - url: 'https://webhook.site/unique-id', - headers: { - Authorization: 'Bearer your-api-token', - 'X-Source': 'walkerOS-quickstart', - }, - }, - mapping: { - // Map page views to custom event name - page: { - view: { - settings: { - eventName: 'pageview_tracked', - additionalData: { - source: 'walkerOS', - version: '1.0', - }, - }, - }, - }, - // Map purchases with custom data - order: { - complete: { - settings: { - eventName: 'purchase_completed', - additionalData: { - channel: 'web', - currency: 'USD', - }, - }, - }, - }, - }, - }, - }, - }, - }); - - return { collector, elb }; -} - -export async function trackCustomDestinationEvents( - elb: WalkerOS.Elb, -): Promise { - // Track page view - await elb('page view', { - title: 'Custom Destination Demo', - url: '/demo', - }); - - // Track purchase - await elb('order complete', { - id: 'order-12345', - total: 99.99, - items: 2, - }); - - // Track custom event - await elb('feature used', { - feature: 'custom-destination', - success: true, - }); -} diff --git a/apps/quickstart/src/web-destinations/ga4-complete.ts b/apps/quickstart/src/web-destinations/ga4-complete.ts deleted file mode 100644 index 100391326..000000000 --- a/apps/quickstart/src/web-destinations/ga4-complete.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationGtag } from '@walkeros/web-destination-gtag'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupGA4Complete(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - gtag: { - ...destinationGtag, - config: { - settings: { - ga4: { - measurementId: 'G-XXXXXXXXXX', - }, - }, - mapping: { - // Page view - page: { - view: { - name: 'page_view', - settings: { ga4: {} }, - }, - }, - // Product events - product: { - view: { - name: 'view_item', - settings: { ga4: {} }, - data: { - map: { - currency: { value: 'USD' }, - value: 'data.price', - }, - }, - }, - }, - // Purchase event - order: { - complete: { - name: 'purchase', - settings: { ga4: {} }, - data: { - map: { - transaction_id: 'data.id', - value: 'data.total', - currency: { value: 'USD' }, - }, - }, - }, - }, - }, - }, - }, - }, - }); - return { collector, elb }; -} - -export async function trackGA4Events(elb: WalkerOS.Elb): Promise { - await elb('page view', { - title: 'Home Page', - path: '/', - }); - - await elb('product view', { - id: 'prod-123', - name: 'Red Sneakers', - price: 99.99, - }); - - await elb('order complete', { - id: 'order-456', - total: 109.98, - }); -} diff --git a/apps/quickstart/src/web-destinations/gtag-ads.ts b/apps/quickstart/src/web-destinations/gtag-ads.ts deleted file mode 100644 index 20e85a70b..000000000 --- a/apps/quickstart/src/web-destinations/gtag-ads.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationGtag } from '@walkeros/web-destination-gtag'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupGoogleAds(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - gtag: { - ...destinationGtag, - config: { - settings: { - ads: { - conversionId: 'AW-XXXXXXXXX', - }, - }, - mapping: { - order: { - complete: { - name: 'conversion', - settings: { ads: {} }, - data: { - map: { - value: 'data.total', - currency: 'data.currency', - transaction_id: 'data.id', - }, - }, - }, - }, - form: { - submit: { - name: 'conversion', - settings: { ads: { conversionLabel: 'LEAD' } }, - data: { - map: { - value: { value: 0, key: 'data.value' }, - }, - }, - }, - }, - }, - }, - }, - }, - }); - return { collector, elb }; -} - -export async function trackAdsConversions(elb: WalkerOS.Elb): Promise { - await elb('order complete', { - id: 'order-789', - total: 129.99, - currency: 'USD', - }); - - await elb('form submit', { - type: 'lead', - value: 50, - }); -} diff --git a/apps/quickstart/src/web-destinations/gtag-complete.ts b/apps/quickstart/src/web-destinations/gtag-complete.ts deleted file mode 100644 index 91b9da9db..000000000 --- a/apps/quickstart/src/web-destinations/gtag-complete.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { createSource, createDestination } from '@walkeros/core'; -import { destinationGtag } from '@walkeros/web-destination-gtag'; -import { sourceBrowser } from '@walkeros/web-source-browser'; -import type { WalkerOS, Collector, Source } from '@walkeros/core'; - -export async function setupGtagComplete(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - // Single big config file approach - complete tracking setup - const trackingConfig = { - run: true, - globals: { - environment: 'production', - version: '1.0.0', - }, - sources: { - browser: createSource(sourceBrowser, { - settings: { - scope: document.body, - session: true, - }, - }), - }, - destinations: { - gtag: createDestination(destinationGtag, { - settings: { - ga4: { - measurementId: 'G-XXXXXXXXXX', - }, - ads: { - conversionId: 'AW-XXXXXXXXX', - }, - gtm: { - containerId: 'GTM-XXXXXXX', - }, - }, - mapping: { - // GA4 Purchase mapping - order: { - complete: { - name: 'purchase', - settings: { - ga4: { include: ['data'] }, - }, - data: { - map: { - transaction_id: 'data.id', - value: 'data.total', - currency: { value: 'USD', key: 'data.currency' }, - }, - }, - }, - }, - // Google Ads conversion mapping - form: { - submit: { - name: 'conversion', - settings: { - ads: { label: 'LEAD' }, - }, - data: { - map: { - value: { value: 0, key: 'data.value' }, - currency: { value: 'USD' }, - }, - }, - }, - }, - // GTM custom event - product: { - view: { - name: 'product_view', - settings: { - gtm: {}, - }, - data: { - map: { - product_id: 'data.id', - product_name: 'data.name', - value: 'data.price', - }, - }, - }, - }, - }, - }), - }, - }; - - const { collector, elb } = await createCollector(trackingConfig); - return { collector, elb }; -} - -export async function trackGtagEvents(elb: WalkerOS.Elb): Promise { - // GA4 purchase event - await elb('order complete', { - id: 'order-123', - total: 99.99, - currency: 'USD', - }); - - // Google Ads lead conversion - await elb('form submit', { - type: 'lead', - value: 50, - }); - - // GTM product view - await elb('product view', { - id: 'prod-456', - name: 'Blue Jacket', - price: 79.99, - }); -} diff --git a/apps/quickstart/src/web-destinations/meta-pixel.ts b/apps/quickstart/src/web-destinations/meta-pixel.ts deleted file mode 100644 index d2364f804..000000000 --- a/apps/quickstart/src/web-destinations/meta-pixel.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { createSource, createDestination } from '@walkeros/core'; -import { destinationMeta } from '@walkeros/web-destination-meta'; -import { sourceBrowser } from '@walkeros/web-source-browser'; -import type { WalkerOS, Collector, Source } from '@walkeros/core'; - -export async function setupMetaPixel(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - // Single big config file - Meta Pixel tracking setup - const trackingConfig = { - run: true, - globals: { - environment: 'production', - currency: 'USD', - }, - sources: { - browser: createSource(sourceBrowser, { - settings: { - scope: document.body, - session: true, - }, - }), - }, - destinations: { - meta: createDestination(destinationMeta, { - settings: { - pixelId: 'YOUR_PIXEL_ID', - }, - mapping: { - page: { - view: { name: 'PageView' }, - }, - product: { - add: { - name: 'AddToCart', - data: { - map: { - value: 'data.price', - currency: 'data.currency', - content_ids: ['data.id'], - content_name: 'data.name', - }, - }, - }, - }, - order: { - complete: { - name: 'Purchase', - data: { - map: { - value: 'data.total', - currency: 'data.currency', - content_ids: ['data.id'], - }, - }, - }, - }, - }, - }), - }, - }; - - const { collector, elb } = await createCollector(trackingConfig); - return { collector, elb }; -} - -export async function trackMetaEvents(elb: WalkerOS.Elb): Promise { - await elb('page view'); - - await elb('product add', { - id: 'prod-456', - name: 'Summer Dress', - price: 49.99, - currency: 'USD', - }); - - await elb('order complete', { - id: 'order-123', - total: 99.98, - currency: 'USD', - }); -} diff --git a/apps/quickstart/src/web-destinations/piwikpro.ts b/apps/quickstart/src/web-destinations/piwikpro.ts deleted file mode 100644 index 5d239f1a7..000000000 --- a/apps/quickstart/src/web-destinations/piwikpro.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationPiwikPro } from '@walkeros/web-destination-piwikpro'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupPiwikPro(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - piwikpro: { - ...destinationPiwikPro, - config: { - settings: { - appId: 'XXX-XXX-XXX-XXX-XXX', - url: 'https://your-instance.piwik.pro/', - }, - mapping: { - // Product view to ecommerceProductDetailView - product: { - view: { - name: 'ecommerceProductDetailView', - data: { - set: [ - { - set: [ - { - map: { - sku: 'data.id', - name: 'data.name', - price: 'data.price', - quantity: { value: 1 }, - }, - }, - ], - }, - { - map: { - currencyCode: { value: 'EUR' }, - }, - }, - ], - }, - }, - // Product add to ecommerceAddToCart - add: { - name: 'ecommerceAddToCart', - data: { - set: [ - { - set: [ - { - map: { - sku: 'data.id', - name: 'data.name', - price: 'data.price', - quantity: { value: 1 }, - }, - }, - ], - }, - { - map: { - currencyCode: { value: 'EUR' }, - }, - }, - ], - }, - }, - }, - // Order complete to ecommerceOrder - order: { - complete: { - name: 'ecommerceOrder', - data: { - set: [ - { - map: { - orderId: 'data.id', - grandTotal: 'data.total', - currencyCode: { value: 'EUR' }, - }, - }, - ], - }, - }, - }, - }, - }, - }, - }, - }); - - return { collector, elb }; -} - -export async function trackPiwikProEvents(elb: WalkerOS.Elb): Promise { - await elb('product view', { - id: 'SKU-123', - name: 'Blue T-Shirt', - price: 29.99, - }); - - await elb('product add', { - id: 'SKU-456', - name: 'Red Shoes', - price: 89.99, - }); - - await elb('order complete', { - id: 'order-789', - total: 149.99, - }); -} diff --git a/apps/quickstart/src/web-destinations/plausible.ts b/apps/quickstart/src/web-destinations/plausible.ts deleted file mode 100644 index ca9b413c4..000000000 --- a/apps/quickstart/src/web-destinations/plausible.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { createCollector } from '@walkeros/collector'; -import { destinationPlausible } from '@walkeros/web-destination-plausible'; -import type { WalkerOS, Collector } from '@walkeros/core'; - -export async function setupPlausible(): Promise<{ - collector: Collector.Instance; - elb: WalkerOS.Elb; -}> { - const { collector, elb } = await createCollector({ - destinations: { - plausible: { - ...destinationPlausible, - config: { - settings: { - domain: 'yourdomain.com', - apiHost: 'https://plausible.io', - }, - mapping: { - // Custom goal tracking - form: { - submit: { - name: 'Contact Form', - }, - }, - file: { - download: { - name: 'Download', - data: { - map: { - filename: 'data.filename', - }, - }, - }, - }, - }, - }, - }, - }, - }); - - return { collector, elb }; -} - -export async function trackPlausibleEvents(elb: WalkerOS.Elb): Promise { - await elb('form submit', { - type: 'contact', - }); - - await elb('file download', { - filename: 'whitepaper.pdf', - }); -} diff --git a/apps/storybook-addon/CHANGELOG.md b/apps/storybook-addon/CHANGELOG.md index e32dfefa5..a7dadd2e7 100644 --- a/apps/storybook-addon/CHANGELOG.md +++ b/apps/storybook-addon/CHANGELOG.md @@ -1,5 +1,18 @@ # @walkeros/storybook-addon +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-source-browser@0.1.0 + - @walkeros/web-core@0.1.0 + - @walkeros/core@0.1.0 + ## 0.0.2 ### Patch Changes diff --git a/apps/storybook-addon/package.json b/apps/storybook-addon/package.json index 9d335d1b2..3216fa944 100644 --- a/apps/storybook-addon/package.json +++ b/apps/storybook-addon/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/storybook-addon", - "version": "0.0.2", + "version": "0.1.0", "description": "Integrate walkerOS tagging support for data collection", "keywords": [ "tracking", diff --git a/apps/walkerjs/CHANGELOG.md b/apps/walkerjs/CHANGELOG.md index 92102d3e4..5687bcd48 100644 --- a/apps/walkerjs/CHANGELOG.md +++ b/apps/walkerjs/CHANGELOG.md @@ -1,5 +1,20 @@ # @walkeros/walker.js +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-source-datalayer@0.1.0 + - @walkeros/web-source-browser@0.1.0 + - @walkeros/collector@0.1.0 + - @walkeros/web-core@0.1.0 + - @walkeros/core@0.1.0 + ## 0.0.10 ### Patch Changes diff --git a/apps/walkerjs/README.md b/apps/walkerjs/README.md index c6cb24108..8cd833d5d 100644 --- a/apps/walkerjs/README.md +++ b/apps/walkerjs/README.md @@ -1,8 +1,11 @@ -# Walker.js +# walker.js -A ready-to-use walkerOS bundle that combines the browser source, collector, and -dataLayer support into a single JavaScript file. Perfect for quickly adding -privacy-friendly event tracking to any website. +Walker.js is a pre-built walkerOS application that combines both the +[browser](/docs/sources/web/browser/) and +[dataLayer](/docs/sources/web/dataLayer/) sources with the +[collector](/docs/collector/) and a default `dataLayer` destination into a +pre-build package. It's designed for users who want instant web tracking without +complex setup or configuration. ## Features @@ -16,27 +19,17 @@ privacy-friendly event tracking to any website. - ๐Ÿ“ฆ **Queue support** - Events are queued until walker.js loads (elbLayer) - ๐Ÿงช **Well tested** - Comprehensive test suite included -## Quick Start +## Installation -### 1. Add elbLayer Function (Recommended) +### Option 1: NPM Package -Add this before walker.js loads to queue events: - -```html - +```bash +npm install @walkeros/walker.js ``` -### 2. Include walker.js +### Option 2: CDN ```html - - - - ``` -### 3. Configure +## Basic Setup -#### Option A: Default Configuration +### 1. Add Event Queueing (Recommended) -Just load the `walker.js` script - it will look for `window.elbConfig`: +Add this script before walker.js loads to queue events during initialization: ```html - ``` -#### Option B: Named Configuration Object +### 2. Include Walker.js + +```html + +``` -Use `data-elbconfig` on the script tag to define the configuration object. +### 3. Configure Destinations ```html - -``` - -#### Option C: Inline Configuration - -Configure directly in the script tag. Use simple key:value pairs separated by -semicolon. - -```html - ``` -### 3. Track Events +## Configuration Options -#### Automatic DOM Tracking +Walker.js supports multiple configuration approaches with different priorities: -Add data attributes to your HTML: - -```html - -
- -
+1. **Script tag `data-elbconfig`** (highest priority) +2. **`window.elbConfig`** (default fallback) +3. **Manual initialization** (when `run: false`) - -
+### Settings - -
-``` +| Name | Type | Default | Description | +| :---------- | :------------------ | :------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- | +| `elb` | `string` | `"elb"` | Global function name for event tracking | +| `name` | `string` | `"walkerjs"` | Global instance name | +| `run` | `boolean` | `true` | Auto-initialize walker.js on load | +| `browser` | `object \| boolean` | `{ run: true, session: true, scope: document.body, pageview: true }` | [Browser source configuration](https://www.elbwalker.com/docs/sources/web/browser/) | +| `dataLayer` | `object \| boolean` | `false` | [DataLayer source configuration](https://www.elbwalker.com/docs/sources/web/dataLayer) | +| `collector` | `object` | `{}` | [Collector configuration](https://www.elbwalker.com/docs/collector/) including destinations and consent settings | -#### Manual Event Tracking +#### Browser Source Settings -```javascript -// Using the global elb function -elb('product add', { - id: '123', - price: 29.99, -}); -``` +| Name | Type | Default | Description | +| :----------------- | :-------- | :-------------- | :-------------------------------- | +| `browser.run` | `boolean` | `true` | Auto-start DOM tracking | +| `browser.session` | `boolean` | `true` | Enable session tracking | +| `browser.scope` | `Element` | `document.body` | DOM element scope for tracking | +| `browser.pageview` | `boolean` | `true` | Enable automatic page view events | -## Configuration +#### DataLayer Settings -### Configuration Options +| Name | Type | Default | Description | +| :----------------- | :-------- | :------------ | :----------------------------------------- | +| `dataLayer` | `boolean` | `false` | Enable dataLayer integration with defaults | +| `dataLayer.name` | `string` | `"dataLayer"` | DataLayer variable name | +| `dataLayer.prefix` | `string` | `"dataLayer"` | Event prefix for dataLayer events | -Walker.js can be configured in multiple ways: +#### Collector Settings -1. **Script tag with `data-elbconfig`** - Highest priority -2. **`window.elbConfig`** - Default fallback -3. **Manual initialization** - When `run: false` +| Name | Type | Default | Description | +| :----------------------- | :------- | :--------------------- | :------------------------------------------------------------------------ | +| `collector.consent` | `object` | `{ functional: true }` | Default consent state | +| `collector.destinations` | `object` | `{}` | [Destination configurations](https://www.elbwalker.com/docs/destinations) | ### Full Configuration Object ```javascript window.elbConfig = { - // Global configuration + // Global settings elb: 'elb', // Global function name (default: 'elb') name: 'walkerjs', // Global instance name run: true, // Auto-initialize (default: true) @@ -145,56 +138,118 @@ window.elbConfig = { run: true, // Auto-start DOM tracking session: true, // Enable session tracking scope: document.body, // Tracking scope + pageview: true, // Enable automatic page views }, // DataLayer integration dataLayer: true, // Enable dataLayer // or detailed config: - dataLayer: { - name: 'dataLayer', // DataLayer variable name - prefix: 'dataLayer', // Event prefix - }, + // dataLayer: { + // name: 'dataLayer', // DataLayer variable name + // prefix: 'dataLayer', // Event prefix + // }, // Collector configuration collector: { consent: { functional: true }, // Default consent state destinations: { - // Your destinations + // Your destinations here + console: { + push: (event) => console.log('Event:', event), + }, }, }, }; ``` -### Destination Configuration +### Inline Configuration -```javascript -const walkerjs = Walkerjs({ - collector: { - destinations: { - console: { - type: 'console', - push: (event) => console.log(event), - }, +Configure directly in the script tag using simple key:value pairs: - api: { - type: 'custom-api', - push: async (event) => { - await fetch('/api/events', { - method: 'POST', - body: JSON.stringify(event), - }); - }, +```html + +``` + +### Named Configuration Object + +Use a custom configuration object name: + +```html + + +``` + +## Usage + +### Automatic DOM Tracking + +Walker.js automatically tracks events based on HTML data attributes: + +```html + +
+ +
+ + +
+ + +
+``` + +For detailed information on data attributes, see the +[Browser Source documentation](https://www.elbwalker.com/docs/sources/web/browser/tagging). + +### Manual Event Tracking + +Use the global `elb` function for manual tracking: + +```javascript +// Simple event +elb('button click', { + label: 'interesting', +}); +``` + +### DataLayer Integration + +Walker.js can integrate with existing dataLayer implementations: + +```javascript +// Enable dataLayer integration +window.elbConfig = { + dataLayer: true, // Uses window.dataLayer by default +}; + +// Existing dataLayer events will be processed +dataLayer.push({ + event: 'purchase', + ecommerce: { + transaction_id: '12345', + value: 25.42, }, }); ``` -## Advanced Usage +## Advanced Features ### Async Loading & Event Queueing -Walker.js supports async loading with automatic event queueing: +Walker.js handles async loading gracefully with automatic event queueing: ```html - + ``` -**Benefits:** - -- No timing issues with async scripts -- Events are never lost -- Works with any loading strategy (async, defer, dynamic) -- Zero dependencies for the queue function - ### Build Variants -Walker.js provides multiple build formats: +Walker.js provides multiple build formats for different environments: - `walker.js` - Standard IIFE bundle for browsers - `index.es5.js` - GTM-compatible ES2015 build - `index.mjs` - ES modules for modern bundlers - `index.js` - CommonJS for Node.js environments +### Programmatic Usage + +Use walker.js programmatically in applications: + +```javascript +import { createWalkerjs } from '@walkeros/walker.js'; + +const { collector, elb } = await createWalkerjs({ + collector: { + destinations: { + console: { push: console.log }, + }, + }, + browser: { + session: true, + pageview: true, + }, +}); +``` + +## Destination Configuration + +Configure multiple destinations for your events: + +```javascript +window.elbConfig = { + collector: { + destinations: { + // Console logging for development + console: { + push: (event) => console.log('Walker.js Event:', event), + }, + + // Custom API endpoint + api: { + push: async (event) => { + await fetch('/api/events', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(event), + }); + }, + }, + }, + }, +}; +``` + +For comprehensive destination options, see the +[Destinations documentation](https://www.elbwalker.com/docs/destinations/). + +## Troubleshooting + +### Common Issues + +**Events not firing:** Check that walker.js loaded and configuration is valid. + +**Missing events:** Ensure event queueing function is added before walker.js. + +**Configuration not applied:** Verify `data-elbconfig` points to the correct +object name. + ## API Reference ### Factory Function -#### `createWalkerjs(config?): Walkerjs.Instance` +```typescript +createWalkerjs(config?: Config): Promise +``` Creates a new walker.js instance with the provided configuration. @@ -241,28 +352,19 @@ Creates a new walker.js instance with the provided configuration. - `collector` - The walkerOS collector instance - `elb` - Browser push function for event tracking -### Additional Methods - -- `getAllEvents(scope?, prefix?)` - Get all trackable events on the page -- `getEvents(target, trigger, prefix?)` - Get events for a specific element and - trigger -- `getGlobals(prefix?, scope?)` - Get global properties from the page - ### Utility Functions -You can also import and use the utility functions directly: - ```javascript import { getAllEvents, getEvents, getGlobals } from '@walkeros/walker.js'; -// Get all events on the page +// Get all trackable events on the page const events = getAllEvents(); -// Get events for a specific button click +// Get events for a specific element and trigger const button = document.querySelector('button'); const clickEvents = getEvents(button, 'click'); -// Get global properties +// Get global properties from the page const globals = getGlobals(); ``` @@ -287,19 +389,25 @@ npm run dev # Watch mode npm run preview # Serve examples on localhost:3333 ``` -## License +## Related Documentation -MIT License - see LICENSE file for details. +- **[Browser Source](https://www.elbwalker.com/docs/sources/web/browser/)** - + Detailed DOM tracking capabilities +- **[Collector](https://www.elbwalker.com/docs/collector/)** - Event processing + and routing +- **[Destinations](https://www.elbwalker.com/docs/destinations/)** - Available + destination options +- **[DataLayer Source](https://www.elbwalker.com/docs/sources/web/dataLayer/)** - + DataLayer integration details + +Walker.js combines all these components into a single, easy-to-use package +perfect for getting started with walkerOS quickly. ## Contributing This package is part of the walkerOS monorepo. Please see the main repository for contribution guidelines. -## Related Packages +## License -- [@walkeros/collector](../../../packages/collector) - Core collector -- [@walkeros/web-source-browser](../../../packages/web/sources/browser) - - Browser source -- [@walkeros/web-source-datalayer](../../../packages/web/sources/dataLayer) - - DataLayer source +MIT License - see LICENSE file for details. diff --git a/apps/walkerjs/package.json b/apps/walkerjs/package.json index aabe81cbb..f3a314ff5 100644 --- a/apps/walkerjs/package.json +++ b/apps/walkerjs/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/walker.js", - "version": "0.0.10", + "version": "0.1.0", "description": "Ready-to-use walkerOS bundle with browser source, collector, and dataLayer support", "license": "MIT", "main": "./dist/index.js", @@ -34,11 +34,11 @@ "preview": "npm run build && npx serve -l 3333 examples" }, "dependencies": { - "@walkeros/core": "0.0.8", - "@walkeros/collector": "0.0.8", - "@walkeros/web-core": "0.0.8", - "@walkeros/web-source-browser": "0.0.9", - "@walkeros/web-source-datalayer": "0.0.8" + "@walkeros/core": "0.1.0", + "@walkeros/collector": "0.1.0", + "@walkeros/web-core": "0.1.0", + "@walkeros/web-source-browser": "0.1.0", + "@walkeros/web-source-datalayer": "0.1.0" }, "devDependencies": { "@swc/jest": "^0.2.36", diff --git a/apps/walkerjs/src/__tests__/destination.test.ts b/apps/walkerjs/src/__tests__/destination.test.ts index 0964740c2..936ed2b0d 100644 --- a/apps/walkerjs/src/__tests__/destination.test.ts +++ b/apps/walkerjs/src/__tests__/destination.test.ts @@ -20,7 +20,7 @@ describe('Destination Tests', () => { test('should push event to dataLayer', () => { const destination = dataLayerDestination(); const event = { - event: 'foo bar', + name: 'foo bar', } as unknown as WalkerOS.Event; destination.push(event, {} as unknown as Destination.PushContext); expect(mockDataLayer).toHaveBeenCalledWith(event); @@ -29,7 +29,7 @@ describe('Destination Tests', () => { test('should not push events from dataLayer source', () => { const destination = dataLayerDestination(); const event = { - event: 'foo bar', + name: 'foo bar', source: { type: 'dataLayer', }, @@ -41,7 +41,7 @@ describe('Destination Tests', () => { test('should push context data when available', () => { const destination = dataLayerDestination(); const event = { - event: 'foo bar', + name: 'foo bar', } as unknown as WalkerOS.Event; const contextData = { custom: 'data' }; destination.push(event, { @@ -56,16 +56,16 @@ describe('Destination Tests', () => { const destination = dataLayerDestination(); const batch = { key: 'test-batch', - data: [{ event: 'event1' }, { event: 'event2' }], + data: [{ name: 'event1' }, { name: 'event2' }], events: [], } as unknown as Destination.Batch; destination.pushBatch?.(batch, {} as unknown as Destination.PushContext); expect(mockDataLayer).toHaveBeenCalledWith({ - event: 'batch', + name: 'batch', batched_event: 'test-batch', - events: [{ event: 'event1' }, { event: 'event2' }], + events: [{ name: 'event1' }, { name: 'event2' }], }); }); @@ -74,15 +74,15 @@ describe('Destination Tests', () => { const batch = { key: 'test-batch', data: [], - events: [{ event: 'fallback1' }, { event: 'fallback2' }], + events: [{ name: 'fallback1' }, { name: 'fallback2' }], } as unknown as Destination.Batch; destination.pushBatch?.(batch, {} as unknown as Destination.PushContext); expect(mockDataLayer).toHaveBeenCalledWith({ - event: 'batch', + name: 'batch', batched_event: 'test-batch', - events: [{ event: 'fallback1' }, { event: 'fallback2' }], + events: [{ name: 'fallback1' }, { name: 'fallback2' }], }); }); }); @@ -98,7 +98,7 @@ describe('Destination Tests', () => { test('should handle events with non-object source', () => { const destination = dataLayerDestination(); const event = { - event: 'foo bar', + name: 'foo bar', source: 'string source', } as unknown as WalkerOS.Event; destination.push(event, {} as unknown as Destination.PushContext); @@ -108,7 +108,7 @@ describe('Destination Tests', () => { test('should handle events with dataLayer-like source type', () => { const destination = dataLayerDestination(); const event = { - event: 'foo bar', + name: 'foo bar', source: { type: 'custom-dataLayer-source', }, diff --git a/apps/walkerjs/src/__tests__/integration.test.ts b/apps/walkerjs/src/__tests__/integration.test.ts index e280df5f8..51bb9f860 100644 --- a/apps/walkerjs/src/__tests__/integration.test.ts +++ b/apps/walkerjs/src/__tests__/integration.test.ts @@ -104,7 +104,7 @@ describe('Walker.js Integration Tests', () => { const event = mockPush.mock.calls[0][0] as unknown as WalkerOS.Event; expect(event).toMatchObject({ - event: 'product add', + name: 'product add', data: { id: 123, name: 'Test Product', @@ -135,7 +135,7 @@ describe('Walker.js Integration Tests', () => { expect(mockPush).toHaveBeenCalled(); const event = mockPush.mock.calls[0][0] as unknown as WalkerOS.Event; - expect(event.event).toBe('order complete'); + expect(event.name).toBe('order complete'); expect(event.data).toMatchObject({ transaction_id: 'TRX123', value: 99.99, diff --git a/apps/walkerjs/src/destination.ts b/apps/walkerjs/src/destination.ts index db7a36e68..8ccb6d048 100644 --- a/apps/walkerjs/src/destination.ts +++ b/apps/walkerjs/src/destination.ts @@ -23,7 +23,7 @@ export function dataLayerDestination(): Destination.InitDestination { }, pushBatch: (batch) => { dataLayerPush({ - event: 'batch', + name: 'batch', batched_event: batch.key, events: batch.data.length ? batch.data : batch.events, }); diff --git a/package-lock.json b/package-lock.json index 555c37773..a38b8974a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "packages/web/destinations/*", "packages/web/sources/*", "apps/walkerjs", - "apps/quickstart", "apps/storybook-addon", "apps/demos/react", "apps/demos/storybook", @@ -1576,6 +1575,7 @@ "apps/quickstart": { "name": "@walkeros/quickstart", "version": "0.0.4", + "extraneous": true, "license": "MIT", "dependencies": { "@walkeros/collector": "0.0.8", @@ -1601,20 +1601,6 @@ "jest-environment-jsdom": "^29.7.0" } }, - "apps/quickstart/node_modules/@walkeros/web-source-browser": { - "version": "0.0.9", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/elbwalker" - } - ], - "license": "MIT", - "dependencies": { - "@walkeros/collector": "0.0.8", - "@walkeros/web-core": "0.0.8" - } - }, "apps/storybook-addon": { "name": "@walkeros/storybook-addon", "version": "0.0.2", @@ -2477,7 +2463,7 @@ "@walkeros/collector": "0.0.8", "@walkeros/core": "0.0.8", "@walkeros/web-core": "0.0.8", - "@walkeros/web-source-browser": "0.0.9", + "@walkeros/web-source-browser": "0.0.10", "@walkeros/web-source-datalayer": "0.0.8" }, "devDependencies": { @@ -2487,20 +2473,6 @@ "jest-environment-jsdom": "^29.7.0" } }, - "apps/walkerjs/node_modules/@walkeros/web-source-browser": { - "version": "0.0.9", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/elbwalker" - } - ], - "license": "MIT", - "dependencies": { - "@walkeros/collector": "0.0.8", - "@walkeros/web-core": "0.0.8" - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -15370,10 +15342,6 @@ "resolved": "packages/config/jest", "link": true }, - "node_modules/@walkeros/quickstart": { - "resolved": "apps/quickstart", - "link": true - }, "node_modules/@walkeros/server-core": { "resolved": "packages/server/core", "link": true diff --git a/package.json b/package.json index 27c03ab84..697a21a32 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "packages/web/destinations/*", "packages/web/sources/*", "apps/walkerjs", - "apps/quickstart", "apps/storybook-addon", "apps/demos/react", "apps/demos/storybook", @@ -23,9 +22,9 @@ "clean": "turbo run clean", "dev": "turbo run dev --output-logs=errors-only", "format": "prettier --write .", - "lint": "turbo run lint --output-logs=errors-only", + "lint": "turbo run lint --filter=!@walkeros/website --output-logs=errors-only", "publish-packages": "npm run build lint test && changeset version && changeset publish", - "test": "turbo run test --output-logs=errors-only", + "test": "turbo run test --filter=!@walkeros/website --output-logs=errors-only", "prepare": "is-ci || husky" }, "devDependencies": { diff --git a/packages/collector/CHANGELOG.md b/packages/collector/CHANGELOG.md index f65bc58bb..a275e8007 100644 --- a/packages/collector/CHANGELOG.md +++ b/packages/collector/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/collector +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/collector/README.md b/packages/collector/README.md index 59f59e3bf..aed36e72b 100644 --- a/packages/collector/README.md +++ b/packages/collector/README.md @@ -1,56 +1,47 @@

- +

# Collector for walkerOS -The walkerOS Collector is the central event processing engine that unifies data -collection across web and server environments. It acts as the orchestrator -between sources (where events originate) and destinations (where events are -sent), providing consistent event processing, consent management, and data -validation across your entire data collection infrastructure. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/collector) +• [NPM Package](https://www.npmjs.com/package/@walkeros/collector) -## Role in walkerOS Ecosystem +The collector is the central **processing engine** of walkerOS that receives +events from sources, **enriches** them with additional data, applies consent +rules, and **routes** them to destinations. It acts as the **intelligent +middleware** between event capture and event delivery. -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: +### What it does -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) +The Collector transforms raw events into enriched, compliant data streams by: -The Collector serves as the foundation that both web and server sources depend -on, ensuring consistent event handling regardless of the environment. +- **Event processing** - Validates, normalizes, and enriches incoming events +- **Consent management** - Applies privacy rules and user consent preferences +- **Data enrichment** - Adds session data, user context, and custom properties +- **Destination routing** - Sends processed events to configured analytics + platforms -## Installation - -```sh -npm install @walkeros/collector -``` - -## Usage +### Key features -The collector provides a factory function for creating collector instances: +- **Compatibility** - Works in both web browsers and server environments +- **Privacy-first** - Built-in consent management and data protection +- **Event validation** - Ensures data quality and consistency +- **Flexible routing** - Send events to multiple destinations simultaneously -```typescript -import { createCollector } from '@walkeros/collector'; +### Role in architecture -// Basic setup -const { collector, elb } = await createCollector({ - consent: { functional: true }, - destinations: [ - // Add your destinations here - ], -}); +In the walkerOS data flow, the collector sits between sources and destinations: -// Process events - use elb as the standard API -await elb('page view', { - page: '/home', -}); ``` +Sources โ†’ Collector โ†’ Destinations +``` + +Sources capture events and send them to the collector, which processes and +routes them to your chosen destinations like Google Analytics, custom APIs, or +data warehouses. ## Event Naming Convention @@ -78,15 +69,66 @@ The collector will validate event names and destinations handle platform-specific transformations. If the event name isn't separated into entity action by space the collector won't process it. -## Core Features +## Installation + +```bash +npm install @walkeros/collector +``` + +## Setup + +### Basic setup + +```typescript +import { createCollector } from '@walkeros/collector'; + +const config = { + run: true, + consent: { functional: true }, + sources: [ + // add your event sources + ] + }, +}; + +const { collector, elb } = await createCollector(config); +``` + +### Advanced setup + +```typescript +import { createCollector } from '@walkeros/collector'; + +const { collector, elb } = await createCollector({ + run: true, + consent: { functional: true }, + sources: [ + // add your event sources + ], + destinations: [ + // add your event destinations + ], + verbose: true, + onError: (error: unknown) => { + console.error('Collector error:', error); + }, + onLog: (message: string, level: 'debug' | 'info' | 'warn' | 'error') => { + console.log(`[${level}] ${message}`); + }, +}); +``` -- **Event Processing**: Validates and enriches events with context and metadata -- **Consent Management**: Respects user consent preferences across all - destinations -- **Destination Routing**: Translates and routes events to configured - destinations -- **State Management**: Maintains consistent state across the collection - pipeline +## Configuration + +| Name | Type | Description | Required | Example | +| -------------- | ---------- | -------------------------------------------------------------- | -------- | ------------------------------------------ | +| `run` | `boolean` | Automatically start the collector pipeline on initialization | No | `true` | +| `sources` | `array` | Configurations for sources providing events to the collector | No | `[{ source, config }]` | +| `destinations` | `array` | Configurations for destinations receiving processed events | No | `[{ destination, config }]` | +| `consent` | `object` | Initial consent state to control routing of events | No | `{ analytics: true, marketing: false }` | +| `verbose` | `boolean` | Enable verbose logging for debugging | No | `false` | +| `onError` | `function` | Error handler triggered when the collector encounters failures | No | `(error) => console.error(error)` | +| `onLog` | `function` | Custom log handler for collector messages | No | `(message, level) => console.log(message)` | ## Contribute diff --git a/packages/collector/package.json b/packages/collector/package.json index 15c6158e1..7e43a05fa 100644 --- a/packages/collector/package.json +++ b/packages/collector/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/collector", "description": "Unified platform-agnostic collector for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", @@ -18,7 +18,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "0.0.8" + "@walkeros/core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/collector/src/constants.ts b/packages/collector/src/constants.ts index b38fac324..05755d318 100644 --- a/packages/collector/src/constants.ts +++ b/packages/collector/src/constants.ts @@ -3,6 +3,7 @@ import type { WalkerOS } from '@walkeros/core'; export type CommandTypes = | 'Action' + | 'Actions' | 'Config' | 'Consent' | 'Context' @@ -23,6 +24,7 @@ export type CommandTypes = export const Commands: Record = { Action: 'action', + Actions: 'actions', Config: 'config', Consent: 'consent', Context: 'context', diff --git a/packages/collector/src/destination.ts b/packages/collector/src/destination.ts index 8c065d8a3..c71f564f5 100644 --- a/packages/collector/src/destination.ts +++ b/packages/collector/src/destination.ts @@ -56,14 +56,14 @@ export function createPush( // Event format: event object or string const partialEvent = typeof eventOrCommand === 'string' - ? { event: eventOrCommand } + ? { name: eventOrCommand } : (eventOrCommand as WalkerOS.DeepPartialEvent); const enrichedEvent = prepareEvent(partialEvent); const { event, command } = createEventOrCommand( collector, - enrichedEvent.event, + enrichedEvent.name, enrichedEvent, ); @@ -347,7 +347,7 @@ export async function destinationPush( if (eventMapping.ignore) return false; // Check to use specific event names - if (eventMapping.name) event.event = eventMapping.name; + if (eventMapping.name) event.name = eventMapping.name; // Transform event to a custom data if (eventMapping.data) { diff --git a/packages/collector/src/handle.ts b/packages/collector/src/handle.ts index 205c1e3d1..06031f0a6 100644 --- a/packages/collector/src/handle.ts +++ b/packages/collector/src/handle.ts @@ -115,13 +115,13 @@ export function createEventOrCommand( nameOrEvent, '' as string, ) - ? { event: nameOrEvent, ...defaults } + ? { name: nameOrEvent, ...defaults } : { ...defaults, ...(nameOrEvent || {}) }; - if (!partialEvent.event) throw new Error('Event name is required'); + if (!partialEvent.name) throw new Error('Event name is required'); // Check for valid entity and action event format - const [entityValue, actionValue] = partialEvent.event.split(' '); + const [entityValue, actionValue] = partialEvent.name.split(' '); if (!entityValue || !actionValue) throw new Error('Event name is invalid'); // It's a walker command @@ -141,7 +141,7 @@ export function createEventOrCommand( // Extract properties with default fallbacks const { - event = `${entityValue} ${actionValue}`, + name = `${entityValue} ${actionValue}`, data = {}, context = {}, globals = collector.globals, @@ -162,7 +162,7 @@ export function createEventOrCommand( } = partialEvent; const fullEvent: WalkerOS.Event = { - event, + name, data, context, globals, diff --git a/packages/config/eslint/index.mjs b/packages/config/eslint/index.mjs index d675768f5..76c55b70c 100644 --- a/packages/config/eslint/index.mjs +++ b/packages/config/eslint/index.mjs @@ -8,6 +8,7 @@ export default [ ignores: [ '**/coverage/**', '**/dist/**', + '**/build/**', '**/node_modules/**', '**/__mocks__/**', ], diff --git a/packages/config/jest/src/index.mjs b/packages/config/jest/src/index.mjs index cf5c50cfa..5c548ffd7 100644 --- a/packages/config/jest/src/index.mjs +++ b/packages/config/jest/src/index.mjs @@ -78,6 +78,22 @@ const config = { moduleDirectories: ['node_modules', 'src'], extensionsToTreatAsEsm: ['.ts', '.tsx'], moduleNameMapper: getModuleMapper(), + + // Performance settings - fixed values for consistent behavior + maxWorkers: 4, + testTimeout: 30000, + forceExit: true, + clearMocks: true, + restoreMocks: true, + detectOpenHandles: true, + + // Enhanced ignore patterns + testPathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/build/', + '/coverage/' + ], }; export default config; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 1db854f26..a52710d47 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,11 @@ # @walkeros/core +## 0.1.0 + +### Minor Changes + +- fixes + ## 0.0.8 ### Patch Changes diff --git a/packages/core/README.md b/packages/core/README.md index 205c3d31a..8ceaa07c2 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,108 +1,313 @@

- +

# Core Types & Utilities for walkerOS -The walkerOS Core package provides the foundational TypeScript definitions and -platform-agnostic utilities that power the entire walkerOS ecosystem. It serves -as the bedrock for type safety and shared functionality across all sources, -collectors, and destinations. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/core) +• [NPM Package](https://www.npmjs.com/package/@walkeros/core) -## Role in walkerOS Ecosystem +Core utilities are a collection of platform-agnostic functions that can be used +across all walkerOS environments. They provide standardized building blocks for +data manipulation, validation, mapping, and more. -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: +## Installation -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) +Import the core utilities directly from the `@walkeros/core` package: -The Core package provides the essential building blocks that all other packages -depend on, ensuring consistent data structures, type definitions, and utility -functions across the entire platform. +```ts +import { assign, anonymizeIP, getMappingValue } from '@walkeros/core'; +``` -## Installation +## Core Utilities + +### Data Manipulation + +#### assign + +`assign(target: T, source: U, options?): T & U` merges two objects with +advanced merging capabilities. It has special behavior for arrays: when merging, +it concatenates arrays from both objects, removing duplicates. + +```ts +interface AssignOptions { + merge?: boolean; // Merge array properties (default: true) + shallow?: boolean; // Create shallow copy (default: true) + extend?: boolean; // Extend with new properties (default: true) +} + +const obj1 = { a: 1, b: [1, 2] }; +const obj2 = { b: [2, 3], c: 3 }; + +assign(obj1, obj2); // Returns { a: 1, b: [1, 2, 3], c: 3 } +assign(obj1, obj2, { merge: false }); // Returns { a: 1, b: [2, 3], c: 3 } +``` + +#### Path Operations + +##### getByPath + +`getByPath(object: unknown, path: string, defaultValue?: unknown): unknown` +accesses nested properties using dot notation. Supports wildcard `*` for array +iteration. + +```js +getByPath({ data: { id: 'wow' } }, 'data.id'); // Returns "wow" +getByPath({ nested: [1, 2, { id: 'cool' }] }, 'nested.*.id'); // Returns ['', '', 'cool'] +getByPath({ arr: ['foo', 'bar'] }, 'arr.1'); // Returns "bar" +``` + +##### setByPath + +`setByPath(object: WalkerOS.Event, path: string, value: unknown): WalkerOS.Event` +sets nested values using dot notation, returning a new object with the updated +value. + +```js +const updatedEvent = setByPath(event, 'data.id', 'new-value'); +// Returns a new event with data.id set to 'new-value' +``` + +#### clone + +`clone(original: T): T` creates a deep copy of objects/arrays with circular +reference handling. -```sh -npm install @walkeros/core +```js +const original = { foo: true, arr: ['a', 'b'] }; +const cloned = clone(original); +original.foo = false; // cloned.foo remains true ``` -## Usage +#### castValue -The core package exports essential types and utilities: +`castValue(value: unknown): WalkerOS.PropertyType` converts string values to +appropriate types (number, boolean). -```typescript -import { - // Core event types - WalkerOS, +```js +castValue('123'); // Returns 123 (number) +castValue('true'); // Returns true (boolean) +castValue('hello'); // Returns 'hello' (unchanged) +``` + +### Privacy & Security + +#### anonymizeIP + +`anonymizeIP(ip: string): string` anonymizes IPv4 addresses by setting the last +oclet to zero. + +```js +anonymizeIP('192.168.1.100'); // Returns '192.168.1.0' +``` + +#### Hashing + +`getId(length?: number): string` generates random alphanumeric strings for +unique identifiers. + +```js +getId(); // Returns random 6-char string like 'a1b2c3' +getId(10); // Returns 10-character string +``` + +### Event Processing - // Utility functions - assign, - clone, - validateEvent, +#### getMappingValue - // Consent management - Consent, +`getMappingValue(event: WalkerOS.Event, mapping: Mapping.Data, options?: Mapping.Options): Promise` +extracts values from events using +[mapping configurations](https://www.elbwalker.com/docs/destinations/event-mapping). - // Mapping utilities - byPath, - mapping, -} from '@walkeros/core'; +```ts +// Simple path mapping +await getMappingValue(event, 'data.productId'); -// Example: Validate an event -const event: WalkerOS.Event = { - event: 'order complete', - data: { value: 9001 }, - // ... other properties +// Complex mapping with conditions and loops +const mapping = { + map: { + orderId: 'data.id', + products: { + loop: [ + 'nested', + { + condition: (entity) => entity.entity === 'product', + map: { id: 'data.id', name: 'data.name' }, + }, + ], + }, + }, }; +await getMappingValue(event, mapping); +``` -if (validateEvent(event)) { - console.log('Event is valid!'); -} +#### getMappingEvent + +`getMappingEvent(event: WalkerOS.PartialEvent, mapping?: Mapping.Rules): Promise` +finds the appropriate mapping rule for an event. + +### Marketing & Analytics + +#### getMarketingParameters + +`getMarketingParameters(url: URL, custom?: MarketingParameters): WalkerOS.Properties` +extracts UTM and click ID parameters from URLs. + +```js +getMarketingParameters( + new URL('https://example.com/?utm_source=docs&gclid=123'), +); +// Returns { source: "docs", gclid: "123", clickId: "gclid" } + +// With custom parameters +getMarketingParameters(url, { utm_custom: 'custom', partner: 'partnerId' }); ``` -## Event Naming Convention +### Type Validation + +#### Type Checkers + +A comprehensive set of type checking functions: + +- `isString(value)`, `isNumber(value)`, `isBoolean(value)` +- `isArray(value)`, `isObject(value)`, `isFunction(value)` +- `isDefined(value)`, `isSameType(a, b)` +- `isPropertyType(value)` - Checks if value is valid walkerOS property -walkerOS follows a strict **"entity action"** naming convention for events: +#### Property Utilities -โœ… **Correct**: Use spaces to separate entity and action +- `castToProperty(value)` - Casts to valid property type +- `filterValues(object)` - Filters object to valid properties only +- `isPropertyType(value)` - Type guard for property validation -```typescript -elb('order complete', { value: 99.99 }); -elb('product add', { id: 'abc123' }); -elb('page view', { path: '/home' }); -elb('user register', { email: 'user@example.com' }); +### Request Handling + +#### requestToData + +`requestToData(parameter: unknown): WalkerOS.AnyObject | undefined` converts +query strings to JavaScript objects with type casting. + +```js +requestToData('a=1&b=true&c=hello&arr[0]=x&arr[1]=y'); +// Returns { a: 1, b: true, c: 'hello', arr: ['x', 'y'] } +``` + +#### requestToParameter + +`requestToParameter(data: WalkerOS.AnyObject): string` converts objects to +URL-encoded query strings. + +```js +requestToParameter({ a: 1, b: true, arr: ['x', 'y'] }); +// Returns 'a=1&b=true&arr[0]=x&arr[1]=y' +``` + +### User Agent Parsing + +#### parseUserAgent + +`parseUserAgent(userAgent?: string): WalkerOS.User` extracts browser, OS, and +device information. + +```js +parseUserAgent(navigator.userAgent); +// Returns { browser: 'Chrome', browserVersion: '91.0', os: 'Windows', ... } ``` -โŒ **Incorrect**: Do not use underscores or other separators +Individual functions are also available: + +- `getBrowser(userAgent)` - Returns browser name +- `getBrowserVersion(userAgent)` - Returns browser version +- `getOS(userAgent)` - Returns operating system +- `getOSVersion(userAgent)` - Returns OS version +- `getDeviceType(userAgent)` - Returns 'Desktop', 'Tablet', or 'Mobile' + +### Error Handling -```typescript -// Don't do this -elb('order_complete', data); // Wrong: underscores -elb('orderComplete', data); // Wrong: camelCase -elb('purchase', data); // Wrong: single word +#### tryCatch + +`tryCatch(tryFn: Function, catchFn?: Function, finallyFn?: Function)` wraps +functions with error handling. + +```js +const safeParse = tryCatch(JSON.parse, () => ({})); +safeParse('{"valid": "json"}'); // Parses successfully +safeParse('invalid'); // Returns {} instead of throwing ``` -**Why spaces matter**: walkerOS destinations automatically transform your -semantic event names into platform-specific formats. For example, -`'order complete'` becomes `'purchase'` for Google Analytics 4, while preserving -the original meaning in your data model. +#### tryCatchAsync + +`tryCatchAsync(tryFn: Function, catchFn?: Function, finallyFn?: Function)` for +async operations. + +```js +const safeAsyncCall = tryCatchAsync( + () => fetchUserData(), + (error) => ({ error: 'Failed to load user' }), +); +``` + +### Performance Optimization + +#### debounce + +`debounce(fn: Function, wait?: number)` delays function execution until after +the wait time. + +```js +const debouncedSearch = debounce(searchFunction, 300); +// Only executes after 300ms of inactivity +``` + +#### throttle + +`throttle(fn: Function, wait?: number)` limits function execution frequency. + +```js +const throttledScroll = throttle(scrollHandler, 100); +// Executes at most every 100ms +``` + +### Utilities + +#### trim + +`trim(str: string): string` removes whitespace from string ends. + +#### throwError + +`throwError(message: string)` throws descriptive errors. + +#### onLog + +`onLog(message: unknown, verbose?: boolean)` provides consistent logging. + +```js +onLog('Debug info', true); // Logs message +onLog('Silent message'); // No output +``` + +### Validation + +#### validateEvent + +`validateEvent(obj: unknown, customContracts?: Schema.Contracts): WalkerOS.Event | never` +validates event structure and throws on invalid events. + +#### validateProperty + +Validates that values conform to walkerOS property types. + +--- -## Core Features +For platform-specific utilities, see: -- **TypeScript Definitions**: Complete type system for walkerOS events and - configurations -- **Platform-Agnostic Utilities**: Shared functions for data manipulation and - validation -- **Consent Types**: Standardized consent management interfaces -- **Event Validation**: Built-in validation for event structure and data - integrity -- **Mapping Utilities**: Tools for transforming data between different formats -- **Privacy Utilities**: Functions for data anonymization and privacy compliance +- [Web Core](https://www.elbwalker.com/docs/core/web) - Browser-specific + functions +- [Server Core](https://www.elbwalker.com/docs/core/server) - Node.js server + functions ## Contribute diff --git a/packages/core/package.json b/packages/core/package.json index 1d1fe353b..8ef84f3e7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/core", "description": "Core types and platform-agnostic utilities for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", diff --git a/packages/core/src/__tests__/eventGenerator.test.ts b/packages/core/src/__tests__/eventGenerator.test.ts index baf16e28d..0ac788df9 100644 --- a/packages/core/src/__tests__/eventGenerator.test.ts +++ b/packages/core/src/__tests__/eventGenerator.test.ts @@ -7,7 +7,7 @@ describe('createEvent', () => { const id = `${timestamp}-${group}-${count}`; const defaultEvent = { - event: 'entity action', + name: 'entity action', data: { string: 'foo', number: 1, @@ -21,7 +21,7 @@ describe('createEvent', () => { user: { id: 'us3r', device: 'c00k13', session: 's3ss10n' }, nested: [ { - type: 'child', + entity: 'child', data: { is: 'subordinated' }, nested: [], context: { element: ['child', 0] }, @@ -54,7 +54,7 @@ describe('createEvent', () => { test('getEvent', () => { expect(getEvent('page view')).toStrictEqual( expect.objectContaining({ - event: 'page view', + name: 'page view', data: { domain: 'www.example.com', title: 'walkerOS documentation', @@ -71,7 +71,7 @@ describe('createEvent', () => { expect(getEvent('page view', { data: { id: '/custom' } })).toStrictEqual( expect.objectContaining({ - event: 'page view', + name: 'page view', data: { id: '/custom' }, trigger: 'load', entity: 'page', @@ -81,7 +81,7 @@ describe('createEvent', () => { expect(getEvent('promotion visible')).toStrictEqual( expect.objectContaining({ - event: 'promotion visible', + name: 'promotion visible', data: { name: 'Setting up tracking easily', position: 'hero', diff --git a/packages/core/src/__tests__/mapping.test.ts b/packages/core/src/__tests__/mapping.test.ts index 88af9a222..2d2dd446d 100644 --- a/packages/core/src/__tests__/mapping.test.ts +++ b/packages/core/src/__tests__/mapping.test.ts @@ -14,7 +14,7 @@ describe('getMappingEvent', () => { expect( await getMappingEvent( - { event: 'page view' }, + { name: 'page view' }, { page: { view: pageViewConfig } }, ), ).toStrictEqual({ @@ -27,7 +27,7 @@ describe('getMappingEvent', () => { const entityAsterisksConfig = { name: 'entity_*' }; expect( await getMappingEvent( - { event: 'page random' }, + { name: 'page random' }, { page: { '*': entityAsterisksConfig } }, ), ).toStrictEqual({ @@ -38,7 +38,7 @@ describe('getMappingEvent', () => { const asterisksActionConfig = { name: '*_view' }; expect( await getMappingEvent( - { event: 'random view' }, + { name: 'random view' }, { '*': { view: asterisksActionConfig } }, ), ).toStrictEqual({ @@ -56,28 +56,28 @@ describe('getMappingEvent', () => { }; expect( - await getMappingEvent({ event: 'not existing' }, mapping), + await getMappingEvent({ name: 'not existing' }, mapping), ).toStrictEqual({ eventMapping: { name: 'asterisk' }, mappingKey: '* *', }); expect( - await getMappingEvent({ event: 'asterisk action' }, mapping), + await getMappingEvent({ name: 'asterisk action' }, mapping), ).toStrictEqual({ eventMapping: { name: 'action' }, mappingKey: '* action', }); expect( - await getMappingEvent({ event: 'foo something' }, mapping), + await getMappingEvent({ name: 'foo something' }, mapping), ).toStrictEqual({ eventMapping: { name: 'foo_asterisk' }, mappingKey: 'foo *', }); expect( - await getMappingEvent({ event: 'bar something' }, mapping), + await getMappingEvent({ name: 'bar something' }, mapping), ).toStrictEqual({ eventMapping: { name: 'asterisk' }, mappingKey: '* *', @@ -103,7 +103,7 @@ describe('getMappingEvent', () => { }; expect( - await getMappingEvent({ event: 'order complete' }, mapping), + await getMappingEvent({ name: 'order complete' }, mapping), ).toStrictEqual({ eventMapping: (mapping.order!.complete as Array)[1], mappingKey: 'order complete', @@ -111,7 +111,7 @@ describe('getMappingEvent', () => { expect( await getMappingEvent( - { event: 'order complete', globals: { env: 'prod' } }, + { name: 'order complete', globals: { env: 'prod' } }, mapping, ), ).toStrictEqual({ @@ -146,7 +146,7 @@ describe('getMappingValue', () => { function getNested(data: WalkerOS.Properties) { return { - type: 'child', + entity: 'child', data, nested: [], context: { element: ['child', 0] }, @@ -204,11 +204,11 @@ describe('getMappingValue', () => { }); test('fn', async () => { - const pageView = createEvent({ event: 'page view' }); - const pageClick = createEvent({ event: 'page click' }); + const pageView = createEvent({ name: 'page view' }); + const pageClick = createEvent({ name: 'page click' }); const mockFn = jest.fn((event) => { - if (event.event === 'page view') return 'foo'; + if (event.name === 'page view') return 'foo'; return 'bar'; }); @@ -236,7 +236,7 @@ describe('getMappingValue', () => { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', key: 'data.name', }, ], @@ -248,11 +248,11 @@ describe('getMappingValue', () => { loop: [ 'this', { - key: 'event', + key: 'name', }, ], }), - ).toStrictEqual([event.event]); + ).toStrictEqual([event.name]); }); test('set', async () => { @@ -260,7 +260,7 @@ describe('getMappingValue', () => { expect( await getMappingValue(event, { - set: ['event', 'data', { value: 'static' }, { fn: () => 'fn' }], + set: ['name', 'data', { value: 'static' }, { fn: () => 'fn' }], }), ).toStrictEqual(['order complete', event.data, 'static', 'fn']); }); @@ -421,12 +421,12 @@ describe('getMappingValue', () => { test('condition', async () => { const mockCondition = jest.fn((event) => { - return event.event === 'page view'; + return event.name === 'page view'; }); // Condition met expect( - await getMappingValue(createEvent({ event: 'page view' }), { + await getMappingValue(createEvent({ name: 'page view' }), { key: 'data.string', condition: mockCondition, }), diff --git a/packages/core/src/__tests__/validate.test.ts b/packages/core/src/__tests__/validate.test.ts index 089d9854f..9bf7189a7 100644 --- a/packages/core/src/__tests__/validate.test.ts +++ b/packages/core/src/__tests__/validate.test.ts @@ -6,11 +6,11 @@ describe('validate', () => { // should return valid event with missing properties filled expect( validateEvent({ - event: 'e a', + name: 'e a', data: { k: 'v' }, }), ).toStrictEqual({ - event: 'e a', + name: 'e a', data: { k: 'v' }, context: {}, custom: {}, @@ -33,7 +33,7 @@ describe('validate', () => { // should throw error for invalid event name expect(() => validateEvent({ - event: 'e', + name: 'e', }), ).toThrow('Invalid event name'); @@ -42,27 +42,27 @@ describe('validate', () => { validateEvent({ data: { key: 'value' }, }), - ).toThrow('Missing or invalid event, entity, or action'); + ).toThrow('Missing or invalid name, entity, or action'); // long event names expect( validateEvent({ - event: 'e ' + 'a'.repeat(256), - }).event, + name: 'e ' + 'a'.repeat(256), + }).name, ).toHaveLength(255); expect(() => validateEvent( { - event: 'e ' + 'a'.repeat(11), + name: 'e ' + 'a'.repeat(11), }, - [{ e: { '*': { event: { maxLength: 10, strict: true } } } }], + [{ e: { '*': { name: { maxLength: 10, strict: true } } } }], ), ).toThrow('Value exceeds maxLength'); // should throw error for invalid type expect( validateEvent({ - event: 'some event', + name: 'some event', data: 'invalid type', }), ).toHaveProperty('data', {}); @@ -70,7 +70,7 @@ describe('validate', () => { // should throw error for extra properties expect(() => validateEvent({ - event: 'some event', + name: 'some event', extraProp: 'should not be here', }), ).not.toHaveProperty('extraProp'); @@ -87,7 +87,7 @@ describe('validate', () => { expect( validateEvent( { - event: 'e a', + name: 'e a', data: { k: 'v', remove: 'me' }, }, contract, @@ -96,7 +96,7 @@ describe('validate', () => { expect(() => validateEvent( { - event: 'e s', + name: 'e s', data: { k: 'v', remove: 'me' }, }, contract, @@ -107,7 +107,7 @@ describe('validate', () => { expect(() => validateEvent( { - event: 'p r', + name: 'p r', data: {}, }, requireContract, @@ -116,7 +116,7 @@ describe('validate', () => { expect( validateEvent( { - event: 'a n', + name: 'a n', }, requireContract, ), @@ -125,7 +125,7 @@ describe('validate', () => { // should remove unknown properties expect( validateEvent({ - event: 'some event', + name: 'some event', randomProp: 123, // doesn't belong here }), ).not.toHaveProperty('randomProp'); @@ -133,7 +133,7 @@ describe('validate', () => { // should throw error for invalid number range expect( validateEvent({ - event: 'e a', + name: 'e a', count: -1, // should be >= 0 }), ).toHaveProperty('count', 0); @@ -143,7 +143,7 @@ describe('validate', () => { { entity: { throw: { - event: { + name: { validate: ( value: unknown, key: string, @@ -155,7 +155,7 @@ describe('validate', () => { }, }, name: { - event: { + name: { validate: () => { // With great power comes great responsibility... return 'invalideventname'; @@ -173,14 +173,14 @@ describe('validate', () => { }, ]; expect(() => - validateEvent({ event: 'entity throw' }, customValidationContract), + validateEvent({ name: 'entity throw' }, customValidationContract), ).toThrow('Custom'); expect( - validateEvent({ event: 'entity name' }, customValidationContract), - ).toHaveProperty('event', 'invalideventname'); // If one really wants + validateEvent({ name: 'entity name' }, customValidationContract), + ).toHaveProperty('name', 'invalideventname'); // If one really wants expect( validateEvent( - { event: 'entity type', data: {} }, + { name: 'entity type', data: {} }, customValidationContract, ), ).toHaveProperty('data', {}); // If one really wants @@ -188,10 +188,10 @@ describe('validate', () => { // should validate wildcard rules expect( validateEvent({ - event: 'product add', + name: 'product add', data: { id: '123', price: 9.99 }, }), - ).toMatchObject({ event: 'product add', data: { id: '123', price: 9.99 } }); + ).toMatchObject({ name: 'product add', data: { id: '123', price: 9.99 } }); const typeContract = { e: { @@ -208,7 +208,7 @@ describe('validate', () => { expect(() => validateEvent( { - event: 'e a', + name: 'e a', globals: { n: 'no number', }, @@ -219,7 +219,7 @@ describe('validate', () => { expect( validateEvent( { - event: 'e a', + name: 'e a', globals: { n: 1, k: 'v', @@ -227,6 +227,6 @@ describe('validate', () => { }, [typeContract], ), - ).toMatchObject({ event: 'e a' }); + ).toMatchObject({ name: 'e a' }); }); }); diff --git a/packages/core/src/eventGenerator.ts b/packages/core/src/eventGenerator.ts index 8417f38a2..930155d84 100644 --- a/packages/core/src/eventGenerator.ts +++ b/packages/core/src/eventGenerator.ts @@ -19,7 +19,7 @@ export function createEvent( const id = `${timestamp}-${group}-${count}`; const defaultEvent: WalkerOS.Event = { - event: 'entity action', + name: 'entity action', data: { string: 'foo', number: 1, @@ -33,7 +33,7 @@ export function createEvent( user: { id: 'us3r', device: 'c00k13', session: 's3ss10n' }, nested: [ { - type: 'child', + entity: 'child', data: { is: 'subordinated' }, nested: [], context: { element: ['child', 0] }, @@ -67,8 +67,8 @@ export function createEvent( // Update conditions // Entity and action from event - if (props.event) { - const [entity, action] = props.event.split(' ') ?? []; + if (props.name) { + const [entity, action] = props.name.split(' ') ?? []; if (entity && action) { event.entity = entity; @@ -122,7 +122,7 @@ export function getEvent( globals: { pagegroup: 'shop' }, nested: [ { - type: 'product', + entity: 'product', data: { ...product1.data, quantity }, context: { shopping: ['cart', 0] }, nested: [], @@ -140,13 +140,13 @@ export function getEvent( globals: { pagegroup: 'shop' }, nested: [ { - type: 'product', + entity: 'product', ...product1, context: { shopping: ['checkout', 0] }, nested: [], }, { - type: 'product', + entity: 'product', ...product2, context: { shopping: ['checkout', 0] }, nested: [], @@ -166,19 +166,19 @@ export function getEvent( globals: { pagegroup: 'shop' }, nested: [ { - type: 'product', + entity: 'product', ...product1, context: { shopping: ['complete', 0] }, nested: [], }, { - type: 'product', + entity: 'product', ...product2, context: { shopping: ['complete', 0] }, nested: [], }, { - type: 'gift', + entity: 'gift', data: { name: 'Surprise', }, @@ -270,5 +270,5 @@ export function getEvent( }, }; - return createEvent({ ...defaultEvents[name], ...props, event: name }); + return createEvent({ ...defaultEvents[name], ...props, name: name }); } diff --git a/packages/core/src/mapping.ts b/packages/core/src/mapping.ts index d6580f0c6..8b556442b 100644 --- a/packages/core/src/mapping.ts +++ b/packages/core/src/mapping.ts @@ -16,7 +16,7 @@ export async function getMappingEvent( event: WalkerOS.PartialEvent, mapping?: Mapping.Rules, ): Promise { - const [entity, action] = (event.event || '').split(' '); + const [entity, action] = (event.name || '').split(' '); if (!mapping || !entity || !action) return {}; let eventMapping: Mapping.Rule | undefined; diff --git a/packages/core/src/types/flow.ts b/packages/core/src/types/flow.ts new file mode 100644 index 000000000..b74da3cf4 --- /dev/null +++ b/packages/core/src/types/flow.ts @@ -0,0 +1,13 @@ +import type { Collector } from '.'; + +/** + * Flow configuration interface for dynamic walkerOS setup + * Used by bundlers and other tools to configure walkerOS dynamically + */ +export interface Config { + /** Collector configuration - uses existing Collector.Config from core */ + collector: Collector.Config; + + /** NPM packages required for this configuration */ + packages: Record; +} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 622c63d5c..711118534 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -2,6 +2,7 @@ export * as Collector from './collector'; export * as Data from './data'; export * as Destination from './destination'; export * as Elb from './elb'; +export * as Flow from './flow'; export * as Handler from './handler'; export * as Hooks from './hooks'; export * as Mapping from './mapping'; diff --git a/packages/core/src/types/walkeros.ts b/packages/core/src/types/walkeros.ts index 0d2242bc4..7e30d9932 100644 --- a/packages/core/src/types/walkeros.ts +++ b/packages/core/src/types/walkeros.ts @@ -16,7 +16,7 @@ export type Events = Array; export type PartialEvent = Partial; export type DeepPartialEvent = DeepPartial; export interface Event { - event: string; + name: string; data: Properties; context: OrderedProperties; globals: Properties; @@ -97,7 +97,7 @@ export interface OrderedProperties { export type Entities = Array; export interface Entity { - type: string; + entity: string; data: Properties; nested: Entities; context: OrderedProperties; diff --git a/packages/core/src/validate.ts b/packages/core/src/validate.ts index c8b61a458..31ade7fe4 100644 --- a/packages/core/src/validate.ts +++ b/packages/core/src/validate.ts @@ -20,9 +20,9 @@ export function validateEvent( let entity: string; let action: string; - // Check if event.event is available and it's a string - if (isSameType(obj.event, '')) { - event = obj.event; + // Check if event.name is available and it's a string + if (isSameType(obj.name, '')) { + event = obj.name; [entity, action] = event.split(' '); if (!entity || !action) throwError('Invalid event name'); } else if (isSameType(obj.entity, '') && isSameType(obj.action, '')) { @@ -30,13 +30,13 @@ export function validateEvent( action = obj.action; event = `${entity} ${action}`; } else { - throwError('Missing or invalid event, entity, or action'); + throwError('Missing or invalid name, entity, or action'); } const basicContract: Schema.Contract = { '*': { '*': { - event: { maxLength: 255 }, // @TODO as general rule? + name: { maxLength: 255 }, // @TODO as general rule? user: { allowedKeys: ['id', 'device', 'session'] }, consent: { allowedValues: [true, false] }, timestamp: { min: 0 }, @@ -49,7 +49,7 @@ export function validateEvent( }; const basicEvent: WalkerOS.Event = { - event, + name: event, data: {}, context: {}, custom: {}, diff --git a/packages/server/core/CHANGELOG.md b/packages/server/core/CHANGELOG.md index c39416a15..1d75d0c53 100644 --- a/packages/server/core/CHANGELOG.md +++ b/packages/server/core/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/server-core +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/server/core/README.md b/packages/server/core/README.md index a3b79ca40..1e4e35dc1 100644 --- a/packages/server/core/README.md +++ b/packages/server/core/README.md @@ -1,74 +1,271 @@

- +

# Server Core Utilities for walkerOS -The walkerOS Server Core package provides server-specific utilities and -functions that power server-side data collection. It extends the -platform-agnostic Core package with Node.js-specific functionality for server -environment detection, request handling, and server-side event processing. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/core) +• [NPM Package](https://www.npmjs.com/package/@walkeros/server-core) -## Role in walkerOS Ecosystem +Server core utilities are Node.js-specific functions designed for server-side +walkerOS implementations. These utilities handle server communication, +cryptographic hashing, and other backend operations. -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: +## Installation + +Import server utilities from the `@walkeros/server-core` package: -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) +```ts +import { sendServer, getHashServer } from '@walkeros/server-core'; +``` -The Server Core package serves as the foundation for all server-based sources -and destinations, providing essential Node.js utilities, request handling, and -server-specific event processing capabilities. +## Server Communication -## Installation +### sendServer + +`sendServer(url: string, data?: SendDataValue, options?: SendServerOptions): Promise` +sends HTTP requests using Node.js built-in modules (`http`/`https`). + +```js +// Simple POST request +const response = await sendServer('https://api.example.com/events', { + name: 'page view', + data: { url: '/home' }, +}); + +// With custom options +const response = await sendServer(url, data, { + method: 'PUT', + headers: { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', + }, + timeout: 10000, // 10 seconds +}); + +if (response.ok) { + console.log('Data sent successfully:', response.data); +} else { + console.error('Send failed:', response.error); +} +``` + +#### SendServerOptions + +```ts +interface SendServerOptions { + headers?: Record; // Custom HTTP headers + method?: string; // HTTP method (default: 'POST') + timeout?: number; // Request timeout in milliseconds (default: 5000) +} +``` + +#### SendResponse + +```ts +interface SendResponse { + ok: boolean; // Indicates if the request was successful (2xx status) + data?: unknown; // Parsed response data (if available) + error?: string; // Error message (if request failed) +} +``` + +## Cryptographic Operations + +### getHashServer + +`getHashServer(str: string, length?: number): Promise` generates SHA-256 +hashes using Node.js crypto module. + +```js +// Generate full SHA-256 hash +const fullHash = await getHashServer('user123@example.com'); +// Returns full 64-character hash + +// Generate shortened hash for anonymization +const userFingerprint = await getHashServer( + userAgent + language + ipAddress + date.getDate(), + 16, +); +// Returns 16-character hash like '47e0bdd10f04ef13' + +// User identification while preserving privacy +const anonymousId = await getHashServer(`${userEmail}${deviceId}${salt}`, 12); +``` + +This function is commonly used for: + +- **User Anonymization**: Creating privacy-safe user identifiers +- **Fingerprinting**: Generating device/session fingerprints +- **Data Deduplication**: Creating consistent identifiers +- **Privacy Compliance**: Hashing PII for GDPR/CCPA compliance + +## Usage Examples + +### Event Processing Pipeline + +```js +import { sendServer, getHashServer } from '@walkeros/server-core'; + +async function processUserEvent(event, userInfo) { + // Anonymize user identification + const anonymousUserId = await getHashServer( + `${userInfo.email}${userInfo.deviceId}`, + 16, + ); -```sh -npm install @walkeros/server-core + // Prepare event with anonymized data + const processedEvent = { + ...event, + user: { + ...event.user, + id: anonymousUserId, + }, + }; + + // Send to analytics service + const result = await sendServer( + 'https://analytics.example.com/collect', + processedEvent, + { + headers: { + 'X-API-Key': process.env.ANALYTICS_API_KEY, + }, + timeout: 8000, + }, + ); + + return result; +} +``` + +### Privacy-Safe Session Tracking + +```js +async function createSessionId(request) { + const fingerprint = [ + request.headers['user-agent'], + request.ip.replace(/\.\d+$/, '.0'), // Anonymize IP + new Date().toDateString(), // Daily rotation + ].join('|'); + + return await getHashServer(fingerprint, 20); +} +``` + +## Error Handling + +Server utilities include comprehensive error handling: + +```js +try { + const response = await sendServer(url, data, { timeout: 5000 }); + + if (response.ok) { + // Success - response.data contains the result + console.log('Success:', response.data); + } else { + // Request completed but with error status + console.warn('Request failed:', response.error); + } +} catch (error) { + // Network error, timeout, or other exception + console.error('Network error:', error.message); +} +``` + +## Performance Considerations + +### Timeout Configuration + +Configure appropriate timeouts based on your use case: + +```js +// Fast analytics endpoint +await sendServer(url, data, { timeout: 2000 }); + +// Critical business data +await sendServer(url, data, { timeout: 15000 }); +``` + +### Batch Processing + +For high-volume scenarios, consider batching: + +```js +const events = [ + /* ... multiple events ... */ +]; + +const response = await sendServer( + '/api/events/batch', + { + events, + timestamp: Date.now(), + }, + { + timeout: 10000, + }, +); ``` -## Usage +### Connection Reuse -The server core package provides Node.js-specific utilities: +The underlying Node.js HTTP agent automatically reuses connections for better +performance with multiple requests to the same host. -```typescript -import { - // Server utilities - getHashServer, - sendServer, +## Security Notes - // Type definitions - ServerDestination, -} from '@walkeros/server-core'; +- **HTTPS Only**: Use HTTPS URLs in production for encrypted transmission +- **API Keys**: Store sensitive credentials in environment variables +- **Timeout Limits**: Set reasonable timeouts to prevent hanging requests +- **Hash Salting**: Use application-specific salts when hashing sensitive data -// Example: Generate server-side hash -const hash = await getHashServer('user-id', 'additional-data'); -console.log('Generated hash:', hash); +```js +// Good security practices +const apiKey = process.env.ANALYTICS_API_KEY; +const saltedHash = await getHashServer(`${userData}${process.env.HASH_SALT}`); -// Example: Send event from server -await sendServer({ - event: 'order complete', - data: { orderId: '12345', profit: 42 }, - // ... other event properties +await sendServer('https://secure-api.example.com/events', data, { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + timeout: 5000, }); ``` -## Core Features - -- **Server Environment Detection**: Identify Node.js version and server - capabilities -- **Request Processing**: Handle incoming HTTP requests and extract event data -- **Server-Side Hashing**: Generate consistent identifiers in server - environments -- **Event Transmission**: Server-optimized event sending mechanisms -- **Server Destinations**: Base classes and utilities for server-side - destinations -- **Performance Optimization**: Memory-efficient processing for high-throughput - scenarios +## Integration with Core + +Server utilities work seamlessly with +[Core Utilities](https://www.elbwalker.com/docs/core): + +```js +import { getMappingValue, anonymizeIP } from '@walkeros/core'; +import { sendServer, getHashServer } from '@walkeros/server-core'; + +async function processServerSideEvent(rawEvent, clientIP) { + // Use core utilities for data processing + const processedData = await getMappingValue(rawEvent, mappingConfig); + const safeIP = anonymizeIP(clientIP); + + // Use server utilities for transmission + const sessionId = await getHashServer(`${safeIP}${userAgent}`, 16); + + return await sendServer(endpoint, { + ...processedData, + sessionId, + ip: safeIP, + }); +} +``` + +--- + +For platform-agnostic utilities, see +[Core Utilities](https://www.elbwalker.com/docs/core). ## Contribute diff --git a/packages/server/core/package.json b/packages/server/core/package.json index dddf15e72..5a2c34912 100644 --- a/packages/server/core/package.json +++ b/packages/server/core/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-core", "description": "Server-specific utilities for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,7 +25,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "0.0.8" + "@walkeros/core": "0.1.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/server/core/src/__tests__/collector.test.ts b/packages/server/core/src/__tests__/collector.test.ts index aae5bbc2b..005282a34 100644 --- a/packages/server/core/src/__tests__/collector.test.ts +++ b/packages/server/core/src/__tests__/collector.test.ts @@ -88,7 +88,7 @@ describe('Server Collector', () => { tagging: 42, }); const event = { - event: 'e a', + name: 'e a', data: {}, context: {}, custom: {}, diff --git a/packages/server/core/src/__tests__/destination.test.ts b/packages/server/core/src/__tests__/destination.test.ts index d2539ce4b..55341c428 100644 --- a/packages/server/core/src/__tests__/destination.test.ts +++ b/packages/server/core/src/__tests__/destination.test.ts @@ -138,12 +138,12 @@ describe('Destination', () => { expect(eventCall).toHaveBeenCalledWith({ ...mockEvent, ...changes }); jest.clearAllMocks(); - await elb({ ...mockEvent, event: 'entity rename' }); + await elb({ ...mockEvent, name: 'entity rename' }); expect(mockDestination.push).toHaveBeenCalledWith( expect.objectContaining({ ...mockEvent, ...changes, - event: 'NewEventName', + name: 'NewEventName', }), expect.objectContaining({ mapping: eventMapping, @@ -162,7 +162,7 @@ describe('Destination', () => { expect(mockDestination.push).toHaveBeenCalledTimes(1); expect(mockDestination.push).toHaveBeenCalledWith( expect.objectContaining({ - event: 'custom', + name: 'custom', }), expect.objectContaining({ mapping: eventMapping, @@ -182,7 +182,7 @@ describe('Destination', () => { result = await elb(mockEvent); expect(mockPush).toHaveBeenCalledWith( - expect.objectContaining({ event: 'entity action' }), + expect.objectContaining({ name: 'entity action' }), expect.objectContaining({ mapping: eventMapping, data: 'bar', @@ -265,7 +265,7 @@ describe('Destination', () => { expect(second).toHaveBeenCalledTimes(1); expect(first).toHaveBeenCalledWith({ ...mockEvent, - event: 'new name', + name: 'new name', custom: { foo: 'bar' }, }); expect(second).toHaveBeenCalledWith({ ...mockEvent }); @@ -359,11 +359,11 @@ describe('Destination', () => { // DLQ expect(collector.destinations['initFail'].dlq).toContainEqual([ - expect.objectContaining({ event: mockEvent.event }), + expect.objectContaining({ name: mockEvent.name }), new Error('init kaputt'), ]); expect(collector.destinations['pushFail'].dlq).toContainEqual([ - expect.objectContaining({ event: mockEvent.event }), + expect.objectContaining({ name: mockEvent.name }), new Error('push kaputt'), ]); }); @@ -420,11 +420,11 @@ describe('Destination', () => { const event = createEvent(); const policy = { - event: { + name: { value: 'new name', }, 'data.string': { value: 'bar' }, - 'nested.0.type': { value: 'kid' }, + 'nested.0.entity': { value: 'kid' }, 'data.number': { consent: { marketing: true }, }, @@ -447,13 +447,13 @@ describe('Destination', () => { expect(mockPush).toHaveBeenCalledWith({ ...event, - event: 'new name', + name: 'new name', data: expect.objectContaining({ string: 'bar', number: undefined, // Redacted due to missing consent new: 'value', }), - nested: [expect.objectContaining({ type: 'kid' })], + nested: [expect.objectContaining({ entity: 'kid' })], // timing: 0, // @TODO should be set to default type }); }); @@ -476,7 +476,7 @@ describe('Destination', () => { expect(mockPushWithEnvironment).toHaveBeenCalledWith( expect.objectContaining({ - event: mockEvent.event, + name: mockEvent.name, }), expect.objectContaining({ env: expect.objectContaining({ diff --git a/packages/server/destinations/aws/CHANGELOG.md b/packages/server/destinations/aws/CHANGELOG.md index 5447b0969..9cbc6b3e0 100644 --- a/packages/server/destinations/aws/CHANGELOG.md +++ b/packages/server/destinations/aws/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/server-destination-aws +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/server-core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/server/destinations/aws/README.md b/packages/server/destinations/aws/README.md index d89b4ff7b..b7cf5b58b 100644 --- a/packages/server/destinations/aws/README.md +++ b/packages/server/destinations/aws/README.md @@ -1,30 +1,20 @@

- +

-# AWS Destination for walkerOS +# AWS (Firehose) Destination for walkerOS -This package provides an AWS destination for walkerOS. It allows you to send -events to various AWS services. Currently, it supports AWS Firehose. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/destinations/aws) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/server-destination-aws) -[View documentation](https://www.elbwalker.com/docs/destinations/server/aws/) - -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This AWS destination receives processed events from the walkerOS collector and -streams them to AWS services like Firehose, enabling real-time data ingestion -into AWS data lakes, warehouses, and analytics services for large-scale event -processing and analysis. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This AWS +destination receives processed events from the walkerOS collector and streams +them to AWS services like Firehose, enabling real-time data ingestion into AWS +data lakes, warehouses, and analytics services for large-scale event processing +and analysis. ## Installation @@ -34,26 +24,45 @@ npm install @walkeros/server-destination-aws ## Usage -Here's a basic example of how to use the AWS destination: +Here's a basic example of how to use the AWS Firehose destination: ```typescript import { elb } from '@walkeros/collector'; import { destinationFirehose } from '@walkeros/server-destination-aws'; elb('walker destination', destinationFirehose, { - custom: { + settings: { firehose: { streamName: 'your-firehose-stream-name', region: 'eu-central-1', - credentials: { - accessKeyId: 'your-access-key-id', - secretAccessKey: 'your-secret-access-key', + config: { + credentials: { + accessKeyId: 'your-access-key-id', + secretAccessKey: 'your-secret-access-key', + }, }, }, }, }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| ---------- | ---------------- | ----------------------------------- | -------- | ------------------------------------------------------ | +| `firehose` | `FirehoseConfig` | AWS Firehose configuration settings | No | `{ streamName: 'walker-events', region: 'us-east-1' }` | + +### Firehose Configuration + +The `firehose` object has the following properties: + +| Name | Type | Description | Required | Example | +| ------------ | ---------------------- | ------------------------------------------------- | -------- | --------------------------------- | +| `streamName` | `string` | Name of the Kinesis Data Firehose delivery stream | Yes | `'walker-events'` | +| `client` | `FirehoseClient` | Pre-configured AWS Firehose client instance | No | `new FirehoseClient(config)` | +| `region` | `string` | AWS region for the Firehose service | No | `'us-east-1'` | +| `config` | `FirehoseClientConfig` | AWS SDK client configuration options | No | `{ credentials: awsCredentials }` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/server/destinations/aws/package.json b/packages/server/destinations/aws/package.json index e0c707b4a..b01044dd8 100644 --- a/packages/server/destinations/aws/package.json +++ b/packages/server/destinations/aws/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-aws", "description": "AWS server destination for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -31,7 +31,7 @@ }, "dependencies": { "@aws-sdk/client-firehose": "^3.606.0", - "@walkeros/server-core": "0.0.8" + "@walkeros/server-core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/server/destinations/gcp/CHANGELOG.md b/packages/server/destinations/gcp/CHANGELOG.md index b73dd59cd..fe994d386 100644 --- a/packages/server/destinations/gcp/CHANGELOG.md +++ b/packages/server/destinations/gcp/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/server-destination-gcp +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/server-core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/server/destinations/gcp/README.md b/packages/server/destinations/gcp/README.md index 6514490f5..51da5f25e 100644 --- a/packages/server/destinations/gcp/README.md +++ b/packages/server/destinations/gcp/README.md @@ -1,30 +1,19 @@

- +

-# Google Cloud Platform (GCP) Destination for walkerOS +# GCP (BigQuery) Destination for walkerOS -This package provides a Google Cloud Platform (GCP) destination for walkerOS. It -allows you to send events to Google BigQuery. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/destinations/gcp) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/server-destination-gcp) -[View documentation](https://www.elbwalker.com/docs/destinations/server/gcp/) - -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This GCP destination receives processed events from the walkerOS collector and -streams them to Google BigQuery, enabling real-time data warehousing and -analytics with Google Cloud's powerful data processing and machine learning -capabilities. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This GCP +destination receives processed events from the walkerOS collector and streams +them to Google BigQuery, enabling real-time data warehousing and analytics with +Google Cloud's powerful data processing and machine learning capabilities. ## Installation @@ -34,14 +23,14 @@ npm install @walkeros/server-destination-gcp ## Usage -Here's a basic example of how to use the GCP destination: +Here's a basic example of how to use the GCP BigQuery destination: ```typescript import { elb } from '@walkeros/collector'; import { destinationBigQuery } from '@walkeros/server-destination-gcp'; elb('walker destination', destinationBigQuery, { - custom: { + settings: { projectId: 'YOUR_PROJECT_ID', datasetId: 'YOUR_DATASET_ID', tableId: 'YOUR_TABLE_ID', @@ -49,6 +38,17 @@ elb('walker destination', destinationBigQuery, { }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| ----------- | ----------------- | ------------------------------------------------ | -------- | ------------------------------------------ | +| `client` | `BigQuery` | Google Cloud BigQuery client instance | Yes | `new BigQuery({ projectId, keyFilename })` | +| `projectId` | `string` | Google Cloud Project ID | Yes | `'my-gcp-project'` | +| `datasetId` | `string` | BigQuery dataset ID where events will be stored | Yes | `'walker_events'` | +| `tableId` | `string` | BigQuery table ID for event storage | Yes | `'events'` | +| `location` | `string` | Geographic location for the BigQuery dataset | No | `'US'` | +| `bigquery` | `BigQueryOptions` | Additional BigQuery client configuration options | No | `{ keyFilename: "path/to/key.json" }` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/server/destinations/gcp/package.json b/packages/server/destinations/gcp/package.json index 66eaef746..b2ad09537 100644 --- a/packages/server/destinations/gcp/package.json +++ b/packages/server/destinations/gcp/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-gcp", "description": "Google Cloud Platform server destination for walkerOS (BigQuery)", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -19,7 +19,7 @@ }, "dependencies": { "@google-cloud/bigquery": "^7.8.0", - "@walkeros/server-core": "0.0.8" + "@walkeros/server-core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/server/destinations/gcp/src/bigquery/__tests__/index.test.ts b/packages/server/destinations/gcp/src/bigquery/__tests__/index.test.ts index 97a029a15..9ed25df0c 100644 --- a/packages/server/destinations/gcp/src/bigquery/__tests__/index.test.ts +++ b/packages/server/destinations/gcp/src/bigquery/__tests__/index.test.ts @@ -77,7 +77,7 @@ describe('Server Destination BigQuery', () => { expect(mockFn).toHaveBeenCalledWith('insert', [ { timestamp: expect.any(Date), - event: 'entity action', + name: 'entity action', id: event.id, entity: 'entity', action: 'action', @@ -88,7 +88,7 @@ describe('Server Destination BigQuery', () => { custom: '{"completely":"random"}', user: '{"id":"us3r","device":"c00k13","session":"s3ss10n"}', nested: - '[{"type":"child","data":{"is":"subordinated"},"nested":[],"context":{"element":["child",0]}}]', + '[{"entity":"child","data":{"is":"subordinated"},"nested":[],"context":{"element":["child",0]}}]', trigger: 'test', timing: 3.14, group: 'gr0up', diff --git a/packages/server/destinations/meta/CHANGELOG.md b/packages/server/destinations/meta/CHANGELOG.md index 65cc713a2..1d2282d0e 100644 --- a/packages/server/destinations/meta/CHANGELOG.md +++ b/packages/server/destinations/meta/CHANGELOG.md @@ -1,5 +1,17 @@ # @walkeros/server-destination-meta +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/server-core@0.1.0 + - @walkeros/core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/server/destinations/meta/README.md b/packages/server/destinations/meta/README.md index ce313c45f..c17062cc7 100644 --- a/packages/server/destinations/meta/README.md +++ b/packages/server/destinations/meta/README.md @@ -1,30 +1,20 @@

- +

# Meta (CAPI) Destination for walkerOS -This package provides a Meta Conversion API (CAPI) destination for walkerOS. It -allows you to send events to the Meta Conversions API. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/server/destinations/meta) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/server-destination-meta) -[View documentation](https://www.elbwalker.com/docs/destinations/server/meta/) - -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This Meta CAPI destination receives processed events from the walkerOS collector -and sends them server-to-server to Meta's Conversions API, providing enhanced -data accuracy and attribution for Meta advertising campaigns while bypassing -browser limitations. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This Meta +CAPI destination receives processed events from the walkerOS collector and sends +them server-to-server to Meta's Conversions API, providing enhanced data +accuracy and attribution for Meta advertising campaigns while bypassing browser +limitations. ## Installation @@ -41,13 +31,25 @@ import { elb } from '@walkeros/collector'; import { destinationMeta } from '@walkeros/server-destination-meta'; elb('walker destination', destinationMeta, { - custom: { + settings: { accessToken: 'YOUR_ACCESS_TOKEN', pixelId: 'YOUR_PIXEL_ID', }, }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| ----------------- | --------------------- | --------------------------------------------------------- | -------- | ---------------------------------------------- | +| `accessToken` | `string` | Meta access token for Conversions API authentication | Yes | `'your_access_token'` | +| `pixelId` | `string` | Meta Pixel ID from your Facebook Business account | Yes | `'1234567890'` | +| `action_source` | `ActionSource` | Source of the event (website, app, phone_call, etc.) | No | `'website'` | +| `doNotHash` | `string[]` | Array of user_data fields that should not be hashed | No | `['client_ip_address', 'client_user_agent']` | +| `test_event_code` | `string` | Test event code for debugging Meta Conversions API events | No | `'TEST12345'` | +| `url` | `string` | Custom URL for Meta Conversions API endpoint | No | `'https://graph.facebook.com/v17.0'` | +| `user_data` | `WalkerOSMapping.Map` | Mapping configuration for user data fields | No | `{ email: 'user.email', phone: 'user.phone' }` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/server/destinations/meta/package.json b/packages/server/destinations/meta/package.json index d68d87ffb..816d39e21 100644 --- a/packages/server/destinations/meta/package.json +++ b/packages/server/destinations/meta/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/server-destination-meta", "description": "Meta server destination for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,8 +30,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "0.0.8", - "@walkeros/server-core": "0.0.8" + "@walkeros/core": "0.1.0", + "@walkeros/server-core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/server/destinations/meta/src/examples/events.ts b/packages/server/destinations/meta/src/examples/events.ts index c965956be..7390e1178 100644 --- a/packages/server/destinations/meta/src/examples/events.ts +++ b/packages/server/destinations/meta/src/examples/events.ts @@ -22,7 +22,7 @@ export function Purchase(): BodyParameters { currency: 'EUR', value: Number(event.data.total), contents: event.nested - .filter((item) => item.type === 'product') + .filter((item) => item.entity === 'product') .map((item) => ({ id: String(item.data.id), quantity: Number(item.data.quantity) || 1, diff --git a/packages/server/destinations/meta/src/examples/mapping.ts b/packages/server/destinations/meta/src/examples/mapping.ts index 5a529ec39..b5c7d3645 100644 --- a/packages/server/destinations/meta/src/examples/mapping.ts +++ b/packages/server/destinations/meta/src/examples/mapping.ts @@ -22,7 +22,7 @@ export const Purchase: DestinationMeta.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: { id: 'data.id', item_price: 'data.price', @@ -34,7 +34,7 @@ export const Purchase: DestinationMeta.Rule = { num_items: { fn: (event) => (event as WalkerOS.Event).nested.filter( - (item) => item.type === 'product', + (item) => item.entity === 'product', ).length, }, }, diff --git a/packages/server/destinations/meta/src/push.ts b/packages/server/destinations/meta/src/push.ts index 951ada14e..3589214d2 100644 --- a/packages/server/destinations/meta/src/push.ts +++ b/packages/server/destinations/meta/src/push.ts @@ -51,7 +51,7 @@ export const push: PushFn = async function ( delete userData.fbclid; } const serverEvent: ServerEventParameters = { - event_name: event.event, + event_name: event.name, event_id: event.id, event_time: Math.round((event.timestamp || Date.now()) / 1000), action_source, diff --git a/packages/web/core/CHANGELOG.md b/packages/web/core/CHANGELOG.md index cd87ca624..cb7b20f54 100644 --- a/packages/web/core/CHANGELOG.md +++ b/packages/web/core/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/web-core +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/core/README.md b/packages/web/core/README.md index 56fe9f409..76a823c8d 100644 --- a/packages/web/core/README.md +++ b/packages/web/core/README.md @@ -1,79 +1,321 @@

- +

# Web Core Utilities for walkerOS -The walkerOS Web Core package provides browser-specific utilities and functions -that power web-based data collection. It extends the platform-agnostic Core -package with web-specific functionality for DOM interaction, session management, -and browser environment detection. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/core) +• [NPM Package](https://www.npmjs.com/package/@walkeros/web-core) -## Role in walkerOS Ecosystem +Web core utilities are browser-specific functions designed for client-side +walkerOS implementations. These utilities handle DOM interactions, browser +information, storage, sessions, and web-based communication. -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: +## Installation + +Import web utilities from the `@walkeros/web-core` package: -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) +```ts +import { getAttribute, sendWeb, sessionStart } from '@walkeros/web-core'; +``` -The Web Core package serves as the foundation for all web-based sources and -destinations, providing essential browser utilities, session handling, and -web-specific event processing capabilities. +## Utilities -## Installation +### DOM Utilities + +#### getAttribute + +`getAttribute(element: Element, name: string): string` retrieves attribute +values from DOM elements with enhanced handling. + +```js +const element = document.querySelector('[data-elb="product"]'); +const entityType = getAttribute(element, 'data-elb'); // Returns 'product' +``` + +#### Attribute Parsing + +##### splitAttribute + +`splitAttribute(str: string, separator?: string): string[]` splits attribute +strings using specified separators. + +```js +splitAttribute('id:123,name:shirt', ','); // Returns ['id:123', 'name:shirt'] +``` + +##### splitKeyVal + +`splitKeyVal(str: string): [string, string]` splits key-value pairs from +attribute strings. + +```js +splitKeyVal('id:123'); // Returns ['id', '123'] +``` + +##### parseInlineConfig + +`parseInlineConfig(str: string): Record` parses inline +configuration strings from HTML attributes. + +```js +parseInlineConfig('{"tracking": true, "debug": false}'); +// Returns { tracking: true, debug: false } +``` + +### Browser Information + +#### getLanguage + +`getLanguage(navigatorRef: Navigator): string | undefined` extracts the user's +preferred language. + +```js +getLanguage(navigator); // Returns 'en-US' or user's language +``` + +#### getTimezone + +`getTimezone(): string | undefined` gets the user's timezone from the Intl API. + +```js +getTimezone(); // Returns 'America/New_York' or user's timezone +``` + +#### getScreenSize + +`getScreenSize(windowRef: Window): string` returns the window's screen +dimensions. + +```js +getScreenSize(window); // Returns '1920x1080' or current screen size +``` + +### Element Visibility + +#### isVisible + +`isVisible(element: HTMLElement): boolean` checks if an element is visible to +the user. + +```js +const promoElement = document.getElementById('promotion'); +if (isVisible(promoElement)) { + // Element is visible on screen +} +``` + +This function considers: + +- Element display and visibility styles +- Element position within viewport +- Parent element visibility +- Intersection with the visible area + +### Storage Management + +#### Storage Operations + +##### storageRead + +`storageRead(key: string, storage?: StorageType): WalkerOS.PropertyType` reads +data from browser storage with automatic type conversion. + +```js +// Default uses localStorage +const userId = storageRead('walker_user_id'); + +// Use sessionStorage +const sessionData = storageRead('session_data', 'sessionStorage'); +``` -```sh -npm install @walkeros/web-core +##### storageWrite + +`storageWrite(key: string, value: WalkerOS.PropertyType, maxAgeInMinutes?: number, storage?: StorageType, domain?: string): WalkerOS.PropertyType` +writes data to storage with expiration and domain options. + +```js +// Store with 30-minute expiration +storageWrite('user_preference', 'dark-mode', 30); + +// Store in sessionStorage +storageWrite('temp_data', { id: 123 }, undefined, 'sessionStorage'); + +// Store with custom domain for cookies +storageWrite('tracking_id', 'abc123', 1440, 'cookie', '.example.com'); ``` -## Usage +##### storageDelete + +`storageDelete(key: string, storage?: StorageType)` removes data from storage. + +```js +storageDelete('expired_data'); +storageDelete('session_temp', 'sessionStorage'); +``` + +### Session Management + +#### sessionStart + +`sessionStart(config?: SessionConfig): WalkerOS.SessionData | void` initializes +and manages user sessions with automatic renewal and tracking. + +```js +// Start session with default config +const session = sessionStart(); + +// Custom session configuration +const session = sessionStart({ + storage: true, + domain: '.example.com', + maxAge: 1440, // 24 hours + sampling: 1.0, // 100% sampling +}); +``` -The web core package provides browser-specific utilities: +Session data includes: -```typescript -import { - // Browser utilities - getBrowser, - getHash, - isVisible, +- `id` - Unique session identifier +- `start` - Session start timestamp +- `isNew` - Whether this is a new session +- `count` - Number of events in session +- `device` - Device identifier +- `storage` - Whether storage is available - // Session management - sessionStart, - sessionStorage, +#### Advanced Session Functions - // Web-specific event handling - sendWeb, +- `sessionStorage` - Session-specific storage operations +- `sessionWindow` - Window/tab session management + +### Web Communication + +#### sendWeb + +`sendWeb(url: string, data?: SendDataValue, options?: SendWebOptionsDynamic): SendWebReturn` +sends data using various web transport methods. + +```js +// Default fetch transport +await sendWeb('https://api.example.com/events', eventData); + +// Use specific transport +await sendWeb(url, data, { transport: 'beacon' }); +await sendWeb(url, data, { transport: 'xhr' }); + +// With custom headers +await sendWeb(url, data, { + headers: { Authorization: 'Bearer token' }, + method: 'PUT', +}); +``` + +#### Transport-Specific Functions + +##### sendWebAsFetch + +`sendWebAsFetch(url: string, data?: SendDataValue, options?: SendWebOptionsFetch): Promise` +uses the modern Fetch API with advanced options. + +```js +await sendWebAsFetch(url, data, { + credentials: 'include', + noCors: true, + headers: { 'Content-Type': 'application/json' }, +}); +``` - // Storage utilities - storage, -} from '@walkeros/web-core'; +##### sendWebAsBeacon -// Example: Check if element is visible -const element = document.getElementById('my-element'); -if (isVisible(element)) { - console.log('Element is visible in viewport'); +`sendWebAsBeacon(url: string, data?: SendDataValue): SendResponse` uses the +Beacon API for reliable data transmission, especially during page unload. + +```js +// Reliable sending during page unload +window.addEventListener('beforeunload', () => { + sendWebAsBeacon('/analytics/pageview', { duration: Date.now() - startTime }); +}); +``` + +##### sendWebAsXhr + +`sendWebAsXhr(url: string, data?: SendDataValue, options?: SendWebOptions): SendResponse` +uses XMLHttpRequest for synchronous communication. + +```js +// Synchronous request (blocks execution) +const response = sendWebAsXhr(url, data, { method: 'POST' }); +``` + +### Web Hashing + +#### getHashWeb + +`getHashWeb(str: string, length?: number): Promise` generates SHA-256 +hashes using the Web Crypto API. + +```js +// Generate hash for fingerprinting +const userFingerprint = await getHashWeb( + navigator.userAgent + navigator.language + screen.width, + 16, +); +// Returns shortened hash like '47e0bdd10f04ef13' +``` + +## Configuration Types + +### SendWebOptions + +```ts +interface SendWebOptions { + headers?: Record; + method?: string; // Default: 'POST' + transport?: 'fetch' | 'beacon' | 'xhr'; // Default: 'fetch' +} + +interface SendWebOptionsFetch extends SendWebOptions { + credentials?: 'omit' | 'same-origin' | 'include'; + noCors?: boolean; +} +``` + +### SessionConfig + +```ts +interface SessionConfig { + storage?: boolean; // Enable storage persistence + domain?: string; // Cookie domain + maxAge?: number; // Session duration in minutes + sampling?: number; // Sampling rate (0-1) } +``` -// Example: Get browser information -const browserInfo = getBrowser(); -console.log('Browser:', browserInfo.name, browserInfo.version); +### StorageType + +```ts +type StorageType = 'localStorage' | 'sessionStorage' | 'cookie'; ``` -## Core Features +## Usage Notes + +- **Consent Required**: Browser information functions may require user consent + depending on privacy regulations +- **Storage Fallbacks**: Storage functions gracefully handle unavailable storage + with fallbacks +- **Transport Selection**: Choose transport based on use case: + - `fetch` - Modern, flexible, supports responses + - `beacon` - Reliable during page unload, small payloads + - `xhr` - Synchronous when needed, broader browser support +- **Performance**: Session and storage operations are optimized for minimal + performance impact + +--- -- **Browser Detection**: Identify browser type, version, and capabilities -- **DOM Utilities**: Functions for element visibility, attributes, and - manipulation -- **Session Management**: Handle web session lifecycle and storage -- **Viewport Detection**: Determine element visibility and user interaction -- **Web Storage**: Unified interface for localStorage and sessionStorage -- **Hash Generation**: Create consistent identifiers for web environments -- **Event Transmission**: Web-optimized event sending mechanisms +For platform-agnostic utilities, see +[Core Utilities](https://www.elbwalker.com/docs/core). ## Contribute diff --git a/packages/web/core/package.json b/packages/web/core/package.json index 5e05f5c75..958994dd1 100644 --- a/packages/web/core/package.json +++ b/packages/web/core/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-core", "description": "Web-specific utilities for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,7 +25,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "0.0.8" + "@walkeros/core": "0.1.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/web/core/src/__tests__/sessionStart.test.ts b/packages/web/core/src/__tests__/sessionStart.test.ts index 590e8b08e..3a4d9b45f 100644 --- a/packages/web/core/src/__tests__/sessionStart.test.ts +++ b/packages/web/core/src/__tests__/sessionStart.test.ts @@ -142,7 +142,7 @@ describe('sessionStart', () => { expect.any(Object), ); expect(mockElb).toHaveBeenNthCalledWith(2, { - event: 'session start', + name: 'session start', data: expect.any(Object), }); }); @@ -166,7 +166,7 @@ describe('sessionStart', () => { test('Callback default elb calls', () => { const session = sessionStart({ data: { isNew: true, isStart: true } }); expect(mockElb).toHaveBeenCalledWith({ - event: 'session start', + name: 'session start', data: session, }); }); diff --git a/packages/web/core/src/session/sessionStart.ts b/packages/web/core/src/session/sessionStart.ts index 2facf2f3b..80c010f68 100644 --- a/packages/web/core/src/session/sessionStart.ts +++ b/packages/web/core/src/session/sessionStart.ts @@ -105,7 +105,7 @@ const defaultCb: SessionCallback = ( if (session.isStart) { // Convert session start to an event object elb({ - event: 'session start', + name: 'session start', data: session, }); } diff --git a/packages/web/destinations/api/CHANGELOG.md b/packages/web/destinations/api/CHANGELOG.md index 716560787..fbd288fc3 100644 --- a/packages/web/destinations/api/CHANGELOG.md +++ b/packages/web/destinations/api/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/web-destination-api +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/destinations/api/README.md b/packages/web/destinations/api/README.md index 6ba75eec1..cd973c6e1 100644 --- a/packages/web/destinations/api/README.md +++ b/packages/web/destinations/api/README.md @@ -1,30 +1,23 @@

- +

# Web API Destination for walkerOS -This package provides a web API destination for walkerOS. It allows you to send -events to a custom API endpoint. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/destinations/api) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/web-destination-api) -[View documentation](https://www.elbwalker.com/docs/destinations/web/api/) +The API destination allows you to send events to any HTTP endpoint with +customizable data transformation and transport methods. -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This API destination receives processed events from the walkerOS collector and -sends them to your custom API endpoint, enabling integration with internal -analytics systems, data warehouses, or custom business logic that requires -real-time event data. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This API +destination receives processed events from the walkerOS collector and sends them +to your custom API endpoint, enabling integration with internal analytics +systems, data warehouses, or custom business logic that requires real-time event +data. ## Installation @@ -32,21 +25,134 @@ real-time event data. npm install @walkeros/web-destination-api ``` +## Configuration + +| Name | Type | Description | Required | Example | +| ----------- | ------------------------------ | ------------------------------------------------ | -------- | ------------------------------------------------------------------------- | +| `url` | `string` | The HTTP endpoint URL to send events to | Yes | `'https://api.example.com/events'` | +| `headers` | `Record` | Additional HTTP headers to include with requests | No | `{ 'Authorization': 'Bearer token', 'Content-Type': 'application/json' }` | +| `method` | `string` | HTTP method for the request | No | `'POST'` | +| `transform` | `function` | Function to transform event data before sending | No | `(data, config, mapping) => JSON.stringify(data)` | +| `transport` | `'fetch' \| 'xhr' \| 'beacon'` | Transport method for sending requests | No | `'fetch'` | + ## Usage -Here's a basic example of how to use the web API destination: +### Basic Usage + +```typescript +import { createCollector } from '@walkeros/collector'; +import { destinationAPI } from '@walkeros/web-destination-api'; + +const { elb } = await createCollector(); + +elb('walker destination', destinationAPI, { + settings: { + url: 'https://api.example.com/events', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer your-token', + }, + }, +}); +``` + +### Advanced Usage with Transform + +```typescript +import { createCollector } from '@walkeros/collector'; +import { destinationAPI } from '@walkeros/web-destination-api'; + +const { elb } = await createCollector(); + +elb('walker destination', destinationAPI, { + settings: { + url: 'https://api.example.com/events', + transport: 'fetch', + transform: (event, config, mapping) => { + // Custom transformation logic + return JSON.stringify({ + timestamp: Date.now(), + event_name: `${event.entity}_${event.action}`, + properties: event.data, + context: event.context, + }); + }, + }, +}); +``` + +## Examples + +### Sending to Analytics API ```typescript -import { elb } from '@walkeros/collector'; +import { createCollector } from '@walkeros/collector'; import { destinationAPI } from '@walkeros/web-destination-api'; +const { elb } = await createCollector(); + +// Configure for analytics API +elb('walker destination', destinationAPI, { + settings: { + url: 'https://analytics.example.com/track', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': 'your-api-key', + }, + transform: (event) => { + return JSON.stringify({ + event_type: `${event.entity}_${event.action}`, + user_id: event.user?.id, + session_id: event.user?.session, + properties: event.data, + timestamp: event.timing, + }); + }, + }, +}); +``` + +### Using Beacon Transport + +For critical events that need to be sent even when the page is unloading: + +```typescript +elb('walker destination', destinationAPI, { + settings: { + url: 'https://api.example.com/critical-events', + transport: 'beacon', // Reliable for page unload scenarios + }, +}); +``` + +### Custom Data Mapping + +Use mapping rules to control which events are sent: + +```typescript elb('walker destination', destinationAPI, { - custom: { + settings: { url: 'https://api.example.com/events', }, + mapping: { + entity: { + action: { + data: 'data', + }, + }, + }, }); ``` +## Transport Methods + +- **fetch** (default): Modern, promise-based HTTP requests +- **xhr**: Traditional XMLHttpRequest for older browser compatibility +- **beacon**: Uses Navigator.sendBeacon() for reliable data transmission during + page unload + ## Contribute Feel free to contribute by submitting an diff --git a/packages/web/destinations/api/package.json b/packages/web/destinations/api/package.json index 2cfcd928b..e506d5f31 100644 --- a/packages/web/destinations/api/package.json +++ b/packages/web/destinations/api/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-api", "description": "Web API destination for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,7 +30,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "0.0.8" + "@walkeros/web-core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/web/destinations/gtag/CHANGELOG.md b/packages/web/destinations/gtag/CHANGELOG.md index 967dbd47f..bbf08ddd6 100644 --- a/packages/web/destinations/gtag/CHANGELOG.md +++ b/packages/web/destinations/gtag/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/web-destination-gtag +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/destinations/gtag/README.md b/packages/web/destinations/gtag/README.md index 45566a747..0c09b5391 100644 --- a/packages/web/destinations/gtag/README.md +++ b/packages/web/destinations/gtag/README.md @@ -1,7 +1,12 @@ -# @walkeros/web-destination-gtag +# Google Gtag Destination for walkerOS -Unified Google destination for walkerOS supporting Google Analytics 4 (GA4), -Google Ads, and Google Tag Manager (GTM) through a single gtag implementation. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/destinations/gtag) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/web-destination-gtag) + +The Google Gtag destination provides a unified interface for sending events to +Google Analytics 4 (GA4), Google Ads, and Google Tag Manager (GTM) through a +single destination configuration. ## Features @@ -20,163 +25,46 @@ Google Ads, and Google Tag Manager (GTM) through a single gtag implementation. npm install @walkeros/web-destination-gtag ``` -## Basic Usage - -### Single Tool (GA4 Only) +## Usage ```typescript +import { createCollector } from '@walkeros/collector'; import { destinationGtag } from '@walkeros/web-destination-gtag'; -const destination = destinationGtag({ - settings: { - ga4: { - measurementId: 'G-XXXXXXXXXX', - }, - }, -}); -``` +const { elb } = await createCollector(); -### Multiple Tools - -```typescript -import { destinationGtag } from '@walkeros/web-destination-gtag'; - -const destination = destinationGtag({ +elb('walker destination', destinationGtag, { settings: { ga4: { - measurementId: 'G-XXXXXXXXXX', - debug: true, - pageview: false, + measurementId: 'G-XXXXXXXXXX', // Required for GA4 }, ads: { - conversionId: 'AW-XXXXXXXXX', - currency: 'EUR', + conversionId: 'AW-XXXXXXXXX', // Required for Google Ads }, gtm: { - containerId: 'GTM-XXXXXXX', + containerId: 'GTM-XXXXXXX', // Required for GTM }, }, }); ``` -### With Collector - -```typescript -import { collector } from '@walkeros/collector'; -import { destinationGtag } from '@walkeros/web-destination-gtag'; - -const instance = collector({ - destinations: [ - destinationGtag({ - settings: { - ga4: { measurementId: 'G-XXXXXXXXXX' }, - ads: { conversionId: 'AW-XXXXXXXXX' }, - gtm: { containerId: 'GTM-XXXXXXX' }, - }, - }), - ], -}); -``` - ## Configuration -### GA4 Settings +| Name | Type | Description | Required | Example | +| ----- | ------------- | -------------------------------------------------- | -------- | ----------------------------------- | +| `ga4` | `GA4Settings` | GA4-specific configuration settings | No | `{ measurementId: 'G-XXXXXXXXXX' }` | +| `ads` | `AdsSettings` | Google Ads specific configuration settings | No | `{ conversionId: 'AW-XXXXXXXXX' }` | +| `gtm` | `GTMSettings` | Google Tag Manager specific configuration settings | No | `{ containerId: 'GTM-XXXXXXX' }` | -```typescript -interface GA4Settings { - measurementId: string; // Required: GA4 Measurement ID - debug?: boolean; // Enable debug mode - include?: Include; // Data groups to include - pageview?: boolean; // Send automatic pageviews (default: true) - server_container_url?: string; // Server-side GTM URL - snakeCase?: boolean; // Convert event names to snake_case (default: true) - transport_url?: string; // Custom transport URL -} -``` +### Event Mapping -### Google Ads Settings +For custom event mapping (`mapping.entity.action.settings`): -```typescript -interface AdsSettings { - conversionId: string; // Required: Google Ads Conversion ID - currency?: string; // Default currency (default: 'EUR') -} -``` - -### GTM Settings - -```typescript -interface GTMSettings { - containerId: string; // Required: GTM Container ID - dataLayer?: string; // Custom dataLayer name (default: 'dataLayer') - domain?: string; // Custom GTM domain -} -``` - -## Mapping - -Each tool supports individual mapping configurations: - -```typescript -const mapping = { - order: { - complete: { - name: 'purchase', - settings: { - ga4: { - include: ['data', 'context'], - }, - ads: { - conversionId: 'abcxyz', - }, - gtm: {}, // Uses 'purchase' as event name - }, - data: { - map: { - transaction_id: 'data.id', - value: 'data.total', - currency: 'data.currency', - }, - }, - }, - }, -}; -``` - -### GA4-Specific Mapping - -```typescript -settings: { - ga4: { - include: ['data', 'context', 'user'], // Data groups to include - } -} -``` - -### Google Ads Conversion Mapping - -For Google Ads, specify the conversion label in the `settings.ads.label` field: - -```typescript -{ - name: 'purchase', // GA4/GTM event name - settings: { - ads: { - label: 'CONVERSION_LABEL', // This becomes AW-XXXXXXXXX/CONVERSION_LABEL - }, - } -} -``` - -### GTM DataLayer Mapping - -GTM receives the full event data and pushes to the configured dataLayer: - -```typescript -settings: { - gtm: {}, // Uses default dataLayer behavior -} -``` +| Name | Type | Description | Required | Example | +| ----- | ------------ | ----------------------------------------------- | -------- | ---------------------------------- | +| `ga4` | `GA4Mapping` | GA4-specific event mapping configuration | No | `{ include: ['data', 'context'] }` | +| `ads` | `AdsMapping` | Google Ads specific event mapping configuration | No | `{ label: 'conversion_label' }` | +| `gtm` | `GTMMapping` | GTM specific event mapping configuration | No | `{}` | ## Examples @@ -209,7 +97,7 @@ const destination = destinationGtag({ loop: [ 'nested', { - condition: (entity) => entity.type === 'product', + condition: (entity) => entity.entity === 'product', map: { item_id: 'data.id', item_name: 'data.name', @@ -325,6 +213,13 @@ const rules: DestinationGtag.Rules = { - Check the dataLayer name matches your GTM configuration - Use GTM Preview mode to debug event flow +## Contribute + +Feel free to contribute by submitting an +[issue](https://github.com/elbwalker/walkerOS/issues), starting a +[discussion](https://github.com/elbwalker/walkerOS/discussions), or getting in +[contact](https://calendly.com/elb-alexander/30min). + ## License MIT diff --git a/packages/web/destinations/gtag/package.json b/packages/web/destinations/gtag/package.json index add413adc..e2aa2fa42 100644 --- a/packages/web/destinations/gtag/package.json +++ b/packages/web/destinations/gtag/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-gtag", "description": "Unified Google destination for walkerOS (GA4, Ads, GTM)", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,7 +25,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "0.0.8" + "@walkeros/web-core": "0.1.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/web/destinations/gtag/src/__tests__/ga4.test.ts b/packages/web/destinations/gtag/src/__tests__/ga4.test.ts index d3efca6f8..809a43230 100644 --- a/packages/web/destinations/gtag/src/__tests__/ga4.test.ts +++ b/packages/web/destinations/gtag/src/__tests__/ga4.test.ts @@ -83,7 +83,7 @@ describe('GA4 Implementation', () => { describe('pushGA4Event', () => { const mockEvent = { - event: 'page view', + name: 'page view', data: {}, timestamp: 1234567890, id: 'test-id', diff --git a/packages/web/destinations/gtag/src/__tests__/gtm.test.ts b/packages/web/destinations/gtag/src/__tests__/gtm.test.ts index 27a70e7fe..5727638b3 100644 --- a/packages/web/destinations/gtag/src/__tests__/gtm.test.ts +++ b/packages/web/destinations/gtag/src/__tests__/gtm.test.ts @@ -72,7 +72,7 @@ describe('GTM Implementation', () => { describe('pushGTMEvent', () => { const mockEvent = { - event: 'product view', + name: 'product view', entity: 'product', action: 'view', data: { id: 'product-1', name: 'Test Product' }, @@ -108,6 +108,7 @@ describe('GTM Implementation', () => { expect(mockDataLayer).toHaveLength(1); expect(mockDataLayer[0]).toEqual({ event: 'product view', + name: 'product view', entity: 'product', action: 'view', data: { id: 'product-1', name: 'Test Product' }, diff --git a/packages/web/destinations/gtag/src/examples/events.ts b/packages/web/destinations/gtag/src/examples/events.ts index 99ea3cde2..f0ee89d07 100644 --- a/packages/web/destinations/gtag/src/examples/events.ts +++ b/packages/web/destinations/gtag/src/examples/events.ts @@ -14,7 +14,7 @@ export function ga4Purchase(): unknown[] { shipping: event.data.shipping, currency: 'EUR', items: event.nested - .filter((item) => item.type === 'product') + .filter((item) => item.entity === 'product') .map((item) => ({ item_id: item.data.id, item_name: item.data.name, diff --git a/packages/web/destinations/gtag/src/examples/mapping.ts b/packages/web/destinations/gtag/src/examples/mapping.ts index d50628a90..f41719092 100644 --- a/packages/web/destinations/gtag/src/examples/mapping.ts +++ b/packages/web/destinations/gtag/src/examples/mapping.ts @@ -22,7 +22,7 @@ export const ga4Purchase: DestinationGtag.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: { item_id: 'data.id', item_name: 'data.name', @@ -115,7 +115,7 @@ export const combinedPurchase: DestinationGtag.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: { item_id: 'data.id', item_name: 'data.name', diff --git a/packages/web/destinations/gtag/src/ga4/push.ts b/packages/web/destinations/gtag/src/ga4/push.ts index ff6167b63..6579beaf3 100644 --- a/packages/web/destinations/gtag/src/ga4/push.ts +++ b/packages/web/destinations/gtag/src/ga4/push.ts @@ -31,7 +31,7 @@ export function pushGA4Event( }; // Event name (snake_case default) - let eventName = event.event; // Assume custom mapped name + let eventName = event.name; // Assume custom mapped name if (settings.snakeCase !== false) { // Use snake case if not disabled eventName = normalizeEventName(eventName); diff --git a/packages/web/destinations/gtag/src/gtm/push.ts b/packages/web/destinations/gtag/src/gtm/push.ts index 8822e94dc..adaf62794 100644 --- a/packages/web/destinations/gtag/src/gtm/push.ts +++ b/packages/web/destinations/gtag/src/gtm/push.ts @@ -12,7 +12,7 @@ export function pushGTMEvent( env?: DestinationWeb.Environment, ): void { const { window } = getEnvironment(env); - const obj = { event: event.event }; // Use the name mapping by default + const obj = { event: event.name }; // Use the name mapping by default (window.dataLayer as unknown[]).push({ ...obj, diff --git a/packages/web/destinations/meta/CHANGELOG.md b/packages/web/destinations/meta/CHANGELOG.md index 4d5eac3e7..45baad711 100644 --- a/packages/web/destinations/meta/CHANGELOG.md +++ b/packages/web/destinations/meta/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/web-destination-meta +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/destinations/meta/README.md b/packages/web/destinations/meta/README.md index 2de195fb1..177c880b7 100644 --- a/packages/web/destinations/meta/README.md +++ b/packages/web/destinations/meta/README.md @@ -1,30 +1,23 @@

- +

# Meta Pixel Destination for walkerOS -This package provides a Meta Pixel (formerly Facebook Pixel) destination for -walkerOS. It allows you to send events to Meta Pixel. - -[View documentation](https://www.elbwalker.com/docs/destinations/web/meta/) - -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/destinations/meta) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/web-destination-meta) -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) +This package provides a Meta Pixel (formerly Facebook Pixel) destination for +walkerOS. It sends events to Meta Pixel to track visitor activity and +conversions for Facebook and Instagram advertising campaigns. -This Meta Pixel destination receives processed events from the walkerOS -collector and transforms them into Meta's Pixel API format, handling conversion -events, custom events, and audience building data to optimize your Meta -advertising campaigns. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This Meta +Pixel destination receives processed events from the walkerOS collector and +transforms them into Meta's Pixel API format, handling conversion events, custom +events, and audience building data to optimize your Meta advertising campaigns. ## Installation @@ -37,16 +30,26 @@ npm install @walkeros/web-destination-meta Here's a basic example of how to use the Meta Pixel destination: ```typescript -import { elb } from '@walkeros/collector'; +import { createCollector } from '@walkeros/collector'; import { destinationMeta } from '@walkeros/web-destination-meta'; +const { elb } = await createCollector(); + elb('walker destination', destinationMeta, { - custom: { - pixelId: '1234567890', + settings: { + pixelId: '1234567890', // Your Meta Pixel ID }, + loadScript: true, // Load Meta Pixel script automatically }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| ------------ | --------- | ----------------------------------------------------------------- | -------- | -------------- | +| `pixelId` | `string` | Your Meta Pixel ID from Facebook Business Manager | Yes | `'1234567890'` | +| `loadScript` | `boolean` | Whether to automatically load the Meta Pixel script (fbevents.js) | No | `true` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/web/destinations/meta/package.json b/packages/web/destinations/meta/package.json index edfd61a9e..12508396c 100644 --- a/packages/web/destinations/meta/package.json +++ b/packages/web/destinations/meta/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-meta", "description": "Meta pixel web destination for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,7 +30,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "0.0.8" + "@walkeros/web-core": "0.1.0" }, "devDependencies": { "@types/facebook-pixel": "^0.0.31" diff --git a/packages/web/destinations/meta/src/examples/events.ts b/packages/web/destinations/meta/src/examples/events.ts index 3e1f2c970..0900b486d 100644 --- a/packages/web/destinations/meta/src/examples/events.ts +++ b/packages/web/destinations/meta/src/examples/events.ts @@ -10,7 +10,7 @@ export function Purchase(): unknown[] { value: event.data.total, currency: 'EUR', contents: event.nested - .filter((item) => item.type === 'product') + .filter((item) => item.entity === 'product') .map((item) => ({ id: item.data.id, quantity: 1 })), content_type: 'product', num_items: 2, @@ -45,12 +45,13 @@ export function InitiateCheckout(): unknown[] { currency: 'EUR', value: event.data.value, contents: event.nested - .filter((entity) => entity.type === 'product') + .filter((entity) => entity.entity === 'product') .map((entity) => ({ id: entity.data.id, quantity: entity.data.quantity, })), - num_items: event.nested.filter((item) => item.type === 'product').length, + num_items: event.nested.filter((item) => item.entity === 'product') + .length, }, { eventID: event.id }, ]; diff --git a/packages/web/destinations/meta/src/examples/mapping.ts b/packages/web/destinations/meta/src/examples/mapping.ts index cbc5796de..1ff0610ab 100644 --- a/packages/web/destinations/meta/src/examples/mapping.ts +++ b/packages/web/destinations/meta/src/examples/mapping.ts @@ -13,7 +13,7 @@ export const Purchase: DestinationMeta.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: { id: 'data.id', quantity: { key: 'data.quantity', value: 1 }, @@ -25,7 +25,7 @@ export const Purchase: DestinationMeta.Rule = { num_items: { fn: (event) => (event as WalkerOS.Event).nested.filter( - (item) => item.type === 'product', + (item) => item.entity === 'product', ).length, }, }, @@ -64,7 +64,7 @@ export const InitiateCheckout: DestinationMeta.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: { id: 'data.id', quantity: { key: 'data.quantity', value: 1 }, @@ -75,7 +75,7 @@ export const InitiateCheckout: DestinationMeta.Rule = { num_items: { fn: (event) => (event as WalkerOS.Event).nested.filter( - (item) => item.type === 'product', + (item) => item.entity === 'product', ).length, }, }, diff --git a/packages/web/destinations/meta/src/index.test.ts b/packages/web/destinations/meta/src/index.test.ts index 5b9fb663b..4caf42235 100644 --- a/packages/web/destinations/meta/src/index.test.ts +++ b/packages/web/destinations/meta/src/index.test.ts @@ -102,7 +102,7 @@ describe('Destination Meta Pixel', () => { await elb(event); expect(mockFn).toHaveBeenCalledWith( 'track', - event.event, + event.name, {}, { eventID: event.id }, ); diff --git a/packages/web/destinations/meta/src/index.ts b/packages/web/destinations/meta/src/index.ts index 9fb400a8f..24fea659c 100644 --- a/packages/web/destinations/meta/src/index.ts +++ b/packages/web/destinations/meta/src/index.ts @@ -40,12 +40,12 @@ export const destinationMeta: Destination = { const fbq = window.fbq as facebook.Pixel.Event; // page view - if (event.event === 'page view' && !mapping.settings) { + if (event.name === 'page view' && !mapping.settings) { // Define a custom mapping - event.event = 'PageView'; + event.name = 'PageView'; } - const eventName = track || trackCustom || event.event; + const eventName = track || trackCustom || event.name; fbq( trackCustom ? 'trackCustom' : 'track', diff --git a/packages/web/destinations/piwikpro/CHANGELOG.md b/packages/web/destinations/piwikpro/CHANGELOG.md index 10f76662e..4e7371336 100644 --- a/packages/web/destinations/piwikpro/CHANGELOG.md +++ b/packages/web/destinations/piwikpro/CHANGELOG.md @@ -1,5 +1,17 @@ # @walkeros/web-destination-piwikpro +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-core@0.1.0 + - @walkeros/core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/destinations/piwikpro/README.md b/packages/web/destinations/piwikpro/README.md index 53c5f3cc0..6fe950d80 100644 --- a/packages/web/destinations/piwikpro/README.md +++ b/packages/web/destinations/piwikpro/README.md @@ -1,29 +1,23 @@

- +

# Piwik PRO Destination for walkerOS -This package provides a Piwik PRO destination for walkerOS. It allows you to -send events to Piwik PRO. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/destinations/piwikpro) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/web-destination-piwikpro) -[View documentation](https://www.elbwalker.com/docs/destinations/web/piwikpro/) +This package provides a [Piwik PRO](https://piwik.pro/) destination for +walkerOS. Piwik PRO is a European, privacy-focused web analytics and marketing +platform that helps businesses track website traffic and user behavior. -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This Piwik PRO destination receives processed events from the walkerOS collector -and transforms them into Piwik PRO's analytics format, providing -privacy-compliant analytics with GDPR compliance and data ownership control. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This Piwik +PRO destination receives processed events from the walkerOS collector and +transforms them into Piwik PRO's analytics format, providing privacy-compliant +analytics with GDPR compliance and data ownership control. ## Installation @@ -36,17 +30,36 @@ npm install @walkeros/web-destination-piwikpro Here's a basic example of how to use the Piwik PRO destination: ```typescript -import { elb } from '@walkeros/collector'; +import { createCollector } from '@walkeros/collector'; import { destinationPiwikPro } from '@walkeros/web-destination-piwikpro'; +const { elb } = await createCollector(); + elb('walker destination', destinationPiwikPro, { - custom: { - appId: 'YOUR_APP_ID', - url: 'https://your-account.piwik.pro/', + settings: { + appId: 'XXX-XXX-XXX-XXX-XXX', // Required + url: 'https://your_account_name.piwik.pro/', // Required }, }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| -------------- | --------- | ---------------------------------------------- | -------- | ---------------------------------------- | +| `appId` | `string` | ID of the Piwik PRO site | Yes | `'XXX-XXX-XXX-XXX-XXX'` | +| `url` | `string` | URL of your Piwik PRO account | Yes | `'https://your_account_name.piwik.pro/'` | +| `linkTracking` | `boolean` | Enables/Disables download and outlink tracking | No | `false` | + +### Event Mapping + +For custom event mapping (`mapping.entity.action.settings`): + +| Name | Type | Description | Required | Example | +| ----------- | -------- | ------------------------------------- | -------- | -------------- | +| `goalId` | `string` | ID to count the event as a goal | No | `'1'` | +| `goalValue` | `string` | Property to be used as the goal value | No | `'data.value'` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/web/destinations/piwikpro/package.json b/packages/web/destinations/piwikpro/package.json index ce819dc82..1681be6e8 100644 --- a/packages/web/destinations/piwikpro/package.json +++ b/packages/web/destinations/piwikpro/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-piwikpro", "description": "Piwik PRO destination for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,8 +30,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "0.0.8", - "@walkeros/web-core": "0.0.8" + "@walkeros/core": "0.1.0", + "@walkeros/web-core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/web/destinations/piwikpro/src/examples/events.ts b/packages/web/destinations/piwikpro/src/examples/events.ts index b0f22dedb..278988b80 100644 --- a/packages/web/destinations/piwikpro/src/examples/events.ts +++ b/packages/web/destinations/piwikpro/src/examples/events.ts @@ -20,7 +20,7 @@ export function ecommerceOrder(): unknown[] { return [ [ 'ecommerceOrder', - event.nested.filter((item) => item.type === 'product').map(getProduct), + event.nested.filter((item) => item.entity === 'product').map(getProduct), { orderId: event.data.id, grandTotal: event.data.total, @@ -58,7 +58,7 @@ export function ecommerceCartUpdate(): unknown[] { return [ [ 'ecommerceCartUpdate', - event.nested.filter((item) => item.type === 'product').map(getProduct), + event.nested.filter((item) => item.entity === 'product').map(getProduct), event.data.value, { currencyCode: 'EUR' }, ], diff --git a/packages/web/destinations/piwikpro/src/examples/mapping.ts b/packages/web/destinations/piwikpro/src/examples/mapping.ts index 07f7dd386..7b36770f5 100644 --- a/packages/web/destinations/piwikpro/src/examples/mapping.ts +++ b/packages/web/destinations/piwikpro/src/examples/mapping.ts @@ -23,7 +23,7 @@ export const ecommerceOrder: DestinationPiwikPro.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: productMap, }, ], @@ -94,7 +94,7 @@ export const ecommerceCartUpdate: DestinationPiwikPro.Rule = { 'nested', { condition: (entity) => - isObject(entity) && entity.type === 'product', + isObject(entity) && entity.entity === 'product', map: productMap, }, ], diff --git a/packages/web/destinations/piwikpro/src/index.ts b/packages/web/destinations/piwikpro/src/index.ts index 532e35127..38de4e0b4 100644 --- a/packages/web/destinations/piwikpro/src/index.ts +++ b/packages/web/destinations/piwikpro/src/index.ts @@ -49,7 +49,7 @@ export const destinationPiwikPro: Destination = { const paq = (window as Window)._paq!.push; // Send pageviews if not disabled - if (event.event === 'page view' && !mapping.settings) { + if (event.name === 'page view' && !mapping.settings) { paq(['trackPageView', await getMappingValue(event, 'data.title')]); return; } @@ -58,7 +58,7 @@ export const destinationPiwikPro: Destination = { const parameters = isArray(data) ? data : [data]; - paq([event.event, ...parameters]); + paq([event.name, ...parameters]); if (eventMapping.goalId) { const goalValue = eventMapping.goalValue diff --git a/packages/web/destinations/plausible/CHANGELOG.md b/packages/web/destinations/plausible/CHANGELOG.md index c27ca1a3d..256daf1e0 100644 --- a/packages/web/destinations/plausible/CHANGELOG.md +++ b/packages/web/destinations/plausible/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/web-destination-plausible +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/web-core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/destinations/plausible/README.md b/packages/web/destinations/plausible/README.md index db6f53bf8..074a2375b 100644 --- a/packages/web/destinations/plausible/README.md +++ b/packages/web/destinations/plausible/README.md @@ -1,28 +1,22 @@

- +

# Plausible Destination for walkerOS -This package provides a Plausible destination for walkerOS. It allows you to -send events to Plausible Analytics. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/destinations/plausible) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/web-destination-plausible) -[View documentation](https://www.elbwalker.com/docs/destinations/web/plausible/) +This package provides a [Plausible Analytics](https://plausible.io/) destination +for walkerOS. Plausible is a simple, and privacy-friendly Google Analytics +Alternative. -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This Plausible destination receives processed events from the walkerOS collector -and transforms them into Plausible Analytics format, providing lightweight, +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This +Plausible destination receives processed events from the walkerOS collector and +transforms them into Plausible Analytics format, providing lightweight, privacy-focused web analytics without cookies or personal data collection. ## Installation @@ -36,16 +30,24 @@ npm install @walkeros/web-destination-plausible Here's a basic example of how to use the Plausible destination: ```typescript -import { elb } from '@walkeros/collector'; +import { createCollector } from '@walkeros/collector'; import { destinationPlausible } from '@walkeros/web-destination-plausible'; +const { elb } = await createCollector(); + elb('walker destination', destinationPlausible, { - custom: { - domain: 'your-domain.com', + settings: { + domain: 'elbwalker.com', // Optional, domain of your site as registered }, }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| -------- | -------- | -------------------------------------------------- | -------- | ----------------- | +| `domain` | `string` | The domain of your site as registered in Plausible | No | `'elbwalker.com'` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/web/destinations/plausible/package.json b/packages/web/destinations/plausible/package.json index bf2fd51ef..f894ee246 100644 --- a/packages/web/destinations/plausible/package.json +++ b/packages/web/destinations/plausible/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-destination-plausible", "description": "Plausible web destination for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,7 +30,7 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/web-core": "0.0.8" + "@walkeros/web-core": "0.1.0" }, "devDependencies": {}, "repository": { diff --git a/packages/web/destinations/plausible/src/index.ts b/packages/web/destinations/plausible/src/index.ts index 14b35fee5..4055e0368 100644 --- a/packages/web/destinations/plausible/src/index.ts +++ b/packages/web/destinations/plausible/src/index.ts @@ -35,7 +35,7 @@ export const destinationPlausible: Destination = { const { window } = getEnvironment(env); const plausible = (window as Window).plausible!; - plausible(`${event.event}`, params); + plausible(`${event.name}`, params); }, }; diff --git a/packages/web/sources/browser/CHANGELOG.md b/packages/web/sources/browser/CHANGELOG.md index 1d72b3a7b..97115fb5a 100644 --- a/packages/web/sources/browser/CHANGELOG.md +++ b/packages/web/sources/browser/CHANGELOG.md @@ -1,5 +1,17 @@ # @walkeros/web-source-browser +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/collector@0.1.0 + - @walkeros/web-core@0.1.0 + ## 0.0.10 ### Patch Changes diff --git a/packages/web/sources/browser/README.md b/packages/web/sources/browser/README.md index 40efde344..39e2c36ca 100644 --- a/packages/web/sources/browser/README.md +++ b/packages/web/sources/browser/README.md @@ -6,81 +6,118 @@ # Browser DOM Source for walkerOS -The walkerOS Browser DOM Source provides automatic event collection from browser -interactions and DOM elements, plus a tagger utility for generating HTML data -attributes. It serves as the primary source for capturing user behavior, page -views, and element interactions directly from the DOM without requiring manual -event instrumentation. +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/sources/browser) +• [NPM Package](https://www.npmjs.com/package/@walkeros/web-source-browser) -## Role in walkerOS Ecosystem +The Browser Source is walkerOS's primary web tracking solution that you can use +to capture user interactions directly from the browsers DOM. -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: +## What It Does -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) +The Browser Source transforms your website into a comprehensive tracking +environment by: -The Browser DOM Source automatically detects and captures user interactions, -page lifecycle events, and element visibility changes, transforming them into -standardized walkerOS events that flow through the collector to your configured -destinations. +- **Data attribute reading**: Extracts custom tracking data from HTML `data-elb` + attributes +- **Session management**: Detects and handles user sessions automatically ## Installation -```sh -npm install @walkeros/web-source-browser -``` - -## Usage - -Here's a basic example of how to use the Browser DOM source: - -```typescript -import { elb } from '@walkeros/collector'; -import { sourceBrowser, createTagger } from '@walkeros/web-source-browser'; - -// Initialize the browser source -sourceBrowser({ elb }); +### With npm -// Use the tagger to generate HTML data attributes -const tagger = createTagger(); -const attrs = tagger('product').data('id', '123').action('load', 'view').get(); -// Result: { 'data-elb': 'product', 'data-elb-product': 'id:123', 'data-elbaction': 'load:view' } +Install the source via npm: -// The source will now automatically capture: -// - Page views -// - Click events -// - Form submissions -// - Element visibility changes -// - Custom data attributes +```bash +npm install @walkeros/web-source-browser ``` -## Automatic Event Capture - -The browser source automatically captures: - -- **Page Events**: Page views, navigation, and lifecycle events -- **Click Events**: Button clicks, link clicks, and element interactions -- **Form Events**: Form submissions and field interactions -- **Visibility Events**: When elements become visible in the viewport -- **Custom Events**: Events defined through data attributes in HTML +Setup in your project: + +```javascript +import { createCollector } from '@walkeros/collector'; +import { createSource } from '@walkeros/core'; +import { sourceBrowser } from '@walkeros/web-source-browser'; + +const { collector } = await createCollector({ + sources: { + browser: createSource(sourceBrowser, { + settings: { + pageview: true, + session: true, + elb: 'elb', // Browser source will set window.elb automatically + }, + }), + }, +}); +``` -## Data Attributes +### With a script tag -Use HTML data attributes to define custom tracking: +Load the source via dynamic import: ```html - - - - -
- Product content -
+ ``` +## Configuration reference + +| Name | Type | Description | Required | Example | +| ---------- | -------------------------------- | ------------------------------------------------ | -------- | -------------------------------- | +| `prefix` | `string` | Prefix for data attributes used in DOM tracking | No | `'data-elb'` | +| `scope` | `Element \| Document` | DOM scope for event tracking (default: document) | No | `document.querySelector("#app")` | +| `pageview` | `boolean` | Enable automatic pageview tracking | No | `true` | +| `session` | `boolean` | Enable session tracking and management | No | `true` | +| `elb` | `string` | Custom name for the global elb function | No | `'elb'` | +| `name` | `string` | Custom name for the browser source instance | No | `'mySource'` | +| `elbLayer` | `boolean \| string \| Elb.Layer` | Enable elbLayer for async command queuing | No | `true` | + +### elb + +> **Two Different elb Functions** +> +> The collector provides **two different elb functions**: +> +> 1. **Collector elb** (`elb` from `createCollector`): Basic event tracking +> that works with all sources and destinations +> 2. **Browser Source elb** (`collector.sources.browser.elb` or direct from +> `createSource`): Enhanced function with browser-specific features +> +> **Browser Source elb adds:** +> +> - **DOM Commands**: `walker init` for asynchronous loading of DOM elements +> - **Flexible Arguments**: Support for multiple argument patterns +> - **elbLayer Integration**: Automatic processing of queued commands +> - **Element parameters**: Support for element parameters in DOM commands +> +> Use **separate source creation** for direct access to the enhanced elb +> function, or access it via `collector.sources.browser.elb` in the unified API. +> +> See [Commands](https://www.elbwalker.com/docs/sources/web/browser/commands) +> for full browser source API documentation. + ## Contribute Feel free to contribute by submitting an diff --git a/packages/web/sources/browser/package.json b/packages/web/sources/browser/package.json index 528033dd5..02a993f51 100644 --- a/packages/web/sources/browser/package.json +++ b/packages/web/sources/browser/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-source-browser", "description": "Browser DOM source for walkerOS", - "version": "0.0.10", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -25,8 +25,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/collector": "0.0.8", - "@walkeros/web-core": "0.0.8" + "@walkeros/collector": "0.1.0", + "@walkeros/web-core": "0.1.0" }, "repository": { "url": "git+https://github.com/elbwalker/walkerOS.git", diff --git a/packages/web/sources/browser/src/__tests__/edgeCases.test.ts b/packages/web/sources/browser/src/__tests__/edgeCases.test.ts index 7f15ae98d..a1e7b2c4d 100644 --- a/packages/web/sources/browser/src/__tests__/edgeCases.test.ts +++ b/packages/web/sources/browser/src/__tests__/edgeCases.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/no-disabled-tests */ import type { WalkerOS, Collector } from '@walkeros/core'; import { createCollector } from '@walkeros/collector'; import { createBrowserSource } from './test-utils'; diff --git a/packages/web/sources/browser/src/__tests__/elbLayer.test.ts b/packages/web/sources/browser/src/__tests__/elbLayer.test.ts index dd5119eb9..04ca3553b 100644 --- a/packages/web/sources/browser/src/__tests__/elbLayer.test.ts +++ b/packages/web/sources/browser/src/__tests__/elbLayer.test.ts @@ -134,11 +134,11 @@ describe('Elb Layer', () => { // Then regular events expect(mockPush).toHaveBeenNthCalledWith( 3, - expect.objectContaining({ event: 'product' }), + expect.objectContaining({ name: 'product' }), ); expect(mockPush).toHaveBeenNthCalledWith( 4, - expect.objectContaining({ event: 'page' }), + expect.objectContaining({ name: 'page' }), ); }); @@ -157,7 +157,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledTimes(1); expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'test_event', + name: 'test_event', data: { key: 'value' }, context: { context: 'test' }, trigger: 'load', @@ -167,7 +167,7 @@ describe('Elb Layer', () => { test('handles object commands', () => { const eventObject: WalkerOS.DeepPartialEvent = { - event: 'custom_event', + name: 'custom_event', data: { test: 'data' }, context: { page: ['home', 0] as [string, number] }, }; @@ -241,7 +241,7 @@ describe('Elb Layer', () => { }); expect(mockPush).toHaveBeenNthCalledWith( 2, - expect.objectContaining({ event: 'page' }), + expect.objectContaining({ name: 'page' }), ); expect(mockPush).toHaveBeenNthCalledWith( 3, @@ -268,7 +268,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'entity_name', + name: 'entity_name', data: { prop: 'value' }, context: { ctx: 'context' }, trigger: 'trigger_type', @@ -337,7 +337,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'test_event', + name: 'test_event', data: { key: 'value' }, trigger: 'load', }), @@ -355,7 +355,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'immediate_event', + name: 'immediate_event', data: { test: true }, }), ); @@ -380,7 +380,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'product', + name: 'product', data: expect.objectContaining({ id: 123, // Values are cast by castValue utility name: 'Test Product', @@ -401,7 +401,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'page', + name: 'page', data: expect.objectContaining({ id: '/test-page', }), @@ -444,7 +444,7 @@ describe('Elb Layer', () => { expect(mockPush).toHaveBeenCalledTimes(1); expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'page view', + name: 'page view', data: expect.objectContaining({ id: '/walker-run-test', }), diff --git a/packages/web/sources/browser/src/__tests__/html/walker.html b/packages/web/sources/browser/src/__tests__/html/walker.html index d79cc3ea9..b5895a6f4 100644 --- a/packages/web/sources/browser/src/__tests__/html/walker.html +++ b/packages/web/sources/browser/src/__tests__/html/walker.html @@ -15,7 +15,7 @@ id="son" data-elb="son" data-elb-son="interested_in:pizza" - data-elbaction="load:speak" + data-elbactions="load:speak" >
@@ -158,7 +158,7 @@ data-elb-l="l2:2" data-elbcontext="child:link" > - +
@@ -171,3 +171,13 @@
+ + +
+
+ +
+
+ +
+
diff --git a/packages/web/sources/browser/src/__tests__/integration.test.ts b/packages/web/sources/browser/src/__tests__/integration.test.ts index dfbf1bbf3..372e876cd 100644 --- a/packages/web/sources/browser/src/__tests__/integration.test.ts +++ b/packages/web/sources/browser/src/__tests__/integration.test.ts @@ -53,7 +53,7 @@ describe('Browser Source Integration Tests', () => { // Should have processed the event with source information expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'product view', + name: 'product view', data: expect.objectContaining({ id: 123, name: 'Test Product', @@ -101,7 +101,7 @@ describe('Browser Source Integration Tests', () => { // Should have processed the pageview event expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'page view', + name: 'page view', trigger: 'load', data: expect.objectContaining({ id: '/test-page', @@ -129,7 +129,7 @@ describe('Browser Source Integration Tests', () => { // Should have processed the click expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'cta press', + name: 'cta press', entity: 'cta', action: 'press', trigger: 'click', @@ -161,7 +161,7 @@ describe('Browser Source Integration Tests', () => { expect(mockPush).toHaveBeenNthCalledWith( 2, expect.objectContaining({ - event: 'page view', + name: 'page view', data: expect.objectContaining({ id: '/test-page', title: 'Home' }), context: { url: '/' }, trigger: 'load', @@ -170,7 +170,7 @@ describe('Browser Source Integration Tests', () => { expect(mockPush).toHaveBeenNthCalledWith( 3, expect.objectContaining({ - event: 'product click', + name: 'product click', data: { id: '123' }, context: { position: 1 }, trigger: 'click', diff --git a/packages/web/sources/browser/src/__tests__/tagger.test.ts b/packages/web/sources/browser/src/__tests__/tagger.test.ts index 9fee98189..bd7181835 100644 --- a/packages/web/sources/browser/src/__tests__/tagger.test.ts +++ b/packages/web/sources/browser/src/__tests__/tagger.test.ts @@ -149,10 +149,10 @@ describe('Tagger', () => { test('object with multiple actions', () => { const result = createTagger()() - .action({ load: 'view', click: 'select', visible: 'impression' }) + .action({ load: 'view', click: 'select', impression: 'view' }) .get(); expect(result).toMatchObject({ - 'data-elbaction': 'load:view;click:select;visible:impression', + 'data-elbaction': 'load:view;click:select;impression:view', }); }); @@ -160,10 +160,10 @@ describe('Tagger', () => { const result = createTagger()() .action('load', 'view') .action('click', 'select') - .action({ visible: 'impression' }) + .action({ impression: 'view' }) .get(); expect(result).toMatchObject({ - 'data-elbaction': 'load:view;click:select;visible:impression', + 'data-elbaction': 'load:view;click:select;impression:view', }); }); @@ -181,6 +181,66 @@ describe('Tagger', () => { }); }); + describe('Actions Method', () => { + test('single trigger and action', () => { + const result = createTagger()().actions('load', 'view').get(); + expect(result).toMatchObject({ + 'data-elbactions': 'load:view', + }); + }); + + test('single combined trigger:action', () => { + const result = createTagger()().actions('load:view').get(); + expect(result).toMatchObject({ + 'data-elbactions': 'load:view', + }); + }); + + test('object with multiple actions', () => { + const result = createTagger()() + .actions({ load: 'view', click: 'select', impression: 'view' }) + .get(); + expect(result).toMatchObject({ + 'data-elbactions': 'load:view;click:select;impression:view', + }); + }); + + test('accumulates multiple actions calls', () => { + const result = createTagger()() + .actions('load', 'view') + .actions({ click: 'select' }) + .actions('impression:view') + .get(); + expect(result).toMatchObject({ + 'data-elbactions': 'load:view;click:select;impression:view', + }); + }); + + test('works with entity', () => { + const result = createTagger()() + .entity('product') + .data('id', 123) + .actions('load', 'view') + .get(); + expect(result).toMatchObject({ + 'data-elb': 'product', + 'data-elbactions': 'load:view', + 'data-elb-product': 'id:123', + }); + }); + + test('can be used alongside action method', () => { + const result = createTagger()() + .action('click', 'select') + .actions('load', 'view') + .get(); + expect(result).toMatchObject({ + 'data-elbaction': 'click:select', + 'data-elbactions': 'load:view', + }); + }); + }); + describe('Context Method', () => { test('single key-value', () => { const result = createTagger()().context('test', 'engagement').get(); @@ -335,14 +395,14 @@ describe('Tagger', () => { test('full chain without entity (generic)', () => { const result = createTagger()() .data({ category: 'electronics', brand: 'TechCorp' }) - .action({ load: 'view', visible: 'impression' }) + .action({ load: 'view', impression: 'view' }) .context({ test: 'a/b', position: 'header' }) .globals({ lang: 'en', plan: 'paid' }) .get(); expect(result).toMatchObject({ 'data-elb-': 'category:electronics;brand:TechCorp', - 'data-elbaction': 'load:view;visible:impression', + 'data-elbaction': 'load:view;impression:view', 'data-elbcontext': 'test:a/b;position:header', 'data-elbglobals': 'lang:en;plan:paid', }); diff --git a/packages/web/sources/browser/src/__tests__/translation.test.ts b/packages/web/sources/browser/src/__tests__/translation.test.ts index d91f8cf32..7e427910a 100644 --- a/packages/web/sources/browser/src/__tests__/translation.test.ts +++ b/packages/web/sources/browser/src/__tests__/translation.test.ts @@ -66,7 +66,7 @@ describe('Translation Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'test event', + name: 'test event', data: { id: 123 }, context: { page: ['test', 0] }, source: { @@ -90,7 +90,7 @@ describe('Translation Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: '123', + name: '123', data: { value: 'test' }, context: { context: ['info', 0] }, source: { @@ -114,7 +114,7 @@ describe('Translation Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'test event', + name: 'test event', data: {}, // Should be empty object, not { value: 'primitive string data' } context: { page: ['test', 0] }, source: { @@ -143,7 +143,7 @@ describe('Translation Layer', () => { test('does not add source information to object events', async () => { // Test object event - should pass through as-is const eventObject = { - event: 'custom event', + name: 'custom event', data: { test: true }, source: { type: 'custom', id: 'custom-id', previous_id: '' }, }; @@ -224,7 +224,7 @@ describe('Translation Layer', () => { // Should have processed both events with source info expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'product', + name: 'product', data: { id: '123' }, context: { position: 1 }, trigger: 'click', @@ -238,7 +238,7 @@ describe('Translation Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'page', + name: 'page', data: { title: 'Test' }, trigger: 'load', source: { @@ -265,7 +265,7 @@ describe('Translation Layer', () => { expect(mockPush).toHaveBeenCalledTimes(1); expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: 'product view', + name: 'product view', data: { id: 123 }, trigger: 'load', source: { @@ -389,7 +389,7 @@ describe('Translation Layer', () => { expect(mockPush).toHaveBeenCalledWith( expect.objectContaining({ - event: '', + name: '', data: { test: true }, source: { type: 'browser', diff --git a/packages/web/sources/browser/src/__tests__/trigger.test.ts b/packages/web/sources/browser/src/__tests__/trigger.test.ts index d3e392efa..4a0f791c8 100644 --- a/packages/web/sources/browser/src/__tests__/trigger.test.ts +++ b/packages/web/sources/browser/src/__tests__/trigger.test.ts @@ -86,6 +86,7 @@ describe('Trigger System', () => { expect(Triggers.Load).toBe('load'); expect(Triggers.Hover).toBe('hover'); expect(Triggers.Submit).toBe('submit'); + expect(Triggers.Impression).toBe('impression'); expect(Triggers.Visible).toBe('visible'); expect(Triggers.Scroll).toBe('scroll'); expect(Triggers.Pulse).toBe('pulse'); @@ -131,7 +132,7 @@ describe('Trigger System', () => { // Should NOT trigger page view (pageview now only fires on walker run) expect(mockCollector.push).not.toHaveBeenCalledWith( expect.objectContaining({ - event: 'page view', + name: 'page view', }), ); @@ -215,7 +216,7 @@ describe('Trigger System', () => { expect(mockCollector.push).toHaveBeenCalledWith( expect.objectContaining({ - event: 'entity action', + name: 'entity action', entity: 'entity', action: 'action', trigger: Triggers.Click, @@ -448,7 +449,7 @@ describe('Trigger System', () => { // Should have called push with entity load event expect(mockCollector.push).toHaveBeenCalledWith( expect.objectContaining({ - event: 'entity load', + name: 'entity load', trigger: 'load', }), ); diff --git a/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts b/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts index df21a4345..d980e71ad 100644 --- a/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts +++ b/packages/web/sources/browser/src/__tests__/triggerVisible.test.ts @@ -19,6 +19,7 @@ interface CollectorWithVisibility extends Collector.Instance { multiple: boolean; blocked: boolean; context: Context; + trigger: string; } >; }; @@ -47,7 +48,7 @@ jest.mock('@walkeros/web-core', () => ({ jest.mock('../trigger', () => ({ ...jest.requireActual('../trigger'), handleTrigger: jest.fn(), - Triggers: { Visible: 'visible' }, + Triggers: { Impression: 'impression', Visible: 'visible' }, })); // Get references to mocked functions @@ -146,6 +147,7 @@ describe('triggerVisible', () => { multiple: true, blocked: false, context: expect.any(Object), + trigger: 'visible', }); }); @@ -202,7 +204,7 @@ describe('triggerVisible', () => { }), }), element, - 'visible', + 'impression', ); }); @@ -243,7 +245,7 @@ describe('triggerVisible', () => { }), }), element, - 'visible', + 'impression', ); }); diff --git a/packages/web/sources/browser/src/__tests__/walker.test.ts b/packages/web/sources/browser/src/__tests__/walker.test.ts index 14c913088..952a2699d 100644 --- a/packages/web/sources/browser/src/__tests__/walker.test.ts +++ b/packages/web/sources/browser/src/__tests__/walker.test.ts @@ -39,13 +39,13 @@ describe('Walker', () => { data: { label: 'grandmother' }, trigger: Triggers.Load, nested: [ - { type: 'son', data: { interested_in: 'pizza' } }, + { entity: 'son', data: { interested_in: 'pizza' } }, { - type: 'daughter', + entity: 'daughter', data: { status: 'hungry' }, - nested: [{ type: 'baby', data: { status: 'infant' } }], + nested: [{ entity: 'baby', data: { status: 'infant' } }], }, - { type: 'baby', data: { status: 'infant' } }, + { entity: 'baby', data: { status: 'infant' } }, ], }, ]); @@ -291,7 +291,7 @@ describe('Walker', () => { parent: ['link', 1], entity: ['link', 2], }, - nested: [{ type: 'n', data: { k: 'v' } }], + nested: [{ entity: 'n', data: { k: 'v' } }], }, ]); }); @@ -343,6 +343,38 @@ describe('Walker', () => { }, ]); }); + + test('data-elbaction applies to nearest entity only', () => { + expect( + getEvents(getElem('test-nearest-only'), Triggers.Click), + ).toMatchObject([ + { + entity: 'child', + action: 'test', + data: { scope: 'inner' }, + trigger: 'click', + }, + ]); + }); + + test('data-elbactions applies to all entities', () => { + expect( + getEvents(getElem('test-all-entities'), Triggers.Click), + ).toMatchObject([ + { + entity: 'child', + action: 'test', + data: { scope: 'inner' }, + trigger: 'click', + }, + { + entity: 'parent', + action: 'test', + data: { scope: 'outer' }, + trigger: 'click', + }, + ]); + }); }); function getElem(selector: string) { diff --git a/packages/web/sources/browser/src/tagger.ts b/packages/web/sources/browser/src/tagger.ts index cf11bcf32..f0b870ac8 100644 --- a/packages/web/sources/browser/src/tagger.ts +++ b/packages/web/sources/browser/src/tagger.ts @@ -11,6 +11,8 @@ export interface TaggerInstance { ((data: WalkerOS.Properties) => TaggerInstance); action: ((trigger: string, action?: string) => TaggerInstance) & ((actions: Record) => TaggerInstance); + actions: ((trigger: string, action?: string) => TaggerInstance) & + ((actions: Record) => TaggerInstance); context: ((key: string, value: WalkerOS.Property) => TaggerInstance) & ((context: WalkerOS.Properties) => TaggerInstance); globals: ((key: string, value: WalkerOS.Property) => TaggerInstance) & @@ -37,6 +39,7 @@ export function createTagger( let namingEntity: string | undefined = entity; // Used for data attribute naming const dataProperties: Record = {}; const actionProperties: Record = {}; + const actionsProperties: Record = {}; const contextProperties: WalkerOS.Properties = {}; const globalProperties: WalkerOS.Properties = {}; const linkProperties: Record = {}; @@ -113,6 +116,30 @@ export function createTagger( return instance; }, + actions( + triggerOrActions: string | Record, + actionValue?: string, + ): TaggerInstance { + if (isString(triggerOrActions)) { + if (isDefined(actionValue)) { + // Two parameters: trigger and action + actionsProperties[triggerOrActions] = actionValue; + } else { + // Single parameter: could be "trigger:action" or just "trigger" + if (triggerOrActions.includes(':')) { + const [trigger, action] = triggerOrActions.split(':', 2); + actionsProperties[trigger] = action; + } else { + actionsProperties[triggerOrActions] = triggerOrActions; + } + } + } else { + Object.assign(actionsProperties, triggerOrActions); + } + + return instance; + }, + context( keyOrContext: string | WalkerOS.Properties, value?: WalkerOS.Property, @@ -175,6 +202,11 @@ export function createTagger( attributes[`${prefix}action`] = serializeKeyValue(actionProperties); } + // Add actions attributes (for all entities) + if (Object.keys(actionsProperties).length > 0) { + attributes[`${prefix}actions`] = serializeKeyValue(actionsProperties); + } + // Add context attributes if (Object.keys(contextProperties).length > 0) { attributes[`${prefix}context`] = serializeKeyValue(contextProperties); diff --git a/packages/web/sources/browser/src/translation.ts b/packages/web/sources/browser/src/translation.ts index e5aa2b84d..cd65ef760 100644 --- a/packages/web/sources/browser/src/translation.ts +++ b/packages/web/sources/browser/src/translation.ts @@ -40,7 +40,7 @@ export function translateToCoreCollector( // Extract entity name from event string const [entity] = String( - isObject(eventOrCommand) ? eventOrCommand.event : eventOrCommand, + isObject(eventOrCommand) ? eventOrCommand.name : eventOrCommand, ).split(' '); // Get data and context either from elements or parameters @@ -68,7 +68,7 @@ export function translateToCoreCollector( const entityObj = getEntities( settings.prefix || 'data-elb', elemParameter, - ).find((obj) => obj.type === entity); + ).find((obj) => obj.entity === entity); if (entityObj) { if (dataIsElem) eventData = entityObj.data; eventContext = entityObj.context; @@ -82,7 +82,7 @@ export function translateToCoreCollector( // Build unified event from various elb usage patterns const event: WalkerOS.DeepPartialEvent = { - event: String(eventOrCommand || ''), + name: String(eventOrCommand || ''), data: eventData, context: eventContext, nested, diff --git a/packages/web/sources/browser/src/trigger.ts b/packages/web/sources/browser/src/trigger.ts index 4bda64d17..16bb7e069 100644 --- a/packages/web/sources/browser/src/trigger.ts +++ b/packages/web/sources/browser/src/trigger.ts @@ -44,8 +44,8 @@ export const Triggers: { [key: string]: Walker.Trigger } = { Pulse: 'pulse', Scroll: 'scroll', Submit: 'submit', + Impression: 'impression', Visible: 'visible', - Visibles: 'visibles', Wait: 'wait', } as const; @@ -157,7 +157,7 @@ export async function handleTrigger( return Promise.all( events.map((event: Walker.Event) => translateToCoreCollector(context, { - event: `${event.entity} ${event.action}`, + name: `${event.entity} ${event.action}`, ...event, trigger, }), @@ -195,10 +195,10 @@ function handleActionElem( case Triggers.Scroll: triggerScroll(elem, triggerAction.triggerParams); break; - case Triggers.Visible: + case Triggers.Impression: triggerVisible(context, elem); break; - case Triggers.Visibles: + case Triggers.Visible: triggerVisible(context, elem, { multiple: true }); break; case Triggers.Wait: diff --git a/packages/web/sources/browser/src/triggerVisible.ts b/packages/web/sources/browser/src/triggerVisible.ts index 031376fbf..b065e94c8 100644 --- a/packages/web/sources/browser/src/triggerVisible.ts +++ b/packages/web/sources/browser/src/triggerVisible.ts @@ -23,7 +23,7 @@ interface VisibilityState { duration: number; elementConfigs?: WeakMap< HTMLElement, - { multiple: boolean; blocked: boolean; context: Context } + { multiple: boolean; blocked: boolean; context: Context; trigger: string } >; } @@ -156,7 +156,7 @@ function handleIntersection( await handleTrigger( elementConfig.context, target as Element, - Triggers.Visible, + elementConfig.trigger, ); } @@ -226,6 +226,7 @@ export function triggerVisible( multiple: config.multiple ?? false, blocked: false, context, + trigger: config.multiple ? 'visible' : 'impression', }); state.observer.observe(element); } diff --git a/packages/web/sources/browser/src/walker.ts b/packages/web/sources/browser/src/walker.ts index be15d01cd..9a0b0484d 100644 --- a/packages/web/sources/browser/src/walker.ts +++ b/packages/web/sources/browser/src/walker.ts @@ -103,11 +103,11 @@ export function getEvents( ): Walker.Events { const events: Walker.Events = []; - // Check for an action (data-elbaction) attribute and resolve it - const actions = resolveAttributes(prefix, target, trigger); + // Check for actions and get entity collection strategy + const { actions, nearestOnly } = resolveAttributes(prefix, target, trigger); // Stop if there's no valid action combo - if (!actions) return events; + if (!actions.length) return events; actions.forEach((triggerAction) => { const filter = splitAttribute(triggerAction.actionParams || '', ',').reduce( @@ -118,35 +118,35 @@ export function getEvents( {} as Walker.Filter, ); - // Get the entities with their properties - const entities = getEntities(prefix, target, filter); + // Get entities - using nearestOnly flag to determine collection strategy + const entities = getEntities(prefix, target, filter, nearestOnly); // Use page as default entity if no one was set if (!entities.length) { - const type = 'page'; + const entity = 'page'; // Only use explicit page properties and ignore generic properties - const entitySelector = `[${getElbAttributeName(prefix, type)}]`; + const entitySelector = `[${getElbAttributeName(prefix, entity)}]`; // Get matching properties from the element and its parents const [data, context] = getThisAndParentProperties( target, entitySelector, prefix, - type, + entity, ); entities.push({ - type, // page + entity, // page data, // Consider only upper data nested: [], // Skip nested in this faked page case context, }); } - // Return a list of all full events + // Return a list of full events entities.forEach((entity) => { events.push({ - entity: entity.type, + entity: entity.entity, action: triggerAction.action, data: entity.data, trigger, @@ -234,6 +234,7 @@ export function getEntities( prefix: string, target: Element, filter?: Walker.Filter, + nearestOnly = false, ): WalkerOS.Entities { const entities: WalkerOS.Entities = []; let element = target as Node['parentElement']; @@ -243,7 +244,10 @@ export function getEntities( while (element) { const entity = getEntity(prefix, element, target, filter); - if (entity) entities.push(entity); + if (entity) { + entities.push(entity); + if (nearestOnly) break; // Stop after first entity for data-elbaction + } element = getParent(prefix, element); } @@ -257,15 +261,15 @@ function getEntity( origin?: Element, filter?: Walker.Filter, ): WalkerOS.Entity | null { - const type = getAttribute(element, getElbAttributeName(prefix)); + const entity = getAttribute(element, getElbAttributeName(prefix)); // It's not a (valid) entity element or should be filtered - if (!type || (filter && !filter[type])) return null; + if (!entity || (filter && !filter[entity])) return null; const scopeElems = [element]; // All related elements const dataSelector = `[${getElbAttributeName( prefix, - type, + entity, )}],[${getElbAttributeName(prefix, '')}]`; // [data-elb-entity,data-elb-] const linkName = getElbAttributeName(prefix, Const.Commands.Link, false); // data-elblink @@ -275,7 +279,7 @@ function getEntity( origin || element, dataSelector, prefix, - type, + entity, ); // Add linked elements (data-elblink) @@ -311,7 +315,7 @@ function getEntity( propertyElems.forEach((child) => { // Eventually override closer properties genericData = assign(genericData, getElbValues(prefix, child, '')); - data = assign(data, getElbValues(prefix, child, type)); + data = assign(data, getElbValues(prefix, child, entity)); }); // Merge properties with the hierarchy generic > data > parent @@ -329,7 +333,7 @@ function getEntity( ); }); - return { type, data, context, nested }; + return { entity, data, context, nested }; } function getParent(prefix: string, elem: HTMLElement): HTMLElement | null { @@ -417,27 +421,41 @@ function resolveAttributes( prefix: string, target: Element, trigger: string, -): Walker.TriggerActions { +): { actions: Walker.TriggerActions; nearestOnly: boolean } { let element = target as Node['parentElement']; while (element) { - const attribute = getAttribute( + // Check for data-elbactions first (takes precedence) + const multiAttribute = getAttribute( element, - getElbAttributeName(prefix, Const.Commands.Action, false), + getElbAttributeName(prefix, Const.Commands.Actions, false), ); - // Get action string related to trigger - const triggerActions = getTriggerActions(attribute); + if (multiAttribute) { + const triggerActions = getTriggerActions(multiAttribute); + if (triggerActions[trigger]) { + return { actions: triggerActions[trigger], nearestOnly: false }; + } + } + + // Check for data-elbaction (nearest entity only) + const singleAttribute = getAttribute( + element, + getElbAttributeName(prefix, Const.Commands.Action, false), + ); - // Action found on element or is not a click trigger - // @TODO aggregate all click triggers, too - if (triggerActions[trigger] || trigger !== 'click') - return triggerActions[trigger]; + if (singleAttribute) { + const triggerActions = getTriggerActions(singleAttribute); + // Action found on element or is not a click trigger + if (triggerActions[trigger] || trigger !== 'click') { + return { actions: triggerActions[trigger] || [], nearestOnly: true }; + } + } element = getParent(prefix, element); } - return []; + return { actions: [], nearestOnly: false }; } function splitAttribute(str: string, separator = ';'): Walker.Attributes { diff --git a/packages/web/sources/dataLayer/CHANGELOG.md b/packages/web/sources/dataLayer/CHANGELOG.md index 434d7ccec..df4153983 100644 --- a/packages/web/sources/dataLayer/CHANGELOG.md +++ b/packages/web/sources/dataLayer/CHANGELOG.md @@ -1,5 +1,17 @@ # @walkeros/web-source-datalayer +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/collector@0.1.0 + - @walkeros/core@0.1.0 + ## 0.0.8 ### Patch Changes diff --git a/packages/web/sources/dataLayer/README.md b/packages/web/sources/dataLayer/README.md index 4e6a64761..321283d93 100644 --- a/packages/web/sources/dataLayer/README.md +++ b/packages/web/sources/dataLayer/README.md @@ -1,30 +1,23 @@

- +

# DataLayer Source for walkerOS +[Source Code](https://github.com/elbwalker/walkerOS/tree/main/packages/web/sources/dataLayer) +• +[NPM Package](https://www.npmjs.com/package/@walkeros/web-source-datalayer) + This package provides a dataLayer source for walkerOS. It allows you to process events from a dataLayer and send them to the walkerOS collector. -[View documentation](https://www.elbwalker.com/docs/sources/datalayer/) - -## Role in walkerOS Ecosystem - -walkerOS follows a **source โ†’ collector โ†’ destination** architecture: - -- **Sources**: Capture events from various environments (browser DOM, dataLayer, - server requests) -- **Collector**: Processes, validates, and routes events with consent awareness -- **Destinations**: Send processed events to analytics platforms (GA4, Meta, - custom APIs) - -This dataLayer source monitors the browser's dataLayer (commonly used with -Google Tag Manager) and transforms existing gtag() calls and dataLayer.push() -events into standardized walkerOS events, enabling seamless migration from -traditional dataLayer implementations. +walkerOS follows a **source โ†’ collector โ†’ destination** architecture. This +dataLayer source monitors the browser's dataLayer (commonly used with Google Tag +Manager) and transforms existing gtag() calls and dataLayer.push() events into +standardized walkerOS events, enabling seamless migration from traditional +dataLayer implementations. ## Installation @@ -43,6 +36,14 @@ import { sourceDataLayer } from '@walkeros/web-source-datalayer'; sourceDataLayer({ elb }); ``` +## Configuration + +| Name | Type | Description | Required | Example | +| -------- | ------------------------------------------------------ | ------------------------------------------------------------- | -------- | ----------------------------------------------- | +| `name` | `string` | DataLayer variable name (default: "dataLayer") | No | `'dataLayer'` | +| `prefix` | `string` | Event prefix for filtering dataLayer events (default: "gtag") | No | `'gtag'` | +| `filter` | `(event: unknown) => WalkerOS.PromiseOrValue` | Function to filter which dataLayer events to process | No | `(event) => event && typeof event === "object"` | + ## Contribute Feel free to contribute by submitting an diff --git a/packages/web/sources/dataLayer/package.json b/packages/web/sources/dataLayer/package.json index 3816cd5e2..5ada0cb5a 100644 --- a/packages/web/sources/dataLayer/package.json +++ b/packages/web/sources/dataLayer/package.json @@ -1,7 +1,7 @@ { "name": "@walkeros/web-source-datalayer", "description": "DataLayer source for walkerOS", - "version": "0.0.8", + "version": "0.1.0", "license": "MIT", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -30,8 +30,8 @@ "update": "npx npm-check-updates -u && npm update" }, "dependencies": { - "@walkeros/core": "0.0.8", - "@walkeros/collector": "0.0.8" + "@walkeros/core": "0.1.0", + "@walkeros/collector": "0.1.0" }, "devDependencies": { "@types/gtag.js": "^0.0.20" diff --git a/packages/web/sources/dataLayer/src/__tests__/consent-simple.test.ts b/packages/web/sources/dataLayer/src/__tests__/consent-simple.test.ts index 360b611c8..b71191fa9 100644 --- a/packages/web/sources/dataLayer/src/__tests__/consent-simple.test.ts +++ b/packages/web/sources/dataLayer/src/__tests__/consent-simple.test.ts @@ -38,9 +38,8 @@ describe('DataLayer Source - Consent Mode (Simple)', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer consent update', + name: 'dataLayer consent update', data: { - event: 'consent update', ad_storage: 'denied', analytics_storage: 'granted', }, @@ -65,9 +64,8 @@ describe('DataLayer Source - Consent Mode (Simple)', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer consent default', + name: 'dataLayer consent default', data: { - event: 'consent default', ad_storage: 'denied', analytics_storage: 'denied', }, @@ -93,9 +91,8 @@ describe('DataLayer Source - Consent Mode (Simple)', () => { // Should have processed the existing event expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer consent update', + name: 'dataLayer consent update', data: { - event: 'consent update', ad_storage: 'granted', analytics_storage: 'denied', }, @@ -133,15 +130,12 @@ describe('DataLayer Source - Consent Mode (Simple)', () => { expect(collectedEvents).toHaveLength(3); expect(collectedEvents[0].data).toMatchObject({ - event: 'consent update', ad_storage: 'granted', }); expect(collectedEvents[1].data).toMatchObject({ - event: 'consent update', analytics_storage: 'granted', }); expect(collectedEvents[2].data).toMatchObject({ - event: 'consent update', ad_storage: 'denied', }); }); @@ -155,6 +149,6 @@ describe('DataLayer Source - Consent Mode (Simple)', () => { getDataLayer().push(['consent', 'update', { ad_storage: 'granted' }]); expect(collectedEvents).toHaveLength(1); - expect(collectedEvents[0].event).toBe('gtag consent update'); + expect(collectedEvents[0].name).toBe('gtag consent update'); }); }); diff --git a/packages/web/sources/dataLayer/src/__tests__/enhanced.test.ts b/packages/web/sources/dataLayer/src/__tests__/enhanced.test.ts index d5134c217..296017c0b 100644 --- a/packages/web/sources/dataLayer/src/__tests__/enhanced.test.ts +++ b/packages/web/sources/dataLayer/src/__tests__/enhanced.test.ts @@ -42,9 +42,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer consent update', + name: 'dataLayer consent update', data: { - event: 'consent update', ad_storage: 'granted', analytics_storage: 'denied', }, @@ -69,9 +68,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer purchase', + name: 'dataLayer purchase', data: { - event: 'purchase', transaction_id: '123', value: 25.99, }, @@ -95,9 +93,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer config GA_MEASUREMENT_ID', + name: 'dataLayer config GA_MEASUREMENT_ID', data: { - event: 'config GA_MEASUREMENT_ID', send_page_view: false, }, }); @@ -114,9 +111,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer set currency', + name: 'dataLayer set currency', data: { - event: 'set currency', value: 'EUR', }, }); @@ -133,9 +129,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer set custom', + name: 'dataLayer set custom', data: { - event: 'set custom', currency: 'EUR', country: 'DE', }, @@ -153,9 +148,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer custom_event', + name: 'dataLayer custom_event', data: { - event: 'custom_event', user_id: 'user123', }, }); @@ -205,9 +199,8 @@ describe('DataLayer Source - Enhanced with gtag support', () => { expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer consent update', + name: 'dataLayer consent update', data: { - event: 'consent update', ad_storage: 'granted', }, }); diff --git a/packages/web/sources/dataLayer/src/__tests__/filter.test.ts b/packages/web/sources/dataLayer/src/__tests__/filter.test.ts index d1786f12b..2a51763b6 100644 --- a/packages/web/sources/dataLayer/src/__tests__/filter.test.ts +++ b/packages/web/sources/dataLayer/src/__tests__/filter.test.ts @@ -44,8 +44,8 @@ describe('DataLayer Source - Filtering', () => { expect(mockFilter).toHaveBeenCalledTimes(3); expect(collectedEvents).toHaveLength(2); - expect(collectedEvents[0].event).toBe('dataLayer allowed_event'); - expect(collectedEvents[1].event).toBe('dataLayer consent update'); + expect(collectedEvents[0].name).toBe('dataLayer allowed_event'); + expect(collectedEvents[1].name).toBe('dataLayer consent update'); }); test('filter with consent events only', () => { @@ -72,8 +72,8 @@ describe('DataLayer Source - Filtering', () => { ]); expect(collectedEvents).toHaveLength(2); - expect(collectedEvents[0].event).toBe('dataLayer consent update'); - expect(collectedEvents[1].event).toBe('dataLayer consent default'); + expect(collectedEvents[0].name).toBe('dataLayer consent update'); + expect(collectedEvents[1].name).toBe('dataLayer consent default'); }); test('filter processes existing events', () => { @@ -102,8 +102,8 @@ describe('DataLayer Source - Filtering', () => { // Should have processed 2 events (filtered out 'existing_bad') expect(collectedEvents).toHaveLength(2); - expect(collectedEvents[0].event).toBe('dataLayer existing_good'); - expect(collectedEvents[1].event).toBe('dataLayer consent update'); + expect(collectedEvents[0].name).toBe('dataLayer existing_good'); + expect(collectedEvents[1].name).toBe('dataLayer consent update'); }); test('handles filter errors gracefully', () => { @@ -121,7 +121,7 @@ describe('DataLayer Source - Filtering', () => { getDataLayer().push({ event: 'test_event', data: 'test' }); expect(collectedEvents).toHaveLength(1); - expect(collectedEvents[0].event).toBe('dataLayer test_event'); + expect(collectedEvents[0].name).toBe('dataLayer test_event'); }); test('filter return value determines processing', () => { @@ -145,6 +145,6 @@ describe('DataLayer Source - Filtering', () => { getDataLayer().push({ event: 'event2', data: 'test' }); expect(collectedEvents).toHaveLength(1); - expect(collectedEvents[0].event).toBe('dataLayer event1'); + expect(collectedEvents[0].name).toBe('dataLayer event1'); }); }); diff --git a/packages/web/sources/dataLayer/src/__tests__/integration.test.ts b/packages/web/sources/dataLayer/src/__tests__/integration.test.ts index bc5a74051..0ea0e3d44 100644 --- a/packages/web/sources/dataLayer/src/__tests__/integration.test.ts +++ b/packages/web/sources/dataLayer/src/__tests__/integration.test.ts @@ -71,9 +71,8 @@ describe('DataLayer Source - Integration', () => { // Check all consent events were processed expect(collectedEvents[0]).toMatchObject({ - event: 'gtag consent default', + name: 'gtag consent default', data: { - event: 'consent default', ad_storage: 'denied', analytics_storage: 'denied', ad_user_data: 'denied', @@ -82,17 +81,14 @@ describe('DataLayer Source - Integration', () => { }); expect(collectedEvents[1]).toMatchObject({ - event: 'gtag consent update', + name: 'gtag consent update', data: { - event: 'consent update', analytics_storage: 'granted', }, }); expect(collectedEvents[2]).toMatchObject({ - event: 'gtag consent update', data: { - event: 'consent update', ad_storage: 'granted', ad_user_data: 'granted', }, @@ -123,7 +119,7 @@ describe('DataLayer Source - Integration', () => { expect(collectedEvents).toHaveLength(5); - const eventNames = collectedEvents.map((e) => e.event); + const eventNames = collectedEvents.map((e) => e.name); expect(eventNames).toEqual([ 'dataLayer consent update', 'dataLayer purchase', @@ -150,10 +146,10 @@ describe('DataLayer Source - Integration', () => { expect(collectedEvents).toHaveLength(4); // Check order: existing events first, then new events - expect(collectedEvents[0].event).toBe('dataLayer consent default'); - expect(collectedEvents[1].event).toBe('dataLayer existing_event'); - expect(collectedEvents[2].event).toBe('dataLayer consent update'); - expect(collectedEvents[3].event).toBe('dataLayer new_event'); + expect(collectedEvents[0].name).toBe('dataLayer consent default'); + expect(collectedEvents[1].name).toBe('dataLayer existing_event'); + expect(collectedEvents[2].name).toBe('dataLayer consent update'); + expect(collectedEvents[3].name).toBe('dataLayer new_event'); }); test('error handling and robustness', () => { @@ -191,7 +187,7 @@ describe('DataLayer Source - Integration', () => { // Should have processed 3 good events (bad_filter is invalid gtag format) expect(collectedEvents).toHaveLength(3); - const eventNames = collectedEvents.map((e) => e.event); + const eventNames = collectedEvents.map((e) => e.name); expect(eventNames).toEqual([ 'dataLayer consent update', 'dataLayer after_error', diff --git a/packages/web/sources/dataLayer/src/__tests__/minimal.test.ts b/packages/web/sources/dataLayer/src/__tests__/minimal.test.ts index 093bbb1f7..4574c10ff 100644 --- a/packages/web/sources/dataLayer/src/__tests__/minimal.test.ts +++ b/packages/web/sources/dataLayer/src/__tests__/minimal.test.ts @@ -61,8 +61,8 @@ describe('DataLayer Source - Minimal', () => { // Should have captured the event immediately (synchronous) expect(collectedEvents).toHaveLength(1); expect(collectedEvents[0]).toMatchObject({ - event: 'dataLayer test_event', - data: { event: 'test_event', test: 'data' }, + name: 'dataLayer test_event', + data: { test: 'data' }, source: { type: 'dataLayer' }, }); }); @@ -81,8 +81,8 @@ describe('DataLayer Source - Minimal', () => { // Should have processed both existing events expect(collectedEvents).toHaveLength(2); - expect(collectedEvents[0].event).toBe('dataLayer existing_event_1'); - expect(collectedEvents[1].event).toBe('dataLayer existing_event_2'); + expect(collectedEvents[0].name).toBe('dataLayer existing_event_1'); + expect(collectedEvents[1].name).toBe('dataLayer existing_event_2'); }); test('ignores non-object events', () => { @@ -124,7 +124,7 @@ describe('DataLayer Source - Minimal', () => { getDataLayer().push({ event: 'test_event', data: 'test' }); expect(collectedEvents).toHaveLength(1); - expect(collectedEvents[0].event).toBe('custom test_event'); + expect(collectedEvents[0].name).toBe('custom test_event'); }); test('uses custom dataLayer name', () => { @@ -144,6 +144,6 @@ describe('DataLayer Source - Minimal', () => { }); expect(collectedEvents).toHaveLength(1); - expect(collectedEvents[0].event).toBe('dataLayer test_event'); + expect(collectedEvents[0].name).toBe('dataLayer test_event'); }); }); diff --git a/packages/web/sources/dataLayer/src/interceptor.ts b/packages/web/sources/dataLayer/src/interceptor.ts index 2b27e0df8..e7afcd077 100644 --- a/packages/web/sources/dataLayer/src/interceptor.ts +++ b/packages/web/sources/dataLayer/src/interceptor.ts @@ -110,11 +110,11 @@ function processEvent( } const prefix = settings.prefix || 'dataLayer'; - const eventName = `${prefix} ${transformedEvent.event}`; + const eventName = `${prefix} ${transformedEvent.name}`; // Create WalkerOS event structure const walkerEvent: WalkerOS.Event = { - event: eventName, + name: eventName, data: transformedEvent as WalkerOS.Properties, context: {}, globals: {}, @@ -151,10 +151,11 @@ function processEvent( */ function transformDataLayerEvent( rawEvent: unknown, -): { event: string; [key: string]: unknown } | null { +): { name: string; [key: string]: unknown } | null { // Handle direct object format: { event: 'test', data: 'value' } if (isObject(rawEvent) && isString(rawEvent.event)) { - return rawEvent as { event: string; [key: string]: unknown }; + const { event, ...rest } = rawEvent; + return { name: event, ...rest }; } // Handle gtag argument format: ['consent', 'update', { ad_storage: 'granted' }] @@ -177,7 +178,7 @@ function transformDataLayerEvent( */ function transformGtagArgs( args: unknown[], -): { event: string; [key: string]: unknown } | null { +): { name: string; [key: string]: unknown } | null { const [command, action, params] = args; if (!isString(command)) return null; @@ -234,7 +235,7 @@ function transformGtagArgs( } return { - event: eventName, + name: eventName, ...eventData, }; } diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index a0c20e7d8..5fd001ba2 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,5 +1,16 @@ # @walkeros/website +## 0.1.0 + +### Minor Changes + +- fixes + +### Patch Changes + +- Updated dependencies + - @walkeros/core@0.1.0 + ## 0.0.1 ### Patch Changes diff --git a/website/docs/core/index.mdx b/website/docs/core/index.mdx index d13b5760a..9d38eb773 100644 --- a/website/docs/core/index.mdx +++ b/website/docs/core/index.mdx @@ -133,7 +133,7 @@ const mapping = { loop: [ 'nested', { - condition: (entity) => entity.type === 'product', + condition: (entity) => entity.entity === 'product', map: { id: 'data.id', name: 'data.name' }, }, ], diff --git a/website/docs/core/server.mdx b/website/docs/core/server.mdx index b73144bf1..3ac875514 100644 --- a/website/docs/core/server.mdx +++ b/website/docs/core/server.mdx @@ -32,7 +32,7 @@ sends HTTP requests using Node.js built-in modules (`http`/`https`). ```js // Simple POST request const response = await sendServer('https://api.example.com/events', { - event: 'page view', + name: 'page view', data: { url: '/home' }, }); diff --git a/website/docs/destinations/event-mapping.mdx b/website/docs/destinations/event-mapping.mdx index 234d886d2..94ed03861 100644 --- a/website/docs/destinations/event-mapping.mdx +++ b/website/docs/destinations/event-mapping.mdx @@ -54,7 +54,7 @@ Map specific entity-action combinations to custom event names: showMiddle={false} labelInput="Configuration" input={`await getMappingEvent( - { event: 'product view' }, + { name: 'product view' }, { product: { view: { name: 'product_viewed' }, @@ -78,7 +78,7 @@ Use wildcards (`*`) to match multiple entities or actions: showMiddle={false} labelInput="Configuration" input={`await getMappingEvent( - { event: 'product click' }, + { name: 'product click' }, { product: { '*': { name: 'product_interaction' }, @@ -106,7 +106,7 @@ Use conditions to apply different mappings based on event properties: labelInput="Configuration" input={`await getMappingEvent( { - event: 'order complete', + name: 'order complete', data: { value: 100 }, }, { @@ -138,7 +138,7 @@ Skip processing certain events by setting `ignore: true`: showMiddle={false} labelInput="Configuration" input={`await getMappingEvent( - { event: 'test event' }, + { name: 'test event' }, { test: { event: { ignore: true }, @@ -198,7 +198,7 @@ Return static values using the `value` property: showMiddle={false} labelInput="Configuration" input={`await getMappingValue( - { event: 'page view' }, + { name: 'page view' }, { value: 'pageview' } );`} output={`"pageview"`} @@ -333,7 +333,7 @@ const destination = { }), push: (event, { data }) => { // Use mapped data - gtag('event', event.event, data); + gtag('event', event.name, data); }, config: { mapping: { diff --git a/website/docs/getting-started/event-model.mdx b/website/docs/getting-started/event-model.mdx index 2e49620db..ea0115d84 100644 --- a/website/docs/getting-started/event-model.mdx +++ b/website/docs/getting-started/event-model.mdx @@ -46,7 +46,7 @@ static, their content can be defined dynamically with different value types. ```js { - event: 'promotion view', // Name as a combination of entity and action + name: 'promotion view', // Name as a combination of entity and action data: { // Arbitrary properties related to the entity name: 'Setting up tracking easily', diff --git a/website/docs/guides/migration.mdx b/website/docs/guides/migration.mdx index 8637c9c66..b9bf0b2fd 100644 --- a/website/docs/guides/migration.mdx +++ b/website/docs/guides/migration.mdx @@ -195,3 +195,127 @@ Solution: `elb` is returned by `createCollector()` function **TypeScript cannot find types** Solution: Import types from `@walkeros/core` + +## data-elbaction vs data-elbactions + +### Breaking Change in @walkeros packages + +The behavior of `data-elbaction` has changed to improve performance and provide more precise entity targeting: + +- **@elbwalker behavior**: `data-elbaction` applied to **all entities** in the DOM hierarchy +- **@walkeros behavior**: `data-elbaction` applies to **nearest entity only** + +### Migration Strategy + +#### Option 1: Keep Current Behavior (Recommended for migration) + +Replace `data-elbaction` with `data-elbactions` to maintain the same behavior: + +```html + +
+
+ +
+
+ + +
+
+ +
+
+``` + +#### Option 2: Use New Behavior (Recommended for new projects) + +Use `data-elbaction` for more precise, performance-optimized tracking: + +```html + +
+
+ +
+
+``` + +### When to Use Each + +**Use `data-elbaction` (nearest entity) when:** +- You want precise tracking of only the specific entity +- Performance is critical (fewer events) +- Implementing new features + +**Use `data-elbactions` (all entities) when:** +- Migrating from @elbwalker and need same behavior +- You need context from parent entities +- Analytics requires full DOM hierarchy + +### Tagger API + +The tagger also provides both methods: + +```typescript +// Nearest entity only (data-elbaction) +tagger().action('click', 'select').get() + +// All entities (data-elbactions) +tagger().actions('click', 'select').get() +``` + +## visible vs impression Triggers + +### Breaking Change in @walkeros packages + +The visibility trigger names have been updated to better reflect their behavior: + +- **`visible` trigger**: Now fires **multiple times** when element re-enters viewport (was `visibles`) +- **`impression` trigger**: Fires **once only** when element first becomes visible (was `visible`) + +### Migration Strategy + +#### Option 1: Update Trigger Names (Recommended) + +Update your HTML to use the new trigger names: + +```html + +
Single fire
+
Multiple fires
+ + +
Single fire
+
Multiple fires
+``` + +#### Option 2: Understand the Behavior Change + +If you keep using the old names, understand the behavior has changed: + +- Old `visible` behavior (single-fire) โ†’ Now use `impression` +- Old `visibles` behavior (multiple-fire) โ†’ Now use `visible` + +### When to Use Each + +**Use `impression` trigger when:** +- You want to track when content is first seen +- Measuring ad impressions or content views +- One-time engagement metrics + +**Use `visible` trigger when:** +- You want to track repeated interactions +- Measuring scroll behavior or re-engagement +- Analytics requires multiple visibility events + +### Tagger API + +The tagger supports both trigger types: + +```typescript +// Single impression (fires once) +tagger().action('impression', 'view').get() + +// Multiple visibility (fires each time visible) +tagger().action('visible', 'track').get() +``` diff --git a/website/docs/sources/web/browser/tagger.mdx b/website/docs/sources/web/browser/tagger.mdx index 2c87dbf3e..4cbd2133f 100644 --- a/website/docs/sources/web/browser/tagger.mdx +++ b/website/docs/sources/web/browser/tagger.mdx @@ -244,7 +244,7 @@ tagger('product').data({ id: 123, name: 'Widget', price: 99.99 }); ##### `action(trigger: string, action?: string)` | `action(object: Record)` -Adds action mappings for event triggers. +Adds action mappings for event triggers. Creates a `data-elbaction` attribute. ```typescript // Single action @@ -254,7 +254,25 @@ tagger().action('load', 'view'); tagger().action('load:view'); // Multiple actions -tagger().action({ load: 'view', click: 'select', visible: 'impression' }); +tagger().action({ load: 'view', click: 'select', impression: 'view' }); +``` + +##### `actions(trigger: string, action?: string)` | `actions(object: Record)` + +Adds action mappings for event triggers. Creates a `data-elbactions` attribute. + +```typescript +// Single action +tagger().actions('load', 'view'); + +// Combined trigger:action +tagger().actions('load:view'); + +// Multiple actions +tagger().actions({ load: 'view', click: 'select', visible: 'visible' }); + +// Can be combined with action() method +tagger().action('click', 'select').actions('load', 'view'); ``` ##### `context(key: string, value: Property)` | `context(object: Properties)` @@ -315,7 +333,7 @@ returns the final attributes object. ### Product Listing Page ```typescript -// For product cards that don't need entity tracking +// For product cards with nearest entity tracking only function ProductCard({ product }) { return (
{product.name}
); } + +// For product cards that also need page context tracking +function ProductCardWithContext({ product }) { + return ( +
+
+ {product.name} +
+
+ ); +} ``` ### Shopping Cart diff --git a/website/docs/sources/web/browser/tagging.mdx b/website/docs/sources/web/browser/tagging.mdx index 1e852bfc3..9edbccda2 100644 --- a/website/docs/sources/web/browser/tagging.mdx +++ b/website/docs/sources/web/browser/tagging.mdx @@ -40,7 +40,8 @@ Tag a page...
+ data-elbactions="TRIGGER:ACTION" data-elbcontext="KEY:VALUE" data-elbglobals="KEY:VALUE" /> @@ -62,7 +63,7 @@ Tag a page... ```js { - event: 'promotion view', // Name as a combination of entity and action + name: 'promotion view', // Name as a combination of entity and action data: { // Arbitrary properties related to the entity name: 'Setting up tracking easily', @@ -120,10 +121,25 @@ You define the entity **scope** by setting the `data-elb` attribute with the name of an entity to an element, e.g. `data-elb="promotion"`. The default entity is `page` when no `data-elb` is set. -An **action** can be added by setting the `data-elbaction` attribute on the -**same level** or **child elements** in combination with a **matching trigger**, -e.g., `data-elbaction="visible:view"` to fire a promotion view event when an -element has been in the viewport for at least 50% for one second. +An **action** can be added by setting one of the following attributes on the +**same level** or **child elements** in combination with a **matching trigger**: + +- **`data-elbaction`** - applies action to the **nearest entity only** +- **`data-elbactions`** - applies action to **all entities** in the DOM + hierarchy + +Both attributes use the same syntax, e.g., `data-elbaction="visible:view"` or +`data-elbactions="click:select"` to fire events when triggered. + +:::info Migration Note + +The behavior of `data-elbaction` changed in @walkeros to apply to nearest entity +only. For the previous @elbwalker "all entities" behavior, use +`data-elbactions`. See the +[Migration Guide](/docs/guides/migration#data-elbaction-vs-data-elbactions) for +details. + +::: To define the entities' **properties**, set the **composited attribute** `data-elb-ENTITY` with the key and value, e.g. @@ -138,8 +154,8 @@ listener or mutation observer initialization. | ----------- | ------------------------------------------------------------------------------------ | | load | after loading a page when DOM is ready | | click | when an element or a child is clicked | -| visible | after an element has been in the viewport for at least 50% for one second | -| visibles | each time an element re-enters the viewport after being out of view | +| impression | after an element has been in the viewport for at least 50% for one second | +| visible | each time an element re-enters the viewport after being out of view | | hover | each time the mouse enters the corresponding element | | submit | on valid form submission | | wait(ms) | waits ms seconds (15 seconds by default) until triggering | @@ -180,10 +196,10 @@ gets triggered. Use brackets behind the trigger to pass that information. ### Action filter At some point, you might want to nest one entity inside another. To prevent an -action to trigger both entities, you can restrict the action to a specific -entity by adding the name, e.g. `data-elbaction="load:view(product)`. If the -trigger event gets called, the result will only include the property values from -the specific entities. +action to trigger unwanted entities, you can restrict the action to a specific +entity by adding the name, e.g. `data-elbaction="load:view(product)` or +`data-elbactions="load:view(product)"`. If the trigger event gets called, the +result will only include the property values from the specific entities. ```html @@ -521,13 +537,13 @@ This example will lead to the following event on load: "event": "mother view", "data": { "label": "caring" }, "nested": [ - { "type": "son", "data": { "age": 23 } }, + { "entity": "son", "data": { "age": 23 } }, { - "type": "daughter", + "entity": "daughter", "data": { "age": 32 }, - "nested": [{ "type": "baby", "data": { "status": "infant" } }], + "nested": [{ "entity": "baby", "data": { "status": "infant" } }], }, - { "type": "baby", "data": { "status": "infant" } }, + { "entity": "baby", "data": { "status": "infant" } }, ], // other properties omitted } diff --git a/website/package.json b/website/package.json index 25ae14752..6a18167b6 100644 --- a/website/package.json +++ b/website/package.json @@ -1,6 +1,6 @@ { "name": "@walkeros/website", - "version": "0.0.1", + "version": "0.1.0", "private": true, "scripts": { "docusaurus": "docusaurus",