@callforge/tracking-client 0.7.0 → 0.8.0
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 +33 -6
- package/dist/index.d.mts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +125 -19
- package/dist/index.mjs +125 -19
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,6 +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
|
|
3
|
+
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,11 +10,20 @@ npm install @callforge/tracking-client
|
|
|
10
10
|
pnpm add @callforge/tracking-client
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Lease Hardening Migration (v0.8+)
|
|
14
|
+
|
|
15
|
+
This release adds bootstrap-token support for lease hardening and bot suppression.
|
|
16
|
+
|
|
17
|
+
Client integration requirements:
|
|
18
|
+
- Add the generated preload snippet in `<head>`. This prefetches bootstrap tokens and keeps session lookup fast.
|
|
19
|
+
- Keep handling `phoneNumber` and `leaseId` separately. A request can return a phone number with `leaseId: null` when lease assignment is intentionally suppressed.
|
|
20
|
+
- For attribution/scale metrics, treat `leaseId` as the source of truth for lease-backed traffic.
|
|
21
|
+
|
|
13
22
|
## Quick Start
|
|
14
23
|
|
|
15
|
-
### 1. Add preload snippet to `<head>` (
|
|
24
|
+
### 1. Add preload snippet to `<head>` (required for deterministic leases)
|
|
16
25
|
|
|
17
|
-
For optimal performance
|
|
26
|
+
For optimal performance and lease hardening, add this snippet to your HTML `<head>`:
|
|
18
27
|
|
|
19
28
|
```typescript
|
|
20
29
|
import { getPreloadSnippet } from '@callforge/tracking-client';
|
|
@@ -43,7 +52,13 @@ const client = CallForge.init({
|
|
|
43
52
|
const { session, location } = client.getSessionAsync();
|
|
44
53
|
|
|
45
54
|
// Location is delivered independently (often faster than phone number assignment)
|
|
46
|
-
console.log(await location);
|
|
55
|
+
console.log(await location);
|
|
56
|
+
// {
|
|
57
|
+
// city: "Woodstock",
|
|
58
|
+
// state: "Georgia",
|
|
59
|
+
// stateCode: "GA",
|
|
60
|
+
// zipOptions: ["30188", "30189", "30066", ...] // may be []
|
|
61
|
+
// } or null
|
|
47
62
|
|
|
48
63
|
// Phone session data (deterministic token + phone number)
|
|
49
64
|
console.log(await session); // { sessionToken, leaseId, phoneNumber }
|
|
@@ -152,6 +167,7 @@ Behavior:
|
|
|
152
167
|
- Returns cached data if valid.
|
|
153
168
|
- Fetches fresh data when cache is missing/expired.
|
|
154
169
|
- If `loc_physical_ms` is present in the URL, cached sessions are only reused when it matches the cached `locId`.
|
|
170
|
+
- If lease assignment is suppressed (for example bot traffic or missing/invalid bootstrap), `phoneNumber` may be present while `leaseId` is `null`.
|
|
155
171
|
- Throws on network errors or API errors.
|
|
156
172
|
|
|
157
173
|
### `client.getLocation()`
|
|
@@ -160,7 +176,16 @@ Get location data only. Returns cached data if valid, otherwise fetches from the
|
|
|
160
176
|
|
|
161
177
|
```typescript
|
|
162
178
|
const location = await client.getLocation();
|
|
163
|
-
// { city, state, stateCode } or null
|
|
179
|
+
// { city, state, stateCode, zipOptions } or null
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
interface TrackingLocation {
|
|
184
|
+
city: string;
|
|
185
|
+
state: string;
|
|
186
|
+
stateCode: string;
|
|
187
|
+
zipOptions?: string[]; // ordered by proximity, may be []
|
|
188
|
+
}
|
|
164
189
|
```
|
|
165
190
|
|
|
166
191
|
### `client.getSessionAsync()`
|
|
@@ -172,6 +197,7 @@ const { session, location } = client.getSessionAsync();
|
|
|
172
197
|
|
|
173
198
|
location.then((loc) => {
|
|
174
199
|
// show city/state ASAP
|
|
200
|
+
// optionally render loc?.zipOptions in a ZIP picker
|
|
175
201
|
});
|
|
176
202
|
|
|
177
203
|
session.then((sess) => {
|
|
@@ -204,7 +230,7 @@ Subscribe to location ready event. Callback is called once location data is avai
|
|
|
204
230
|
|
|
205
231
|
```typescript
|
|
206
232
|
client.onLocationReady((location) => {
|
|
207
|
-
// location is { city, state, stateCode } or null
|
|
233
|
+
// location is { city, state, stateCode, zipOptions } or null
|
|
208
234
|
});
|
|
209
235
|
```
|
|
210
236
|
|
|
@@ -266,6 +292,7 @@ Parameters are sent as a sorted query string for cache consistency:
|
|
|
266
292
|
|
|
267
293
|
- Session cache key: `cf_tracking_v1_<siteKey>_<categoryId>`
|
|
268
294
|
- Location cache key: `cf_location_v1_<siteKey>`
|
|
295
|
+
- Bootstrap cache key: `cf_bootstrap_v1_<siteKey>_<categoryId>`
|
|
269
296
|
- TTL: controlled by the server `expiresAt` response (currently 30 minutes)
|
|
270
297
|
- Storage: localStorage (falls back to memory if unavailable)
|
|
271
298
|
|
package/dist/index.d.mts
CHANGED
|
@@ -21,6 +21,8 @@ interface TrackingLocation {
|
|
|
21
21
|
state: string;
|
|
22
22
|
/** State abbreviation (e.g., "GA") */
|
|
23
23
|
stateCode: string;
|
|
24
|
+
/** Suggested ZIPs ordered by proximity (may be empty) */
|
|
25
|
+
zipOptions?: string[];
|
|
24
26
|
}
|
|
25
27
|
type TrackingLocationSource = 'google_criteria' | 'cloudflare_geo';
|
|
26
28
|
/**
|
|
@@ -70,6 +72,7 @@ interface ApiLocationResponse {
|
|
|
70
72
|
stateCode: string;
|
|
71
73
|
source: TrackingLocationSource;
|
|
72
74
|
} | null;
|
|
75
|
+
zipOptions?: string[];
|
|
73
76
|
expiresAt: number;
|
|
74
77
|
}
|
|
75
78
|
interface CallIntentResponse {
|
|
@@ -100,6 +103,8 @@ declare class CallForge {
|
|
|
100
103
|
private readonly config;
|
|
101
104
|
private readonly cache;
|
|
102
105
|
private readonly locationCache;
|
|
106
|
+
private readonly bootstrapCacheKey;
|
|
107
|
+
private bootstrapMemoryCache;
|
|
103
108
|
private sessionPromise;
|
|
104
109
|
private locationPromise;
|
|
105
110
|
private customParams;
|
|
@@ -171,13 +176,19 @@ declare class CallForge {
|
|
|
171
176
|
private getLocationId;
|
|
172
177
|
private getAutoParams;
|
|
173
178
|
private fetchFromApi;
|
|
179
|
+
private getBootstrapToken;
|
|
174
180
|
private fetchLocationFromApi;
|
|
181
|
+
private getCachedBootstrapToken;
|
|
182
|
+
private saveBootstrapToken;
|
|
175
183
|
private saveToCache;
|
|
176
184
|
private saveLocationToCache;
|
|
185
|
+
private toTrackingLocation;
|
|
186
|
+
private normalizeZipOptions;
|
|
177
187
|
private formatSession;
|
|
178
188
|
private formatApiResponse;
|
|
179
189
|
private syncParamsToCallForgeIfPossible;
|
|
180
190
|
private buildUrl;
|
|
191
|
+
private buildBootstrapUrl;
|
|
181
192
|
private buildLocationUrl;
|
|
182
193
|
}
|
|
183
194
|
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ interface TrackingLocation {
|
|
|
21
21
|
state: string;
|
|
22
22
|
/** State abbreviation (e.g., "GA") */
|
|
23
23
|
stateCode: string;
|
|
24
|
+
/** Suggested ZIPs ordered by proximity (may be empty) */
|
|
25
|
+
zipOptions?: string[];
|
|
24
26
|
}
|
|
25
27
|
type TrackingLocationSource = 'google_criteria' | 'cloudflare_geo';
|
|
26
28
|
/**
|
|
@@ -70,6 +72,7 @@ interface ApiLocationResponse {
|
|
|
70
72
|
stateCode: string;
|
|
71
73
|
source: TrackingLocationSource;
|
|
72
74
|
} | null;
|
|
75
|
+
zipOptions?: string[];
|
|
73
76
|
expiresAt: number;
|
|
74
77
|
}
|
|
75
78
|
interface CallIntentResponse {
|
|
@@ -100,6 +103,8 @@ declare class CallForge {
|
|
|
100
103
|
private readonly config;
|
|
101
104
|
private readonly cache;
|
|
102
105
|
private readonly locationCache;
|
|
106
|
+
private readonly bootstrapCacheKey;
|
|
107
|
+
private bootstrapMemoryCache;
|
|
103
108
|
private sessionPromise;
|
|
104
109
|
private locationPromise;
|
|
105
110
|
private customParams;
|
|
@@ -171,13 +176,19 @@ declare class CallForge {
|
|
|
171
176
|
private getLocationId;
|
|
172
177
|
private getAutoParams;
|
|
173
178
|
private fetchFromApi;
|
|
179
|
+
private getBootstrapToken;
|
|
174
180
|
private fetchLocationFromApi;
|
|
181
|
+
private getCachedBootstrapToken;
|
|
182
|
+
private saveBootstrapToken;
|
|
175
183
|
private saveToCache;
|
|
176
184
|
private saveLocationToCache;
|
|
185
|
+
private toTrackingLocation;
|
|
186
|
+
private normalizeZipOptions;
|
|
177
187
|
private formatSession;
|
|
178
188
|
private formatApiResponse;
|
|
179
189
|
private syncParamsToCallForgeIfPossible;
|
|
180
190
|
private buildUrl;
|
|
191
|
+
private buildBootstrapUrl;
|
|
181
192
|
private buildLocationUrl;
|
|
182
193
|
}
|
|
183
194
|
|
package/dist/index.js
CHANGED
|
@@ -203,9 +203,11 @@ var LocationCache = class {
|
|
|
203
203
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
204
204
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
205
205
|
var CALL_INTENT_TIMEOUT_MS = 8e3;
|
|
206
|
+
var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
|
|
206
207
|
var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
|
|
207
208
|
var CallForge = class _CallForge {
|
|
208
209
|
constructor(config) {
|
|
210
|
+
this.bootstrapMemoryCache = null;
|
|
209
211
|
this.sessionPromise = null;
|
|
210
212
|
this.locationPromise = null;
|
|
211
213
|
this.customParams = {};
|
|
@@ -215,8 +217,10 @@ var CallForge = class _CallForge {
|
|
|
215
217
|
ga4MeasurementId: config.ga4MeasurementId,
|
|
216
218
|
siteKey: config.siteKey
|
|
217
219
|
};
|
|
220
|
+
const resolvedSiteKey = config.siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
218
221
|
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
219
222
|
this.locationCache = new LocationCache(config.siteKey);
|
|
223
|
+
this.bootstrapCacheKey = `cf_bootstrap_v1_${resolvedSiteKey}_${config.categoryId}`;
|
|
220
224
|
this.captureGA4ClientId();
|
|
221
225
|
this.startGA4ClientIdPolling();
|
|
222
226
|
}
|
|
@@ -424,18 +428,14 @@ var CallForge = class _CallForge {
|
|
|
424
428
|
return this.formatApiResponse(data);
|
|
425
429
|
}
|
|
426
430
|
async fetchLocation() {
|
|
427
|
-
var _a;
|
|
431
|
+
var _a, _b;
|
|
428
432
|
const locationId = this.getLocationId();
|
|
429
433
|
if (typeof window !== "undefined" && window.__cfTrackingLocation) {
|
|
430
434
|
try {
|
|
431
435
|
const data2 = await window.__cfTrackingLocation;
|
|
432
436
|
const dataWithExtras = data2;
|
|
433
437
|
const effectiveLocId = (_a = dataWithExtras.locId) != null ? _a : locationId;
|
|
434
|
-
const location2 = data2.location
|
|
435
|
-
city: data2.location.city,
|
|
436
|
-
state: data2.location.state,
|
|
437
|
-
stateCode: data2.location.stateCode
|
|
438
|
-
} : null;
|
|
438
|
+
const location2 = this.toTrackingLocation(data2.location, dataWithExtras.zipOptions);
|
|
439
439
|
this.saveLocationToCache(effectiveLocId, location2, data2.expiresAt);
|
|
440
440
|
return location2;
|
|
441
441
|
} catch (e) {
|
|
@@ -443,14 +443,10 @@ var CallForge = class _CallForge {
|
|
|
443
443
|
}
|
|
444
444
|
const cached = this.locationCache.get(locationId);
|
|
445
445
|
if (cached) {
|
|
446
|
-
return cached.location;
|
|
446
|
+
return this.toTrackingLocation(cached.location, (_b = cached.location) == null ? void 0 : _b.zipOptions);
|
|
447
447
|
}
|
|
448
448
|
const data = await this.fetchLocationFromApi(locationId);
|
|
449
|
-
const location = data.location
|
|
450
|
-
city: data.location.city,
|
|
451
|
-
state: data.location.state,
|
|
452
|
-
stateCode: data.location.stateCode
|
|
453
|
-
} : null;
|
|
449
|
+
const location = this.toTrackingLocation(data.location, data.zipOptions);
|
|
454
450
|
this.saveLocationToCache(locationId, location, data.expiresAt);
|
|
455
451
|
return location;
|
|
456
452
|
}
|
|
@@ -472,7 +468,43 @@ var CallForge = class _CallForge {
|
|
|
472
468
|
return params;
|
|
473
469
|
}
|
|
474
470
|
async fetchFromApi(locationId, sessionToken, params) {
|
|
475
|
-
const
|
|
471
|
+
const controller = new AbortController();
|
|
472
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
473
|
+
try {
|
|
474
|
+
let bootstrapToken = this.getCachedBootstrapToken();
|
|
475
|
+
let response = await fetch(
|
|
476
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
477
|
+
{
|
|
478
|
+
credentials: "omit",
|
|
479
|
+
signal: controller.signal
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
if (response.status === 401) {
|
|
483
|
+
bootstrapToken = await this.getBootstrapToken(true);
|
|
484
|
+
if (bootstrapToken) {
|
|
485
|
+
response = await fetch(
|
|
486
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
487
|
+
{
|
|
488
|
+
credentials: "omit",
|
|
489
|
+
signal: controller.signal
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
496
|
+
}
|
|
497
|
+
return await response.json();
|
|
498
|
+
} finally {
|
|
499
|
+
clearTimeout(timeoutId);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async getBootstrapToken(forceRefresh = false) {
|
|
503
|
+
const cached = this.getCachedBootstrapToken();
|
|
504
|
+
if (!forceRefresh && cached) {
|
|
505
|
+
return cached;
|
|
506
|
+
}
|
|
507
|
+
const url = this.buildBootstrapUrl();
|
|
476
508
|
const controller = new AbortController();
|
|
477
509
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
478
510
|
try {
|
|
@@ -481,9 +513,14 @@ var CallForge = class _CallForge {
|
|
|
481
513
|
signal: controller.signal
|
|
482
514
|
});
|
|
483
515
|
if (!response.ok) {
|
|
484
|
-
|
|
516
|
+
return null;
|
|
485
517
|
}
|
|
486
|
-
|
|
518
|
+
const data = await response.json();
|
|
519
|
+
if (typeof data.bootstrapToken !== "string" || typeof data.expiresAt !== "number") {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
this.saveBootstrapToken(data.bootstrapToken, data.expiresAt);
|
|
523
|
+
return data.bootstrapToken;
|
|
487
524
|
} finally {
|
|
488
525
|
clearTimeout(timeoutId);
|
|
489
526
|
}
|
|
@@ -505,6 +542,40 @@ var CallForge = class _CallForge {
|
|
|
505
542
|
clearTimeout(timeoutId);
|
|
506
543
|
}
|
|
507
544
|
}
|
|
545
|
+
getCachedBootstrapToken() {
|
|
546
|
+
var _a;
|
|
547
|
+
const now = Date.now();
|
|
548
|
+
const fromMemory = this.bootstrapMemoryCache;
|
|
549
|
+
if (fromMemory && fromMemory.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS > now) {
|
|
550
|
+
return fromMemory.token;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const raw = localStorage.getItem(this.bootstrapCacheKey);
|
|
554
|
+
if (!raw) return null;
|
|
555
|
+
const cached = JSON.parse(raw);
|
|
556
|
+
if (typeof cached.token !== "string" || typeof cached.expiresAt !== "number") {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
if (cached.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS <= now) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
return cached.token;
|
|
563
|
+
} catch (e) {
|
|
564
|
+
return (_a = fromMemory == null ? void 0 : fromMemory.token) != null ? _a : null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
saveBootstrapToken(token, expiresAt) {
|
|
568
|
+
const cached = {
|
|
569
|
+
token,
|
|
570
|
+
expiresAt,
|
|
571
|
+
tokenVersion: "b1"
|
|
572
|
+
};
|
|
573
|
+
this.bootstrapMemoryCache = cached;
|
|
574
|
+
try {
|
|
575
|
+
localStorage.setItem(this.bootstrapCacheKey, JSON.stringify(cached));
|
|
576
|
+
} catch (e) {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
508
579
|
saveToCache(locationId, data, params) {
|
|
509
580
|
const cached = {
|
|
510
581
|
locId: locationId != null ? locationId : null,
|
|
@@ -525,6 +596,27 @@ var CallForge = class _CallForge {
|
|
|
525
596
|
tokenVersion: "v1"
|
|
526
597
|
});
|
|
527
598
|
}
|
|
599
|
+
toTrackingLocation(location, zipOptions) {
|
|
600
|
+
if (!location) {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
city: location.city,
|
|
605
|
+
state: location.state,
|
|
606
|
+
stateCode: location.stateCode,
|
|
607
|
+
zipOptions: this.normalizeZipOptions(zipOptions)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
normalizeZipOptions(value) {
|
|
611
|
+
if (!Array.isArray(value)) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
return Array.from(
|
|
615
|
+
new Set(
|
|
616
|
+
value.filter((zip) => typeof zip === "string" && /^\d{5}$/.test(zip))
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
528
620
|
formatSession(cached) {
|
|
529
621
|
return {
|
|
530
622
|
sessionToken: cached.sessionToken,
|
|
@@ -549,7 +641,7 @@ var CallForge = class _CallForge {
|
|
|
549
641
|
const data = await this.fetchFromApi(locationId, sessionToken, params);
|
|
550
642
|
this.saveToCache(locationId, data, params);
|
|
551
643
|
}
|
|
552
|
-
buildUrl(locationId, sessionToken, params) {
|
|
644
|
+
buildUrl(locationId, sessionToken, params, bootstrapToken) {
|
|
553
645
|
const { categoryId, endpoint } = this.config;
|
|
554
646
|
const queryParams = {
|
|
555
647
|
categoryId
|
|
@@ -560,6 +652,9 @@ var CallForge = class _CallForge {
|
|
|
560
652
|
if (sessionToken) {
|
|
561
653
|
queryParams.sessionToken = sessionToken;
|
|
562
654
|
}
|
|
655
|
+
if (bootstrapToken) {
|
|
656
|
+
queryParams.bootstrapToken = bootstrapToken;
|
|
657
|
+
}
|
|
563
658
|
for (const [key, value] of Object.entries(params)) {
|
|
564
659
|
if (value !== void 0) {
|
|
565
660
|
queryParams[key] = value;
|
|
@@ -569,6 +664,10 @@ var CallForge = class _CallForge {
|
|
|
569
664
|
const qs = sorted.map((k) => `${k}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
570
665
|
return `${endpoint}/v1/tracking/session?${qs}`;
|
|
571
666
|
}
|
|
667
|
+
buildBootstrapUrl() {
|
|
668
|
+
const { endpoint, categoryId } = this.config;
|
|
669
|
+
return `${endpoint}/v1/tracking/bootstrap?categoryId=${encodeURIComponent(categoryId)}`;
|
|
670
|
+
}
|
|
572
671
|
buildLocationUrl(locationId) {
|
|
573
672
|
const { endpoint } = this.config;
|
|
574
673
|
if (!locationId) {
|
|
@@ -606,15 +705,16 @@ for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
|
606
705
|
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
607
706
|
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
608
707
|
var lkey='cf_location_v1_'+site;
|
|
708
|
+
var bkey='cf_bootstrap_v1_'+site+'_${categoryId}';
|
|
609
709
|
try{
|
|
610
710
|
var cl=JSON.parse(localStorage.getItem(lkey));
|
|
611
711
|
if(cl&&cl.expiresAt>Date.now()+30000){
|
|
612
|
-
if(!loc||(loc&&cl.locId===loc)){window.__cfTrackingLocation=Promise.resolve(cl)}
|
|
712
|
+
if(!loc||(loc&&cl.locId===loc)){if(cl.location&&!Array.isArray(cl.location.zipOptions))cl.location.zipOptions=[];window.__cfTrackingLocation=Promise.resolve(cl)}
|
|
613
713
|
}}catch(e){}
|
|
614
714
|
if(!window.__cfTrackingLocation){
|
|
615
715
|
var lurl='${endpoint}/v1/tracking/location';
|
|
616
716
|
if(loc)lurl+='?loc_physical_ms='+encodeURIComponent(loc);
|
|
617
|
-
window.__cfTrackingLocation=fetch(lurl,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload location failed');return r.json()}).then(function(d){d.locId=loc;try{localStorage.setItem(lkey,JSON.stringify({locId:loc,location:d.location?{city:d.location.city,state:d.location.state,stateCode:d.location.stateCode}:null,expiresAt:d.expiresAt,tokenVersion:'v1'}))}catch(e){}return d});
|
|
717
|
+
window.__cfTrackingLocation=fetch(lurl,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload location failed');return r.json()}).then(function(d){var z=Array.isArray(d.zipOptions)?d.zipOptions.filter(function(x){return /^\\d{5}$/.test(x)}):[];d.locId=loc;d.zipOptions=z;try{localStorage.setItem(lkey,JSON.stringify({locId:loc,location:d.location?{city:d.location.city,state:d.location.state,stateCode:d.location.stateCode,zipOptions:z}:null,expiresAt:d.expiresAt,tokenVersion:'v1'}))}catch(e){}return d});
|
|
618
718
|
}
|
|
619
719
|
var token=null;
|
|
620
720
|
try{
|
|
@@ -625,12 +725,18 @@ token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
|
625
725
|
var cp=c.params||{};
|
|
626
726
|
p=Object.assign({},cp,p);
|
|
627
727
|
}}catch(e){}
|
|
728
|
+
var bt=null;
|
|
729
|
+
try{
|
|
730
|
+
var cb=JSON.parse(localStorage.getItem(bkey));
|
|
731
|
+
if(cb&&typeof cb.token==='string'&&cb.expiresAt>Date.now()+10000)bt=cb.token;
|
|
732
|
+
}catch(e){}
|
|
733
|
+
var bp=bt?Promise.resolve({bootstrapToken:bt}):fetch('${endpoint}/v1/tracking/bootstrap?categoryId=${categoryId}',{credentials:'omit'}).then(function(r){if(!r.ok)return null;return r.json()}).then(function(b){if(!b||typeof b.bootstrapToken!=='string'||typeof b.expiresAt!=='number')return null;try{localStorage.setItem(bkey,JSON.stringify({token:b.bootstrapToken,expiresAt:b.expiresAt,tokenVersion:'b1'}))}catch(e){}return b}).catch(function(){return null});
|
|
628
734
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
629
735
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
630
736
|
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
631
737
|
var ks=Object.keys(p).sort();
|
|
632
738
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
633
|
-
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
739
|
+
window.__cfTracking=bp.then(function(b){if(b&&b.bootstrapToken)url+='&bootstrapToken='+encodeURIComponent(b.bootstrapToken);return fetch(url,{credentials:'omit'})}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
634
740
|
})();`.replace(/\n/g, "");
|
|
635
741
|
return `<link rel="preconnect" href="${endpoint}">
|
|
636
742
|
<script>${script}</script>`;
|
package/dist/index.mjs
CHANGED
|
@@ -179,9 +179,11 @@ var LocationCache = class {
|
|
|
179
179
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
180
180
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
181
181
|
var CALL_INTENT_TIMEOUT_MS = 8e3;
|
|
182
|
+
var BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS = 1e4;
|
|
182
183
|
var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
|
|
183
184
|
var CallForge = class _CallForge {
|
|
184
185
|
constructor(config) {
|
|
186
|
+
this.bootstrapMemoryCache = null;
|
|
185
187
|
this.sessionPromise = null;
|
|
186
188
|
this.locationPromise = null;
|
|
187
189
|
this.customParams = {};
|
|
@@ -191,8 +193,10 @@ var CallForge = class _CallForge {
|
|
|
191
193
|
ga4MeasurementId: config.ga4MeasurementId,
|
|
192
194
|
siteKey: config.siteKey
|
|
193
195
|
};
|
|
196
|
+
const resolvedSiteKey = config.siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
194
197
|
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
195
198
|
this.locationCache = new LocationCache(config.siteKey);
|
|
199
|
+
this.bootstrapCacheKey = `cf_bootstrap_v1_${resolvedSiteKey}_${config.categoryId}`;
|
|
196
200
|
this.captureGA4ClientId();
|
|
197
201
|
this.startGA4ClientIdPolling();
|
|
198
202
|
}
|
|
@@ -400,18 +404,14 @@ var CallForge = class _CallForge {
|
|
|
400
404
|
return this.formatApiResponse(data);
|
|
401
405
|
}
|
|
402
406
|
async fetchLocation() {
|
|
403
|
-
var _a;
|
|
407
|
+
var _a, _b;
|
|
404
408
|
const locationId = this.getLocationId();
|
|
405
409
|
if (typeof window !== "undefined" && window.__cfTrackingLocation) {
|
|
406
410
|
try {
|
|
407
411
|
const data2 = await window.__cfTrackingLocation;
|
|
408
412
|
const dataWithExtras = data2;
|
|
409
413
|
const effectiveLocId = (_a = dataWithExtras.locId) != null ? _a : locationId;
|
|
410
|
-
const location2 = data2.location
|
|
411
|
-
city: data2.location.city,
|
|
412
|
-
state: data2.location.state,
|
|
413
|
-
stateCode: data2.location.stateCode
|
|
414
|
-
} : null;
|
|
414
|
+
const location2 = this.toTrackingLocation(data2.location, dataWithExtras.zipOptions);
|
|
415
415
|
this.saveLocationToCache(effectiveLocId, location2, data2.expiresAt);
|
|
416
416
|
return location2;
|
|
417
417
|
} catch (e) {
|
|
@@ -419,14 +419,10 @@ var CallForge = class _CallForge {
|
|
|
419
419
|
}
|
|
420
420
|
const cached = this.locationCache.get(locationId);
|
|
421
421
|
if (cached) {
|
|
422
|
-
return cached.location;
|
|
422
|
+
return this.toTrackingLocation(cached.location, (_b = cached.location) == null ? void 0 : _b.zipOptions);
|
|
423
423
|
}
|
|
424
424
|
const data = await this.fetchLocationFromApi(locationId);
|
|
425
|
-
const location = data.location
|
|
426
|
-
city: data.location.city,
|
|
427
|
-
state: data.location.state,
|
|
428
|
-
stateCode: data.location.stateCode
|
|
429
|
-
} : null;
|
|
425
|
+
const location = this.toTrackingLocation(data.location, data.zipOptions);
|
|
430
426
|
this.saveLocationToCache(locationId, location, data.expiresAt);
|
|
431
427
|
return location;
|
|
432
428
|
}
|
|
@@ -448,7 +444,43 @@ var CallForge = class _CallForge {
|
|
|
448
444
|
return params;
|
|
449
445
|
}
|
|
450
446
|
async fetchFromApi(locationId, sessionToken, params) {
|
|
451
|
-
const
|
|
447
|
+
const controller = new AbortController();
|
|
448
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
449
|
+
try {
|
|
450
|
+
let bootstrapToken = this.getCachedBootstrapToken();
|
|
451
|
+
let response = await fetch(
|
|
452
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
453
|
+
{
|
|
454
|
+
credentials: "omit",
|
|
455
|
+
signal: controller.signal
|
|
456
|
+
}
|
|
457
|
+
);
|
|
458
|
+
if (response.status === 401) {
|
|
459
|
+
bootstrapToken = await this.getBootstrapToken(true);
|
|
460
|
+
if (bootstrapToken) {
|
|
461
|
+
response = await fetch(
|
|
462
|
+
this.buildUrl(locationId, sessionToken, params, bootstrapToken),
|
|
463
|
+
{
|
|
464
|
+
credentials: "omit",
|
|
465
|
+
signal: controller.signal
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!response.ok) {
|
|
471
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
472
|
+
}
|
|
473
|
+
return await response.json();
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(timeoutId);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async getBootstrapToken(forceRefresh = false) {
|
|
479
|
+
const cached = this.getCachedBootstrapToken();
|
|
480
|
+
if (!forceRefresh && cached) {
|
|
481
|
+
return cached;
|
|
482
|
+
}
|
|
483
|
+
const url = this.buildBootstrapUrl();
|
|
452
484
|
const controller = new AbortController();
|
|
453
485
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
454
486
|
try {
|
|
@@ -457,9 +489,14 @@ var CallForge = class _CallForge {
|
|
|
457
489
|
signal: controller.signal
|
|
458
490
|
});
|
|
459
491
|
if (!response.ok) {
|
|
460
|
-
|
|
492
|
+
return null;
|
|
461
493
|
}
|
|
462
|
-
|
|
494
|
+
const data = await response.json();
|
|
495
|
+
if (typeof data.bootstrapToken !== "string" || typeof data.expiresAt !== "number") {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
this.saveBootstrapToken(data.bootstrapToken, data.expiresAt);
|
|
499
|
+
return data.bootstrapToken;
|
|
463
500
|
} finally {
|
|
464
501
|
clearTimeout(timeoutId);
|
|
465
502
|
}
|
|
@@ -481,6 +518,40 @@ var CallForge = class _CallForge {
|
|
|
481
518
|
clearTimeout(timeoutId);
|
|
482
519
|
}
|
|
483
520
|
}
|
|
521
|
+
getCachedBootstrapToken() {
|
|
522
|
+
var _a;
|
|
523
|
+
const now = Date.now();
|
|
524
|
+
const fromMemory = this.bootstrapMemoryCache;
|
|
525
|
+
if (fromMemory && fromMemory.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS > now) {
|
|
526
|
+
return fromMemory.token;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const raw = localStorage.getItem(this.bootstrapCacheKey);
|
|
530
|
+
if (!raw) return null;
|
|
531
|
+
const cached = JSON.parse(raw);
|
|
532
|
+
if (typeof cached.token !== "string" || typeof cached.expiresAt !== "number") {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
if (cached.expiresAt - BOOTSTRAP_TOKEN_EXPIRY_BUFFER_MS <= now) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return cached.token;
|
|
539
|
+
} catch (e) {
|
|
540
|
+
return (_a = fromMemory == null ? void 0 : fromMemory.token) != null ? _a : null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
saveBootstrapToken(token, expiresAt) {
|
|
544
|
+
const cached = {
|
|
545
|
+
token,
|
|
546
|
+
expiresAt,
|
|
547
|
+
tokenVersion: "b1"
|
|
548
|
+
};
|
|
549
|
+
this.bootstrapMemoryCache = cached;
|
|
550
|
+
try {
|
|
551
|
+
localStorage.setItem(this.bootstrapCacheKey, JSON.stringify(cached));
|
|
552
|
+
} catch (e) {
|
|
553
|
+
}
|
|
554
|
+
}
|
|
484
555
|
saveToCache(locationId, data, params) {
|
|
485
556
|
const cached = {
|
|
486
557
|
locId: locationId != null ? locationId : null,
|
|
@@ -501,6 +572,27 @@ var CallForge = class _CallForge {
|
|
|
501
572
|
tokenVersion: "v1"
|
|
502
573
|
});
|
|
503
574
|
}
|
|
575
|
+
toTrackingLocation(location, zipOptions) {
|
|
576
|
+
if (!location) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
city: location.city,
|
|
581
|
+
state: location.state,
|
|
582
|
+
stateCode: location.stateCode,
|
|
583
|
+
zipOptions: this.normalizeZipOptions(zipOptions)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
normalizeZipOptions(value) {
|
|
587
|
+
if (!Array.isArray(value)) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
return Array.from(
|
|
591
|
+
new Set(
|
|
592
|
+
value.filter((zip) => typeof zip === "string" && /^\d{5}$/.test(zip))
|
|
593
|
+
)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
504
596
|
formatSession(cached) {
|
|
505
597
|
return {
|
|
506
598
|
sessionToken: cached.sessionToken,
|
|
@@ -525,7 +617,7 @@ var CallForge = class _CallForge {
|
|
|
525
617
|
const data = await this.fetchFromApi(locationId, sessionToken, params);
|
|
526
618
|
this.saveToCache(locationId, data, params);
|
|
527
619
|
}
|
|
528
|
-
buildUrl(locationId, sessionToken, params) {
|
|
620
|
+
buildUrl(locationId, sessionToken, params, bootstrapToken) {
|
|
529
621
|
const { categoryId, endpoint } = this.config;
|
|
530
622
|
const queryParams = {
|
|
531
623
|
categoryId
|
|
@@ -536,6 +628,9 @@ var CallForge = class _CallForge {
|
|
|
536
628
|
if (sessionToken) {
|
|
537
629
|
queryParams.sessionToken = sessionToken;
|
|
538
630
|
}
|
|
631
|
+
if (bootstrapToken) {
|
|
632
|
+
queryParams.bootstrapToken = bootstrapToken;
|
|
633
|
+
}
|
|
539
634
|
for (const [key, value] of Object.entries(params)) {
|
|
540
635
|
if (value !== void 0) {
|
|
541
636
|
queryParams[key] = value;
|
|
@@ -545,6 +640,10 @@ var CallForge = class _CallForge {
|
|
|
545
640
|
const qs = sorted.map((k) => `${k}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
546
641
|
return `${endpoint}/v1/tracking/session?${qs}`;
|
|
547
642
|
}
|
|
643
|
+
buildBootstrapUrl() {
|
|
644
|
+
const { endpoint, categoryId } = this.config;
|
|
645
|
+
return `${endpoint}/v1/tracking/bootstrap?categoryId=${encodeURIComponent(categoryId)}`;
|
|
646
|
+
}
|
|
548
647
|
buildLocationUrl(locationId) {
|
|
549
648
|
const { endpoint } = this.config;
|
|
550
649
|
if (!locationId) {
|
|
@@ -582,15 +681,16 @@ for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
|
582
681
|
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
583
682
|
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
584
683
|
var lkey='cf_location_v1_'+site;
|
|
684
|
+
var bkey='cf_bootstrap_v1_'+site+'_${categoryId}';
|
|
585
685
|
try{
|
|
586
686
|
var cl=JSON.parse(localStorage.getItem(lkey));
|
|
587
687
|
if(cl&&cl.expiresAt>Date.now()+30000){
|
|
588
|
-
if(!loc||(loc&&cl.locId===loc)){window.__cfTrackingLocation=Promise.resolve(cl)}
|
|
688
|
+
if(!loc||(loc&&cl.locId===loc)){if(cl.location&&!Array.isArray(cl.location.zipOptions))cl.location.zipOptions=[];window.__cfTrackingLocation=Promise.resolve(cl)}
|
|
589
689
|
}}catch(e){}
|
|
590
690
|
if(!window.__cfTrackingLocation){
|
|
591
691
|
var lurl='${endpoint}/v1/tracking/location';
|
|
592
692
|
if(loc)lurl+='?loc_physical_ms='+encodeURIComponent(loc);
|
|
593
|
-
window.__cfTrackingLocation=fetch(lurl,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload location failed');return r.json()}).then(function(d){d.locId=loc;try{localStorage.setItem(lkey,JSON.stringify({locId:loc,location:d.location?{city:d.location.city,state:d.location.state,stateCode:d.location.stateCode}:null,expiresAt:d.expiresAt,tokenVersion:'v1'}))}catch(e){}return d});
|
|
693
|
+
window.__cfTrackingLocation=fetch(lurl,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload location failed');return r.json()}).then(function(d){var z=Array.isArray(d.zipOptions)?d.zipOptions.filter(function(x){return /^\\d{5}$/.test(x)}):[];d.locId=loc;d.zipOptions=z;try{localStorage.setItem(lkey,JSON.stringify({locId:loc,location:d.location?{city:d.location.city,state:d.location.state,stateCode:d.location.stateCode,zipOptions:z}:null,expiresAt:d.expiresAt,tokenVersion:'v1'}))}catch(e){}return d});
|
|
594
694
|
}
|
|
595
695
|
var token=null;
|
|
596
696
|
try{
|
|
@@ -601,12 +701,18 @@ token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
|
601
701
|
var cp=c.params||{};
|
|
602
702
|
p=Object.assign({},cp,p);
|
|
603
703
|
}}catch(e){}
|
|
704
|
+
var bt=null;
|
|
705
|
+
try{
|
|
706
|
+
var cb=JSON.parse(localStorage.getItem(bkey));
|
|
707
|
+
if(cb&&typeof cb.token==='string'&&cb.expiresAt>Date.now()+10000)bt=cb.token;
|
|
708
|
+
}catch(e){}
|
|
709
|
+
var bp=bt?Promise.resolve({bootstrapToken:bt}):fetch('${endpoint}/v1/tracking/bootstrap?categoryId=${categoryId}',{credentials:'omit'}).then(function(r){if(!r.ok)return null;return r.json()}).then(function(b){if(!b||typeof b.bootstrapToken!=='string'||typeof b.expiresAt!=='number')return null;try{localStorage.setItem(bkey,JSON.stringify({token:b.bootstrapToken,expiresAt:b.expiresAt,tokenVersion:'b1'}))}catch(e){}return b}).catch(function(){return null});
|
|
604
710
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
605
711
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
606
712
|
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
607
713
|
var ks=Object.keys(p).sort();
|
|
608
714
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
609
|
-
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
715
|
+
window.__cfTracking=bp.then(function(b){if(b&&b.bootstrapToken)url+='&bootstrapToken='+encodeURIComponent(b.bootstrapToken);return fetch(url,{credentials:'omit'})}).then(function(r){if(!r.ok)throw new Error('tracking preload failed');return r.json()}).then(function(d){d.params=p;d.locId=loc;try{localStorage.setItem(key,JSON.stringify({locId:loc,sessionToken:d.sessionToken,leaseId:d.leaseId,phoneNumber:d.phoneNumber,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
610
716
|
})();`.replace(/\n/g, "");
|
|
611
717
|
return `<link rel="preconnect" href="${endpoint}">
|
|
612
718
|
<script>${script}</script>`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@callforge/tracking-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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
|
-
},
|
|
27
20
|
"scripts": {
|
|
28
21
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
29
22
|
"clean": "rm -rf dist",
|
|
30
23
|
"test": "vitest run",
|
|
31
24
|
"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
|
+
}
|