@callforge/tracking-client 0.6.1 → 0.6.3

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
@@ -67,23 +67,25 @@ Notes:
67
67
 
68
68
  ### 4. GA4 Integration
69
69
 
70
- To enable GA4 call event tracking, provide your GA4 Measurement ID:
70
+ To enable GA4 call event tracking, CallForge needs the GA4 `client_id` for the visitor (from the `_ga` cookie).
71
+ Optionally provide your GA4 Measurement ID to improve client ID capture reliability when Google Analytics loads late.
71
72
 
72
73
  ```typescript
73
74
  const client = CallForge.init({
74
75
  categoryId: 'your-category-id',
75
- ga4MeasurementId: 'G-XXXXXXXXXX',
76
+ ga4MeasurementId: 'G-XXXXXXXXXX', // Recommended
76
77
  });
77
78
  ```
78
79
 
79
80
  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.
81
+ 1. Google Analytics 4 must be installed on your site and allowed to set the `_ga` cookie (gtag.js or GTM).
82
+ 2. Configure GA4 credentials in CallForge dashboard (Measurement ID + API secret).
83
83
 
84
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.
85
+ - Extracts the GA4 `client_id` from the `_ga` cookie and sends it to CallForge as `ga4ClientId`.
86
+ - Polls briefly after init to capture the cookie if Google Analytics sets it slightly later.
87
+ - If `ga4MeasurementId` is configured and `gtag` is available, also uses `gtag('get', measurementId, 'client_id', ...)` (with a short retry window).
88
+ - If `ga4ClientId` becomes available after a session is created, the client will refresh once to sync it to CallForge.
87
89
 
88
90
  Manual override:
89
91
 
@@ -186,6 +188,7 @@ client.setParams({
186
188
 
187
189
  Behavior:
188
190
  - Merges with existing params (later calls override earlier values).
191
+ - If a session already exists (cached `sessionToken`), the client will refresh once to sync updated params server-side (best-effort).
189
192
  - Parameters are sent with every `getSession()` API request.
190
193
  - Persisted in localStorage alongside session data.
191
194
 
package/dist/index.d.mts CHANGED
@@ -8,7 +8,7 @@ interface CallForgeConfig {
8
8
  endpoint?: string;
9
9
  /** Optional - explicit site key for cache partitioning. Defaults to window.location.hostname */
10
10
  siteKey?: string;
11
- /** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag callback instead of cookie polling. */
11
+ /** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag-based client_id capture for late-loading GA. */
12
12
  ga4MeasurementId?: string;
13
13
  }
14
14
  /**
@@ -113,7 +113,10 @@ declare class CallForge {
113
113
  */
114
114
  onReady(callback: ReadyCallback): void;
115
115
  /**
116
- * Set custom tracking parameters for the next session fetch.
116
+ * Set custom tracking parameters for attribution.
117
+ *
118
+ * When possible, the client will also refresh the session once to sync
119
+ * the updated params to CallForge (so server-side events can use them).
117
120
  */
118
121
  setParams(params: Record<string, string>): Promise<void>;
119
122
  /**
@@ -121,10 +124,20 @@ declare class CallForge {
121
124
  */
122
125
  getQueuedParams(): TrackingParams;
123
126
  /**
124
- * Capture the GA4 client ID using gtag callback.
125
- * Only runs if ga4MeasurementId is configured.
127
+ * Capture the GA4 client ID.
128
+ *
129
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
130
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
126
131
  */
127
132
  private captureGA4ClientId;
133
+ private startGA4ClientIdPolling;
134
+ /**
135
+ * Extract GA4 client_id from the `_ga` cookie.
136
+ *
137
+ * Example cookie value: `GA1.1.1234567890.1234567890`
138
+ * Returns: `1234567890.1234567890`
139
+ */
140
+ private getGA4ClientIdFromCookie;
128
141
  private fetchSession;
129
142
  private getLocationId;
130
143
  private getAutoParams;
@@ -132,6 +145,7 @@ declare class CallForge {
132
145
  private saveToCache;
133
146
  private formatSession;
134
147
  private formatApiResponse;
148
+ private syncParamsToCallForgeIfPossible;
135
149
  private buildUrl;
136
150
  }
137
151
 
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ interface CallForgeConfig {
8
8
  endpoint?: string;
9
9
  /** Optional - explicit site key for cache partitioning. Defaults to window.location.hostname */
10
10
  siteKey?: string;
11
- /** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag callback instead of cookie polling. */
11
+ /** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag-based client_id capture for late-loading GA. */
12
12
  ga4MeasurementId?: string;
13
13
  }
14
14
  /**
@@ -113,7 +113,10 @@ declare class CallForge {
113
113
  */
114
114
  onReady(callback: ReadyCallback): void;
115
115
  /**
116
- * Set custom tracking parameters for the next session fetch.
116
+ * Set custom tracking parameters for attribution.
117
+ *
118
+ * When possible, the client will also refresh the session once to sync
119
+ * the updated params to CallForge (so server-side events can use them).
117
120
  */
118
121
  setParams(params: Record<string, string>): Promise<void>;
119
122
  /**
@@ -121,10 +124,20 @@ declare class CallForge {
121
124
  */
122
125
  getQueuedParams(): TrackingParams;
123
126
  /**
124
- * Capture the GA4 client ID using gtag callback.
125
- * Only runs if ga4MeasurementId is configured.
127
+ * Capture the GA4 client ID.
128
+ *
129
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
130
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
126
131
  */
127
132
  private captureGA4ClientId;
133
+ private startGA4ClientIdPolling;
134
+ /**
135
+ * Extract GA4 client_id from the `_ga` cookie.
136
+ *
137
+ * Example cookie value: `GA1.1.1234567890.1234567890`
138
+ * Returns: `1234567890.1234567890`
139
+ */
140
+ private getGA4ClientIdFromCookie;
128
141
  private fetchSession;
129
142
  private getLocationId;
130
143
  private getAutoParams;
@@ -132,6 +145,7 @@ declare class CallForge {
132
145
  private saveToCache;
133
146
  private formatSession;
134
147
  private formatApiResponse;
148
+ private syncParamsToCallForgeIfPossible;
135
149
  private buildUrl;
136
150
  }
137
151
 
package/dist/index.js CHANGED
@@ -155,6 +155,7 @@ var CallForge = class _CallForge {
155
155
  };
156
156
  this.cache = new TrackingCache(config.categoryId, config.siteKey);
157
157
  this.captureGA4ClientId();
158
+ this.startGA4ClientIdPolling();
158
159
  }
159
160
  /**
160
161
  * Initialize the CallForge tracking client.
@@ -209,10 +210,28 @@ var CallForge = class _CallForge {
209
210
  });
210
211
  }
211
212
  /**
212
- * Set custom tracking parameters for the next session fetch.
213
+ * Set custom tracking parameters for attribution.
214
+ *
215
+ * When possible, the client will also refresh the session once to sync
216
+ * the updated params to CallForge (so server-side events can use them).
213
217
  */
214
218
  async setParams(params) {
215
219
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
220
+ const sync = async () => {
221
+ try {
222
+ await this.syncParamsToCallForgeIfPossible();
223
+ } catch (e) {
224
+ }
225
+ };
226
+ if (this.sessionPromise) {
227
+ try {
228
+ await this.sessionPromise;
229
+ } catch (e) {
230
+ }
231
+ await sync();
232
+ } else {
233
+ await sync();
234
+ }
216
235
  }
217
236
  /**
218
237
  * Get the currently queued custom params.
@@ -221,25 +240,66 @@ var CallForge = class _CallForge {
221
240
  return __spreadValues({}, this.customParams);
222
241
  }
223
242
  /**
224
- * Capture the GA4 client ID using gtag callback.
225
- * Only runs if ga4MeasurementId is configured.
243
+ * Capture the GA4 client ID.
244
+ *
245
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
246
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
226
247
  */
227
248
  captureGA4ClientId() {
228
- if (!this.config.ga4MeasurementId) {
249
+ if (this.customParams.ga4ClientId) {
229
250
  return;
230
251
  }
231
- if (typeof window === "undefined" || !window.gtag) {
252
+ const fromCookie = this.getGA4ClientIdFromCookie();
253
+ if (fromCookie) {
254
+ this.setParams({ ga4ClientId: fromCookie });
232
255
  return;
233
256
  }
257
+ if (!this.config.ga4MeasurementId) return;
258
+ if (typeof window === "undefined" || !window.gtag) return;
234
259
  window.gtag("get", this.config.ga4MeasurementId, "client_id", (clientId) => {
235
260
  if (clientId) {
236
261
  this.setParams({ ga4ClientId: clientId });
237
262
  }
238
263
  });
239
264
  }
265
+ startGA4ClientIdPolling() {
266
+ if (typeof window === "undefined") return;
267
+ if (this.customParams.ga4ClientId) return;
268
+ const MAX_ATTEMPTS = 20;
269
+ const INTERVAL_MS = 250;
270
+ let attempts = 0;
271
+ const poll = () => {
272
+ if (this.customParams.ga4ClientId) return;
273
+ this.captureGA4ClientId();
274
+ attempts += 1;
275
+ if (this.customParams.ga4ClientId) return;
276
+ if (attempts >= MAX_ATTEMPTS) return;
277
+ window.setTimeout(poll, INTERVAL_MS);
278
+ };
279
+ window.setTimeout(poll, INTERVAL_MS);
280
+ }
281
+ /**
282
+ * Extract GA4 client_id from the `_ga` cookie.
283
+ *
284
+ * Example cookie value: `GA1.1.1234567890.1234567890`
285
+ * Returns: `1234567890.1234567890`
286
+ */
287
+ getGA4ClientIdFromCookie() {
288
+ if (typeof document === "undefined") return null;
289
+ const match = document.cookie.match(/(?:^|;\s*)_ga=([^;]+)/);
290
+ if (!match) return null;
291
+ const cookieValue = decodeURIComponent(match[1] || "");
292
+ const parts = cookieValue.split(".");
293
+ if (parts.length < 2) return null;
294
+ const partA = parts[parts.length - 2];
295
+ const partB = parts[parts.length - 1];
296
+ if (!/^\d+$/.test(partA) || !/^\d+$/.test(partB)) return null;
297
+ return `${partA}.${partB}`;
298
+ }
240
299
  async fetchSession() {
241
300
  var _a;
242
301
  const locationId = this.getLocationId();
302
+ this.captureGA4ClientId();
243
303
  if (typeof window !== "undefined" && window.__cfTracking) {
244
304
  try {
245
305
  const data2 = await window.__cfTracking;
@@ -254,6 +314,13 @@ var CallForge = class _CallForge {
254
314
  }
255
315
  const cached = this.cache.get(locationId);
256
316
  if (cached) {
317
+ const autoParams2 = this.getAutoParams();
318
+ const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), cached.params), this.customParams);
319
+ if (params2.ga4ClientId && cached.params.ga4ClientId !== params2.ga4ClientId) {
320
+ const data2 = await this.fetchFromApi(locationId, cached.sessionToken, params2);
321
+ this.saveToCache(locationId, data2, params2);
322
+ return this.formatApiResponse(data2);
323
+ }
257
324
  return this.formatSession(cached);
258
325
  }
259
326
  const autoParams = this.getAutoParams();
@@ -327,6 +394,16 @@ var CallForge = class _CallForge {
327
394
  location: data.location
328
395
  };
329
396
  }
397
+ async syncParamsToCallForgeIfPossible() {
398
+ const locationId = this.getLocationId();
399
+ const sessionToken = this.cache.getSessionToken(locationId);
400
+ if (!sessionToken) return;
401
+ const autoParams = this.getAutoParams();
402
+ const cachedParams = this.cache.getParams();
403
+ const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
404
+ const data = await this.fetchFromApi(locationId, sessionToken, params);
405
+ this.saveToCache(locationId, data, params);
406
+ }
330
407
  buildUrl(locationId, sessionToken, params) {
331
408
  const { categoryId, endpoint } = this.config;
332
409
  const queryParams = {
@@ -371,6 +448,8 @@ var u=new URLSearchParams(location.search);
371
448
  var loc=u.get('loc_physical_ms');
372
449
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
373
450
  var p={};
451
+ var m=document.cookie.match(/(?:^|;\\s*)_ga=([^;]+)/);
452
+ if(m){try{var g=decodeURIComponent(m[1]||'');var s=g.split('.');if(s.length>=2){var a=s[s.length-2],b=s[s.length-1];if(/^\\d+$/.test(a)&&/^\\d+$/.test(b))p.ga4ClientId=a+'.'+b}}catch(e){}}
374
453
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
375
454
  var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
376
455
  var key='cf_tracking_v1_'+site+'_${categoryId}';
package/dist/index.mjs CHANGED
@@ -131,6 +131,7 @@ var CallForge = class _CallForge {
131
131
  };
132
132
  this.cache = new TrackingCache(config.categoryId, config.siteKey);
133
133
  this.captureGA4ClientId();
134
+ this.startGA4ClientIdPolling();
134
135
  }
135
136
  /**
136
137
  * Initialize the CallForge tracking client.
@@ -185,10 +186,28 @@ var CallForge = class _CallForge {
185
186
  });
186
187
  }
187
188
  /**
188
- * Set custom tracking parameters for the next session fetch.
189
+ * Set custom tracking parameters for attribution.
190
+ *
191
+ * When possible, the client will also refresh the session once to sync
192
+ * the updated params to CallForge (so server-side events can use them).
189
193
  */
190
194
  async setParams(params) {
191
195
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
196
+ const sync = async () => {
197
+ try {
198
+ await this.syncParamsToCallForgeIfPossible();
199
+ } catch (e) {
200
+ }
201
+ };
202
+ if (this.sessionPromise) {
203
+ try {
204
+ await this.sessionPromise;
205
+ } catch (e) {
206
+ }
207
+ await sync();
208
+ } else {
209
+ await sync();
210
+ }
192
211
  }
193
212
  /**
194
213
  * Get the currently queued custom params.
@@ -197,25 +216,66 @@ var CallForge = class _CallForge {
197
216
  return __spreadValues({}, this.customParams);
198
217
  }
199
218
  /**
200
- * Capture the GA4 client ID using gtag callback.
201
- * Only runs if ga4MeasurementId is configured.
219
+ * Capture the GA4 client ID.
220
+ *
221
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
222
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
202
223
  */
203
224
  captureGA4ClientId() {
204
- if (!this.config.ga4MeasurementId) {
225
+ if (this.customParams.ga4ClientId) {
205
226
  return;
206
227
  }
207
- if (typeof window === "undefined" || !window.gtag) {
228
+ const fromCookie = this.getGA4ClientIdFromCookie();
229
+ if (fromCookie) {
230
+ this.setParams({ ga4ClientId: fromCookie });
208
231
  return;
209
232
  }
233
+ if (!this.config.ga4MeasurementId) return;
234
+ if (typeof window === "undefined" || !window.gtag) return;
210
235
  window.gtag("get", this.config.ga4MeasurementId, "client_id", (clientId) => {
211
236
  if (clientId) {
212
237
  this.setParams({ ga4ClientId: clientId });
213
238
  }
214
239
  });
215
240
  }
241
+ startGA4ClientIdPolling() {
242
+ if (typeof window === "undefined") return;
243
+ if (this.customParams.ga4ClientId) return;
244
+ const MAX_ATTEMPTS = 20;
245
+ const INTERVAL_MS = 250;
246
+ let attempts = 0;
247
+ const poll = () => {
248
+ if (this.customParams.ga4ClientId) return;
249
+ this.captureGA4ClientId();
250
+ attempts += 1;
251
+ if (this.customParams.ga4ClientId) return;
252
+ if (attempts >= MAX_ATTEMPTS) return;
253
+ window.setTimeout(poll, INTERVAL_MS);
254
+ };
255
+ window.setTimeout(poll, INTERVAL_MS);
256
+ }
257
+ /**
258
+ * Extract GA4 client_id from the `_ga` cookie.
259
+ *
260
+ * Example cookie value: `GA1.1.1234567890.1234567890`
261
+ * Returns: `1234567890.1234567890`
262
+ */
263
+ getGA4ClientIdFromCookie() {
264
+ if (typeof document === "undefined") return null;
265
+ const match = document.cookie.match(/(?:^|;\s*)_ga=([^;]+)/);
266
+ if (!match) return null;
267
+ const cookieValue = decodeURIComponent(match[1] || "");
268
+ const parts = cookieValue.split(".");
269
+ if (parts.length < 2) return null;
270
+ const partA = parts[parts.length - 2];
271
+ const partB = parts[parts.length - 1];
272
+ if (!/^\d+$/.test(partA) || !/^\d+$/.test(partB)) return null;
273
+ return `${partA}.${partB}`;
274
+ }
216
275
  async fetchSession() {
217
276
  var _a;
218
277
  const locationId = this.getLocationId();
278
+ this.captureGA4ClientId();
219
279
  if (typeof window !== "undefined" && window.__cfTracking) {
220
280
  try {
221
281
  const data2 = await window.__cfTracking;
@@ -230,6 +290,13 @@ var CallForge = class _CallForge {
230
290
  }
231
291
  const cached = this.cache.get(locationId);
232
292
  if (cached) {
293
+ const autoParams2 = this.getAutoParams();
294
+ const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), cached.params), this.customParams);
295
+ if (params2.ga4ClientId && cached.params.ga4ClientId !== params2.ga4ClientId) {
296
+ const data2 = await this.fetchFromApi(locationId, cached.sessionToken, params2);
297
+ this.saveToCache(locationId, data2, params2);
298
+ return this.formatApiResponse(data2);
299
+ }
233
300
  return this.formatSession(cached);
234
301
  }
235
302
  const autoParams = this.getAutoParams();
@@ -303,6 +370,16 @@ var CallForge = class _CallForge {
303
370
  location: data.location
304
371
  };
305
372
  }
373
+ async syncParamsToCallForgeIfPossible() {
374
+ const locationId = this.getLocationId();
375
+ const sessionToken = this.cache.getSessionToken(locationId);
376
+ if (!sessionToken) return;
377
+ const autoParams = this.getAutoParams();
378
+ const cachedParams = this.cache.getParams();
379
+ const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
380
+ const data = await this.fetchFromApi(locationId, sessionToken, params);
381
+ this.saveToCache(locationId, data, params);
382
+ }
306
383
  buildUrl(locationId, sessionToken, params) {
307
384
  const { categoryId, endpoint } = this.config;
308
385
  const queryParams = {
@@ -347,6 +424,8 @@ var u=new URLSearchParams(location.search);
347
424
  var loc=u.get('loc_physical_ms');
348
425
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
349
426
  var p={};
427
+ var m=document.cookie.match(/(?:^|;\\s*)_ga=([^;]+)/);
428
+ if(m){try{var g=decodeURIComponent(m[1]||'');var s=g.split('.');if(s.length>=2){var a=s[s.length-2],b=s[s.length-1];if(/^\\d+$/.test(a)&&/^\\d+$/.test(b))p.ga4ClientId=a+'.'+b}}catch(e){}}
350
429
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
351
430
  var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
352
431
  var key='cf_tracking_v1_'+site+'_${categoryId}';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callforge/tracking-client",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -17,17 +17,17 @@
17
17
  "files": [
18
18
  "dist"
19
19
  ],
20
+ "devDependencies": {
21
+ "jsdom": "^27.4.0",
22
+ "tsup": "^8.0.0",
23
+ "typescript": "^5.3.0",
24
+ "vitest": "^1.6.0",
25
+ "@callforge/tsconfig": "0.0.0"
26
+ },
20
27
  "scripts": {
21
28
  "build": "tsup src/index.ts --format esm,cjs --dts",
22
29
  "clean": "rm -rf dist",
23
30
  "test": "vitest run",
24
31
  "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"
32
32
  }
33
- }
33
+ }