@callforge/tracking-client 0.6.2 → 0.7.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 +49 -11
- package/dist/index.d.mts +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +187 -26
- package/dist/index.mjs +187 -26
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Generated HTML:
|
|
|
30
30
|
<script>/* preload script */</script>
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
### 2. Initialize and
|
|
33
|
+
### 2. Initialize and start session + location requests
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
36
|
import { CallForge } from '@callforge/tracking-client';
|
|
@@ -40,9 +40,13 @@ const client = CallForge.init({
|
|
|
40
40
|
// endpoint: 'https://tracking-dev.callforge.io', // Optional: override for dev
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
const session =
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const { session, location } = client.getSessionAsync();
|
|
44
|
+
|
|
45
|
+
// Location is delivered independently (often faster than phone number assignment)
|
|
46
|
+
console.log(await location); // { city: "Woodstock", state: "Georgia", stateCode: "GA" } or null
|
|
47
|
+
|
|
48
|
+
// Phone session data (deterministic token + phone number)
|
|
49
|
+
console.log(await session); // { sessionToken, leaseId, phoneNumber }
|
|
46
50
|
```
|
|
47
51
|
|
|
48
52
|
### 3. Deterministic click/callback attribution (optional)
|
|
@@ -83,6 +87,7 @@ Requirements:
|
|
|
83
87
|
|
|
84
88
|
How it works:
|
|
85
89
|
- Extracts the GA4 `client_id` from the `_ga` cookie and sends it to CallForge as `ga4ClientId`.
|
|
90
|
+
- Polls briefly after init to capture the cookie if Google Analytics sets it slightly later.
|
|
86
91
|
- If `ga4MeasurementId` is configured and `gtag` is available, also uses `gtag('get', measurementId, 'client_id', ...)` (with a short retry window).
|
|
87
92
|
- If `ga4ClientId` becomes available after a session is created, the client will refresh once to sync it to CallForge.
|
|
88
93
|
|
|
@@ -133,18 +138,13 @@ interface CallForgeConfig {
|
|
|
133
138
|
|
|
134
139
|
### `client.getSession()`
|
|
135
140
|
|
|
136
|
-
Get tracking session data. Returns cached data if valid, otherwise fetches from the API.
|
|
141
|
+
Get tracking session data (phone number + deterministic session token). Returns cached data if valid, otherwise fetches from the API.
|
|
137
142
|
|
|
138
143
|
```typescript
|
|
139
144
|
interface TrackingSession {
|
|
140
145
|
sessionToken: string; // Signed, opaque token used to refresh the session
|
|
141
146
|
leaseId: string | null; // Deterministic assignment lease ID (when available)
|
|
142
147
|
phoneNumber: string | null;
|
|
143
|
-
location: {
|
|
144
|
-
city: string;
|
|
145
|
-
state: string; // Full name: "Georgia"
|
|
146
|
-
stateCode: string; // Abbreviation: "GA"
|
|
147
|
-
} | null;
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -154,6 +154,31 @@ Behavior:
|
|
|
154
154
|
- If `loc_physical_ms` is present in the URL, cached sessions are only reused when it matches the cached `locId`.
|
|
155
155
|
- Throws on network errors or API errors.
|
|
156
156
|
|
|
157
|
+
### `client.getLocation()`
|
|
158
|
+
|
|
159
|
+
Get location data only. Returns cached data if valid, otherwise fetches from the API.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const location = await client.getLocation();
|
|
163
|
+
// { city, state, stateCode } or null
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `client.getSessionAsync()`
|
|
167
|
+
|
|
168
|
+
Kick off both requests and use each as soon as it resolves.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const { session, location } = client.getSessionAsync();
|
|
172
|
+
|
|
173
|
+
location.then((loc) => {
|
|
174
|
+
// show city/state ASAP
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
session.then((sess) => {
|
|
178
|
+
// show phone number when ready
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
157
182
|
### `client.createCallIntent()`
|
|
158
183
|
|
|
159
184
|
Create a short-lived call intent token for click/callback deterministic attribution.
|
|
@@ -173,6 +198,16 @@ client.onReady((session) => {
|
|
|
173
198
|
});
|
|
174
199
|
```
|
|
175
200
|
|
|
201
|
+
### `client.onLocationReady(callback)`
|
|
202
|
+
|
|
203
|
+
Subscribe to location ready event. Callback is called once location data is available.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
client.onLocationReady((location) => {
|
|
207
|
+
// location is { city, state, stateCode } or null
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
176
211
|
### `client.setParams(params)`
|
|
177
212
|
|
|
178
213
|
Set custom tracking parameters for conversion attribution.
|
|
@@ -187,6 +222,7 @@ client.setParams({
|
|
|
187
222
|
|
|
188
223
|
Behavior:
|
|
189
224
|
- Merges with existing params (later calls override earlier values).
|
|
225
|
+
- If a session already exists (cached `sessionToken`), the client will refresh once to sync updated params server-side (best-effort).
|
|
190
226
|
- Parameters are sent with every `getSession()` API request.
|
|
191
227
|
- Persisted in localStorage alongside session data.
|
|
192
228
|
|
|
@@ -228,7 +264,8 @@ Parameters are sent as a sorted query string for cache consistency:
|
|
|
228
264
|
|
|
229
265
|
## Caching Behavior
|
|
230
266
|
|
|
231
|
-
-
|
|
267
|
+
- Session cache key: `cf_tracking_v1_<siteKey>_<categoryId>`
|
|
268
|
+
- Location cache key: `cf_location_v1_<siteKey>`
|
|
232
269
|
- TTL: controlled by the server `expiresAt` response (currently 30 minutes)
|
|
233
270
|
- Storage: localStorage (falls back to memory if unavailable)
|
|
234
271
|
|
|
@@ -259,6 +296,7 @@ import type {
|
|
|
259
296
|
TrackingLocation,
|
|
260
297
|
TrackingParams,
|
|
261
298
|
ReadyCallback,
|
|
299
|
+
LocationReadyCallback,
|
|
262
300
|
CallIntentResponse,
|
|
263
301
|
} from '@callforge/tracking-client';
|
|
264
302
|
```
|
package/dist/index.d.mts
CHANGED
|
@@ -22,6 +22,7 @@ interface TrackingLocation {
|
|
|
22
22
|
/** State abbreviation (e.g., "GA") */
|
|
23
23
|
stateCode: string;
|
|
24
24
|
}
|
|
25
|
+
type TrackingLocationSource = 'google_criteria' | 'cloudflare_geo';
|
|
25
26
|
/**
|
|
26
27
|
* Tracking parameters for attribution (auto-captured + custom).
|
|
27
28
|
*/
|
|
@@ -49,20 +50,25 @@ interface TrackingSession {
|
|
|
49
50
|
leaseId: string | null;
|
|
50
51
|
/** Assigned phone number, or null if no number available */
|
|
51
52
|
phoneNumber: string | null;
|
|
52
|
-
/** Location data, or null if location could not be resolved */
|
|
53
|
-
location: TrackingLocation | null;
|
|
54
53
|
}
|
|
55
54
|
/**
|
|
56
|
-
*
|
|
55
|
+
* Session API response format (includes expiresAt).
|
|
57
56
|
*/
|
|
58
|
-
interface
|
|
57
|
+
interface ApiSessionResponse {
|
|
59
58
|
sessionToken: string;
|
|
60
59
|
leaseId: string | null;
|
|
61
60
|
phoneNumber: string | null;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Location API response format (includes expiresAt).
|
|
65
|
+
*/
|
|
66
|
+
interface ApiLocationResponse {
|
|
62
67
|
location: {
|
|
63
68
|
city: string;
|
|
64
69
|
state: string;
|
|
65
70
|
stateCode: string;
|
|
71
|
+
source: TrackingLocationSource;
|
|
66
72
|
} | null;
|
|
67
73
|
expiresAt: number;
|
|
68
74
|
}
|
|
@@ -78,12 +84,14 @@ interface CallIntentResponse {
|
|
|
78
84
|
* Callback function for onReady subscription.
|
|
79
85
|
*/
|
|
80
86
|
type ReadyCallback = (session: TrackingSession) => void;
|
|
87
|
+
type LocationReadyCallback = (location: TrackingLocation | null) => void;
|
|
81
88
|
/**
|
|
82
89
|
* Global window extension for preload promise and gtag.
|
|
83
90
|
*/
|
|
84
91
|
declare global {
|
|
85
92
|
interface Window {
|
|
86
|
-
__cfTracking?: Promise<
|
|
93
|
+
__cfTracking?: Promise<ApiSessionResponse>;
|
|
94
|
+
__cfTrackingLocation?: Promise<ApiLocationResponse>;
|
|
87
95
|
gtag?: (command: 'get', targetId: string, fieldName: string, callback: (value: string) => void) => void;
|
|
88
96
|
}
|
|
89
97
|
}
|
|
@@ -91,7 +99,9 @@ declare global {
|
|
|
91
99
|
declare class CallForge {
|
|
92
100
|
private readonly config;
|
|
93
101
|
private readonly cache;
|
|
102
|
+
private readonly locationCache;
|
|
94
103
|
private sessionPromise;
|
|
104
|
+
private locationPromise;
|
|
95
105
|
private customParams;
|
|
96
106
|
private constructor();
|
|
97
107
|
/**
|
|
@@ -103,6 +113,19 @@ declare class CallForge {
|
|
|
103
113
|
* Returns cached data if valid, otherwise fetches from API.
|
|
104
114
|
*/
|
|
105
115
|
getSession(): Promise<TrackingSession>;
|
|
116
|
+
/**
|
|
117
|
+
* Get location data only.
|
|
118
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
119
|
+
*/
|
|
120
|
+
getLocation(): Promise<TrackingLocation | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Kick off both session and location requests.
|
|
123
|
+
* Returns separate Promises so consumers can use each as soon as it resolves.
|
|
124
|
+
*/
|
|
125
|
+
getSessionAsync(): {
|
|
126
|
+
session: Promise<TrackingSession>;
|
|
127
|
+
location: Promise<TrackingLocation | null>;
|
|
128
|
+
};
|
|
106
129
|
/**
|
|
107
130
|
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
108
131
|
*/
|
|
@@ -113,7 +136,15 @@ declare class CallForge {
|
|
|
113
136
|
*/
|
|
114
137
|
onReady(callback: ReadyCallback): void;
|
|
115
138
|
/**
|
|
116
|
-
*
|
|
139
|
+
* Subscribe to location ready event.
|
|
140
|
+
* Callback is called once location data is available.
|
|
141
|
+
*/
|
|
142
|
+
onLocationReady(callback: LocationReadyCallback): void;
|
|
143
|
+
/**
|
|
144
|
+
* Set custom tracking parameters for attribution.
|
|
145
|
+
*
|
|
146
|
+
* When possible, the client will also refresh the session once to sync
|
|
147
|
+
* the updated params to CallForge (so server-side events can use them).
|
|
117
148
|
*/
|
|
118
149
|
setParams(params: Record<string, string>): Promise<void>;
|
|
119
150
|
/**
|
|
@@ -136,14 +167,18 @@ declare class CallForge {
|
|
|
136
167
|
*/
|
|
137
168
|
private getGA4ClientIdFromCookie;
|
|
138
169
|
private fetchSession;
|
|
170
|
+
private fetchLocation;
|
|
139
171
|
private getLocationId;
|
|
140
172
|
private getAutoParams;
|
|
141
173
|
private fetchFromApi;
|
|
174
|
+
private fetchLocationFromApi;
|
|
142
175
|
private saveToCache;
|
|
176
|
+
private saveLocationToCache;
|
|
143
177
|
private formatSession;
|
|
144
178
|
private formatApiResponse;
|
|
145
|
-
private
|
|
179
|
+
private syncParamsToCallForgeIfPossible;
|
|
146
180
|
private buildUrl;
|
|
181
|
+
private buildLocationUrl;
|
|
147
182
|
}
|
|
148
183
|
|
|
149
184
|
/**
|
|
@@ -152,4 +187,4 @@ declare class CallForge {
|
|
|
152
187
|
*/
|
|
153
188
|
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
154
189
|
|
|
155
|
-
export { CallForge, type CallForgeConfig, type CallIntentResponse, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
|
190
|
+
export { CallForge, type CallForgeConfig, type CallIntentResponse, type LocationReadyCallback, type ReadyCallback, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface TrackingLocation {
|
|
|
22
22
|
/** State abbreviation (e.g., "GA") */
|
|
23
23
|
stateCode: string;
|
|
24
24
|
}
|
|
25
|
+
type TrackingLocationSource = 'google_criteria' | 'cloudflare_geo';
|
|
25
26
|
/**
|
|
26
27
|
* Tracking parameters for attribution (auto-captured + custom).
|
|
27
28
|
*/
|
|
@@ -49,20 +50,25 @@ interface TrackingSession {
|
|
|
49
50
|
leaseId: string | null;
|
|
50
51
|
/** Assigned phone number, or null if no number available */
|
|
51
52
|
phoneNumber: string | null;
|
|
52
|
-
/** Location data, or null if location could not be resolved */
|
|
53
|
-
location: TrackingLocation | null;
|
|
54
53
|
}
|
|
55
54
|
/**
|
|
56
|
-
*
|
|
55
|
+
* Session API response format (includes expiresAt).
|
|
57
56
|
*/
|
|
58
|
-
interface
|
|
57
|
+
interface ApiSessionResponse {
|
|
59
58
|
sessionToken: string;
|
|
60
59
|
leaseId: string | null;
|
|
61
60
|
phoneNumber: string | null;
|
|
61
|
+
expiresAt: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Location API response format (includes expiresAt).
|
|
65
|
+
*/
|
|
66
|
+
interface ApiLocationResponse {
|
|
62
67
|
location: {
|
|
63
68
|
city: string;
|
|
64
69
|
state: string;
|
|
65
70
|
stateCode: string;
|
|
71
|
+
source: TrackingLocationSource;
|
|
66
72
|
} | null;
|
|
67
73
|
expiresAt: number;
|
|
68
74
|
}
|
|
@@ -78,12 +84,14 @@ interface CallIntentResponse {
|
|
|
78
84
|
* Callback function for onReady subscription.
|
|
79
85
|
*/
|
|
80
86
|
type ReadyCallback = (session: TrackingSession) => void;
|
|
87
|
+
type LocationReadyCallback = (location: TrackingLocation | null) => void;
|
|
81
88
|
/**
|
|
82
89
|
* Global window extension for preload promise and gtag.
|
|
83
90
|
*/
|
|
84
91
|
declare global {
|
|
85
92
|
interface Window {
|
|
86
|
-
__cfTracking?: Promise<
|
|
93
|
+
__cfTracking?: Promise<ApiSessionResponse>;
|
|
94
|
+
__cfTrackingLocation?: Promise<ApiLocationResponse>;
|
|
87
95
|
gtag?: (command: 'get', targetId: string, fieldName: string, callback: (value: string) => void) => void;
|
|
88
96
|
}
|
|
89
97
|
}
|
|
@@ -91,7 +99,9 @@ declare global {
|
|
|
91
99
|
declare class CallForge {
|
|
92
100
|
private readonly config;
|
|
93
101
|
private readonly cache;
|
|
102
|
+
private readonly locationCache;
|
|
94
103
|
private sessionPromise;
|
|
104
|
+
private locationPromise;
|
|
95
105
|
private customParams;
|
|
96
106
|
private constructor();
|
|
97
107
|
/**
|
|
@@ -103,6 +113,19 @@ declare class CallForge {
|
|
|
103
113
|
* Returns cached data if valid, otherwise fetches from API.
|
|
104
114
|
*/
|
|
105
115
|
getSession(): Promise<TrackingSession>;
|
|
116
|
+
/**
|
|
117
|
+
* Get location data only.
|
|
118
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
119
|
+
*/
|
|
120
|
+
getLocation(): Promise<TrackingLocation | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Kick off both session and location requests.
|
|
123
|
+
* Returns separate Promises so consumers can use each as soon as it resolves.
|
|
124
|
+
*/
|
|
125
|
+
getSessionAsync(): {
|
|
126
|
+
session: Promise<TrackingSession>;
|
|
127
|
+
location: Promise<TrackingLocation | null>;
|
|
128
|
+
};
|
|
106
129
|
/**
|
|
107
130
|
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
108
131
|
*/
|
|
@@ -113,7 +136,15 @@ declare class CallForge {
|
|
|
113
136
|
*/
|
|
114
137
|
onReady(callback: ReadyCallback): void;
|
|
115
138
|
/**
|
|
116
|
-
*
|
|
139
|
+
* Subscribe to location ready event.
|
|
140
|
+
* Callback is called once location data is available.
|
|
141
|
+
*/
|
|
142
|
+
onLocationReady(callback: LocationReadyCallback): void;
|
|
143
|
+
/**
|
|
144
|
+
* Set custom tracking parameters for attribution.
|
|
145
|
+
*
|
|
146
|
+
* When possible, the client will also refresh the session once to sync
|
|
147
|
+
* the updated params to CallForge (so server-side events can use them).
|
|
117
148
|
*/
|
|
118
149
|
setParams(params: Record<string, string>): Promise<void>;
|
|
119
150
|
/**
|
|
@@ -136,14 +167,18 @@ declare class CallForge {
|
|
|
136
167
|
*/
|
|
137
168
|
private getGA4ClientIdFromCookie;
|
|
138
169
|
private fetchSession;
|
|
170
|
+
private fetchLocation;
|
|
139
171
|
private getLocationId;
|
|
140
172
|
private getAutoParams;
|
|
141
173
|
private fetchFromApi;
|
|
174
|
+
private fetchLocationFromApi;
|
|
142
175
|
private saveToCache;
|
|
176
|
+
private saveLocationToCache;
|
|
143
177
|
private formatSession;
|
|
144
178
|
private formatApiResponse;
|
|
145
|
-
private
|
|
179
|
+
private syncParamsToCallForgeIfPossible;
|
|
146
180
|
private buildUrl;
|
|
181
|
+
private buildLocationUrl;
|
|
147
182
|
}
|
|
148
183
|
|
|
149
184
|
/**
|
|
@@ -152,4 +187,4 @@ declare class CallForge {
|
|
|
152
187
|
*/
|
|
153
188
|
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
154
189
|
|
|
155
|
-
export { CallForge, type CallForgeConfig, type CallIntentResponse, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
|
190
|
+
export { CallForge, type CallForgeConfig, type CallIntentResponse, type LocationReadyCallback, type ReadyCallback, type TrackingLocation, type TrackingLocationSource, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
package/dist/index.js
CHANGED
|
@@ -138,6 +138,67 @@ var TrackingCache = class {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
// src/location-cache.ts
|
|
142
|
+
var EXPIRY_BUFFER_MS2 = 3e4;
|
|
143
|
+
var LocationCache = class {
|
|
144
|
+
constructor(siteKey) {
|
|
145
|
+
this.memoryCache = null;
|
|
146
|
+
this.useMemory = false;
|
|
147
|
+
const resolvedSiteKey = siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
148
|
+
this.key = `cf_location_v1_${resolvedSiteKey}`;
|
|
149
|
+
this.useMemory = !this.isLocalStorageAvailable();
|
|
150
|
+
}
|
|
151
|
+
isLocalStorageAvailable() {
|
|
152
|
+
try {
|
|
153
|
+
localStorage.setItem("__cf_test__", "1");
|
|
154
|
+
localStorage.removeItem("__cf_test__");
|
|
155
|
+
return true;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
get(locationId) {
|
|
161
|
+
const cached = this.read();
|
|
162
|
+
if (!cached) return null;
|
|
163
|
+
if (cached.expiresAt - EXPIRY_BUFFER_MS2 <= Date.now()) return null;
|
|
164
|
+
if (!locationId) return cached;
|
|
165
|
+
if (cached.locId !== locationId) return null;
|
|
166
|
+
return cached;
|
|
167
|
+
}
|
|
168
|
+
set(value) {
|
|
169
|
+
if (this.useMemory) {
|
|
170
|
+
this.memoryCache = value;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
localStorage.setItem(this.key, JSON.stringify(value));
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.memoryCache = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
clear() {
|
|
180
|
+
this.memoryCache = null;
|
|
181
|
+
if (!this.useMemory) {
|
|
182
|
+
try {
|
|
183
|
+
localStorage.removeItem(this.key);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
read() {
|
|
189
|
+
if (this.useMemory) {
|
|
190
|
+
return this.memoryCache;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const raw = localStorage.getItem(this.key);
|
|
194
|
+
if (!raw) return null;
|
|
195
|
+
return JSON.parse(raw);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return this.memoryCache;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
141
202
|
// src/client.ts
|
|
142
203
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
143
204
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -146,6 +207,7 @@ var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campai
|
|
|
146
207
|
var CallForge = class _CallForge {
|
|
147
208
|
constructor(config) {
|
|
148
209
|
this.sessionPromise = null;
|
|
210
|
+
this.locationPromise = null;
|
|
149
211
|
this.customParams = {};
|
|
150
212
|
this.config = {
|
|
151
213
|
categoryId: config.categoryId,
|
|
@@ -154,6 +216,7 @@ var CallForge = class _CallForge {
|
|
|
154
216
|
siteKey: config.siteKey
|
|
155
217
|
};
|
|
156
218
|
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
219
|
+
this.locationCache = new LocationCache(config.siteKey);
|
|
157
220
|
this.captureGA4ClientId();
|
|
158
221
|
this.startGA4ClientIdPolling();
|
|
159
222
|
}
|
|
@@ -174,6 +237,27 @@ var CallForge = class _CallForge {
|
|
|
174
237
|
this.sessionPromise = this.fetchSession();
|
|
175
238
|
return this.sessionPromise;
|
|
176
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Get location data only.
|
|
242
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
243
|
+
*/
|
|
244
|
+
async getLocation() {
|
|
245
|
+
if (this.locationPromise) {
|
|
246
|
+
return this.locationPromise;
|
|
247
|
+
}
|
|
248
|
+
this.locationPromise = this.fetchLocation();
|
|
249
|
+
return this.locationPromise;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Kick off both session and location requests.
|
|
253
|
+
* Returns separate Promises so consumers can use each as soon as it resolves.
|
|
254
|
+
*/
|
|
255
|
+
getSessionAsync() {
|
|
256
|
+
return {
|
|
257
|
+
session: this.getSession(),
|
|
258
|
+
location: this.getLocation()
|
|
259
|
+
};
|
|
260
|
+
}
|
|
177
261
|
/**
|
|
178
262
|
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
179
263
|
*/
|
|
@@ -210,23 +294,35 @@ var CallForge = class _CallForge {
|
|
|
210
294
|
});
|
|
211
295
|
}
|
|
212
296
|
/**
|
|
213
|
-
*
|
|
297
|
+
* Subscribe to location ready event.
|
|
298
|
+
* Callback is called once location data is available.
|
|
299
|
+
*/
|
|
300
|
+
onLocationReady(callback) {
|
|
301
|
+
this.getLocation().then(callback).catch(() => {
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Set custom tracking parameters for attribution.
|
|
306
|
+
*
|
|
307
|
+
* When possible, the client will also refresh the session once to sync
|
|
308
|
+
* the updated params to CallForge (so server-side events can use them).
|
|
214
309
|
*/
|
|
215
310
|
async setParams(params) {
|
|
216
311
|
this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
} else {
|
|
228
|
-
void sync();
|
|
312
|
+
const sync = async () => {
|
|
313
|
+
try {
|
|
314
|
+
await this.syncParamsToCallForgeIfPossible();
|
|
315
|
+
} catch (e) {
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
if (this.sessionPromise) {
|
|
319
|
+
try {
|
|
320
|
+
await this.sessionPromise;
|
|
321
|
+
} catch (e) {
|
|
229
322
|
}
|
|
323
|
+
await sync();
|
|
324
|
+
} else {
|
|
325
|
+
await sync();
|
|
230
326
|
}
|
|
231
327
|
}
|
|
232
328
|
/**
|
|
@@ -259,7 +355,6 @@ var CallForge = class _CallForge {
|
|
|
259
355
|
});
|
|
260
356
|
}
|
|
261
357
|
startGA4ClientIdPolling() {
|
|
262
|
-
if (!this.config.ga4MeasurementId) return;
|
|
263
358
|
if (typeof window === "undefined") return;
|
|
264
359
|
if (this.customParams.ga4ClientId) return;
|
|
265
360
|
const MAX_ATTEMPTS = 20;
|
|
@@ -328,6 +423,37 @@ var CallForge = class _CallForge {
|
|
|
328
423
|
this.saveToCache(locationId, data, params);
|
|
329
424
|
return this.formatApiResponse(data);
|
|
330
425
|
}
|
|
426
|
+
async fetchLocation() {
|
|
427
|
+
var _a;
|
|
428
|
+
const locationId = this.getLocationId();
|
|
429
|
+
if (typeof window !== "undefined" && window.__cfTrackingLocation) {
|
|
430
|
+
try {
|
|
431
|
+
const data2 = await window.__cfTrackingLocation;
|
|
432
|
+
const dataWithExtras = data2;
|
|
433
|
+
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;
|
|
439
|
+
this.saveLocationToCache(effectiveLocId, location2, data2.expiresAt);
|
|
440
|
+
return location2;
|
|
441
|
+
} catch (e) {
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const cached = this.locationCache.get(locationId);
|
|
445
|
+
if (cached) {
|
|
446
|
+
return cached.location;
|
|
447
|
+
}
|
|
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;
|
|
454
|
+
this.saveLocationToCache(locationId, location, data.expiresAt);
|
|
455
|
+
return location;
|
|
456
|
+
}
|
|
331
457
|
getLocationId() {
|
|
332
458
|
if (typeof window === "undefined") return null;
|
|
333
459
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -362,47 +488,64 @@ var CallForge = class _CallForge {
|
|
|
362
488
|
clearTimeout(timeoutId);
|
|
363
489
|
}
|
|
364
490
|
}
|
|
491
|
+
async fetchLocationFromApi(locationId) {
|
|
492
|
+
const url = this.buildLocationUrl(locationId);
|
|
493
|
+
const controller = new AbortController();
|
|
494
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
495
|
+
try {
|
|
496
|
+
const response = await fetch(url, {
|
|
497
|
+
credentials: "omit",
|
|
498
|
+
signal: controller.signal
|
|
499
|
+
});
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
502
|
+
}
|
|
503
|
+
return await response.json();
|
|
504
|
+
} finally {
|
|
505
|
+
clearTimeout(timeoutId);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
365
508
|
saveToCache(locationId, data, params) {
|
|
366
509
|
const cached = {
|
|
367
510
|
locId: locationId != null ? locationId : null,
|
|
368
511
|
sessionToken: data.sessionToken,
|
|
369
512
|
leaseId: data.leaseId,
|
|
370
513
|
phoneNumber: data.phoneNumber,
|
|
371
|
-
location: data.location,
|
|
372
514
|
expiresAt: data.expiresAt,
|
|
373
515
|
tokenVersion: "v1",
|
|
374
516
|
params
|
|
375
517
|
};
|
|
376
518
|
this.cache.set(cached);
|
|
377
519
|
}
|
|
520
|
+
saveLocationToCache(locationId, location, expiresAt) {
|
|
521
|
+
this.locationCache.set({
|
|
522
|
+
locId: locationId != null ? locationId : null,
|
|
523
|
+
location,
|
|
524
|
+
expiresAt,
|
|
525
|
+
tokenVersion: "v1"
|
|
526
|
+
});
|
|
527
|
+
}
|
|
378
528
|
formatSession(cached) {
|
|
379
529
|
return {
|
|
380
530
|
sessionToken: cached.sessionToken,
|
|
381
531
|
leaseId: cached.leaseId,
|
|
382
|
-
phoneNumber: cached.phoneNumber
|
|
383
|
-
location: cached.location
|
|
532
|
+
phoneNumber: cached.phoneNumber
|
|
384
533
|
};
|
|
385
534
|
}
|
|
386
535
|
formatApiResponse(data) {
|
|
387
536
|
return {
|
|
388
537
|
sessionToken: data.sessionToken,
|
|
389
538
|
leaseId: data.leaseId,
|
|
390
|
-
phoneNumber: data.phoneNumber
|
|
391
|
-
location: data.location
|
|
539
|
+
phoneNumber: data.phoneNumber
|
|
392
540
|
};
|
|
393
541
|
}
|
|
394
|
-
async
|
|
395
|
-
const ga4ClientId = this.customParams.ga4ClientId;
|
|
396
|
-
if (!ga4ClientId) return;
|
|
542
|
+
async syncParamsToCallForgeIfPossible() {
|
|
397
543
|
const locationId = this.getLocationId();
|
|
398
544
|
const sessionToken = this.cache.getSessionToken(locationId);
|
|
399
545
|
if (!sessionToken) return;
|
|
400
546
|
const autoParams = this.getAutoParams();
|
|
401
547
|
const cachedParams = this.cache.getParams();
|
|
402
548
|
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
403
|
-
if (cachedParams.ga4ClientId === params.ga4ClientId) {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
549
|
const data = await this.fetchFromApi(locationId, sessionToken, params);
|
|
407
550
|
this.saveToCache(locationId, data, params);
|
|
408
551
|
}
|
|
@@ -426,6 +569,13 @@ var CallForge = class _CallForge {
|
|
|
426
569
|
const qs = sorted.map((k) => `${k}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
427
570
|
return `${endpoint}/v1/tracking/session?${qs}`;
|
|
428
571
|
}
|
|
572
|
+
buildLocationUrl(locationId) {
|
|
573
|
+
const { endpoint } = this.config;
|
|
574
|
+
if (!locationId) {
|
|
575
|
+
return `${endpoint}/v1/tracking/location`;
|
|
576
|
+
}
|
|
577
|
+
return `${endpoint}/v1/tracking/location?loc_physical_ms=${encodeURIComponent(locationId)}`;
|
|
578
|
+
}
|
|
429
579
|
};
|
|
430
580
|
|
|
431
581
|
// src/preload.ts
|
|
@@ -455,6 +605,17 @@ if(m){try{var g=decodeURIComponent(m[1]||'');var s=g.split('.');if(s.length>=2){
|
|
|
455
605
|
for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
456
606
|
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
457
607
|
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
608
|
+
var lkey='cf_location_v1_'+site;
|
|
609
|
+
try{
|
|
610
|
+
var cl=JSON.parse(localStorage.getItem(lkey));
|
|
611
|
+
if(cl&&cl.expiresAt>Date.now()+30000){
|
|
612
|
+
if(!loc||(loc&&cl.locId===loc)){window.__cfTrackingLocation=Promise.resolve(cl)}
|
|
613
|
+
}}catch(e){}
|
|
614
|
+
if(!window.__cfTrackingLocation){
|
|
615
|
+
var lurl='${endpoint}/v1/tracking/location';
|
|
616
|
+
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});
|
|
618
|
+
}
|
|
458
619
|
var token=null;
|
|
459
620
|
try{
|
|
460
621
|
var c=JSON.parse(localStorage.getItem(key));
|
|
@@ -469,7 +630,7 @@ if(loc)url+='&loc_physical_ms='+loc;
|
|
|
469
630
|
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
470
631
|
var ks=Object.keys(p).sort();
|
|
471
632
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
472
|
-
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,
|
|
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});
|
|
473
634
|
})();`.replace(/\n/g, "");
|
|
474
635
|
return `<link rel="preconnect" href="${endpoint}">
|
|
475
636
|
<script>${script}</script>`;
|
package/dist/index.mjs
CHANGED
|
@@ -114,6 +114,67 @@ var TrackingCache = class {
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
// src/location-cache.ts
|
|
118
|
+
var EXPIRY_BUFFER_MS2 = 3e4;
|
|
119
|
+
var LocationCache = class {
|
|
120
|
+
constructor(siteKey) {
|
|
121
|
+
this.memoryCache = null;
|
|
122
|
+
this.useMemory = false;
|
|
123
|
+
const resolvedSiteKey = siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
124
|
+
this.key = `cf_location_v1_${resolvedSiteKey}`;
|
|
125
|
+
this.useMemory = !this.isLocalStorageAvailable();
|
|
126
|
+
}
|
|
127
|
+
isLocalStorageAvailable() {
|
|
128
|
+
try {
|
|
129
|
+
localStorage.setItem("__cf_test__", "1");
|
|
130
|
+
localStorage.removeItem("__cf_test__");
|
|
131
|
+
return true;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
get(locationId) {
|
|
137
|
+
const cached = this.read();
|
|
138
|
+
if (!cached) return null;
|
|
139
|
+
if (cached.expiresAt - EXPIRY_BUFFER_MS2 <= Date.now()) return null;
|
|
140
|
+
if (!locationId) return cached;
|
|
141
|
+
if (cached.locId !== locationId) return null;
|
|
142
|
+
return cached;
|
|
143
|
+
}
|
|
144
|
+
set(value) {
|
|
145
|
+
if (this.useMemory) {
|
|
146
|
+
this.memoryCache = value;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
localStorage.setItem(this.key, JSON.stringify(value));
|
|
151
|
+
} catch (e) {
|
|
152
|
+
this.memoryCache = value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
clear() {
|
|
156
|
+
this.memoryCache = null;
|
|
157
|
+
if (!this.useMemory) {
|
|
158
|
+
try {
|
|
159
|
+
localStorage.removeItem(this.key);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
read() {
|
|
165
|
+
if (this.useMemory) {
|
|
166
|
+
return this.memoryCache;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const raw = localStorage.getItem(this.key);
|
|
170
|
+
if (!raw) return null;
|
|
171
|
+
return JSON.parse(raw);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return this.memoryCache;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
117
178
|
// src/client.ts
|
|
118
179
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
119
180
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -122,6 +183,7 @@ var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campai
|
|
|
122
183
|
var CallForge = class _CallForge {
|
|
123
184
|
constructor(config) {
|
|
124
185
|
this.sessionPromise = null;
|
|
186
|
+
this.locationPromise = null;
|
|
125
187
|
this.customParams = {};
|
|
126
188
|
this.config = {
|
|
127
189
|
categoryId: config.categoryId,
|
|
@@ -130,6 +192,7 @@ var CallForge = class _CallForge {
|
|
|
130
192
|
siteKey: config.siteKey
|
|
131
193
|
};
|
|
132
194
|
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
195
|
+
this.locationCache = new LocationCache(config.siteKey);
|
|
133
196
|
this.captureGA4ClientId();
|
|
134
197
|
this.startGA4ClientIdPolling();
|
|
135
198
|
}
|
|
@@ -150,6 +213,27 @@ var CallForge = class _CallForge {
|
|
|
150
213
|
this.sessionPromise = this.fetchSession();
|
|
151
214
|
return this.sessionPromise;
|
|
152
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Get location data only.
|
|
218
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
219
|
+
*/
|
|
220
|
+
async getLocation() {
|
|
221
|
+
if (this.locationPromise) {
|
|
222
|
+
return this.locationPromise;
|
|
223
|
+
}
|
|
224
|
+
this.locationPromise = this.fetchLocation();
|
|
225
|
+
return this.locationPromise;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Kick off both session and location requests.
|
|
229
|
+
* Returns separate Promises so consumers can use each as soon as it resolves.
|
|
230
|
+
*/
|
|
231
|
+
getSessionAsync() {
|
|
232
|
+
return {
|
|
233
|
+
session: this.getSession(),
|
|
234
|
+
location: this.getLocation()
|
|
235
|
+
};
|
|
236
|
+
}
|
|
153
237
|
/**
|
|
154
238
|
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
155
239
|
*/
|
|
@@ -186,23 +270,35 @@ var CallForge = class _CallForge {
|
|
|
186
270
|
});
|
|
187
271
|
}
|
|
188
272
|
/**
|
|
189
|
-
*
|
|
273
|
+
* Subscribe to location ready event.
|
|
274
|
+
* Callback is called once location data is available.
|
|
275
|
+
*/
|
|
276
|
+
onLocationReady(callback) {
|
|
277
|
+
this.getLocation().then(callback).catch(() => {
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Set custom tracking parameters for attribution.
|
|
282
|
+
*
|
|
283
|
+
* When possible, the client will also refresh the session once to sync
|
|
284
|
+
* the updated params to CallForge (so server-side events can use them).
|
|
190
285
|
*/
|
|
191
286
|
async setParams(params) {
|
|
192
287
|
this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
} else {
|
|
204
|
-
void sync();
|
|
288
|
+
const sync = async () => {
|
|
289
|
+
try {
|
|
290
|
+
await this.syncParamsToCallForgeIfPossible();
|
|
291
|
+
} catch (e) {
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
if (this.sessionPromise) {
|
|
295
|
+
try {
|
|
296
|
+
await this.sessionPromise;
|
|
297
|
+
} catch (e) {
|
|
205
298
|
}
|
|
299
|
+
await sync();
|
|
300
|
+
} else {
|
|
301
|
+
await sync();
|
|
206
302
|
}
|
|
207
303
|
}
|
|
208
304
|
/**
|
|
@@ -235,7 +331,6 @@ var CallForge = class _CallForge {
|
|
|
235
331
|
});
|
|
236
332
|
}
|
|
237
333
|
startGA4ClientIdPolling() {
|
|
238
|
-
if (!this.config.ga4MeasurementId) return;
|
|
239
334
|
if (typeof window === "undefined") return;
|
|
240
335
|
if (this.customParams.ga4ClientId) return;
|
|
241
336
|
const MAX_ATTEMPTS = 20;
|
|
@@ -304,6 +399,37 @@ var CallForge = class _CallForge {
|
|
|
304
399
|
this.saveToCache(locationId, data, params);
|
|
305
400
|
return this.formatApiResponse(data);
|
|
306
401
|
}
|
|
402
|
+
async fetchLocation() {
|
|
403
|
+
var _a;
|
|
404
|
+
const locationId = this.getLocationId();
|
|
405
|
+
if (typeof window !== "undefined" && window.__cfTrackingLocation) {
|
|
406
|
+
try {
|
|
407
|
+
const data2 = await window.__cfTrackingLocation;
|
|
408
|
+
const dataWithExtras = data2;
|
|
409
|
+
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;
|
|
415
|
+
this.saveLocationToCache(effectiveLocId, location2, data2.expiresAt);
|
|
416
|
+
return location2;
|
|
417
|
+
} catch (e) {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const cached = this.locationCache.get(locationId);
|
|
421
|
+
if (cached) {
|
|
422
|
+
return cached.location;
|
|
423
|
+
}
|
|
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;
|
|
430
|
+
this.saveLocationToCache(locationId, location, data.expiresAt);
|
|
431
|
+
return location;
|
|
432
|
+
}
|
|
307
433
|
getLocationId() {
|
|
308
434
|
if (typeof window === "undefined") return null;
|
|
309
435
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -338,47 +464,64 @@ var CallForge = class _CallForge {
|
|
|
338
464
|
clearTimeout(timeoutId);
|
|
339
465
|
}
|
|
340
466
|
}
|
|
467
|
+
async fetchLocationFromApi(locationId) {
|
|
468
|
+
const url = this.buildLocationUrl(locationId);
|
|
469
|
+
const controller = new AbortController();
|
|
470
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
471
|
+
try {
|
|
472
|
+
const response = await fetch(url, {
|
|
473
|
+
credentials: "omit",
|
|
474
|
+
signal: controller.signal
|
|
475
|
+
});
|
|
476
|
+
if (!response.ok) {
|
|
477
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
478
|
+
}
|
|
479
|
+
return await response.json();
|
|
480
|
+
} finally {
|
|
481
|
+
clearTimeout(timeoutId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
341
484
|
saveToCache(locationId, data, params) {
|
|
342
485
|
const cached = {
|
|
343
486
|
locId: locationId != null ? locationId : null,
|
|
344
487
|
sessionToken: data.sessionToken,
|
|
345
488
|
leaseId: data.leaseId,
|
|
346
489
|
phoneNumber: data.phoneNumber,
|
|
347
|
-
location: data.location,
|
|
348
490
|
expiresAt: data.expiresAt,
|
|
349
491
|
tokenVersion: "v1",
|
|
350
492
|
params
|
|
351
493
|
};
|
|
352
494
|
this.cache.set(cached);
|
|
353
495
|
}
|
|
496
|
+
saveLocationToCache(locationId, location, expiresAt) {
|
|
497
|
+
this.locationCache.set({
|
|
498
|
+
locId: locationId != null ? locationId : null,
|
|
499
|
+
location,
|
|
500
|
+
expiresAt,
|
|
501
|
+
tokenVersion: "v1"
|
|
502
|
+
});
|
|
503
|
+
}
|
|
354
504
|
formatSession(cached) {
|
|
355
505
|
return {
|
|
356
506
|
sessionToken: cached.sessionToken,
|
|
357
507
|
leaseId: cached.leaseId,
|
|
358
|
-
phoneNumber: cached.phoneNumber
|
|
359
|
-
location: cached.location
|
|
508
|
+
phoneNumber: cached.phoneNumber
|
|
360
509
|
};
|
|
361
510
|
}
|
|
362
511
|
formatApiResponse(data) {
|
|
363
512
|
return {
|
|
364
513
|
sessionToken: data.sessionToken,
|
|
365
514
|
leaseId: data.leaseId,
|
|
366
|
-
phoneNumber: data.phoneNumber
|
|
367
|
-
location: data.location
|
|
515
|
+
phoneNumber: data.phoneNumber
|
|
368
516
|
};
|
|
369
517
|
}
|
|
370
|
-
async
|
|
371
|
-
const ga4ClientId = this.customParams.ga4ClientId;
|
|
372
|
-
if (!ga4ClientId) return;
|
|
518
|
+
async syncParamsToCallForgeIfPossible() {
|
|
373
519
|
const locationId = this.getLocationId();
|
|
374
520
|
const sessionToken = this.cache.getSessionToken(locationId);
|
|
375
521
|
if (!sessionToken) return;
|
|
376
522
|
const autoParams = this.getAutoParams();
|
|
377
523
|
const cachedParams = this.cache.getParams();
|
|
378
524
|
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
379
|
-
if (cachedParams.ga4ClientId === params.ga4ClientId) {
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
525
|
const data = await this.fetchFromApi(locationId, sessionToken, params);
|
|
383
526
|
this.saveToCache(locationId, data, params);
|
|
384
527
|
}
|
|
@@ -402,6 +545,13 @@ var CallForge = class _CallForge {
|
|
|
402
545
|
const qs = sorted.map((k) => `${k}=${encodeURIComponent(queryParams[k])}`).join("&");
|
|
403
546
|
return `${endpoint}/v1/tracking/session?${qs}`;
|
|
404
547
|
}
|
|
548
|
+
buildLocationUrl(locationId) {
|
|
549
|
+
const { endpoint } = this.config;
|
|
550
|
+
if (!locationId) {
|
|
551
|
+
return `${endpoint}/v1/tracking/location`;
|
|
552
|
+
}
|
|
553
|
+
return `${endpoint}/v1/tracking/location?loc_physical_ms=${encodeURIComponent(locationId)}`;
|
|
554
|
+
}
|
|
405
555
|
};
|
|
406
556
|
|
|
407
557
|
// src/preload.ts
|
|
@@ -431,6 +581,17 @@ if(m){try{var g=decodeURIComponent(m[1]||'');var s=g.split('.');if(s.length>=2){
|
|
|
431
581
|
for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
432
582
|
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
433
583
|
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
584
|
+
var lkey='cf_location_v1_'+site;
|
|
585
|
+
try{
|
|
586
|
+
var cl=JSON.parse(localStorage.getItem(lkey));
|
|
587
|
+
if(cl&&cl.expiresAt>Date.now()+30000){
|
|
588
|
+
if(!loc||(loc&&cl.locId===loc)){window.__cfTrackingLocation=Promise.resolve(cl)}
|
|
589
|
+
}}catch(e){}
|
|
590
|
+
if(!window.__cfTrackingLocation){
|
|
591
|
+
var lurl='${endpoint}/v1/tracking/location';
|
|
592
|
+
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});
|
|
594
|
+
}
|
|
434
595
|
var token=null;
|
|
435
596
|
try{
|
|
436
597
|
var c=JSON.parse(localStorage.getItem(key));
|
|
@@ -445,7 +606,7 @@ if(loc)url+='&loc_physical_ms='+loc;
|
|
|
445
606
|
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
446
607
|
var ks=Object.keys(p).sort();
|
|
447
608
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
448
|
-
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,
|
|
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});
|
|
449
610
|
})();`.replace(/\n/g, "");
|
|
450
611
|
return `<link rel="preconnect" href="${endpoint}">
|
|
451
612
|
<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.7.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
|
+
},
|
|
20
27
|
"scripts": {
|
|
21
28
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
22
29
|
"clean": "rm -rf dist",
|
|
23
30
|
"test": "vitest run",
|
|
24
31
|
"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
|
+
}
|