@callforge/tracking-client 0.6.0 → 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
@@ -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.6.0** - Adds `createCallIntent()` helper for click/callback deterministic attribution and fixes ESM/CJS exports.
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,53 @@ 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
- 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.
59
72
 
60
73
  ```typescript
61
74
  const client = CallForge.init({
62
75
  categoryId: 'your-category-id',
63
- ga4MeasurementId: 'G-XXXXXXXXXX', // Required for GA4 integration
76
+ ga4MeasurementId: 'G-XXXXXXXXXX', // Recommended
64
77
  });
65
78
  ```
66
79
 
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)
80
+ Requirements:
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).
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
+ - 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.
76
88
 
77
- **Manual override:** If you need to pass a custom GA4 client ID:
89
+ Manual override:
78
90
 
79
91
  ```typescript
80
92
  client.setParams({
@@ -82,7 +94,7 @@ client.setParams({
82
94
  });
83
95
  ```
84
96
 
85
- ### 4. Track conversion parameters (optional)
97
+ ### 5. Track conversion parameters (optional)
86
98
 
87
99
  The client automatically captures ad platform click IDs from the URL:
88
100
 
@@ -95,14 +107,13 @@ The client automatically captures ad platform click IDs from the URL:
95
107
  You can also add custom parameters:
96
108
 
97
109
  ```typescript
98
- // Add custom tracking parameters
99
110
  client.setParams({
100
111
  customerId: '456',
101
112
  landingPage: 'pricing',
102
113
  });
103
114
 
104
115
  // Parameters are automatically sent with every API request
105
- const session = await client.getSession();
116
+ await client.getSession();
106
117
  ```
107
118
 
108
119
  ## API Reference
@@ -115,18 +126,19 @@ Initialize the tracking client.
115
126
  interface CallForgeConfig {
116
127
  categoryId: string; // Required - which number pool to use
117
128
  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
129
+ siteKey?: string; // Optional - cache partition key (defaults to window.location.hostname)
130
+ ga4MeasurementId?: string; // Optional - GA4 Measurement ID (e.g., 'G-XXXXXXXXXX')
120
131
  }
121
132
  ```
122
133
 
123
134
  ### `client.getSession()`
124
135
 
125
- Get tracking session data. Returns cached data if valid, otherwise fetches from API.
136
+ Get tracking session data. Returns cached data if valid, otherwise fetches from the API.
126
137
 
127
138
  ```typescript
128
139
  interface TrackingSession {
129
- sessionId: string | null;
140
+ sessionToken: string; // Signed, opaque token used to refresh the session
141
+ leaseId: string | null; // Deterministic assignment lease ID (when available)
130
142
  phoneNumber: string | null;
131
143
  location: {
132
144
  city: string;
@@ -136,11 +148,20 @@ interface TrackingSession {
136
148
  }
137
149
  ```
138
150
 
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
151
+ Behavior:
152
+ - Returns cached data if valid.
153
+ - Fetches fresh data when cache is missing/expired.
154
+ - If `loc_physical_ms` is present in the URL, cached sessions are only reused when it matches the cached `locId`.
155
+ - Throws on network errors or API errors.
156
+
157
+ ### `client.createCallIntent()`
158
+
159
+ Create a short-lived call intent token for click/callback deterministic attribution.
160
+
161
+ ```typescript
162
+ const intent = await client.createCallIntent();
163
+ console.log(intent.callIntentToken);
164
+ ```
144
165
 
145
166
  ### `client.onReady(callback)`
146
167
 
@@ -164,11 +185,10 @@ client.setParams({
164
185
  });
165
186
  ```
166
187
 
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
188
+ Behavior:
189
+ - Merges with existing params (later calls override earlier values).
190
+ - Parameters are sent with every `getSession()` API request.
191
+ - Persisted in localStorage alongside session data.
172
192
 
173
193
  ### `getPreloadSnippet(config)`
174
194
 
@@ -180,6 +200,7 @@ import { getPreloadSnippet } from '@callforge/tracking-client';
180
200
  const html = getPreloadSnippet({
181
201
  categoryId: 'your-category-id',
182
202
  endpoint: 'https://tracking.callforge.io', // Optional
203
+ siteKey: 'example.com', // Optional
183
204
  });
184
205
  ```
185
206
 
@@ -194,32 +215,22 @@ The client automatically extracts these parameters from the URL:
194
215
  | `gclid` | Google Ads Click ID |
195
216
  | `gbraid` | Google app-to-web (iOS 14+) |
196
217
  | `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)
218
+ | `msclkid` | Microsoft/Bing Ads Click ID |
219
+ | `fbclid` | Facebook/Meta Ads Click ID |
208
220
 
209
221
  ### API Request Format
210
222
 
211
- Parameters are sent as sorted query string for cache consistency:
223
+ Parameters are sent as a sorted query string for cache consistency:
212
224
 
213
225
  ```
214
- /v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221
226
+ /v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221&sessionToken=...
215
227
  ```
216
228
 
217
229
  ## Caching Behavior
218
230
 
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
231
+ - Cache key: `cf_tracking_v1_<siteKey>_<categoryId>`
232
+ - TTL: controlled by the server `expiresAt` response (currently 30 minutes)
233
+ - Storage: localStorage (falls back to memory if unavailable)
223
234
 
224
235
  ## Error Handling
225
236
 
@@ -229,7 +240,7 @@ try {
229
240
  if (session.phoneNumber) {
230
241
  // Use phone number
231
242
  } else {
232
- // No phone number available for this location
243
+ // No phone number available
233
244
  }
234
245
  } catch (err) {
235
246
  // Network error or API error
@@ -248,52 +259,10 @@ import type {
248
259
  TrackingLocation,
249
260
  TrackingParams,
250
261
  ReadyCallback,
262
+ CallIntentResponse,
251
263
  } from '@callforge/tracking-client';
252
264
  ```
253
265
 
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
266
  ## Environment URLs
298
267
 
299
268
  | Environment | Endpoint |
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.0",
3
+ "version": "0.6.2",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",