@callforge/tracking-client 0.6.1 → 0.6.2

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,24 @@ 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
+ - If `ga4MeasurementId` is configured and `gtag` is available, also uses `gtag('get', measurementId, 'client_id', ...)` (with a short retry window).
87
+ - If `ga4ClientId` becomes available after a session is created, the client will refresh once to sync it to CallForge.
87
88
 
88
89
  Manual override:
89
90
 
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
  /**
@@ -121,10 +121,20 @@ declare class CallForge {
121
121
  */
122
122
  getQueuedParams(): TrackingParams;
123
123
  /**
124
- * Capture the GA4 client ID using gtag callback.
125
- * Only runs if ga4MeasurementId is configured.
124
+ * Capture the GA4 client ID.
125
+ *
126
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
127
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
126
128
  */
127
129
  private captureGA4ClientId;
130
+ private startGA4ClientIdPolling;
131
+ /**
132
+ * Extract GA4 client_id from the `_ga` cookie.
133
+ *
134
+ * Example cookie value: `GA1.1.1234567890.1234567890`
135
+ * Returns: `1234567890.1234567890`
136
+ */
137
+ private getGA4ClientIdFromCookie;
128
138
  private fetchSession;
129
139
  private getLocationId;
130
140
  private getAutoParams;
@@ -132,6 +142,7 @@ declare class CallForge {
132
142
  private saveToCache;
133
143
  private formatSession;
134
144
  private formatApiResponse;
145
+ private syncGA4ClientIdIfNeeded;
135
146
  private buildUrl;
136
147
  }
137
148
 
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
  /**
@@ -121,10 +121,20 @@ declare class CallForge {
121
121
  */
122
122
  getQueuedParams(): TrackingParams;
123
123
  /**
124
- * Capture the GA4 client ID using gtag callback.
125
- * Only runs if ga4MeasurementId is configured.
124
+ * Capture the GA4 client ID.
125
+ *
126
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
127
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
126
128
  */
127
129
  private captureGA4ClientId;
130
+ private startGA4ClientIdPolling;
131
+ /**
132
+ * Extract GA4 client_id from the `_ga` cookie.
133
+ *
134
+ * Example cookie value: `GA1.1.1234567890.1234567890`
135
+ * Returns: `1234567890.1234567890`
136
+ */
137
+ private getGA4ClientIdFromCookie;
128
138
  private fetchSession;
129
139
  private getLocationId;
130
140
  private getAutoParams;
@@ -132,6 +142,7 @@ declare class CallForge {
132
142
  private saveToCache;
133
143
  private formatSession;
134
144
  private formatApiResponse;
145
+ private syncGA4ClientIdIfNeeded;
135
146
  private buildUrl;
136
147
  }
137
148
 
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.
@@ -213,6 +214,20 @@ var CallForge = class _CallForge {
213
214
  */
214
215
  async setParams(params) {
215
216
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
217
+ if (params.ga4ClientId) {
218
+ const sync = async () => {
219
+ try {
220
+ await this.syncGA4ClientIdIfNeeded();
221
+ } catch (e) {
222
+ }
223
+ };
224
+ if (this.sessionPromise) {
225
+ void this.sessionPromise.then(sync).catch(() => {
226
+ });
227
+ } else {
228
+ void sync();
229
+ }
230
+ }
216
231
  }
217
232
  /**
218
233
  * Get the currently queued custom params.
@@ -221,25 +236,67 @@ var CallForge = class _CallForge {
221
236
  return __spreadValues({}, this.customParams);
222
237
  }
223
238
  /**
224
- * Capture the GA4 client ID using gtag callback.
225
- * Only runs if ga4MeasurementId is configured.
239
+ * Capture the GA4 client ID.
240
+ *
241
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
242
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
226
243
  */
227
244
  captureGA4ClientId() {
228
- if (!this.config.ga4MeasurementId) {
245
+ if (this.customParams.ga4ClientId) {
229
246
  return;
230
247
  }
231
- if (typeof window === "undefined" || !window.gtag) {
248
+ const fromCookie = this.getGA4ClientIdFromCookie();
249
+ if (fromCookie) {
250
+ this.setParams({ ga4ClientId: fromCookie });
232
251
  return;
233
252
  }
253
+ if (!this.config.ga4MeasurementId) return;
254
+ if (typeof window === "undefined" || !window.gtag) return;
234
255
  window.gtag("get", this.config.ga4MeasurementId, "client_id", (clientId) => {
235
256
  if (clientId) {
236
257
  this.setParams({ ga4ClientId: clientId });
237
258
  }
238
259
  });
239
260
  }
261
+ startGA4ClientIdPolling() {
262
+ if (!this.config.ga4MeasurementId) return;
263
+ if (typeof window === "undefined") return;
264
+ if (this.customParams.ga4ClientId) return;
265
+ const MAX_ATTEMPTS = 20;
266
+ const INTERVAL_MS = 250;
267
+ let attempts = 0;
268
+ const poll = () => {
269
+ if (this.customParams.ga4ClientId) return;
270
+ this.captureGA4ClientId();
271
+ attempts += 1;
272
+ if (this.customParams.ga4ClientId) return;
273
+ if (attempts >= MAX_ATTEMPTS) return;
274
+ window.setTimeout(poll, INTERVAL_MS);
275
+ };
276
+ window.setTimeout(poll, INTERVAL_MS);
277
+ }
278
+ /**
279
+ * Extract GA4 client_id from the `_ga` cookie.
280
+ *
281
+ * Example cookie value: `GA1.1.1234567890.1234567890`
282
+ * Returns: `1234567890.1234567890`
283
+ */
284
+ getGA4ClientIdFromCookie() {
285
+ if (typeof document === "undefined") return null;
286
+ const match = document.cookie.match(/(?:^|;\s*)_ga=([^;]+)/);
287
+ if (!match) return null;
288
+ const cookieValue = decodeURIComponent(match[1] || "");
289
+ const parts = cookieValue.split(".");
290
+ if (parts.length < 2) return null;
291
+ const partA = parts[parts.length - 2];
292
+ const partB = parts[parts.length - 1];
293
+ if (!/^\d+$/.test(partA) || !/^\d+$/.test(partB)) return null;
294
+ return `${partA}.${partB}`;
295
+ }
240
296
  async fetchSession() {
241
297
  var _a;
242
298
  const locationId = this.getLocationId();
299
+ this.captureGA4ClientId();
243
300
  if (typeof window !== "undefined" && window.__cfTracking) {
244
301
  try {
245
302
  const data2 = await window.__cfTracking;
@@ -254,6 +311,13 @@ var CallForge = class _CallForge {
254
311
  }
255
312
  const cached = this.cache.get(locationId);
256
313
  if (cached) {
314
+ const autoParams2 = this.getAutoParams();
315
+ const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), cached.params), this.customParams);
316
+ if (params2.ga4ClientId && cached.params.ga4ClientId !== params2.ga4ClientId) {
317
+ const data2 = await this.fetchFromApi(locationId, cached.sessionToken, params2);
318
+ this.saveToCache(locationId, data2, params2);
319
+ return this.formatApiResponse(data2);
320
+ }
257
321
  return this.formatSession(cached);
258
322
  }
259
323
  const autoParams = this.getAutoParams();
@@ -327,6 +391,21 @@ var CallForge = class _CallForge {
327
391
  location: data.location
328
392
  };
329
393
  }
394
+ async syncGA4ClientIdIfNeeded() {
395
+ const ga4ClientId = this.customParams.ga4ClientId;
396
+ if (!ga4ClientId) return;
397
+ const locationId = this.getLocationId();
398
+ const sessionToken = this.cache.getSessionToken(locationId);
399
+ if (!sessionToken) return;
400
+ const autoParams = this.getAutoParams();
401
+ const cachedParams = this.cache.getParams();
402
+ const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
403
+ if (cachedParams.ga4ClientId === params.ga4ClientId) {
404
+ return;
405
+ }
406
+ const data = await this.fetchFromApi(locationId, sessionToken, params);
407
+ this.saveToCache(locationId, data, params);
408
+ }
330
409
  buildUrl(locationId, sessionToken, params) {
331
410
  const { categoryId, endpoint } = this.config;
332
411
  const queryParams = {
@@ -371,6 +450,8 @@ var u=new URLSearchParams(location.search);
371
450
  var loc=u.get('loc_physical_ms');
372
451
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
373
452
  var p={};
453
+ var m=document.cookie.match(/(?:^|;\\s*)_ga=([^;]+)/);
454
+ 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
455
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
375
456
  var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
376
457
  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.
@@ -189,6 +190,20 @@ var CallForge = class _CallForge {
189
190
  */
190
191
  async setParams(params) {
191
192
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
193
+ if (params.ga4ClientId) {
194
+ const sync = async () => {
195
+ try {
196
+ await this.syncGA4ClientIdIfNeeded();
197
+ } catch (e) {
198
+ }
199
+ };
200
+ if (this.sessionPromise) {
201
+ void this.sessionPromise.then(sync).catch(() => {
202
+ });
203
+ } else {
204
+ void sync();
205
+ }
206
+ }
192
207
  }
193
208
  /**
194
209
  * Get the currently queued custom params.
@@ -197,25 +212,67 @@ var CallForge = class _CallForge {
197
212
  return __spreadValues({}, this.customParams);
198
213
  }
199
214
  /**
200
- * Capture the GA4 client ID using gtag callback.
201
- * Only runs if ga4MeasurementId is configured.
215
+ * Capture the GA4 client ID.
216
+ *
217
+ * Prefer `_ga` cookie parsing (works even when gtag is not loaded yet).
218
+ * If ga4MeasurementId is configured and gtag is available, use gtag as a fallback.
202
219
  */
203
220
  captureGA4ClientId() {
204
- if (!this.config.ga4MeasurementId) {
221
+ if (this.customParams.ga4ClientId) {
205
222
  return;
206
223
  }
207
- if (typeof window === "undefined" || !window.gtag) {
224
+ const fromCookie = this.getGA4ClientIdFromCookie();
225
+ if (fromCookie) {
226
+ this.setParams({ ga4ClientId: fromCookie });
208
227
  return;
209
228
  }
229
+ if (!this.config.ga4MeasurementId) return;
230
+ if (typeof window === "undefined" || !window.gtag) return;
210
231
  window.gtag("get", this.config.ga4MeasurementId, "client_id", (clientId) => {
211
232
  if (clientId) {
212
233
  this.setParams({ ga4ClientId: clientId });
213
234
  }
214
235
  });
215
236
  }
237
+ startGA4ClientIdPolling() {
238
+ if (!this.config.ga4MeasurementId) return;
239
+ if (typeof window === "undefined") return;
240
+ if (this.customParams.ga4ClientId) return;
241
+ const MAX_ATTEMPTS = 20;
242
+ const INTERVAL_MS = 250;
243
+ let attempts = 0;
244
+ const poll = () => {
245
+ if (this.customParams.ga4ClientId) return;
246
+ this.captureGA4ClientId();
247
+ attempts += 1;
248
+ if (this.customParams.ga4ClientId) return;
249
+ if (attempts >= MAX_ATTEMPTS) return;
250
+ window.setTimeout(poll, INTERVAL_MS);
251
+ };
252
+ window.setTimeout(poll, INTERVAL_MS);
253
+ }
254
+ /**
255
+ * Extract GA4 client_id from the `_ga` cookie.
256
+ *
257
+ * Example cookie value: `GA1.1.1234567890.1234567890`
258
+ * Returns: `1234567890.1234567890`
259
+ */
260
+ getGA4ClientIdFromCookie() {
261
+ if (typeof document === "undefined") return null;
262
+ const match = document.cookie.match(/(?:^|;\s*)_ga=([^;]+)/);
263
+ if (!match) return null;
264
+ const cookieValue = decodeURIComponent(match[1] || "");
265
+ const parts = cookieValue.split(".");
266
+ if (parts.length < 2) return null;
267
+ const partA = parts[parts.length - 2];
268
+ const partB = parts[parts.length - 1];
269
+ if (!/^\d+$/.test(partA) || !/^\d+$/.test(partB)) return null;
270
+ return `${partA}.${partB}`;
271
+ }
216
272
  async fetchSession() {
217
273
  var _a;
218
274
  const locationId = this.getLocationId();
275
+ this.captureGA4ClientId();
219
276
  if (typeof window !== "undefined" && window.__cfTracking) {
220
277
  try {
221
278
  const data2 = await window.__cfTracking;
@@ -230,6 +287,13 @@ var CallForge = class _CallForge {
230
287
  }
231
288
  const cached = this.cache.get(locationId);
232
289
  if (cached) {
290
+ const autoParams2 = this.getAutoParams();
291
+ const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), cached.params), this.customParams);
292
+ if (params2.ga4ClientId && cached.params.ga4ClientId !== params2.ga4ClientId) {
293
+ const data2 = await this.fetchFromApi(locationId, cached.sessionToken, params2);
294
+ this.saveToCache(locationId, data2, params2);
295
+ return this.formatApiResponse(data2);
296
+ }
233
297
  return this.formatSession(cached);
234
298
  }
235
299
  const autoParams = this.getAutoParams();
@@ -303,6 +367,21 @@ var CallForge = class _CallForge {
303
367
  location: data.location
304
368
  };
305
369
  }
370
+ async syncGA4ClientIdIfNeeded() {
371
+ const ga4ClientId = this.customParams.ga4ClientId;
372
+ if (!ga4ClientId) return;
373
+ const locationId = this.getLocationId();
374
+ const sessionToken = this.cache.getSessionToken(locationId);
375
+ if (!sessionToken) return;
376
+ const autoParams = this.getAutoParams();
377
+ const cachedParams = this.cache.getParams();
378
+ const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
379
+ if (cachedParams.ga4ClientId === params.ga4ClientId) {
380
+ return;
381
+ }
382
+ const data = await this.fetchFromApi(locationId, sessionToken, params);
383
+ this.saveToCache(locationId, data, params);
384
+ }
306
385
  buildUrl(locationId, sessionToken, params) {
307
386
  const { categoryId, endpoint } = this.config;
308
387
  const queryParams = {
@@ -347,6 +426,8 @@ var u=new URLSearchParams(location.search);
347
426
  var loc=u.get('loc_physical_ms');
348
427
  var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
349
428
  var p={};
429
+ var m=document.cookie.match(/(?:^|;\\s*)_ga=([^;]+)/);
430
+ 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
431
  for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
351
432
  var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
352
433
  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.2",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",