@callforge/tracking-client 0.5.0 → 0.6.0

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
4
4
 
5
- **v0.4.1** - Simplified GA4 capture: uses gtag callback when `ga4MeasurementId` configured, no polling fallback.
5
+ **v0.6.0** - Adds `createCallIntent()` helper for click/callback deterministic attribution and fixes ESM/CJS exports.
6
6
 
7
7
  ## Installation
8
8
 
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.
@@ -234,31 +238,30 @@ var CallForge = class _CallForge {
234
238
  });
235
239
  }
236
240
  async fetchSession() {
241
+ var _a;
237
242
  const locationId = this.getLocationId();
238
- const cached = this.cache.get(locationId);
239
- if (cached) {
240
- this.sessionId = cached.sessionId;
241
- this.sessionCreated = true;
242
- return this.formatSession(cached);
243
- }
244
- const autoParams = this.getAutoParams();
245
- const cachedParams = this.cache.getParams();
246
- const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
247
243
  if (typeof window !== "undefined" && window.__cfTracking) {
248
244
  try {
249
245
  const data2 = await window.__cfTracking;
250
- this.saveToCache(locationId, data2, params);
251
- this.sessionId = data2.sessionId;
252
- this.sessionCreated = true;
246
+ const dataWithExtras = data2;
247
+ const effectiveLocId = (_a = dataWithExtras.locId) != null ? _a : locationId;
248
+ const autoParams2 = this.getAutoParams();
249
+ const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
250
+ this.saveToCache(effectiveLocId, data2, params2);
253
251
  return this.formatApiResponse(data2);
254
252
  } catch (e) {
255
253
  }
256
254
  }
257
- const cachedSessionId = this.cache.getSessionId(locationId);
258
- const data = await this.fetchFromApi(locationId, cachedSessionId, params);
255
+ const cached = this.cache.get(locationId);
256
+ if (cached) {
257
+ return this.formatSession(cached);
258
+ }
259
+ const autoParams = this.getAutoParams();
260
+ const cachedParams = this.cache.getParams();
261
+ const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
262
+ const cachedSessionToken = this.cache.getSessionToken(locationId);
263
+ const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
259
264
  this.saveToCache(locationId, data, params);
260
- this.sessionId = data.sessionId;
261
- this.sessionCreated = true;
262
265
  return this.formatApiResponse(data);
263
266
  }
264
267
  getLocationId() {
@@ -278,8 +281,8 @@ var CallForge = class _CallForge {
278
281
  }
279
282
  return params;
280
283
  }
281
- async fetchFromApi(locationId, sessionId, params) {
282
- const url = this.buildUrl(locationId, sessionId, params);
284
+ async fetchFromApi(locationId, sessionToken, params) {
285
+ const url = this.buildUrl(locationId, sessionToken, params);
283
286
  const controller = new AbortController();
284
287
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
285
288
  try {
@@ -297,30 +300,34 @@ var CallForge = class _CallForge {
297
300
  }
298
301
  saveToCache(locationId, data, params) {
299
302
  const cached = {
300
- locId: locationId,
301
- sessionId: data.sessionId,
303
+ locId: locationId != null ? locationId : null,
304
+ sessionToken: data.sessionToken,
305
+ leaseId: data.leaseId,
302
306
  phoneNumber: data.phoneNumber,
303
307
  location: data.location,
304
308
  expiresAt: data.expiresAt,
309
+ tokenVersion: "v1",
305
310
  params
306
311
  };
307
312
  this.cache.set(cached);
308
313
  }
309
314
  formatSession(cached) {
310
315
  return {
311
- sessionId: cached.sessionId,
316
+ sessionToken: cached.sessionToken,
317
+ leaseId: cached.leaseId,
312
318
  phoneNumber: cached.phoneNumber,
313
319
  location: cached.location
314
320
  };
315
321
  }
316
322
  formatApiResponse(data) {
317
323
  return {
318
- sessionId: data.sessionId,
324
+ sessionToken: data.sessionToken,
325
+ leaseId: data.leaseId,
319
326
  phoneNumber: data.phoneNumber,
320
327
  location: data.location
321
328
  };
322
329
  }
323
- buildUrl(locationId, sessionId, params) {
330
+ buildUrl(locationId, sessionToken, params) {
324
331
  const { categoryId, endpoint } = this.config;
325
332
  const queryParams = {
326
333
  categoryId
@@ -328,8 +335,8 @@ var CallForge = class _CallForge {
328
335
  if (locationId) {
329
336
  queryParams.loc_physical_ms = locationId;
330
337
  }
331
- if (sessionId) {
332
- queryParams.sessionId = sessionId;
338
+ if (sessionToken) {
339
+ queryParams.sessionToken = sessionToken;
333
340
  }
334
341
  for (const [key, value] of Object.entries(params)) {
335
342
  if (value !== void 0) {
@@ -359,29 +366,29 @@ function getPreloadSnippet(config) {
359
366
  `Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
360
367
  );
361
368
  }
362
- const cacheKey = `cf_tracking_${categoryId}`;
363
369
  const script = `(function(){
364
370
  var u=new URLSearchParams(location.search);
365
371
  var loc=u.get('loc_physical_ms');
366
372
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
367
373
  var p={};
368
374
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
369
- var key='${cacheKey}';
370
- var sid=null;
375
+ var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
376
+ var key='cf_tracking_v1_'+site+'_${categoryId}';
377
+ var token=null;
371
378
  try{
372
379
  var c=JSON.parse(localStorage.getItem(key));
373
380
  if(c&&c.expiresAt>Date.now()+30000){
374
381
  if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
375
- sid=(c.locId===loc)?c.sessionId:null;
382
+ token=(!loc||c.locId===loc)?c.sessionToken:null;
376
383
  var cp=c.params||{};
377
384
  p=Object.assign({},cp,p);
378
385
  }}catch(e){}
379
386
  var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
380
387
  if(loc)url+='&loc_physical_ms='+loc;
381
- if(sid)url+='&sessionId='+sid;
388
+ if(token)url+='&sessionToken='+encodeURIComponent(token);
382
389
  var ks=Object.keys(p).sort();
383
390
  for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
384
- 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});
385
392
  })();`.replace(/\n/g, "");
386
393
  return `<link rel="preconnect" href="${endpoint}">
387
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.
@@ -210,31 +214,30 @@ var CallForge = class _CallForge {
210
214
  });
211
215
  }
212
216
  async fetchSession() {
217
+ var _a;
213
218
  const locationId = this.getLocationId();
214
- const cached = this.cache.get(locationId);
215
- if (cached) {
216
- this.sessionId = cached.sessionId;
217
- this.sessionCreated = true;
218
- return this.formatSession(cached);
219
- }
220
- const autoParams = this.getAutoParams();
221
- const cachedParams = this.cache.getParams();
222
- const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
223
219
  if (typeof window !== "undefined" && window.__cfTracking) {
224
220
  try {
225
221
  const data2 = await window.__cfTracking;
226
- this.saveToCache(locationId, data2, params);
227
- this.sessionId = data2.sessionId;
228
- this.sessionCreated = true;
222
+ const dataWithExtras = data2;
223
+ const effectiveLocId = (_a = dataWithExtras.locId) != null ? _a : locationId;
224
+ const autoParams2 = this.getAutoParams();
225
+ const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
226
+ this.saveToCache(effectiveLocId, data2, params2);
229
227
  return this.formatApiResponse(data2);
230
228
  } catch (e) {
231
229
  }
232
230
  }
233
- const cachedSessionId = this.cache.getSessionId(locationId);
234
- const data = await this.fetchFromApi(locationId, cachedSessionId, params);
231
+ const cached = this.cache.get(locationId);
232
+ if (cached) {
233
+ return this.formatSession(cached);
234
+ }
235
+ const autoParams = this.getAutoParams();
236
+ const cachedParams = this.cache.getParams();
237
+ const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
238
+ const cachedSessionToken = this.cache.getSessionToken(locationId);
239
+ const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
235
240
  this.saveToCache(locationId, data, params);
236
- this.sessionId = data.sessionId;
237
- this.sessionCreated = true;
238
241
  return this.formatApiResponse(data);
239
242
  }
240
243
  getLocationId() {
@@ -254,8 +257,8 @@ var CallForge = class _CallForge {
254
257
  }
255
258
  return params;
256
259
  }
257
- async fetchFromApi(locationId, sessionId, params) {
258
- const url = this.buildUrl(locationId, sessionId, params);
260
+ async fetchFromApi(locationId, sessionToken, params) {
261
+ const url = this.buildUrl(locationId, sessionToken, params);
259
262
  const controller = new AbortController();
260
263
  const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
261
264
  try {
@@ -273,30 +276,34 @@ var CallForge = class _CallForge {
273
276
  }
274
277
  saveToCache(locationId, data, params) {
275
278
  const cached = {
276
- locId: locationId,
277
- sessionId: data.sessionId,
279
+ locId: locationId != null ? locationId : null,
280
+ sessionToken: data.sessionToken,
281
+ leaseId: data.leaseId,
278
282
  phoneNumber: data.phoneNumber,
279
283
  location: data.location,
280
284
  expiresAt: data.expiresAt,
285
+ tokenVersion: "v1",
281
286
  params
282
287
  };
283
288
  this.cache.set(cached);
284
289
  }
285
290
  formatSession(cached) {
286
291
  return {
287
- sessionId: cached.sessionId,
292
+ sessionToken: cached.sessionToken,
293
+ leaseId: cached.leaseId,
288
294
  phoneNumber: cached.phoneNumber,
289
295
  location: cached.location
290
296
  };
291
297
  }
292
298
  formatApiResponse(data) {
293
299
  return {
294
- sessionId: data.sessionId,
300
+ sessionToken: data.sessionToken,
301
+ leaseId: data.leaseId,
295
302
  phoneNumber: data.phoneNumber,
296
303
  location: data.location
297
304
  };
298
305
  }
299
- buildUrl(locationId, sessionId, params) {
306
+ buildUrl(locationId, sessionToken, params) {
300
307
  const { categoryId, endpoint } = this.config;
301
308
  const queryParams = {
302
309
  categoryId
@@ -304,8 +311,8 @@ var CallForge = class _CallForge {
304
311
  if (locationId) {
305
312
  queryParams.loc_physical_ms = locationId;
306
313
  }
307
- if (sessionId) {
308
- queryParams.sessionId = sessionId;
314
+ if (sessionToken) {
315
+ queryParams.sessionToken = sessionToken;
309
316
  }
310
317
  for (const [key, value] of Object.entries(params)) {
311
318
  if (value !== void 0) {
@@ -335,29 +342,29 @@ function getPreloadSnippet(config) {
335
342
  `Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
336
343
  );
337
344
  }
338
- const cacheKey = `cf_tracking_${categoryId}`;
339
345
  const script = `(function(){
340
346
  var u=new URLSearchParams(location.search);
341
347
  var loc=u.get('loc_physical_ms');
342
348
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
343
349
  var p={};
344
350
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
345
- var key='${cacheKey}';
346
- var sid=null;
351
+ var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
352
+ var key='cf_tracking_v1_'+site+'_${categoryId}';
353
+ var token=null;
347
354
  try{
348
355
  var c=JSON.parse(localStorage.getItem(key));
349
356
  if(c&&c.expiresAt>Date.now()+30000){
350
357
  if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
351
- sid=(c.locId===loc)?c.sessionId:null;
358
+ token=(!loc||c.locId===loc)?c.sessionToken:null;
352
359
  var cp=c.params||{};
353
360
  p=Object.assign({},cp,p);
354
361
  }}catch(e){}
355
362
  var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
356
363
  if(loc)url+='&loc_physical_ms='+loc;
357
- if(sid)url+='&sessionId='+sid;
364
+ if(token)url+='&sessionToken='+encodeURIComponent(token);
358
365
  var ks=Object.keys(p).sort();
359
366
  for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
360
- 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});
361
368
  })();`.replace(/\n/g, "");
362
369
  return `<link rel="preconnect" href="${endpoint}">
363
370
  <script>${script}</script>`;
package/package.json CHANGED
@@ -1,30 +1,33 @@
1
1
  {
2
2
  "name": "@callforge/tracking-client",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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
+ }