From 21723f571c6a141a54e6960bef23f79ea245552d Mon Sep 17 00:00:00 2001 From: David Vallejo Date: Sat, 29 Mar 2025 22:12:35 +0100 Subject: [PATCH 1/5] chore: release v0.0.1-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 289c60b..d1577d4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@analytics-debugger/ads-click-tracker", "type": "module", - "version": "0.0.1-beta.1", + "version": "0.0.1-beta.2", "packageManager": "pnpm@10.6.2", "description": "Library to keep track of clickIds from different advertising platforms", "author": "David Vallejo ", From 4a9806fe520075e91c30f0bfb999a6e918d35daf Mon Sep 17 00:00:00 2001 From: David Vallejo Date: Sat, 29 Mar 2025 23:31:49 +0100 Subject: [PATCH 2/5] CallBacks Added --- src/index.ts | 81 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4dbd06e..e3b0b35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,11 @@ interface TrackerOptions { debug?: boolean checkHash?: boolean decodeValues?: boolean + callbacks?: { + onLoaded?: (clicks: Record) => void + onNewClickId?: (click: ClickData) => void + onUpdatedClickId?: (click: ClickData) => void + } } interface ClickData { @@ -43,6 +48,11 @@ class AdsClickTracker { }) this.load() + if (typeof this.options.callbacks?.onLoaded === 'function') { + const latest = true + const clickData = this.get('fbclid', latest) + this.options.callbacks.onLoaded(clickData) + } this.checkUrl() } @@ -121,30 +131,31 @@ class AdsClickTracker { const now = Date.now() const landing = typeof window !== 'undefined' ? window.location.href : '' const referrer = typeof document !== 'undefined' ? document.referrer : '' - // Initialize array if not exists - if (!Array.isArray(this.clicks[source])) { - this.clicks[source] = [] + + // Create click data object once + const clickData = { + value, + timestamp: now, + expiresAt: now + expiresMs, + landing, + referrer, } + // Initialize array if not exists + this.clicks[source] = this.clicks[source] || [] + // Update existing or add new const existingIndex = this.clicks[source].findIndex(c => c.value === value) + if (existingIndex >= 0) { - this.clicks[source][existingIndex] = { - value, - timestamp: now, - expiresAt: now + expiresMs, - landing, - referrer, - } + this.clicks[source][existingIndex] = clickData + typeof this.options.callbacks?.onUpdatedClickId === 'function' + && this.options.callbacks.onUpdatedClickId(clickData) } else { - this.clicks[source].push({ - value, - timestamp: now, - expiresAt: now + expiresMs, - landing, - referrer, - }) + this.clicks[source].push(clickData) + typeof this.options.callbacks?.onNewClickId === 'function' + && this.options.callbacks.onNewClickId(clickData) } this.save() @@ -156,16 +167,36 @@ class AdsClickTracker { this.recordClick(source, value, config.expires) } - get(source?: string): ClickData[] { - const now = Date.now() + get(source?: string, latest?: boolean): Record { + // If latest is false, return all clicks + if (!latest) { + if (source && this.clicks[source]) { + return { [source]: this.clicks[source] } + } + return this.clicks + } + + // Helper function to get the latest click + const getLatestClick = (clicks: ClickData[]): ClickData | null => { + if (!clicks || !clicks.length) + return null + return clicks.reduce((newest, current) => + current.timestamp > newest.timestamp ? current : newest, clicks[0]) + } + + // If latest is true, return object with arrays containing single items if (source) { - return Array.isArray(this.clicks[source]) - ? this.clicks[source].filter(c => c && c.expiresAt > now) - : [] + // Return object with array containing the latest click for the specific source + const latestClick = getLatestClick(this.clicks[source] || []) + return { [source]: [latestClick] } + } + else { + // Return object with arrays containing the latest click for each source + return Object.entries(this.clicks).reduce((result, [sourceName, clicks]) => { + result[sourceName] = [getLatestClick(clicks)] + return result + }, {} as Record) } - return Object.values(this.clicks) - .flat() - .filter(c => c && c.expiresAt > now) } clear(source?: string): void { From ea0df332aa8457bc521dce6755de47e3c6eb4723 Mon Sep 17 00:00:00 2001 From: David Vallejo Date: Sat, 29 Mar 2025 23:49:50 +0100 Subject: [PATCH 3/5] refactor get method. --- src/index.ts | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index e3b0b35..aff8efe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,9 +49,7 @@ class AdsClickTracker { this.load() if (typeof this.options.callbacks?.onLoaded === 'function') { - const latest = true - const clickData = this.get('fbclid', latest) - this.options.callbacks.onLoaded(clickData) + this.options.callbacks.onLoaded(this.get(true)) } this.checkUrl() } @@ -167,36 +165,34 @@ class AdsClickTracker { this.recordClick(source, value, config.expires) } - get(source?: string, latest?: boolean): Record { - // If latest is false, return all clicks - if (!latest) { - if (source && this.clicks[source]) { - return { [source]: this.clicks[source] } - } + get(latest?: boolean): Record { + // If latest is false or not provided, return all clicks + if (!latest || latest !== true) { return this.clicks } // Helper function to get the latest click - const getLatestClick = (clicks: ClickData[]): ClickData | null => { - if (!clicks || !clicks.length) - return null - return clicks.reduce((newest, current) => + const getLatestClick = (clicks: ClickData[] = []): ClickData[] => { + if (!clicks.length) + return [] + + const latest = clicks.reduce((newest, current) => current.timestamp > newest.timestamp ? current : newest, clicks[0]) - } - // If latest is true, return object with arrays containing single items - if (source) { - // Return object with array containing the latest click for the specific source - const latestClick = getLatestClick(this.clicks[source] || []) - return { [source]: [latestClick] } - } - else { - // Return object with arrays containing the latest click for each source - return Object.entries(this.clicks).reduce((result, [sourceName, clicks]) => { - result[sourceName] = [getLatestClick(clicks)] - return result - }, {} as Record) + return [latest] } + + // If latest is true, return object with arrays containing single items or empty arrays + const result: Record = {} + + // Process all sources and ensure they all exist in the result + const allSources = Array.from(this.configs.keys()) + + allSources.forEach((sourceName) => { + result[sourceName] = getLatestClick(this.clicks[sourceName]) + }) + + return result } clear(source?: string): void { From fbb1e731c2f75413033fadd389877b6b82fbb717 Mon Sep 17 00:00:00 2001 From: David Vallejo Date: Sun, 30 Mar 2025 00:29:57 +0100 Subject: [PATCH 4/5] expirationMs renamed to ttl , readme update --- README.md | 240 +++++++++++++++++++++++++++------------------------ src/index.ts | 6 +- 2 files changed, 132 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index ce5a5d2..95a1622 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # 🎯 Ads Click Tracker -[![npm version](https://img.shields.io/npm/v/ads-click-tracker)](https://www.npmjs.com/package/ads-click-tracker) -[![bundle size](https://img.shields.io/bundlephobia/minzip/ads-click-tracker)](https://bundlephobia.com/package/ads-click-tracker) -[![license](https://img.shields.io/npm/l/ads-click-tracker)](LICENSE) -[![Downloads](https://img.shields.io/npm/dm/ads-click-tracker)](https://www.npmjs.com/package/ads-click-tracker) +[![npm version](https://img.shields.io/npm/v/@analytics-debugger/ads-click-tracker)](https://www.npmjs.com/package/@analytics-debugger/ads-click-tracker) +[![bundle size](https://bundlephobia.com/package/@analytics-debugger/ads-click-tracker)](https://bundlephobia.com/package/@analytics-debugger/ads-click-tracker@latest) +[![license](https://img.shields.io/npm/l/@analytics-debugger/ads-click-tracker)](LICENSE) +[![Downloads](https://img.shields.io/npm/dm/@analytics-debugger/ads-click-tracker)](https://www.npmjs.com/package/@analytics-debugger/ads-click-tracker) -## 📖 Overview +# @analytics-debugger/ads-click-tracker -Ads Click Tracker is a powerful, lightweight solution for marketing attribution tracking that simplifies the complexity of capturing and managing click data across multiple advertising platforms. +A lightweight, client-side library for tracking and managing ad click IDs across user sessions. -## 🌟 Key Features +## Features - **💡 Comprehensive Attribution Tracking** - Automatically capture click IDs from various sources @@ -30,166 +30,184 @@ Ads Click Tracker is a powerful, lightweight solution for marketing attribution - Automatic data management and cleanup - Minimal performance overhead +- **🪝 Event callbacks** + - For onLoad, New Click Id found, Updated ClickID + - **🚀 Developer-Friendly** - Zero dependencies - Full TypeScript support - < 912 bytes gzipped - Simple, intuitive API -## 🔧 Installation +## Installation ```bash -# npm -npm install ads-click-tracker - -# Yarn -yarn add ads-click-tracker - -# pnpm -pnpm add ads-click-tracker +npm install @analytics-debugger/ads-click-tracker +# or +yarn add @analytics-debugger/ads-click-tracker ``` -## 💻 Quick Start - -### Basic Usage +## Quick Start ```javascript -import { initTracker } from 'ads-click-tracker' +import AdsClickTracker from '@analytics-debugger/ads-click-tracker' -// Initialize with multiple click ID configurations -const tracker = initTracker({ +// Initialize the tracker +const tracker = AdsClickTracker({ + debug: true, clickIdConfigs: [ - { name: 'gclid', expirationMs: 30 * 24 * 60 * 60 * 1000 }, // 30 days for Google Ads - { name: 'fbclid', maxClicks: 50 } // 50 clicks limit for Facebook + { name: 'gclid', ttl: 30 * 24 * 60 * 60 * 1000, maxClicks: 100 }, // Google Ads (30 days) + { name: 'fbclid', ttl: 7 * 24 * 60 * 60 * 1000, maxClicks: 50 }, // Facebook (7 days) + { name: 'ttclid', ttl: 14 * 24 * 60 * 60 * 1000, maxClicks: 50 }, // TikTok (14 days) + { name: 'msclkid', ttl: 30 * 24 * 60 * 60 * 1000, maxClicks: 100 } // Microsoft (30 days) ], - debug: true // Enable console logging + callbacks: { + onLoaded: (clicks) => { + console.log('Tracker initialized with clicks:', clicks) + }, + onNewClickId: (click) => { + console.log('New click ID detected:', click) + }, + onUpdatedClickId: (click) => { + console.log('Click ID updated:', click) + } + } }) +// Get all tracked clicks +const allClicks = tracker.get() + +// Get only the latest click for each source +const latestClicks = tracker.get(true) + // Manually track a click -tracker.trackClick('affiliate', 'ref123', 24 * 60 * 60 * 1000) // 1 day expiration +tracker.track('gclid', 'EAIaIQobChMIh76p9Y3', 2592000000) // value, expiration in ms + +// Clear clicks for a specific source +tracker.clear('fbclid') -// Retrieve tracked clicks -const googleClicks = tracker.getClicks('gclid') +// Clear all clicks +tracker.clear() ``` -## 📊 Click Data Structure +## API Reference -Each tracked click includes: +### Initialization ```typescript -interface TrackedClick { - value: string // Click ID (e.g., "abc123") - timestamp: number // Recording timestamp (milliseconds) - expiresAt: number // Expiration timestamp (milliseconds) - landing: string // Landing page URL - referrer: string // Referring URL -} +AdsClickTracker(options: TrackerOptions): AdsClickTracker ``` -## ⚙️ Configuration Options +#### TrackerOptions -### `initTracker(options)` +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `storageKey` | string | `'_act_'` | Key used for localStorage | +| `clickIdConfigs` | ClickConfig[] | *required* | Array of click ID configurations | +| `debug` | boolean | `false` | Enable debug logging | +| `checkHash` | boolean | `true` | Check hash fragment for click IDs | +| `decodeValues` | boolean | `true` | URL decode click ID values | +| `callbacks` | object | `{}` | Callback functions | -| Option | Type | Description | Default | -|--------|------|-------------|---------| -| `storageKey` | `string` | Custom storage key in localStorage | `'_ads_clicks'` | -| `clickIdConfigs` | `ClickConfig[]` | Click ID tracking configurations | `[]` | -| `debug` | `boolean` | Enable debug logging | `false` | +#### ClickConfig -### `ClickConfig` +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `name` | string | *required* | Parameter name to track (e.g., 'gclid') | +| `ttl` | number | `2592000000` (30 days) | Time in milliseconds before click expires | +| `maxClicks` | number | `100` | Maximum number of clicks to store per source | -| Property | Type | Description | -|----------|------|-------------| -| `name` | `string` | Parameter name (e.g., 'gclid') | -| `expirationMs` | `number?` | Expiration time in milliseconds | -| `maxClicks` | `number?` | Maximum number of clicks to store | +### Methods -## 📚 API Reference +#### get(latest?: boolean): Record -### `trackClick(source, value, expirationMs?)` +Get all tracked clicks. If `latest` is `true`, returns only the most recent click for each source. -Manually track a click: +#### track(source: string, value: string, ttl?: number): void -```javascript -tracker.trackClick( - 'campaign_id', // Source name - 'summer_sale', // Click ID value - 7 * 24 * 60 * 60 * 1000 // Optional 7-day expiration -) -``` +Manually track a click ID. -### `getClicks(source?)` +#### clear(source?: string): void -Retrieve tracked clicks: +Clear clicks for a specific source or all sources if no source is provided. -```javascript -// Get all active clicks -const allClicks = tracker.getClicks() +### ClickData Structure -// Get clicks for a specific source -const fbClicks = tracker.getClicks('fbclid') +```typescript +interface ClickData { + value: string // Click ID value + timestamp: number // When the click was recorded + expiresAt: number // When the click data expires + landing: string // Landing page URL + referrer: string // Referrer URL +} ``` -### `clear(source?)` +## Use Cases -Clear stored clicks: +### Attribution Tracking -```javascript -// Clear Google clicks -tracker.clear('gclid') - -// Clear all clicks -tracker.clear() -``` - -## 🚀 Advanced Examples - -### Marketing Attribution Setup +Track which ad campaigns are driving conversions by capturing click IDs when users arrive on your site. ```javascript -const marketingTracker = initTracker({ - storageKey: 'marketing_clicks', +// On your conversion/thank you page +import AdsClickTracker from '@analytics-debugger/ads-click-tracker' + +const tracker = AdsClickTracker({ clickIdConfigs: [ - { name: 'gclid', expirationMs: 30 * 24 * 60 * 60 * 1000 }, // Google (30 days) - { name: 'fbclid', expirationMs: 7 * 24 * 60 * 60 * 1000 }, // Facebook (7 days) - { name: 'utm_campaign', maxClicks: 200 } // Custom campaign tracking + { name: 'gclid' }, + { name: 'fbclid' } ] }) -``` -### React Integration +// Get the latest clicks to include in your conversion tracking +const latestClicks = tracker.get(true) -```javascript -import { initTracker } from 'ads-click-tracker' -import { useEffect } from 'react' - -function useClickTracker() { - useEffect(() => { - const tracker = initTracker({ - clickIdConfigs: [{ name: 'campaign_id' }] - }) - - return () => tracker.clear() - }, []) -} +// Send this data to your analytics or CRM system +sendToAnalytics({ + conversion: true, + value: 100, + adClicks: latestClicks +}) ``` -## 🌐 Browser Support +### Integration with Forms -- Chrome -- Firefox -- Safari -- Edge (latest versions) +Automatically include ad click data in your form submissions for attribution. + +```javascript +document.querySelector('#leadForm').addEventListener('submit', (event) => { + const tracker = initTracker({ + clickIdConfigs: [ + { name: 'gclid' }, + { name: 'fbclid' }, + { name: 'ttclid' }, + { name: 'msclkid' } + ] + }) + + const latestClicks = tracker.get(true) + + // Add hidden fields to your form with click data + Object.entries(latestClicks).forEach(([source, clicks]) => { + if (clicks.length > 0) { + const hiddenField = document.createElement('input') + hiddenField.type = 'hidden' + hiddenField.name = source + hiddenField.value = clicks[0].value + event.target.appendChild(hiddenField) + } + }) +}) +``` -**Requirements:** -- localStorage support -- Modern browsers +## Browser Support -**Note:** Internet Explorer 11 is not supported +AdsClickTracker works in all modern browsers that support localStorage and ES6 features. For older browsers, consider using a transpiler like Babel. -## 📜 License +## License -MIT License +MIT ## 🤝 Contributing diff --git a/src/index.ts b/src/index.ts index aff8efe..4ffe2f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ interface ClickConfig { name: string - expirationMs?: number + ttl?: number maxClicks?: number } @@ -42,8 +42,8 @@ class AdsClickTracker { } // Initialize configurations - options.clickIdConfigs.forEach(({ name, expirationMs = 2592000000, maxClicks = 100 }) => { - this.configs.set(name, { expires: expirationMs, max: maxClicks }) + options.clickIdConfigs.forEach(({ name, ttl = 2592000000, maxClicks = 100 }) => { + this.configs.set(name, { expires: ttl, max: maxClicks }) this.clicks[name] = this.clicks[name] || [] }) From 7fd7abc7fabbe49a94e3f7ec899fcad4f9f319d4 Mon Sep 17 00:00:00 2001 From: David Vallejo Date: Sun, 30 Mar 2025 00:31:53 +0100 Subject: [PATCH 5/5] gitignore update --- README.md | 5 ++++- eslint.config.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95a1622..5c3c73a 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,10 @@ tracker.clear() ### Initialization ```typescript -AdsClickTracker(options: TrackerOptions): AdsClickTracker +class AdsClickTracker { + constructor(options: TrackerOptions) + // ... other methods +} ``` #### TrackerOptions diff --git a/eslint.config.js b/eslint.config.js index 902f043..7f732f6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ export default antfu( { ignores: [ 'pnpm-workspace.yaml', + 'README.md', ], type: 'lib', pnpm: true,