@callforge/tracking-client 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
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.3.0** - Added automatic GA4 integration for sending call events to Google Analytics 4.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -51,7 +53,37 @@ client.onReady((session) => {
51
53
  });
52
54
  ```
53
55
 
54
- ### 3. Track conversion parameters (optional)
56
+ ### 3. GA4 Integration (automatic)
57
+
58
+ The client automatically captures the GA4 client ID from the `_ga` cookie and associates it with the tracking session. This enables CallForge to send call events (phone call, conversion, billable conversion) to your Google Analytics 4 property.
59
+
60
+ **Requirements:**
61
+ 1. Google Analytics 4 must be installed on your site
62
+ 2. Configure GA4 credentials in CallForge dashboard (Categories → Edit → GA4 tab)
63
+
64
+ **How it works:**
65
+ - On initialization, the client checks for the `_ga` cookie
66
+ - If not found immediately (GA4 script may load async), polls every 500ms for up to 10 seconds
67
+ - Once found, extracts the client ID and sends it to CallForge via `setParams()`
68
+ - If session already exists, uses PATCH to update with the GA4 client ID
69
+
70
+ **Manual override:** If you need to pass a custom GA4 client ID:
71
+
72
+ ```typescript
73
+ client.setParams({
74
+ ga4ClientId: '1234567890.1234567890',
75
+ });
76
+ ```
77
+
78
+ **Extract GA4 client ID yourself:**
79
+
80
+ ```typescript
81
+ import { getGA4ClientId } from '@callforge/tracking-client';
82
+
83
+ const clientId = getGA4ClientId(); // "1234567890.1234567890" or null
84
+ ```
85
+
86
+ ### 4. Track conversion parameters (optional)
55
87
 
56
88
  The client automatically captures ad platform click IDs from the URL:
57
89
 
@@ -150,19 +182,35 @@ const html = getPreloadSnippet({
150
182
  });
151
183
  ```
152
184
 
185
+ ### `getGA4ClientId()`
186
+
187
+ Extract the GA4 client ID from the `_ga` cookie.
188
+
189
+ ```typescript
190
+ import { getGA4ClientId } from '@callforge/tracking-client';
191
+
192
+ const clientId = getGA4ClientId();
193
+ // Returns: "1234567890.1234567890" or null if cookie not found
194
+ ```
195
+
196
+ **Cookie format:** `GA1.1.1234567890.1234567890`
197
+
198
+ The client ID is the last two dot-separated segments (timestamp.random).
199
+
153
200
  ## Tracking Parameters
154
201
 
155
202
  ### Auto-Capture
156
203
 
157
- The client automatically extracts these parameters from the URL on first visit:
204
+ The client automatically extracts these parameters:
158
205
 
159
- | Parameter | Source |
160
- |-----------|--------|
161
- | `gclid` | Google Ads Click ID |
162
- | `gbraid` | Google app-to-web (iOS 14+) |
163
- | `wbraid` | Google web-to-app |
164
- | `msclkid` | Microsoft/Bing Ads |
165
- | `fbclid` | Facebook/Meta Ads |
206
+ | Parameter | Source | Capture Method |
207
+ |-----------|--------|----------------|
208
+ | `gclid` | Google Ads Click ID | URL query param |
209
+ | `gbraid` | Google app-to-web (iOS 14+) | URL query param |
210
+ | `wbraid` | Google web-to-app | URL query param |
211
+ | `msclkid` | Microsoft/Bing Ads | URL query param |
212
+ | `fbclid` | Facebook/Meta Ads | URL query param |
213
+ | `ga4ClientId` | Google Analytics 4 | `_ga` cookie (polled) |
166
214
 
167
215
  ### Persistence
168
216
 
@@ -214,21 +262,53 @@ import type {
214
262
  TrackingParams,
215
263
  ReadyCallback,
216
264
  } from '@callforge/tracking-client';
265
+
266
+ import { getGA4ClientId } from '@callforge/tracking-client';
217
267
  ```
218
268
 
219
269
  ### TrackingParams
220
270
 
221
271
  ```typescript
222
272
  interface TrackingParams {
223
- gclid?: string; // Google Ads Click ID
224
- gbraid?: string; // Google app-to-web (iOS 14+)
225
- wbraid?: string; // Google web-to-app
226
- msclkid?: string; // Microsoft/Bing Ads
227
- fbclid?: string; // Facebook/Meta Ads
273
+ gclid?: string; // Google Ads Click ID
274
+ gbraid?: string; // Google app-to-web (iOS 14+)
275
+ wbraid?: string; // Google web-to-app
276
+ msclkid?: string; // Microsoft/Bing Ads
277
+ fbclid?: string; // Facebook/Meta Ads
278
+ ga4ClientId?: string; // Google Analytics 4 Client ID (auto-captured from _ga cookie)
228
279
  [key: string]: string | undefined; // Custom params
229
280
  }
230
281
  ```
231
282
 
283
+ ## GA4 Events
284
+
285
+ When GA4 is configured for a category, CallForge sends these events to Google Analytics 4 via the Measurement Protocol:
286
+
287
+ | Event Name | When Sent | Description |
288
+ |------------|-----------|-------------|
289
+ | `phone_call` | Call initiated | A phone call was placed to the tracking number |
290
+ | `call_conversion` | Call qualified | The call met conversion criteria (e.g., duration threshold) |
291
+ | `call_conversion_billable` | Call billable | The call was both qualified AND billable |
292
+
293
+ **Event Parameters:**
294
+
295
+ All events include:
296
+ - `session_id` - CallForge session ID
297
+ - `phone_number` - Tracking number dialed
298
+ - `category` - Category slug
299
+
300
+ `call_conversion` and `call_conversion_billable` also include:
301
+ - `call_duration` - Call duration in seconds
302
+ - `buyer` - Buyer the call was routed to (if applicable)
303
+
304
+ **Setup:**
305
+ 1. In Google Analytics 4, go to Admin → Data Streams → your web stream
306
+ 2. Copy the **Measurement ID** (e.g., `G-XXXXXXXXXX`)
307
+ 3. Go to Admin → Data Streams → Measurement Protocol API secrets → Create
308
+ 4. Copy the **API Secret**
309
+ 5. In CallForge dashboard: Categories → Edit category → GA4 tab
310
+ 6. Enable GA4, paste Measurement ID and API Secret, save
311
+
232
312
  ## Environment URLs
233
313
 
234
314
  | Environment | Endpoint |
package/dist/index.d.mts CHANGED
@@ -72,11 +72,21 @@ declare global {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * Extract GA4 client_id from the _ga cookie.
77
+ * Cookie format: GA1.1.1234567890.1234567890
78
+ * Client ID is the last two segments: 1234567890.1234567890
79
+ */
80
+ declare function getGA4ClientId(): string | null;
75
81
  declare class CallForge {
76
82
  private readonly config;
77
83
  private readonly cache;
78
84
  private sessionPromise;
79
85
  private customParams;
86
+ private ga4PollInterval;
87
+ private ga4PollTimeout;
88
+ private sessionId;
89
+ private sessionCreated;
80
90
  private constructor();
81
91
  /**
82
92
  * Initialize the CallForge tracking client.
@@ -94,9 +104,22 @@ declare class CallForge {
94
104
  onReady(callback: ReadyCallback): void;
95
105
  /**
96
106
  * Set custom tracking parameters.
97
- * Merges with existing params (new values override).
107
+ * If session already exists, sends PATCH to update.
108
+ * Otherwise queues params for next session request.
109
+ */
110
+ setParams(params: Record<string, string>): Promise<void>;
111
+ /**
112
+ * Get the currently queued custom params.
113
+ */
114
+ getQueuedParams(): TrackingParams;
115
+ private patchSession;
116
+ /**
117
+ * Start polling for the GA4 _ga cookie.
118
+ * If found immediately, calls setParams right away.
119
+ * Otherwise polls every 500ms for up to 10 seconds.
98
120
  */
99
- setParams(params: Record<string, string>): void;
121
+ startGA4CookiePolling(): void;
122
+ private stopGA4CookiePolling;
100
123
  private fetchSession;
101
124
  private getLocationId;
102
125
  private getAutoParams;
@@ -124,4 +147,4 @@ declare class CallForge {
124
147
  */
125
148
  declare function getPreloadSnippet(config: CallForgeConfig): string;
126
149
 
127
- export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
150
+ export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getGA4ClientId, getPreloadSnippet };
package/dist/index.d.ts CHANGED
@@ -72,11 +72,21 @@ declare global {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * Extract GA4 client_id from the _ga cookie.
77
+ * Cookie format: GA1.1.1234567890.1234567890
78
+ * Client ID is the last two segments: 1234567890.1234567890
79
+ */
80
+ declare function getGA4ClientId(): string | null;
75
81
  declare class CallForge {
76
82
  private readonly config;
77
83
  private readonly cache;
78
84
  private sessionPromise;
79
85
  private customParams;
86
+ private ga4PollInterval;
87
+ private ga4PollTimeout;
88
+ private sessionId;
89
+ private sessionCreated;
80
90
  private constructor();
81
91
  /**
82
92
  * Initialize the CallForge tracking client.
@@ -94,9 +104,22 @@ declare class CallForge {
94
104
  onReady(callback: ReadyCallback): void;
95
105
  /**
96
106
  * Set custom tracking parameters.
97
- * Merges with existing params (new values override).
107
+ * If session already exists, sends PATCH to update.
108
+ * Otherwise queues params for next session request.
109
+ */
110
+ setParams(params: Record<string, string>): Promise<void>;
111
+ /**
112
+ * Get the currently queued custom params.
113
+ */
114
+ getQueuedParams(): TrackingParams;
115
+ private patchSession;
116
+ /**
117
+ * Start polling for the GA4 _ga cookie.
118
+ * If found immediately, calls setParams right away.
119
+ * Otherwise polls every 500ms for up to 10 seconds.
98
120
  */
99
- setParams(params: Record<string, string>): void;
121
+ startGA4CookiePolling(): void;
122
+ private stopGA4CookiePolling;
100
123
  private fetchSession;
101
124
  private getLocationId;
102
125
  private getAutoParams;
@@ -124,4 +147,4 @@ declare class CallForge {
124
147
  */
125
148
  declare function getPreloadSnippet(config: CallForgeConfig): string;
126
149
 
127
- export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
150
+ export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getGA4ClientId, getPreloadSnippet };
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
35
35
  var index_exports = {};
36
36
  __export(index_exports, {
37
37
  CallForge: () => CallForge,
38
+ getGA4ClientId: () => getGA4ClientId,
38
39
  getPreloadSnippet: () => getPreloadSnippet
39
40
  });
40
41
  module.exports = __toCommonJS(index_exports);
@@ -129,17 +130,27 @@ var TrackingCache = class {
129
130
 
130
131
  // src/client.ts
131
132
  var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
133
+ function getGA4ClientId() {
134
+ if (typeof document === "undefined") return null;
135
+ const match = document.cookie.match(/_ga=GA\d+\.\d+\.(.+?)(?:;|$)/);
136
+ return match ? match[1] : null;
137
+ }
132
138
  var FETCH_TIMEOUT_MS = 1e4;
133
139
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid"];
134
140
  var CallForge = class _CallForge {
135
141
  constructor(config) {
136
142
  this.sessionPromise = null;
137
143
  this.customParams = {};
144
+ this.ga4PollInterval = null;
145
+ this.ga4PollTimeout = null;
146
+ this.sessionId = null;
147
+ this.sessionCreated = false;
138
148
  this.config = {
139
149
  categoryId: config.categoryId,
140
150
  endpoint: config.endpoint || DEFAULT_ENDPOINT
141
151
  };
142
152
  this.cache = new TrackingCache(config.categoryId);
153
+ this.startGA4CookiePolling();
143
154
  }
144
155
  /**
145
156
  * Initialize the CallForge tracking client.
@@ -168,16 +179,80 @@ var CallForge = class _CallForge {
168
179
  }
169
180
  /**
170
181
  * Set custom tracking parameters.
171
- * Merges with existing params (new values override).
182
+ * If session already exists, sends PATCH to update.
183
+ * Otherwise queues params for next session request.
172
184
  */
173
- setParams(params) {
185
+ async setParams(params) {
174
186
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
187
+ if (this.sessionCreated && this.sessionId) {
188
+ await this.patchSession(params);
189
+ }
190
+ }
191
+ /**
192
+ * Get the currently queued custom params.
193
+ */
194
+ getQueuedParams() {
195
+ return __spreadValues({}, this.customParams);
196
+ }
197
+ async patchSession(params) {
198
+ try {
199
+ const response = await fetch(
200
+ `${this.config.endpoint}/v1/tracking/session/${this.sessionId}`,
201
+ {
202
+ method: "PATCH",
203
+ headers: { "Content-Type": "application/json" },
204
+ body: JSON.stringify(params)
205
+ }
206
+ );
207
+ if (!response.ok) {
208
+ console.warn("[CallForge] Failed to patch session:", response.status);
209
+ }
210
+ } catch (error) {
211
+ console.warn("[CallForge] Failed to patch session:", error);
212
+ }
213
+ }
214
+ /**
215
+ * Start polling for the GA4 _ga cookie.
216
+ * If found immediately, calls setParams right away.
217
+ * Otherwise polls every 500ms for up to 10 seconds.
218
+ */
219
+ startGA4CookiePolling() {
220
+ if (this.ga4PollInterval !== null || this.ga4PollTimeout !== null) {
221
+ return;
222
+ }
223
+ const clientId = getGA4ClientId();
224
+ if (clientId) {
225
+ this.setParams({ ga4ClientId: clientId });
226
+ return;
227
+ }
228
+ this.ga4PollInterval = setInterval(() => {
229
+ const clientId2 = getGA4ClientId();
230
+ if (clientId2) {
231
+ this.setParams({ ga4ClientId: clientId2 });
232
+ this.stopGA4CookiePolling();
233
+ }
234
+ }, 500);
235
+ this.ga4PollTimeout = setTimeout(() => {
236
+ this.stopGA4CookiePolling();
237
+ }, 1e4);
238
+ }
239
+ stopGA4CookiePolling() {
240
+ if (this.ga4PollInterval) {
241
+ clearInterval(this.ga4PollInterval);
242
+ this.ga4PollInterval = null;
243
+ }
244
+ if (this.ga4PollTimeout) {
245
+ clearTimeout(this.ga4PollTimeout);
246
+ this.ga4PollTimeout = null;
247
+ }
175
248
  }
176
249
  async fetchSession() {
177
250
  const locationId = this.getLocationId();
178
251
  if (locationId) {
179
252
  const cached = this.cache.get(locationId);
180
253
  if (cached) {
254
+ this.sessionId = cached.sessionId;
255
+ this.sessionCreated = true;
181
256
  return this.formatSession(cached);
182
257
  }
183
258
  }
@@ -188,15 +263,19 @@ var CallForge = class _CallForge {
188
263
  try {
189
264
  const data2 = await window.__cfTracking;
190
265
  this.saveToCache(locationId, data2, params);
266
+ this.sessionId = data2.sessionId;
267
+ this.sessionCreated = true;
191
268
  return this.formatApiResponse(data2);
192
269
  } catch (e) {
193
270
  }
194
271
  }
195
- const sessionId = locationId ? this.cache.getSessionId(locationId) : null;
196
- const data = await this.fetchFromApi(locationId, sessionId, params);
272
+ const cachedSessionId = locationId ? this.cache.getSessionId(locationId) : null;
273
+ const data = await this.fetchFromApi(locationId, cachedSessionId, params);
197
274
  if (locationId) {
198
275
  this.saveToCache(locationId, data, params);
199
276
  }
277
+ this.sessionId = data.sessionId;
278
+ this.sessionCreated = true;
200
279
  return this.formatApiResponse(data);
201
280
  }
202
281
  getLocationId() {
@@ -326,5 +405,6 @@ window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.js
326
405
  // Annotate the CommonJS export names for ESM import in node:
327
406
  0 && (module.exports = {
328
407
  CallForge,
408
+ getGA4ClientId,
329
409
  getPreloadSnippet
330
410
  });
package/dist/index.mjs CHANGED
@@ -105,17 +105,27 @@ var TrackingCache = class {
105
105
 
106
106
  // src/client.ts
107
107
  var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
108
+ function getGA4ClientId() {
109
+ if (typeof document === "undefined") return null;
110
+ const match = document.cookie.match(/_ga=GA\d+\.\d+\.(.+?)(?:;|$)/);
111
+ return match ? match[1] : null;
112
+ }
108
113
  var FETCH_TIMEOUT_MS = 1e4;
109
114
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid"];
110
115
  var CallForge = class _CallForge {
111
116
  constructor(config) {
112
117
  this.sessionPromise = null;
113
118
  this.customParams = {};
119
+ this.ga4PollInterval = null;
120
+ this.ga4PollTimeout = null;
121
+ this.sessionId = null;
122
+ this.sessionCreated = false;
114
123
  this.config = {
115
124
  categoryId: config.categoryId,
116
125
  endpoint: config.endpoint || DEFAULT_ENDPOINT
117
126
  };
118
127
  this.cache = new TrackingCache(config.categoryId);
128
+ this.startGA4CookiePolling();
119
129
  }
120
130
  /**
121
131
  * Initialize the CallForge tracking client.
@@ -144,16 +154,80 @@ var CallForge = class _CallForge {
144
154
  }
145
155
  /**
146
156
  * Set custom tracking parameters.
147
- * Merges with existing params (new values override).
157
+ * If session already exists, sends PATCH to update.
158
+ * Otherwise queues params for next session request.
148
159
  */
149
- setParams(params) {
160
+ async setParams(params) {
150
161
  this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
162
+ if (this.sessionCreated && this.sessionId) {
163
+ await this.patchSession(params);
164
+ }
165
+ }
166
+ /**
167
+ * Get the currently queued custom params.
168
+ */
169
+ getQueuedParams() {
170
+ return __spreadValues({}, this.customParams);
171
+ }
172
+ async patchSession(params) {
173
+ try {
174
+ const response = await fetch(
175
+ `${this.config.endpoint}/v1/tracking/session/${this.sessionId}`,
176
+ {
177
+ method: "PATCH",
178
+ headers: { "Content-Type": "application/json" },
179
+ body: JSON.stringify(params)
180
+ }
181
+ );
182
+ if (!response.ok) {
183
+ console.warn("[CallForge] Failed to patch session:", response.status);
184
+ }
185
+ } catch (error) {
186
+ console.warn("[CallForge] Failed to patch session:", error);
187
+ }
188
+ }
189
+ /**
190
+ * Start polling for the GA4 _ga cookie.
191
+ * If found immediately, calls setParams right away.
192
+ * Otherwise polls every 500ms for up to 10 seconds.
193
+ */
194
+ startGA4CookiePolling() {
195
+ if (this.ga4PollInterval !== null || this.ga4PollTimeout !== null) {
196
+ return;
197
+ }
198
+ const clientId = getGA4ClientId();
199
+ if (clientId) {
200
+ this.setParams({ ga4ClientId: clientId });
201
+ return;
202
+ }
203
+ this.ga4PollInterval = setInterval(() => {
204
+ const clientId2 = getGA4ClientId();
205
+ if (clientId2) {
206
+ this.setParams({ ga4ClientId: clientId2 });
207
+ this.stopGA4CookiePolling();
208
+ }
209
+ }, 500);
210
+ this.ga4PollTimeout = setTimeout(() => {
211
+ this.stopGA4CookiePolling();
212
+ }, 1e4);
213
+ }
214
+ stopGA4CookiePolling() {
215
+ if (this.ga4PollInterval) {
216
+ clearInterval(this.ga4PollInterval);
217
+ this.ga4PollInterval = null;
218
+ }
219
+ if (this.ga4PollTimeout) {
220
+ clearTimeout(this.ga4PollTimeout);
221
+ this.ga4PollTimeout = null;
222
+ }
151
223
  }
152
224
  async fetchSession() {
153
225
  const locationId = this.getLocationId();
154
226
  if (locationId) {
155
227
  const cached = this.cache.get(locationId);
156
228
  if (cached) {
229
+ this.sessionId = cached.sessionId;
230
+ this.sessionCreated = true;
157
231
  return this.formatSession(cached);
158
232
  }
159
233
  }
@@ -164,15 +238,19 @@ var CallForge = class _CallForge {
164
238
  try {
165
239
  const data2 = await window.__cfTracking;
166
240
  this.saveToCache(locationId, data2, params);
241
+ this.sessionId = data2.sessionId;
242
+ this.sessionCreated = true;
167
243
  return this.formatApiResponse(data2);
168
244
  } catch (e) {
169
245
  }
170
246
  }
171
- const sessionId = locationId ? this.cache.getSessionId(locationId) : null;
172
- const data = await this.fetchFromApi(locationId, sessionId, params);
247
+ const cachedSessionId = locationId ? this.cache.getSessionId(locationId) : null;
248
+ const data = await this.fetchFromApi(locationId, cachedSessionId, params);
173
249
  if (locationId) {
174
250
  this.saveToCache(locationId, data, params);
175
251
  }
252
+ this.sessionId = data.sessionId;
253
+ this.sessionCreated = true;
176
254
  return this.formatApiResponse(data);
177
255
  }
178
256
  getLocationId() {
@@ -301,5 +379,6 @@ window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.js
301
379
  }
302
380
  export {
303
381
  CallForge,
382
+ getGA4ClientId,
304
383
  getPreloadSnippet
305
384
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callforge/tracking-client",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -14,17 +14,17 @@
14
14
  "files": [
15
15
  "dist"
16
16
  ],
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
17
  "scripts": {
25
18
  "build": "tsup src/index.ts --format esm,cjs --dts",
26
19
  "clean": "rm -rf dist",
27
20
  "test": "vitest run",
28
21
  "test:watch": "vitest"
22
+ },
23
+ "devDependencies": {
24
+ "@callforge/tsconfig": "workspace:*",
25
+ "jsdom": "^27.4.0",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.3.0",
28
+ "vitest": "^1.6.0"
29
29
  }
30
- }
30
+ }