@callforge/tracking-client 0.8.0 → 0.9.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
@@ -19,6 +19,19 @@ Client integration requirements:
19
19
  - Keep handling `phoneNumber` and `leaseId` separately. A request can return a phone number with `leaseId: null` when lease assignment is intentionally suppressed.
20
20
  - For attribution/scale metrics, treat `leaseId` as the source of truth for lease-backed traffic.
21
21
 
22
+ ## Realtime Data Layer Upgrade (v0.9+)
23
+
24
+ This release adds explicit browser helpers for the new realtime layer:
25
+ - `client.linkPhoneCall({ phoneNumber })` for strict web-click to phone-call linkage.
26
+ - `client.setWebZip(zipCode, { source })` for immediate web ZIP availability to call processing.
27
+
28
+ Integration checklist:
29
+ - Call `linkPhoneCall` immediately before opening a `tel:` link.
30
+ - Pass the exact dialed number string (`+1...`) used by the link.
31
+ - Continue dialing even if `linkPhoneCall` fails (best-effort attribution assist, not UX-blocking).
32
+ - Call `setWebZip` when the visitor selects or types a ZIP.
33
+ - Keep `setParams` for broader attribution params; use `setWebZip` for fast ZIP propagation.
34
+
22
35
  ## Quick Start
23
36
 
24
37
  ### 1. Add preload snippet to `<head>` (required for deterministic leases)
@@ -84,7 +97,34 @@ Notes:
84
97
  - `callIntentToken` is short-lived and single-use.
85
98
  - Treat it as an opaque secret (do not log it).
86
99
 
87
- ### 4. GA4 Integration
100
+ ### 4. Realtime call-link + web ZIP helpers (recommended)
101
+
102
+ Use explicit helpers to write realtime data from the browser.
103
+
104
+ ```typescript
105
+ const client = CallForge.init({ categoryId: 'your-category-id' });
106
+
107
+ async function dialTrackedNumber(phoneNumber: string) {
108
+ try {
109
+ // Default TTL is 30s (clamped server-side to 5-120s).
110
+ await client.linkPhoneCall({ phoneNumber });
111
+ } finally {
112
+ // Do not block dialing on telemetry failure.
113
+ window.location.href = `tel:${phoneNumber}`;
114
+ }
115
+ }
116
+
117
+ // Example ZIP picker handler
118
+ await client.setWebZip('30309', { source: 'manual' });
119
+ ```
120
+
121
+ Notes:
122
+ - `linkPhoneCall` is optimized for mobile tap-to-call timing.
123
+ - `setWebZip` accepts `manual` (typed) and `suggested` (picked from options).
124
+ - `setWebZip` validates ZIP format (`12345`) client-side before sending.
125
+ - `setWebZip` also sends `webZip` as a custom session param in a best-effort sync backup path.
126
+
127
+ ### 5. GA4 Integration
88
128
 
89
129
  To enable GA4 call event tracking, CallForge needs the GA4 `client_id` for the visitor (from the `_ga` cookie).
90
130
  Optionally provide your GA4 Measurement ID to improve client ID capture reliability when Google Analytics loads late.
@@ -114,7 +154,7 @@ client.setParams({
114
154
  });
115
155
  ```
116
156
 
117
- ### 5. Track conversion parameters (optional)
157
+ ### 6. Track conversion parameters (optional)
118
158
 
119
159
  The client automatically captures ad platform click IDs from the URL:
120
160
 
@@ -214,6 +254,41 @@ const intent = await client.createCallIntent();
214
254
  console.log(intent.callIntentToken);
215
255
  ```
216
256
 
257
+ ### `client.linkPhoneCall(input)`
258
+
259
+ Create a short-lived realtime call-link intent keyed by dialed number.
260
+
261
+ ```typescript
262
+ const result = await client.linkPhoneCall({
263
+ phoneNumber: '+13105551234',
264
+ ttlSeconds: 30, // Optional, default 30s
265
+ });
266
+
267
+ console.log(result.status); // 'ready'
268
+ ```
269
+
270
+ Use this right before `tel:` navigation so inbound call handling can perform strict 1:1 consume.
271
+
272
+ ### `client.setWebZip(zipCode, options?)`
273
+
274
+ Store visitor-selected ZIP in the realtime profile layer for immediate call-side enrichment.
275
+
276
+ ```typescript
277
+ const result = await client.setWebZip('30309', {
278
+ source: 'manual', // or 'suggested'
279
+ ttlSeconds: 3600, // Optional
280
+ });
281
+
282
+ if (result.status === 'ready') {
283
+ console.log(result.profile.webZipCode);
284
+ }
285
+ ```
286
+
287
+ Behavior:
288
+ - Rejects invalid ZIP values unless they are 5 digits.
289
+ - Writes profile data keyed by `sessionId`.
290
+ - Also mirrors `webZip` and `webZipSource` into custom params and triggers a best-effort session sync backup.
291
+
217
292
  ### `client.onReady(callback)`
218
293
 
219
294
  Subscribe to session ready event. Callback is called once session data is available.
@@ -325,6 +400,9 @@ import type {
325
400
  ReadyCallback,
326
401
  LocationReadyCallback,
327
402
  CallIntentResponse,
403
+ CallLinkIntentResponse,
404
+ RealtimeProfileResponse,
405
+ RealtimeProfileSource,
328
406
  } from '@callforge/tracking-client';
329
407
  ```
330
408
 
package/dist/index.d.mts CHANGED
@@ -83,6 +83,27 @@ interface CallIntentResponse {
83
83
  expiresAt: string;
84
84
  attributionVersion: 'v1';
85
85
  }
86
+ interface CallLinkIntentResponse {
87
+ status: 'ready';
88
+ intentId: string;
89
+ sessionId: string;
90
+ categoryId: string;
91
+ phoneNumber: string;
92
+ expiresAt: string;
93
+ }
94
+ type RealtimeProfileSource = 'manual' | 'suggested';
95
+ type RealtimeProfileResponse = {
96
+ status: 'ready';
97
+ profile: {
98
+ sessionId: string;
99
+ webZipCode: string;
100
+ webZipSource: RealtimeProfileSource;
101
+ webZipUpdatedAt: string;
102
+ expiresAt: string;
103
+ };
104
+ } | {
105
+ status: 'unavailable';
106
+ };
86
107
  /**
87
108
  * Callback function for onReady subscription.
88
109
  */
@@ -135,6 +156,21 @@ declare class CallForge {
135
156
  * Create a short-lived call intent token for click/callback deterministic attribution.
136
157
  */
137
158
  createCallIntent(): Promise<CallIntentResponse>;
159
+ /**
160
+ * Create a short-lived realtime call-link intent for a specific dialed number.
161
+ * Use this immediately before opening a `tel:` link.
162
+ */
163
+ linkPhoneCall(input: {
164
+ phoneNumber: string;
165
+ ttlSeconds?: number;
166
+ }): Promise<CallLinkIntentResponse>;
167
+ /**
168
+ * Write visitor-selected web ZIP to the realtime data layer.
169
+ */
170
+ setWebZip(zipCode: string, options?: {
171
+ source?: RealtimeProfileSource;
172
+ ttlSeconds?: number;
173
+ }): Promise<RealtimeProfileResponse>;
138
174
  /**
139
175
  * Subscribe to session ready event.
140
176
  * Callback is called once session data is available.
@@ -198,4 +234,4 @@ declare class CallForge {
198
234
  */
199
235
  declare function getPreloadSnippet(config: CallForgeConfig): string;
200
236
 
201
- export { CallForge, type CallForgeConfig, type CallIntentResponse, type LocationReadyCallback, type ReadyCallback, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
237
+ export { CallForge, type CallForgeConfig, type CallIntentResponse, type CallLinkIntentResponse, type LocationReadyCallback, type ReadyCallback, type RealtimeProfileResponse, type RealtimeProfileSource, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
package/dist/index.d.ts CHANGED
@@ -83,6 +83,27 @@ interface CallIntentResponse {
83
83
  expiresAt: string;
84
84
  attributionVersion: 'v1';
85
85
  }
86
+ interface CallLinkIntentResponse {
87
+ status: 'ready';
88
+ intentId: string;
89
+ sessionId: string;
90
+ categoryId: string;
91
+ phoneNumber: string;
92
+ expiresAt: string;
93
+ }
94
+ type RealtimeProfileSource = 'manual' | 'suggested';
95
+ type RealtimeProfileResponse = {
96
+ status: 'ready';
97
+ profile: {
98
+ sessionId: string;
99
+ webZipCode: string;
100
+ webZipSource: RealtimeProfileSource;
101
+ webZipUpdatedAt: string;
102
+ expiresAt: string;
103
+ };
104
+ } | {
105
+ status: 'unavailable';
106
+ };
86
107
  /**
87
108
  * Callback function for onReady subscription.
88
109
  */
@@ -135,6 +156,21 @@ declare class CallForge {
135
156
  * Create a short-lived call intent token for click/callback deterministic attribution.
136
157
  */
137
158
  createCallIntent(): Promise<CallIntentResponse>;
159
+ /**
160
+ * Create a short-lived realtime call-link intent for a specific dialed number.
161
+ * Use this immediately before opening a `tel:` link.
162
+ */
163
+ linkPhoneCall(input: {
164
+ phoneNumber: string;
165
+ ttlSeconds?: number;
166
+ }): Promise<CallLinkIntentResponse>;
167
+ /**
168
+ * Write visitor-selected web ZIP to the realtime data layer.
169
+ */
170
+ setWebZip(zipCode: string, options?: {
171
+ source?: RealtimeProfileSource;
172
+ ttlSeconds?: number;
173
+ }): Promise<RealtimeProfileResponse>;
138
174
  /**
139
175
  * Subscribe to session ready event.
140
176
  * Callback is called once session data is available.
@@ -198,4 +234,4 @@ declare class CallForge {
198
234
  */
199
235
  declare function getPreloadSnippet(config: CallForgeConfig): string;
200
236
 
201
- export { CallForge, type CallForgeConfig, type CallIntentResponse, type LocationReadyCallback, type ReadyCallback, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
237
+ export { CallForge, type CallForgeConfig, type CallIntentResponse, type CallLinkIntentResponse, type LocationReadyCallback, type ReadyCallback, type RealtimeProfileResponse, type RealtimeProfileSource, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
7
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
@@ -17,6 +19,7 @@ var __spreadValues = (a, b) => {
17
19
  }
18
20
  return a;
19
21
  };
22
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
23
  var __export = (target, all) => {
21
24
  for (var name in all)
22
25
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -203,8 +206,11 @@ var LocationCache = class {
203
206
  var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
204
207
  var FETCH_TIMEOUT_MS = 1e4;
205
208
  var CALL_INTENT_TIMEOUT_MS = 8e3;
209
+ var REALTIME_CALL_LINK_TIMEOUT_MS = 5e3;
210
+ var REALTIME_PROFILE_TIMEOUT_MS = 5e3;
206
211
  var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
207
212
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
213
+ var ZIP_CODE_PATTERN = /^\d{5}$/;
208
214
  var CallForge = class _CallForge {
209
215
  constructor(config) {
210
216
  this.bootstrapMemoryCache = null;
@@ -289,6 +295,83 @@ var CallForge = class _CallForge {
289
295
  clearTimeout(timeoutId);
290
296
  }
291
297
  }
298
+ /**
299
+ * Create a short-lived realtime call-link intent for a specific dialed number.
300
+ * Use this immediately before opening a `tel:` link.
301
+ */
302
+ async linkPhoneCall(input) {
303
+ const session = await this.getSession();
304
+ const controller = new AbortController();
305
+ const timeoutId = setTimeout(() => controller.abort(), REALTIME_CALL_LINK_TIMEOUT_MS);
306
+ try {
307
+ const response = await fetch(`${this.config.endpoint}/v1/tracking/call-link-intent`, {
308
+ method: "POST",
309
+ headers: {
310
+ "Content-Type": "application/json"
311
+ },
312
+ credentials: "omit",
313
+ signal: controller.signal,
314
+ body: JSON.stringify({
315
+ sessionToken: session.sessionToken,
316
+ phoneNumber: input.phoneNumber,
317
+ ttlSeconds: input.ttlSeconds
318
+ })
319
+ });
320
+ if (!response.ok) {
321
+ throw new Error(`Realtime call-link API error: ${response.status} ${response.statusText}`);
322
+ }
323
+ return await response.json();
324
+ } finally {
325
+ clearTimeout(timeoutId);
326
+ }
327
+ }
328
+ /**
329
+ * Write visitor-selected web ZIP to the realtime data layer.
330
+ */
331
+ async setWebZip(zipCode, options) {
332
+ var _a;
333
+ const normalizedZip = zipCode.trim();
334
+ if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
335
+ throw new Error("Invalid ZIP code. Expected a 5-digit ZIP.");
336
+ }
337
+ const session = await this.getSession();
338
+ const fallbackSource = (_a = options == null ? void 0 : options.source) != null ? _a : "manual";
339
+ void this.setParams({
340
+ webZip: normalizedZip,
341
+ webZipSource: fallbackSource
342
+ });
343
+ const controller = new AbortController();
344
+ const timeoutId = setTimeout(() => controller.abort(), REALTIME_PROFILE_TIMEOUT_MS);
345
+ try {
346
+ const response = await fetch(`${this.config.endpoint}/v1/tracking/realtime-profile`, {
347
+ method: "POST",
348
+ headers: {
349
+ "Content-Type": "application/json"
350
+ },
351
+ credentials: "omit",
352
+ signal: controller.signal,
353
+ body: JSON.stringify({
354
+ sessionToken: session.sessionToken,
355
+ webZip: normalizedZip,
356
+ source: options == null ? void 0 : options.source,
357
+ ttlSeconds: options == null ? void 0 : options.ttlSeconds
358
+ })
359
+ });
360
+ if (!response.ok) {
361
+ throw new Error(`Realtime profile API error: ${response.status} ${response.statusText}`);
362
+ }
363
+ const result = await response.json();
364
+ if (result.status === "ready") {
365
+ this.customParams = __spreadProps(__spreadValues({}, this.customParams), {
366
+ webZip: result.profile.webZipCode,
367
+ webZipSource: result.profile.webZipSource
368
+ });
369
+ }
370
+ return result;
371
+ } finally {
372
+ clearTimeout(timeoutId);
373
+ }
374
+ }
292
375
  /**
293
376
  * Subscribe to session ready event.
294
377
  * Callback is called once session data is available.
package/dist/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
2
4
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
4
6
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -14,6 +16,7 @@ var __spreadValues = (a, b) => {
14
16
  }
15
17
  return a;
16
18
  };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
17
20
 
18
21
  // src/cache.ts
19
22
  var EXPIRY_BUFFER_MS = 3e4;
@@ -179,8 +182,11 @@ var LocationCache = class {
179
182
  var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
180
183
  var FETCH_TIMEOUT_MS = 1e4;
181
184
  var CALL_INTENT_TIMEOUT_MS = 8e3;
185
+ var REALTIME_CALL_LINK_TIMEOUT_MS = 5e3;
186
+ var REALTIME_PROFILE_TIMEOUT_MS = 5e3;
182
187
  var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
183
188
  var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
189
+ var ZIP_CODE_PATTERN = /^\d{5}$/;
184
190
  var CallForge = class _CallForge {
185
191
  constructor(config) {
186
192
  this.bootstrapMemoryCache = null;
@@ -265,6 +271,83 @@ var CallForge = class _CallForge {
265
271
  clearTimeout(timeoutId);
266
272
  }
267
273
  }
274
+ /**
275
+ * Create a short-lived realtime call-link intent for a specific dialed number.
276
+ * Use this immediately before opening a `tel:` link.
277
+ */
278
+ async linkPhoneCall(input) {
279
+ const session = await this.getSession();
280
+ const controller = new AbortController();
281
+ const timeoutId = setTimeout(() => controller.abort(), REALTIME_CALL_LINK_TIMEOUT_MS);
282
+ try {
283
+ const response = await fetch(`${this.config.endpoint}/v1/tracking/call-link-intent`, {
284
+ method: "POST",
285
+ headers: {
286
+ "Content-Type": "application/json"
287
+ },
288
+ credentials: "omit",
289
+ signal: controller.signal,
290
+ body: JSON.stringify({
291
+ sessionToken: session.sessionToken,
292
+ phoneNumber: input.phoneNumber,
293
+ ttlSeconds: input.ttlSeconds
294
+ })
295
+ });
296
+ if (!response.ok) {
297
+ throw new Error(`Realtime call-link API error: ${response.status} ${response.statusText}`);
298
+ }
299
+ return await response.json();
300
+ } finally {
301
+ clearTimeout(timeoutId);
302
+ }
303
+ }
304
+ /**
305
+ * Write visitor-selected web ZIP to the realtime data layer.
306
+ */
307
+ async setWebZip(zipCode, options) {
308
+ var _a;
309
+ const normalizedZip = zipCode.trim();
310
+ if (!ZIP_CODE_PATTERN.test(normalizedZip)) {
311
+ throw new Error("Invalid ZIP code. Expected a 5-digit ZIP.");
312
+ }
313
+ const session = await this.getSession();
314
+ const fallbackSource = (_a = options == null ? void 0 : options.source) != null ? _a : "manual";
315
+ void this.setParams({
316
+ webZip: normalizedZip,
317
+ webZipSource: fallbackSource
318
+ });
319
+ const controller = new AbortController();
320
+ const timeoutId = setTimeout(() => controller.abort(), REALTIME_PROFILE_TIMEOUT_MS);
321
+ try {
322
+ const response = await fetch(`${this.config.endpoint}/v1/tracking/realtime-profile`, {
323
+ method: "POST",
324
+ headers: {
325
+ "Content-Type": "application/json"
326
+ },
327
+ credentials: "omit",
328
+ signal: controller.signal,
329
+ body: JSON.stringify({
330
+ sessionToken: session.sessionToken,
331
+ webZip: normalizedZip,
332
+ source: options == null ? void 0 : options.source,
333
+ ttlSeconds: options == null ? void 0 : options.ttlSeconds
334
+ })
335
+ });
336
+ if (!response.ok) {
337
+ throw new Error(`Realtime profile API error: ${response.status} ${response.statusText}`);
338
+ }
339
+ const result = await response.json();
340
+ if (result.status === "ready") {
341
+ this.customParams = __spreadProps(__spreadValues({}, this.customParams), {
342
+ webZip: result.profile.webZipCode,
343
+ webZipSource: result.profile.webZipSource
344
+ });
345
+ }
346
+ return result;
347
+ } finally {
348
+ clearTimeout(timeoutId);
349
+ }
350
+ }
268
351
  /**
269
352
  * Subscribe to session ready event.
270
353
  * Callback is called once session data is available.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callforge/tracking-client",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",