@callforge/tracking-client 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @callforge/tracking-client
2
2
 
3
- Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
4
-
5
- **v0.4.1** - Simplified GA4 capture: uses gtag callback when `ga4MeasurementId` configured, no polling fallback.
3
+ Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and optional preload optimization.
6
4
 
7
5
  ## Installation
8
6
 
@@ -26,12 +24,13 @@ const snippet = getPreloadSnippet({ categoryId: 'your-category-id' });
26
24
  ```
27
25
 
28
26
  Generated HTML:
27
+
29
28
  ```html
30
29
  <link rel="preconnect" href="https://tracking.callforge.io">
31
30
  <script>/* preload script */</script>
32
31
  ```
33
32
 
34
- ### 2. Initialize and use the client
33
+ ### 2. Initialize and fetch a tracking session
35
34
 
36
35
  ```typescript
37
36
  import { CallForge } from '@callforge/tracking-client';
@@ -41,40 +40,52 @@ const client = CallForge.init({
41
40
  // endpoint: 'https://tracking-dev.callforge.io', // Optional: override for dev
42
41
  });
43
42
 
44
- // Promise style
45
43
  const session = await client.getSession();
46
44
  console.log(session.phoneNumber); // "+17705550000" or null
47
45
  console.log(session.location); // { city: "Woodstock", state: "Georgia", stateCode: "GA" } or null
46
+ ```
48
47
 
49
- // Subscription style
50
- client.onReady((session) => {
51
- document.getElementById('phone').textContent = session.phoneNumber;
52
- document.getElementById('city').textContent = session.location?.city;
48
+ ### 3. Deterministic click/callback attribution (optional)
49
+
50
+ If you initiate calls programmatically (click-to-call / callback), you can request a short-lived `callIntentToken`. CallForge will consume this once to deterministically map the call back to the web session that requested it.
51
+
52
+ ```typescript
53
+ // In the browser:
54
+ const { callIntentToken } = await client.createCallIntent();
55
+
56
+ // Send to your backend and attach to the call you initiate
57
+ await fetch('/api/callback', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({ callIntentToken }),
53
61
  });
54
62
  ```
55
63
 
56
- ### 3. GA4 Integration
64
+ Notes:
65
+ - `callIntentToken` is short-lived and single-use.
66
+ - Treat it as an opaque secret (do not log it).
67
+
68
+ ### 4. GA4 Integration
57
69
 
58
70
  To enable GA4 call event tracking, provide your GA4 Measurement ID:
59
71
 
60
72
  ```typescript
61
73
  const client = CallForge.init({
62
74
  categoryId: 'your-category-id',
63
- ga4MeasurementId: 'G-XXXXXXXXXX', // Required for GA4 integration
75
+ ga4MeasurementId: 'G-XXXXXXXXXX',
64
76
  });
65
77
  ```
66
78
 
67
- **Requirements:**
68
- 1. Google Analytics 4 must be installed on your site (`gtag.js`)
69
- 2. Provide `ga4MeasurementId` in client config
70
- 3. Configure GA4 credentials in CallForge dashboard (Categories → Edit → GA4 tab)
79
+ Requirements:
80
+ 1. Google Analytics 4 must be installed on your site (`gtag.js`).
81
+ 2. Provide `ga4MeasurementId` in client config.
82
+ 3. Configure GA4 credentials in CallForge dashboard.
71
83
 
72
- **How it works:**
73
- - Uses `gtag('get', measurementId, 'client_id', callback)` to capture the GA4 client ID
74
- - Callback fires when GA4 is ready - no polling needed
75
- - Client ID is automatically sent to CallForge for call event attribution
84
+ How it works:
85
+ - Uses `gtag('get', measurementId, 'client_id', callback)` to capture the GA4 client ID.
86
+ - Client ID is sent to CallForge for call event attribution.
76
87
 
77
- **Manual override:** If you need to pass a custom GA4 client ID:
88
+ Manual override:
78
89
 
79
90
  ```typescript
80
91
  client.setParams({
@@ -82,7 +93,7 @@ client.setParams({
82
93
  });
83
94
  ```
84
95
 
85
- ### 4. Track conversion parameters (optional)
96
+ ### 5. Track conversion parameters (optional)
86
97
 
87
98
  The client automatically captures ad platform click IDs from the URL:
88
99
 
@@ -95,14 +106,13 @@ The client automatically captures ad platform click IDs from the URL:
95
106
  You can also add custom parameters:
96
107
 
97
108
  ```typescript
98
- // Add custom tracking parameters
99
109
  client.setParams({
100
110
  customerId: '456',
101
111
  landingPage: 'pricing',
102
112
  });
103
113
 
104
114
  // Parameters are automatically sent with every API request
105
- const session = await client.getSession();
115
+ await client.getSession();
106
116
  ```
107
117
 
108
118
  ## API Reference
@@ -115,18 +125,19 @@ Initialize the tracking client.
115
125
  interface CallForgeConfig {
116
126
  categoryId: string; // Required - which number pool to use
117
127
  endpoint?: string; // Optional - defaults to 'https://tracking.callforge.io'
118
- ga4MeasurementId?: string; // Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX")
119
- // Enables gtag callback instead of cookie polling
128
+ siteKey?: string; // Optional - cache partition key (defaults to window.location.hostname)
129
+ ga4MeasurementId?: string; // Optional - GA4 Measurement ID (e.g., 'G-XXXXXXXXXX')
120
130
  }
121
131
  ```
122
132
 
123
133
  ### `client.getSession()`
124
134
 
125
- Get tracking session data. Returns cached data if valid, otherwise fetches from API.
135
+ Get tracking session data. Returns cached data if valid, otherwise fetches from the API.
126
136
 
127
137
  ```typescript
128
138
  interface TrackingSession {
129
- sessionId: string | null;
139
+ sessionToken: string; // Signed, opaque token used to refresh the session
140
+ leaseId: string | null; // Deterministic assignment lease ID (when available)
130
141
  phoneNumber: string | null;
131
142
  location: {
132
143
  city: string;
@@ -136,11 +147,20 @@ interface TrackingSession {
136
147
  }
137
148
  ```
138
149
 
139
- **Behavior:**
140
- - Returns `null` values immediately if no `loc_physical_ms` in URL (no API call)
141
- - Returns cached data if valid (same location, not expired)
142
- - Fetches fresh data if cache expired or location changed
143
- - Throws on network errors or API errors
150
+ Behavior:
151
+ - Returns cached data if valid.
152
+ - Fetches fresh data when cache is missing/expired.
153
+ - If `loc_physical_ms` is present in the URL, cached sessions are only reused when it matches the cached `locId`.
154
+ - Throws on network errors or API errors.
155
+
156
+ ### `client.createCallIntent()`
157
+
158
+ Create a short-lived call intent token for click/callback deterministic attribution.
159
+
160
+ ```typescript
161
+ const intent = await client.createCallIntent();
162
+ console.log(intent.callIntentToken);
163
+ ```
144
164
 
145
165
  ### `client.onReady(callback)`
146
166
 
@@ -164,11 +184,10 @@ client.setParams({
164
184
  });
165
185
  ```
166
186
 
167
- **Behavior:**
168
- - Merges with existing params (later calls override earlier values)
169
- - Parameters are sent with every `getSession()` API request
170
- - Persisted in localStorage alongside session data
171
- - Auto-captured params (gclid, etc.) are included automatically
187
+ Behavior:
188
+ - Merges with existing params (later calls override earlier values).
189
+ - Parameters are sent with every `getSession()` API request.
190
+ - Persisted in localStorage alongside session data.
172
191
 
173
192
  ### `getPreloadSnippet(config)`
174
193
 
@@ -180,6 +199,7 @@ import { getPreloadSnippet } from '@callforge/tracking-client';
180
199
  const html = getPreloadSnippet({
181
200
  categoryId: 'your-category-id',
182
201
  endpoint: 'https://tracking.callforge.io', // Optional
202
+ siteKey: 'example.com', // Optional
183
203
  });
184
204
  ```
185
205
 
@@ -194,32 +214,22 @@ The client automatically extracts these parameters from the URL:
194
214
  | `gclid` | Google Ads Click ID |
195
215
  | `gbraid` | Google app-to-web (iOS 14+) |
196
216
  | `wbraid` | Google web-to-app |
197
- | `msclkid` | Microsoft/Bing Ads |
198
- | `fbclid` | Facebook/Meta Ads |
199
-
200
- **Note:** `ga4ClientId` is captured via `gtag('get')` callback when `ga4MeasurementId` is configured.
201
-
202
- ### Persistence
203
-
204
- - Parameters are stored in localStorage alongside the session
205
- - On subsequent visits, cached params are used (URL may no longer have them)
206
- - Custom params via `setParams()` merge with auto-captured params
207
- - Later values override earlier ones (custom > cached > auto-captured)
217
+ | `msclkid` | Microsoft/Bing Ads Click ID |
218
+ | `fbclid` | Facebook/Meta Ads Click ID |
208
219
 
209
220
  ### API Request Format
210
221
 
211
- Parameters are sent as sorted query string for cache consistency:
222
+ Parameters are sent as a sorted query string for cache consistency:
212
223
 
213
224
  ```
214
- /v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221
225
+ /v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221&sessionToken=...
215
226
  ```
216
227
 
217
228
  ## Caching Behavior
218
229
 
219
- - **Cache key:** `loc_physical_ms` parameter from URL
220
- - **TTL:** 30 minutes (controlled by server)
221
- - **Storage:** localStorage (falls back to memory if unavailable)
222
- - **Invalidation:** Cache clears when location changes
230
+ - Cache key: `cf_tracking_v1_<siteKey>_<categoryId>`
231
+ - TTL: controlled by the server `expiresAt` response (currently 30 minutes)
232
+ - Storage: localStorage (falls back to memory if unavailable)
223
233
 
224
234
  ## Error Handling
225
235
 
@@ -229,7 +239,7 @@ try {
229
239
  if (session.phoneNumber) {
230
240
  // Use phone number
231
241
  } else {
232
- // No phone number available for this location
242
+ // No phone number available
233
243
  }
234
244
  } catch (err) {
235
245
  // Network error or API error
@@ -248,52 +258,10 @@ import type {
248
258
  TrackingLocation,
249
259
  TrackingParams,
250
260
  ReadyCallback,
261
+ CallIntentResponse,
251
262
  } from '@callforge/tracking-client';
252
263
  ```
253
264
 
254
- ### TrackingParams
255
-
256
- ```typescript
257
- interface TrackingParams {
258
- gclid?: string; // Google Ads Click ID
259
- gbraid?: string; // Google app-to-web (iOS 14+)
260
- wbraid?: string; // Google web-to-app
261
- msclkid?: string; // Microsoft/Bing Ads
262
- fbclid?: string; // Facebook/Meta Ads
263
- ga4ClientId?: string; // Google Analytics 4 Client ID (auto-captured via gtag callback)
264
- [key: string]: string | undefined; // Custom params
265
- }
266
- ```
267
-
268
- ## GA4 Events
269
-
270
- When GA4 is configured for a category, CallForge sends these events to Google Analytics 4 via the Measurement Protocol:
271
-
272
- | Event Name | When Sent | Description |
273
- |------------|-----------|-------------|
274
- | `phone_call` | Call initiated | A phone call was placed to the tracking number |
275
- | `call_conversion` | Call qualified | The call met conversion criteria (e.g., duration threshold) |
276
- | `call_conversion_billable` | Call billable | The call was both qualified AND billable |
277
-
278
- **Event Parameters:**
279
-
280
- All events include:
281
- - `session_id` - CallForge session ID
282
- - `phone_number` - Tracking number dialed
283
- - `category` - Category slug
284
-
285
- `call_conversion` and `call_conversion_billable` also include:
286
- - `call_duration` - Call duration in seconds
287
- - `buyer` - Buyer the call was routed to (if applicable)
288
-
289
- **Setup:**
290
- 1. In Google Analytics 4, go to Admin → Data Streams → your web stream
291
- 2. Copy the **Measurement ID** (e.g., `G-XXXXXXXXXX`)
292
- 3. Go to Admin → Data Streams → Measurement Protocol API secrets → Create
293
- 4. Copy the **API Secret**
294
- 5. In CallForge dashboard: Categories → Edit category → GA4 tab
295
- 6. Enable GA4, paste Measurement ID and API Secret, save
296
-
297
265
  ## Environment URLs
298
266
 
299
267
  | Environment | Endpoint |
package/dist/index.d.mts CHANGED
@@ -6,6 +6,8 @@ interface CallForgeConfig {
6
6
  categoryId: string;
7
7
  /** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
8
8
  endpoint?: string;
9
+ /** Optional - explicit site key for cache partitioning. Defaults to window.location.hostname */
10
+ siteKey?: string;
9
11
  /** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag callback instead of cookie polling. */
10
12
  ga4MeasurementId?: string;
11
13
  }
@@ -41,8 +43,10 @@ interface TrackingParams {
41
43
  * Session data returned by getSession().
42
44
  */
43
45
  interface TrackingSession {
44
- /** Session ID from the tracking API */
45
- sessionId: string | null;
46
+ /** Signed session token */
47
+ sessionToken: string;
48
+ /** Lease ID for deterministic number assignment */
49
+ leaseId: string | null;
46
50
  /** Assigned phone number, or null if no number available */
47
51
  phoneNumber: string | null;
48
52
  /** Location data, or null if location could not be resolved */
@@ -52,7 +56,8 @@ interface TrackingSession {
52
56
  * Internal API response format (includes expiresAt).
53
57
  */
54
58
  interface ApiResponse {
55
- sessionId: string;
59
+ sessionToken: string;
60
+ leaseId: string | null;
56
61
  phoneNumber: string | null;
57
62
  location: {
58
63
  city: string;
@@ -61,6 +66,14 @@ interface ApiResponse {
61
66
  } | null;
62
67
  expiresAt: number;
63
68
  }
69
+ interface CallIntentResponse {
70
+ callIntentToken: string;
71
+ intentId: string;
72
+ sessionId: string;
73
+ leaseId: string | null;
74
+ expiresAt: string;
75
+ attributionVersion: 'v1';
76
+ }
64
77
  /**
65
78
  * Callback function for onReady subscription.
66
79
  */
@@ -80,8 +93,6 @@ declare class CallForge {
80
93
  private readonly cache;
81
94
  private sessionPromise;
82
95
  private customParams;
83
- private sessionId;
84
- private sessionCreated;
85
96
  private constructor();
86
97
  /**
87
98
  * Initialize the CallForge tracking client.
@@ -92,22 +103,23 @@ declare class CallForge {
92
103
  * Returns cached data if valid, otherwise fetches from API.
93
104
  */
94
105
  getSession(): Promise<TrackingSession>;
106
+ /**
107
+ * Create a short-lived call intent token for click/callback deterministic attribution.
108
+ */
109
+ createCallIntent(): Promise<CallIntentResponse>;
95
110
  /**
96
111
  * Subscribe to session ready event.
97
112
  * Callback is called once session data is available.
98
113
  */
99
114
  onReady(callback: ReadyCallback): void;
100
115
  /**
101
- * Set custom tracking parameters.
102
- * If session already exists, sends PATCH to update.
103
- * Otherwise queues params for next session request.
116
+ * Set custom tracking parameters for the next session fetch.
104
117
  */
105
118
  setParams(params: Record<string, string>): Promise<void>;
106
119
  /**
107
120
  * Get the currently queued custom params.
108
121
  */
109
122
  getQueuedParams(): TrackingParams;
110
- private patchSession;
111
123
  /**
112
124
  * Capture the GA4 client ID using gtag callback.
113
125
  * Only runs if ga4MeasurementId is configured.
@@ -126,18 +138,7 @@ declare class CallForge {
126
138
  /**
127
139
  * Generate HTML snippet for preloading tracking data.
128
140
  * Add this to the <head> of your HTML for optimal performance.
129
- *
130
- * The generated snippet:
131
- * - Checks for loc_physical_ms URL parameter
132
- * - Checks localStorage cache for valid session
133
- * - If cache valid, resolves Promise.resolve(cached) immediately
134
- * - If cache expired but same location, includes sessionId for refresh
135
- * - Otherwise fetches fresh session data
136
- * - Sets window.__cfTracking with the session promise
137
- *
138
- * @throws {Error} If categoryId contains invalid characters
139
- * @throws {Error} If endpoint is not a valid HTTPS URL
140
141
  */
141
142
  declare function getPreloadSnippet(config: CallForgeConfig): string;
142
143
 
143
- export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
144
+ export { CallForge, type CallForgeConfig, type CallIntentResponse, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ interface CallForgeConfig {
6
6
  categoryId: string;
7
7
  /** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
8
8
  endpoint?: string;
9
+ /** Optional - explicit site key for cache partitioning. Defaults to window.location.hostname */
10
+ siteKey?: string;
9
11
  /** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag callback instead of cookie polling. */
10
12
  ga4MeasurementId?: string;
11
13
  }
@@ -41,8 +43,10 @@ interface TrackingParams {
41
43
  * Session data returned by getSession().
42
44
  */
43
45
  interface TrackingSession {
44
- /** Session ID from the tracking API */
45
- sessionId: string | null;
46
+ /** Signed session token */
47
+ sessionToken: string;
48
+ /** Lease ID for deterministic number assignment */
49
+ leaseId: string | null;
46
50
  /** Assigned phone number, or null if no number available */
47
51
  phoneNumber: string | null;
48
52
  /** Location data, or null if location could not be resolved */
@@ -52,7 +56,8 @@ interface TrackingSession {
52
56
  * Internal API response format (includes expiresAt).
53
57
  */
54
58
  interface ApiResponse {
55
- sessionId: string;
59
+ sessionToken: string;
60
+ leaseId: string | null;
56
61
  phoneNumber: string | null;
57
62
  location: {
58
63
  city: string;
@@ -61,6 +66,14 @@ interface ApiResponse {
61
66
  } | null;
62
67
  expiresAt: number;
63
68
  }
69
+ interface CallIntentResponse {
70
+ callIntentToken: string;
71
+ intentId: string;
72
+ sessionId: string;
73
+ leaseId: string | null;
74
+ expiresAt: string;
75
+ attributionVersion: 'v1';
76
+ }
64
77
  /**
65
78
  * Callback function for onReady subscription.
66
79
  */
@@ -80,8 +93,6 @@ declare class CallForge {
80
93
  private readonly cache;
81
94
  private sessionPromise;
82
95
  private customParams;
83
- private sessionId;
84
- private sessionCreated;
85
96
  private constructor();
86
97
  /**
87
98
  * Initialize the CallForge tracking client.
@@ -92,22 +103,23 @@ declare class CallForge {
92
103
  * Returns cached data if valid, otherwise fetches from API.
93
104
  */
94
105
  getSession(): Promise<TrackingSession>;
106
+ /**
107
+ * Create a short-lived call intent token for click/callback deterministic attribution.
108
+ */
109
+ createCallIntent(): Promise<CallIntentResponse>;
95
110
  /**
96
111
  * Subscribe to session ready event.
97
112
  * Callback is called once session data is available.
98
113
  */
99
114
  onReady(callback: ReadyCallback): void;
100
115
  /**
101
- * Set custom tracking parameters.
102
- * If session already exists, sends PATCH to update.
103
- * Otherwise queues params for next session request.
116
+ * Set custom tracking parameters for the next session fetch.
104
117
  */
105
118
  setParams(params: Record<string, string>): Promise<void>;
106
119
  /**
107
120
  * Get the currently queued custom params.
108
121
  */
109
122
  getQueuedParams(): TrackingParams;
110
- private patchSession;
111
123
  /**
112
124
  * Capture the GA4 client ID using gtag callback.
113
125
  * Only runs if ga4MeasurementId is configured.
@@ -126,18 +138,7 @@ declare class CallForge {
126
138
  /**
127
139
  * Generate HTML snippet for preloading tracking data.
128
140
  * Add this to the <head> of your HTML for optimal performance.
129
- *
130
- * The generated snippet:
131
- * - Checks for loc_physical_ms URL parameter
132
- * - Checks localStorage cache for valid session
133
- * - If cache valid, resolves Promise.resolve(cached) immediately
134
- * - If cache expired but same location, includes sessionId for refresh
135
- * - Otherwise fetches fresh session data
136
- * - Sets window.__cfTracking with the session promise
137
- *
138
- * @throws {Error} If categoryId contains invalid characters
139
- * @throws {Error} If endpoint is not a valid HTTPS URL
140
141
  */
141
142
  declare function getPreloadSnippet(config: CallForgeConfig): string;
142
143
 
143
- export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
144
+ export { CallForge, type CallForgeConfig, type CallIntentResponse, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
package/dist/index.js CHANGED
@@ -42,10 +42,11 @@ module.exports = __toCommonJS(index_exports);
42
42
  // src/cache.ts
43
43
  var EXPIRY_BUFFER_MS = 3e4;
44
44
  var TrackingCache = class {
45
- constructor(categoryId) {
45
+ constructor(categoryId, siteKey) {
46
46
  this.memoryCache = null;
47
47
  this.useMemory = false;
48
- this.key = `cf_tracking_${categoryId}`;
48
+ const resolvedSiteKey = siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
49
+ this.key = `cf_tracking_v1_${resolvedSiteKey}_${categoryId}`;
49
50
  this.useMemory = !this.isLocalStorageAvailable();
50
51
  }
51
52
  isLocalStorageAvailable() {
@@ -78,17 +79,15 @@ var TrackingCache = class {
78
79
  return cached;
79
80
  }
80
81
  /**
81
- * Get sessionId for refresh if location matches (regardless of expiry).
82
- * Used when cache is expired but location is same - send sessionId to server.
83
- *
84
- * Only returns sessionId if locationId is provided AND matches cached locId.
85
- * (IP-based sessions don't send sessionId for refresh since location may differ)
82
+ * Get sessionToken for refresh if location matches (regardless of expiry).
83
+ * Used when cache is expired but location is same - send token to server.
86
84
  */
87
- getSessionId(locationId) {
85
+ getSessionToken(locationId) {
88
86
  const cached = this.read();
89
87
  if (!cached) return null;
90
- if (!locationId || cached.locId !== locationId) return null;
91
- return cached.sessionId;
88
+ if (!locationId) return cached.sessionToken;
89
+ if (cached.locId !== locationId) return null;
90
+ return cached.sessionToken;
92
91
  }
93
92
  /**
94
93
  * Get cached params (for merging with new params).
@@ -142,19 +141,19 @@ var TrackingCache = class {
142
141
  // src/client.ts
143
142
  var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
144
143
  var FETCH_TIMEOUT_MS = 1e4;
144
+ var CALL_INTENT_TIMEOUT_MS = 8e3;
145
145
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
146
146
  var CallForge = class _CallForge {
147
147
  constructor(config) {
148
148
  this.sessionPromise = null;
149
149
  this.customParams = {};
150
- this.sessionId = null;
151
- this.sessionCreated = false;
152
150
  this.config = {
153
151
  categoryId: config.categoryId,
154
152
  endpoint: config.endpoint || DEFAULT_ENDPOINT,
155
- ga4MeasurementId: config.ga4MeasurementId
153
+ ga4MeasurementId: config.ga4MeasurementId,
154
+ siteKey: config.siteKey
156
155
  };
157
- this.cache = new TrackingCache(config.categoryId);
156
+ this.cache = new TrackingCache(config.categoryId, config.siteKey);
158
157
  this.captureGA4ClientId();
159
158
  }
160
159
  /**
@@ -174,6 +173,33 @@ var CallForge = class _CallForge {
174
173
  this.sessionPromise = this.fetchSession();
175
174
  return this.sessionPromise;
176
175
  }
176
+ /**
177
+ * Create a short-lived call intent token for click/callback deterministic attribution.
178
+ */
179
+ async createCallIntent() {
180
+ const session = await this.getSession();
181
+ const controller = new AbortController();
182
+ const timeoutId = setTimeout(() => controller.abort(), CALL_INTENT_TIMEOUT_MS);
183
+ try {
184
+ const response = await fetch(`${this.config.endpoint}/v1/tracking/call-intent`, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json"
188
+ },
189
+ credentials: "omit",
190
+ signal: controller.signal,
191
+ body: JSON.stringify({
192
+ sessionToken: session.sessionToken
193
+ })
194
+ });
195
+ if (!response.ok) {
196
+ throw new Error(`Call intent API error: ${response.status} ${response.statusText}`);
197
+ }
198
+ return await response.json();
199
+ } finally {
200
+ clearTimeout(timeoutId);
201
+ }
202
+ }
177
203
  /**
178
204
  * Subscribe to session ready event.
179
205
  * Callback is called once session data is available.
@@ -183,15 +209,10 @@ var CallForge = class _CallForge {
183
209
  });
184
210
  }
185
211
  /**
186
- * Set custom tracking parameters.
187
- * If session already exists, sends PATCH to update.
188
- * Otherwise queues params for next session request.
212
+ * Set custom tracking parameters for the next session fetch.
189
213
  */
190
214
  async setParams(params) {
191
215
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
192
- if (this.sessionCreated && this.sessionId) {
193
- await this.patchSession(params);
194
- }
195
216
  }
196
217
  /**
197
218
  * Get the currently queued custom params.
@@ -199,23 +220,6 @@ var CallForge = class _CallForge {
199
220
  getQueuedParams() {
200
221
  return __spreadValues({}, this.customParams);
201
222
  }
202
- async patchSession(params) {
203
- try {
204
- const response = await fetch(
205
- `${this.config.endpoint}/v1/tracking/session/${this.sessionId}`,
206
- {
207
- method: "PATCH",
208
- headers: { "Content-Type": "application/json" },
209
- body: JSON.stringify(params)
210
- }
211
- );
212
- if (!response.ok) {
213
- console.warn("[CallForge] Failed to patch session:", response.status);
214
- }
215
- } catch (error) {
216
- console.warn("[CallForge] Failed to patch session:", error);
217
- }
218
- }
219
223
  /**
220
224
  * Capture the GA4 client ID using gtag callback.
221
225
  * Only runs if ga4MeasurementId is configured.
@@ -244,26 +248,20 @@ var CallForge = class _CallForge {
244
248
  const autoParams2 = this.getAutoParams();
245
249
  const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
246
250
  this.saveToCache(effectiveLocId, data2, params2);
247
- this.sessionId = data2.sessionId;
248
- this.sessionCreated = true;
249
251
  return this.formatApiResponse(data2);
250
252
  } catch (e) {
251
253
  }
252
254
  }
253
255
  const cached = this.cache.get(locationId);
254
256
  if (cached) {
255
- this.sessionId = cached.sessionId;
256
- this.sessionCreated = true;
257
257
  return this.formatSession(cached);
258
258
  }
259
259
  const autoParams = this.getAutoParams();
260
260
  const cachedParams = this.cache.getParams();
261
261
  const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
262
- const cachedSessionId = this.cache.getSessionId(locationId);
263
- const data = await this.fetchFromApi(locationId, cachedSessionId, params);
262
+ const cachedSessionToken = this.cache.getSessionToken(locationId);
263
+ const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
264
264
  this.saveToCache(locationId, data, params);
265
- this.sessionId = data.sessionId;
266
- this.sessionCreated = true;
267
265
  return this.formatApiResponse(data);
268
266
  }
269
267
  getLocationId() {
@@ -283,8 +281,8 @@ var CallForge = class _CallForge {
283
281
  }
284
282
  return params;
285
283
  }
286
- async fetchFromApi(locationId, sessionId, params) {
287
- const url = this.buildUrl(locationId, sessionId, params);
284
+ async fetchFromApi(locationId, sessionToken, params) {
285
+ const url = this.buildUrl(locationId, sessionToken, params);
288
286
  const controller = new AbortController();
289
287
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
290
288
  try {
@@ -303,29 +301,33 @@ var CallForge = class _CallForge {
303
301
  saveToCache(locationId, data, params) {
304
302
  const cached = {
305
303
  locId: locationId != null ? locationId : null,
306
- sessionId: data.sessionId,
304
+ sessionToken: data.sessionToken,
305
+ leaseId: data.leaseId,
307
306
  phoneNumber: data.phoneNumber,
308
307
  location: data.location,
309
308
  expiresAt: data.expiresAt,
309
+ tokenVersion: "v1",
310
310
  params
311
311
  };
312
312
  this.cache.set(cached);
313
313
  }
314
314
  formatSession(cached) {
315
315
  return {
316
- sessionId: cached.sessionId,
316
+ sessionToken: cached.sessionToken,
317
+ leaseId: cached.leaseId,
317
318
  phoneNumber: cached.phoneNumber,
318
319
  location: cached.location
319
320
  };
320
321
  }
321
322
  formatApiResponse(data) {
322
323
  return {
323
- sessionId: data.sessionId,
324
+ sessionToken: data.sessionToken,
325
+ leaseId: data.leaseId,
324
326
  phoneNumber: data.phoneNumber,
325
327
  location: data.location
326
328
  };
327
329
  }
328
- buildUrl(locationId, sessionId, params) {
330
+ buildUrl(locationId, sessionToken, params) {
329
331
  const { categoryId, endpoint } = this.config;
330
332
  const queryParams = {
331
333
  categoryId
@@ -333,8 +335,8 @@ var CallForge = class _CallForge {
333
335
  if (locationId) {
334
336
  queryParams.loc_physical_ms = locationId;
335
337
  }
336
- if (sessionId) {
337
- queryParams.sessionId = sessionId;
338
+ if (sessionToken) {
339
+ queryParams.sessionToken = sessionToken;
338
340
  }
339
341
  for (const [key, value] of Object.entries(params)) {
340
342
  if (value !== void 0) {
@@ -364,29 +366,29 @@ function getPreloadSnippet(config) {
364
366
  `Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
365
367
  );
366
368
  }
367
- const cacheKey = `cf_tracking_${categoryId}`;
368
369
  const script = `(function(){
369
370
  var u=new URLSearchParams(location.search);
370
371
  var loc=u.get('loc_physical_ms');
371
372
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
372
373
  var p={};
373
374
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
374
- var key='${cacheKey}';
375
- var sid=null;
375
+ var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
376
+ var key='cf_tracking_v1_'+site+'_${categoryId}';
377
+ var token=null;
376
378
  try{
377
379
  var c=JSON.parse(localStorage.getItem(key));
378
380
  if(c&&c.expiresAt>Date.now()+30000){
379
381
  if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
380
- sid=(c.locId===loc)?c.sessionId:null;
382
+ token=(!loc||c.locId===loc)?c.sessionToken:null;
381
383
  var cp=c.params||{};
382
384
  p=Object.assign({},cp,p);
383
385
  }}catch(e){}
384
386
  var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
385
387
  if(loc)url+='&loc_physical_ms='+loc;
386
- if(sid)url+='&sessionId='+sid;
388
+ if(token)url+='&sessionToken='+encodeURIComponent(token);
387
389
  var ks=Object.keys(p).sort();
388
390
  for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
389
- window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()}).then(function(d){d.params=p;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionId:d.sessionId,phoneNumber:d.phoneNumber,location:d.location,expiresAt:d.expiresAt,params:p}))}catch(e){}return d});
391
+ window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,location:d.location,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
390
392
  })();`.replace(/\n/g, "");
391
393
  return `<link rel="preconnect" href="${endpoint}">
392
394
  <script>${script}</script>`;
package/dist/index.mjs CHANGED
@@ -18,10 +18,11 @@ var __spreadValues = (a, b) => {
18
18
  // src/cache.ts
19
19
  var EXPIRY_BUFFER_MS = 3e4;
20
20
  var TrackingCache = class {
21
- constructor(categoryId) {
21
+ constructor(categoryId, siteKey) {
22
22
  this.memoryCache = null;
23
23
  this.useMemory = false;
24
- this.key = `cf_tracking_${categoryId}`;
24
+ const resolvedSiteKey = siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
25
+ this.key = `cf_tracking_v1_${resolvedSiteKey}_${categoryId}`;
25
26
  this.useMemory = !this.isLocalStorageAvailable();
26
27
  }
27
28
  isLocalStorageAvailable() {
@@ -54,17 +55,15 @@ var TrackingCache = class {
54
55
  return cached;
55
56
  }
56
57
  /**
57
- * Get sessionId for refresh if location matches (regardless of expiry).
58
- * Used when cache is expired but location is same - send sessionId to server.
59
- *
60
- * Only returns sessionId if locationId is provided AND matches cached locId.
61
- * (IP-based sessions don't send sessionId for refresh since location may differ)
58
+ * Get sessionToken for refresh if location matches (regardless of expiry).
59
+ * Used when cache is expired but location is same - send token to server.
62
60
  */
63
- getSessionId(locationId) {
61
+ getSessionToken(locationId) {
64
62
  const cached = this.read();
65
63
  if (!cached) return null;
66
- if (!locationId || cached.locId !== locationId) return null;
67
- return cached.sessionId;
64
+ if (!locationId) return cached.sessionToken;
65
+ if (cached.locId !== locationId) return null;
66
+ return cached.sessionToken;
68
67
  }
69
68
  /**
70
69
  * Get cached params (for merging with new params).
@@ -118,19 +117,19 @@ var TrackingCache = class {
118
117
  // src/client.ts
119
118
  var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
120
119
  var FETCH_TIMEOUT_MS = 1e4;
120
+ var CALL_INTENT_TIMEOUT_MS = 8e3;
121
121
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
122
122
  var CallForge = class _CallForge {
123
123
  constructor(config) {
124
124
  this.sessionPromise = null;
125
125
  this.customParams = {};
126
- this.sessionId = null;
127
- this.sessionCreated = false;
128
126
  this.config = {
129
127
  categoryId: config.categoryId,
130
128
  endpoint: config.endpoint || DEFAULT_ENDPOINT,
131
- ga4MeasurementId: config.ga4MeasurementId
129
+ ga4MeasurementId: config.ga4MeasurementId,
130
+ siteKey: config.siteKey
132
131
  };
133
- this.cache = new TrackingCache(config.categoryId);
132
+ this.cache = new TrackingCache(config.categoryId, config.siteKey);
134
133
  this.captureGA4ClientId();
135
134
  }
136
135
  /**
@@ -150,6 +149,33 @@ var CallForge = class _CallForge {
150
149
  this.sessionPromise = this.fetchSession();
151
150
  return this.sessionPromise;
152
151
  }
152
+ /**
153
+ * Create a short-lived call intent token for click/callback deterministic attribution.
154
+ */
155
+ async createCallIntent() {
156
+ const session = await this.getSession();
157
+ const controller = new AbortController();
158
+ const timeoutId = setTimeout(() => controller.abort(), CALL_INTENT_TIMEOUT_MS);
159
+ try {
160
+ const response = await fetch(`${this.config.endpoint}/v1/tracking/call-intent`, {
161
+ method: "POST",
162
+ headers: {
163
+ "Content-Type": "application/json"
164
+ },
165
+ credentials: "omit",
166
+ signal: controller.signal,
167
+ body: JSON.stringify({
168
+ sessionToken: session.sessionToken
169
+ })
170
+ });
171
+ if (!response.ok) {
172
+ throw new Error(`Call intent API error: ${response.status} ${response.statusText}`);
173
+ }
174
+ return await response.json();
175
+ } finally {
176
+ clearTimeout(timeoutId);
177
+ }
178
+ }
153
179
  /**
154
180
  * Subscribe to session ready event.
155
181
  * Callback is called once session data is available.
@@ -159,15 +185,10 @@ var CallForge = class _CallForge {
159
185
  });
160
186
  }
161
187
  /**
162
- * Set custom tracking parameters.
163
- * If session already exists, sends PATCH to update.
164
- * Otherwise queues params for next session request.
188
+ * Set custom tracking parameters for the next session fetch.
165
189
  */
166
190
  async setParams(params) {
167
191
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
168
- if (this.sessionCreated && this.sessionId) {
169
- await this.patchSession(params);
170
- }
171
192
  }
172
193
  /**
173
194
  * Get the currently queued custom params.
@@ -175,23 +196,6 @@ var CallForge = class _CallForge {
175
196
  getQueuedParams() {
176
197
  return __spreadValues({}, this.customParams);
177
198
  }
178
- async patchSession(params) {
179
- try {
180
- const response = await fetch(
181
- `${this.config.endpoint}/v1/tracking/session/${this.sessionId}`,
182
- {
183
- method: "PATCH",
184
- headers: { "Content-Type": "application/json" },
185
- body: JSON.stringify(params)
186
- }
187
- );
188
- if (!response.ok) {
189
- console.warn("[CallForge] Failed to patch session:", response.status);
190
- }
191
- } catch (error) {
192
- console.warn("[CallForge] Failed to patch session:", error);
193
- }
194
- }
195
199
  /**
196
200
  * Capture the GA4 client ID using gtag callback.
197
201
  * Only runs if ga4MeasurementId is configured.
@@ -220,26 +224,20 @@ var CallForge = class _CallForge {
220
224
  const autoParams2 = this.getAutoParams();
221
225
  const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
222
226
  this.saveToCache(effectiveLocId, data2, params2);
223
- this.sessionId = data2.sessionId;
224
- this.sessionCreated = true;
225
227
  return this.formatApiResponse(data2);
226
228
  } catch (e) {
227
229
  }
228
230
  }
229
231
  const cached = this.cache.get(locationId);
230
232
  if (cached) {
231
- this.sessionId = cached.sessionId;
232
- this.sessionCreated = true;
233
233
  return this.formatSession(cached);
234
234
  }
235
235
  const autoParams = this.getAutoParams();
236
236
  const cachedParams = this.cache.getParams();
237
237
  const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
238
- const cachedSessionId = this.cache.getSessionId(locationId);
239
- const data = await this.fetchFromApi(locationId, cachedSessionId, params);
238
+ const cachedSessionToken = this.cache.getSessionToken(locationId);
239
+ const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
240
240
  this.saveToCache(locationId, data, params);
241
- this.sessionId = data.sessionId;
242
- this.sessionCreated = true;
243
241
  return this.formatApiResponse(data);
244
242
  }
245
243
  getLocationId() {
@@ -259,8 +257,8 @@ var CallForge = class _CallForge {
259
257
  }
260
258
  return params;
261
259
  }
262
- async fetchFromApi(locationId, sessionId, params) {
263
- const url = this.buildUrl(locationId, sessionId, params);
260
+ async fetchFromApi(locationId, sessionToken, params) {
261
+ const url = this.buildUrl(locationId, sessionToken, params);
264
262
  const controller = new AbortController();
265
263
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
266
264
  try {
@@ -279,29 +277,33 @@ var CallForge = class _CallForge {
279
277
  saveToCache(locationId, data, params) {
280
278
  const cached = {
281
279
  locId: locationId != null ? locationId : null,
282
- sessionId: data.sessionId,
280
+ sessionToken: data.sessionToken,
281
+ leaseId: data.leaseId,
283
282
  phoneNumber: data.phoneNumber,
284
283
  location: data.location,
285
284
  expiresAt: data.expiresAt,
285
+ tokenVersion: "v1",
286
286
  params
287
287
  };
288
288
  this.cache.set(cached);
289
289
  }
290
290
  formatSession(cached) {
291
291
  return {
292
- sessionId: cached.sessionId,
292
+ sessionToken: cached.sessionToken,
293
+ leaseId: cached.leaseId,
293
294
  phoneNumber: cached.phoneNumber,
294
295
  location: cached.location
295
296
  };
296
297
  }
297
298
  formatApiResponse(data) {
298
299
  return {
299
- sessionId: data.sessionId,
300
+ sessionToken: data.sessionToken,
301
+ leaseId: data.leaseId,
300
302
  phoneNumber: data.phoneNumber,
301
303
  location: data.location
302
304
  };
303
305
  }
304
- buildUrl(locationId, sessionId, params) {
306
+ buildUrl(locationId, sessionToken, params) {
305
307
  const { categoryId, endpoint } = this.config;
306
308
  const queryParams = {
307
309
  categoryId
@@ -309,8 +311,8 @@ var CallForge = class _CallForge {
309
311
  if (locationId) {
310
312
  queryParams.loc_physical_ms = locationId;
311
313
  }
312
- if (sessionId) {
313
- queryParams.sessionId = sessionId;
314
+ if (sessionToken) {
315
+ queryParams.sessionToken = sessionToken;
314
316
  }
315
317
  for (const [key, value] of Object.entries(params)) {
316
318
  if (value !== void 0) {
@@ -340,29 +342,29 @@ function getPreloadSnippet(config) {
340
342
  `Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
341
343
  );
342
344
  }
343
- const cacheKey = `cf_tracking_${categoryId}`;
344
345
  const script = `(function(){
345
346
  var u=new URLSearchParams(location.search);
346
347
  var loc=u.get('loc_physical_ms');
347
348
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
348
349
  var p={};
349
350
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
350
- var key='${cacheKey}';
351
- var sid=null;
351
+ var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
352
+ var key='cf_tracking_v1_'+site+'_${categoryId}';
353
+ var token=null;
352
354
  try{
353
355
  var c=JSON.parse(localStorage.getItem(key));
354
356
  if(c&&c.expiresAt>Date.now()+30000){
355
357
  if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
356
- sid=(c.locId===loc)?c.sessionId:null;
358
+ token=(!loc||c.locId===loc)?c.sessionToken:null;
357
359
  var cp=c.params||{};
358
360
  p=Object.assign({},cp,p);
359
361
  }}catch(e){}
360
362
  var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
361
363
  if(loc)url+='&loc_physical_ms='+loc;
362
- if(sid)url+='&sessionId='+sid;
364
+ if(token)url+='&sessionToken='+encodeURIComponent(token);
363
365
  var ks=Object.keys(p).sort();
364
366
  for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
365
- window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()}).then(function(d){d.params=p;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionId:d.sessionId,phoneNumber:d.phoneNumber,location:d.location,expiresAt:d.expiresAt,params:p}))}catch(e){}return d});
367
+ window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,location:d.location,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
366
368
  })();`.replace(/\n/g, "");
367
369
  return `<link rel="preconnect" href="${endpoint}">
368
370
  <script>${script}</script>`;
package/package.json CHANGED
@@ -1,30 +1,33 @@
1
1
  {
2
2
  "name": "@callforge/tracking-client",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "main": "dist/index.js",
5
- "module": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
- "import": "./dist/index.js",
11
- "require": "./dist/index.cjs"
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
12
  }
13
13
  },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
14
17
  "files": [
15
18
  "dist"
16
19
  ],
17
- "devDependencies": {
18
- "jsdom": "^27.4.0",
19
- "tsup": "^8.0.0",
20
- "typescript": "^5.3.0",
21
- "vitest": "^1.6.0",
22
- "@callforge/tsconfig": "0.0.0"
23
- },
24
20
  "scripts": {
25
21
  "build": "tsup src/index.ts --format esm,cjs --dts",
26
22
  "clean": "rm -rf dist",
27
23
  "test": "vitest run",
28
24
  "test:watch": "vitest"
25
+ },
26
+ "devDependencies": {
27
+ "@callforge/tsconfig": "workspace:*",
28
+ "jsdom": "^27.4.0",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.3.0",
31
+ "vitest": "^1.6.0"
29
32
  }
30
- }
33
+ }