@callforge/tracking-client 0.5.1 → 0.6.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 +66 -98
- package/dist/index.d.mts +22 -21
- package/dist/index.d.ts +22 -21
- package/dist/index.js +61 -59
- package/dist/index.mjs +61 -59
- package/package.json +15 -12
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @callforge/tracking-client
|
|
2
2
|
|
|
3
|
-
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
|
|
4
|
-
|
|
5
|
-
**v0.4.1** - Simplified GA4 capture: uses gtag callback when `ga4MeasurementId` configured, no polling fallback.
|
|
3
|
+
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and optional preload optimization.
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
@@ -26,12 +24,13 @@ const snippet = getPreloadSnippet({ categoryId: 'your-category-id' });
|
|
|
26
24
|
```
|
|
27
25
|
|
|
28
26
|
Generated HTML:
|
|
27
|
+
|
|
29
28
|
```html
|
|
30
29
|
<link rel="preconnect" href="https://tracking.callforge.io">
|
|
31
30
|
<script>/* preload script */</script>
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
### 2. Initialize and
|
|
33
|
+
### 2. Initialize and fetch a tracking session
|
|
35
34
|
|
|
36
35
|
```typescript
|
|
37
36
|
import { CallForge } from '@callforge/tracking-client';
|
|
@@ -41,40 +40,52 @@ const client = CallForge.init({
|
|
|
41
40
|
// endpoint: 'https://tracking-dev.callforge.io', // Optional: override for dev
|
|
42
41
|
});
|
|
43
42
|
|
|
44
|
-
// Promise style
|
|
45
43
|
const session = await client.getSession();
|
|
46
44
|
console.log(session.phoneNumber); // "+17705550000" or null
|
|
47
45
|
console.log(session.location); // { city: "Woodstock", state: "Georgia", stateCode: "GA" } or null
|
|
46
|
+
```
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
### 3. Deterministic click/callback attribution (optional)
|
|
49
|
+
|
|
50
|
+
If you initiate calls programmatically (click-to-call / callback), you can request a short-lived `callIntentToken`. CallForge will consume this once to deterministically map the call back to the web session that requested it.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// In the browser:
|
|
54
|
+
const { callIntentToken } = await client.createCallIntent();
|
|
55
|
+
|
|
56
|
+
// Send to your backend and attach to the call you initiate
|
|
57
|
+
await fetch('/api/callback', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ callIntentToken }),
|
|
53
61
|
});
|
|
54
62
|
```
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
Notes:
|
|
65
|
+
- `callIntentToken` is short-lived and single-use.
|
|
66
|
+
- Treat it as an opaque secret (do not log it).
|
|
67
|
+
|
|
68
|
+
### 4. GA4 Integration
|
|
57
69
|
|
|
58
70
|
To enable GA4 call event tracking, provide your GA4 Measurement ID:
|
|
59
71
|
|
|
60
72
|
```typescript
|
|
61
73
|
const client = CallForge.init({
|
|
62
74
|
categoryId: 'your-category-id',
|
|
63
|
-
ga4MeasurementId: 'G-XXXXXXXXXX',
|
|
75
|
+
ga4MeasurementId: 'G-XXXXXXXXXX',
|
|
64
76
|
});
|
|
65
77
|
```
|
|
66
78
|
|
|
67
|
-
|
|
68
|
-
1. Google Analytics 4 must be installed on your site (`gtag.js`)
|
|
69
|
-
2. Provide `ga4MeasurementId` in client config
|
|
70
|
-
3. Configure GA4 credentials in CallForge dashboard
|
|
79
|
+
Requirements:
|
|
80
|
+
1. Google Analytics 4 must be installed on your site (`gtag.js`).
|
|
81
|
+
2. Provide `ga4MeasurementId` in client config.
|
|
82
|
+
3. Configure GA4 credentials in CallForge dashboard.
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
- Uses `gtag('get', measurementId, 'client_id', callback)` to capture the GA4 client ID
|
|
74
|
-
-
|
|
75
|
-
- Client ID is automatically sent to CallForge for call event attribution
|
|
84
|
+
How it works:
|
|
85
|
+
- Uses `gtag('get', measurementId, 'client_id', callback)` to capture the GA4 client ID.
|
|
86
|
+
- Client ID is sent to CallForge for call event attribution.
|
|
76
87
|
|
|
77
|
-
|
|
88
|
+
Manual override:
|
|
78
89
|
|
|
79
90
|
```typescript
|
|
80
91
|
client.setParams({
|
|
@@ -82,7 +93,7 @@ client.setParams({
|
|
|
82
93
|
});
|
|
83
94
|
```
|
|
84
95
|
|
|
85
|
-
###
|
|
96
|
+
### 5. Track conversion parameters (optional)
|
|
86
97
|
|
|
87
98
|
The client automatically captures ad platform click IDs from the URL:
|
|
88
99
|
|
|
@@ -95,14 +106,13 @@ The client automatically captures ad platform click IDs from the URL:
|
|
|
95
106
|
You can also add custom parameters:
|
|
96
107
|
|
|
97
108
|
```typescript
|
|
98
|
-
// Add custom tracking parameters
|
|
99
109
|
client.setParams({
|
|
100
110
|
customerId: '456',
|
|
101
111
|
landingPage: 'pricing',
|
|
102
112
|
});
|
|
103
113
|
|
|
104
114
|
// Parameters are automatically sent with every API request
|
|
105
|
-
|
|
115
|
+
await client.getSession();
|
|
106
116
|
```
|
|
107
117
|
|
|
108
118
|
## API Reference
|
|
@@ -115,18 +125,19 @@ Initialize the tracking client.
|
|
|
115
125
|
interface CallForgeConfig {
|
|
116
126
|
categoryId: string; // Required - which number pool to use
|
|
117
127
|
endpoint?: string; // Optional - defaults to 'https://tracking.callforge.io'
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
siteKey?: string; // Optional - cache partition key (defaults to window.location.hostname)
|
|
129
|
+
ga4MeasurementId?: string; // Optional - GA4 Measurement ID (e.g., 'G-XXXXXXXXXX')
|
|
120
130
|
}
|
|
121
131
|
```
|
|
122
132
|
|
|
123
133
|
### `client.getSession()`
|
|
124
134
|
|
|
125
|
-
Get tracking session data. Returns cached data if valid, otherwise fetches from API.
|
|
135
|
+
Get tracking session data. Returns cached data if valid, otherwise fetches from the API.
|
|
126
136
|
|
|
127
137
|
```typescript
|
|
128
138
|
interface TrackingSession {
|
|
129
|
-
|
|
139
|
+
sessionToken: string; // Signed, opaque token used to refresh the session
|
|
140
|
+
leaseId: string | null; // Deterministic assignment lease ID (when available)
|
|
130
141
|
phoneNumber: string | null;
|
|
131
142
|
location: {
|
|
132
143
|
city: string;
|
|
@@ -136,11 +147,20 @@ interface TrackingSession {
|
|
|
136
147
|
}
|
|
137
148
|
```
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
- Returns
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
- Throws on network errors or API errors
|
|
150
|
+
Behavior:
|
|
151
|
+
- Returns cached data if valid.
|
|
152
|
+
- Fetches fresh data when cache is missing/expired.
|
|
153
|
+
- If `loc_physical_ms` is present in the URL, cached sessions are only reused when it matches the cached `locId`.
|
|
154
|
+
- Throws on network errors or API errors.
|
|
155
|
+
|
|
156
|
+
### `client.createCallIntent()`
|
|
157
|
+
|
|
158
|
+
Create a short-lived call intent token for click/callback deterministic attribution.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const intent = await client.createCallIntent();
|
|
162
|
+
console.log(intent.callIntentToken);
|
|
163
|
+
```
|
|
144
164
|
|
|
145
165
|
### `client.onReady(callback)`
|
|
146
166
|
|
|
@@ -164,11 +184,10 @@ client.setParams({
|
|
|
164
184
|
});
|
|
165
185
|
```
|
|
166
186
|
|
|
167
|
-
|
|
168
|
-
- Merges with existing params (later calls override earlier values)
|
|
169
|
-
- Parameters are sent with every `getSession()` API request
|
|
170
|
-
- Persisted in localStorage alongside session data
|
|
171
|
-
- Auto-captured params (gclid, etc.) are included automatically
|
|
187
|
+
Behavior:
|
|
188
|
+
- Merges with existing params (later calls override earlier values).
|
|
189
|
+
- Parameters are sent with every `getSession()` API request.
|
|
190
|
+
- Persisted in localStorage alongside session data.
|
|
172
191
|
|
|
173
192
|
### `getPreloadSnippet(config)`
|
|
174
193
|
|
|
@@ -180,6 +199,7 @@ import { getPreloadSnippet } from '@callforge/tracking-client';
|
|
|
180
199
|
const html = getPreloadSnippet({
|
|
181
200
|
categoryId: 'your-category-id',
|
|
182
201
|
endpoint: 'https://tracking.callforge.io', // Optional
|
|
202
|
+
siteKey: 'example.com', // Optional
|
|
183
203
|
});
|
|
184
204
|
```
|
|
185
205
|
|
|
@@ -194,32 +214,22 @@ The client automatically extracts these parameters from the URL:
|
|
|
194
214
|
| `gclid` | Google Ads Click ID |
|
|
195
215
|
| `gbraid` | Google app-to-web (iOS 14+) |
|
|
196
216
|
| `wbraid` | Google web-to-app |
|
|
197
|
-
| `msclkid` | Microsoft/Bing Ads |
|
|
198
|
-
| `fbclid` | Facebook/Meta Ads |
|
|
199
|
-
|
|
200
|
-
**Note:** `ga4ClientId` is captured via `gtag('get')` callback when `ga4MeasurementId` is configured.
|
|
201
|
-
|
|
202
|
-
### Persistence
|
|
203
|
-
|
|
204
|
-
- Parameters are stored in localStorage alongside the session
|
|
205
|
-
- On subsequent visits, cached params are used (URL may no longer have them)
|
|
206
|
-
- Custom params via `setParams()` merge with auto-captured params
|
|
207
|
-
- Later values override earlier ones (custom > cached > auto-captured)
|
|
217
|
+
| `msclkid` | Microsoft/Bing Ads Click ID |
|
|
218
|
+
| `fbclid` | Facebook/Meta Ads Click ID |
|
|
208
219
|
|
|
209
220
|
### API Request Format
|
|
210
221
|
|
|
211
|
-
Parameters are sent as sorted query string for cache consistency:
|
|
222
|
+
Parameters are sent as a sorted query string for cache consistency:
|
|
212
223
|
|
|
213
224
|
```
|
|
214
|
-
/v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221
|
|
225
|
+
/v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221&sessionToken=...
|
|
215
226
|
```
|
|
216
227
|
|
|
217
228
|
## Caching Behavior
|
|
218
229
|
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
-
|
|
222
|
-
- **Invalidation:** Cache clears when location changes
|
|
230
|
+
- Cache key: `cf_tracking_v1_<siteKey>_<categoryId>`
|
|
231
|
+
- TTL: controlled by the server `expiresAt` response (currently 30 minutes)
|
|
232
|
+
- Storage: localStorage (falls back to memory if unavailable)
|
|
223
233
|
|
|
224
234
|
## Error Handling
|
|
225
235
|
|
|
@@ -229,7 +239,7 @@ try {
|
|
|
229
239
|
if (session.phoneNumber) {
|
|
230
240
|
// Use phone number
|
|
231
241
|
} else {
|
|
232
|
-
// No phone number available
|
|
242
|
+
// No phone number available
|
|
233
243
|
}
|
|
234
244
|
} catch (err) {
|
|
235
245
|
// Network error or API error
|
|
@@ -248,52 +258,10 @@ import type {
|
|
|
248
258
|
TrackingLocation,
|
|
249
259
|
TrackingParams,
|
|
250
260
|
ReadyCallback,
|
|
261
|
+
CallIntentResponse,
|
|
251
262
|
} from '@callforge/tracking-client';
|
|
252
263
|
```
|
|
253
264
|
|
|
254
|
-
### TrackingParams
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
interface TrackingParams {
|
|
258
|
-
gclid?: string; // Google Ads Click ID
|
|
259
|
-
gbraid?: string; // Google app-to-web (iOS 14+)
|
|
260
|
-
wbraid?: string; // Google web-to-app
|
|
261
|
-
msclkid?: string; // Microsoft/Bing Ads
|
|
262
|
-
fbclid?: string; // Facebook/Meta Ads
|
|
263
|
-
ga4ClientId?: string; // Google Analytics 4 Client ID (auto-captured via gtag callback)
|
|
264
|
-
[key: string]: string | undefined; // Custom params
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
## GA4 Events
|
|
269
|
-
|
|
270
|
-
When GA4 is configured for a category, CallForge sends these events to Google Analytics 4 via the Measurement Protocol:
|
|
271
|
-
|
|
272
|
-
| Event Name | When Sent | Description |
|
|
273
|
-
|------------|-----------|-------------|
|
|
274
|
-
| `phone_call` | Call initiated | A phone call was placed to the tracking number |
|
|
275
|
-
| `call_conversion` | Call qualified | The call met conversion criteria (e.g., duration threshold) |
|
|
276
|
-
| `call_conversion_billable` | Call billable | The call was both qualified AND billable |
|
|
277
|
-
|
|
278
|
-
**Event Parameters:**
|
|
279
|
-
|
|
280
|
-
All events include:
|
|
281
|
-
- `session_id` - CallForge session ID
|
|
282
|
-
- `phone_number` - Tracking number dialed
|
|
283
|
-
- `category` - Category slug
|
|
284
|
-
|
|
285
|
-
`call_conversion` and `call_conversion_billable` also include:
|
|
286
|
-
- `call_duration` - Call duration in seconds
|
|
287
|
-
- `buyer` - Buyer the call was routed to (if applicable)
|
|
288
|
-
|
|
289
|
-
**Setup:**
|
|
290
|
-
1. In Google Analytics 4, go to Admin → Data Streams → your web stream
|
|
291
|
-
2. Copy the **Measurement ID** (e.g., `G-XXXXXXXXXX`)
|
|
292
|
-
3. Go to Admin → Data Streams → Measurement Protocol API secrets → Create
|
|
293
|
-
4. Copy the **API Secret**
|
|
294
|
-
5. In CallForge dashboard: Categories → Edit category → GA4 tab
|
|
295
|
-
6. Enable GA4, paste Measurement ID and API Secret, save
|
|
296
|
-
|
|
297
265
|
## Environment URLs
|
|
298
266
|
|
|
299
267
|
| Environment | Endpoint |
|
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,8 @@ interface CallForgeConfig {
|
|
|
6
6
|
categoryId: string;
|
|
7
7
|
/** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
|
|
8
8
|
endpoint?: string;
|
|
9
|
+
/** Optional - explicit site key for cache partitioning. Defaults to window.location.hostname */
|
|
10
|
+
siteKey?: string;
|
|
9
11
|
/** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag callback instead of cookie polling. */
|
|
10
12
|
ga4MeasurementId?: string;
|
|
11
13
|
}
|
|
@@ -41,8 +43,10 @@ interface TrackingParams {
|
|
|
41
43
|
* Session data returned by getSession().
|
|
42
44
|
*/
|
|
43
45
|
interface TrackingSession {
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
+
/** Signed session token */
|
|
47
|
+
sessionToken: string;
|
|
48
|
+
/** Lease ID for deterministic number assignment */
|
|
49
|
+
leaseId: string | null;
|
|
46
50
|
/** Assigned phone number, or null if no number available */
|
|
47
51
|
phoneNumber: string | null;
|
|
48
52
|
/** Location data, or null if location could not be resolved */
|
|
@@ -52,7 +56,8 @@ interface TrackingSession {
|
|
|
52
56
|
* Internal API response format (includes expiresAt).
|
|
53
57
|
*/
|
|
54
58
|
interface ApiResponse {
|
|
55
|
-
|
|
59
|
+
sessionToken: string;
|
|
60
|
+
leaseId: string | null;
|
|
56
61
|
phoneNumber: string | null;
|
|
57
62
|
location: {
|
|
58
63
|
city: string;
|
|
@@ -61,6 +66,14 @@ interface ApiResponse {
|
|
|
61
66
|
} | null;
|
|
62
67
|
expiresAt: number;
|
|
63
68
|
}
|
|
69
|
+
interface CallIntentResponse {
|
|
70
|
+
callIntentToken: string;
|
|
71
|
+
intentId: string;
|
|
72
|
+
sessionId: string;
|
|
73
|
+
leaseId: string | null;
|
|
74
|
+
expiresAt: string;
|
|
75
|
+
attributionVersion: 'v1';
|
|
76
|
+
}
|
|
64
77
|
/**
|
|
65
78
|
* Callback function for onReady subscription.
|
|
66
79
|
*/
|
|
@@ -80,8 +93,6 @@ declare class CallForge {
|
|
|
80
93
|
private readonly cache;
|
|
81
94
|
private sessionPromise;
|
|
82
95
|
private customParams;
|
|
83
|
-
private sessionId;
|
|
84
|
-
private sessionCreated;
|
|
85
96
|
private constructor();
|
|
86
97
|
/**
|
|
87
98
|
* Initialize the CallForge tracking client.
|
|
@@ -92,22 +103,23 @@ declare class CallForge {
|
|
|
92
103
|
* Returns cached data if valid, otherwise fetches from API.
|
|
93
104
|
*/
|
|
94
105
|
getSession(): Promise<TrackingSession>;
|
|
106
|
+
/**
|
|
107
|
+
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
108
|
+
*/
|
|
109
|
+
createCallIntent(): Promise<CallIntentResponse>;
|
|
95
110
|
/**
|
|
96
111
|
* Subscribe to session ready event.
|
|
97
112
|
* Callback is called once session data is available.
|
|
98
113
|
*/
|
|
99
114
|
onReady(callback: ReadyCallback): void;
|
|
100
115
|
/**
|
|
101
|
-
* Set custom tracking parameters.
|
|
102
|
-
* If session already exists, sends PATCH to update.
|
|
103
|
-
* Otherwise queues params for next session request.
|
|
116
|
+
* Set custom tracking parameters for the next session fetch.
|
|
104
117
|
*/
|
|
105
118
|
setParams(params: Record<string, string>): Promise<void>;
|
|
106
119
|
/**
|
|
107
120
|
* Get the currently queued custom params.
|
|
108
121
|
*/
|
|
109
122
|
getQueuedParams(): TrackingParams;
|
|
110
|
-
private patchSession;
|
|
111
123
|
/**
|
|
112
124
|
* Capture the GA4 client ID using gtag callback.
|
|
113
125
|
* Only runs if ga4MeasurementId is configured.
|
|
@@ -126,18 +138,7 @@ declare class CallForge {
|
|
|
126
138
|
/**
|
|
127
139
|
* Generate HTML snippet for preloading tracking data.
|
|
128
140
|
* Add this to the <head> of your HTML for optimal performance.
|
|
129
|
-
*
|
|
130
|
-
* The generated snippet:
|
|
131
|
-
* - Checks for loc_physical_ms URL parameter
|
|
132
|
-
* - Checks localStorage cache for valid session
|
|
133
|
-
* - If cache valid, resolves Promise.resolve(cached) immediately
|
|
134
|
-
* - If cache expired but same location, includes sessionId for refresh
|
|
135
|
-
* - Otherwise fetches fresh session data
|
|
136
|
-
* - Sets window.__cfTracking with the session promise
|
|
137
|
-
*
|
|
138
|
-
* @throws {Error} If categoryId contains invalid characters
|
|
139
|
-
* @throws {Error} If endpoint is not a valid HTTPS URL
|
|
140
141
|
*/
|
|
141
142
|
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
142
143
|
|
|
143
|
-
export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
|
144
|
+
export { CallForge, type CallForgeConfig, type CallIntentResponse, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface CallForgeConfig {
|
|
|
6
6
|
categoryId: string;
|
|
7
7
|
/** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
|
|
8
8
|
endpoint?: string;
|
|
9
|
+
/** Optional - explicit site key for cache partitioning. Defaults to window.location.hostname */
|
|
10
|
+
siteKey?: string;
|
|
9
11
|
/** Optional - GA4 Measurement ID (e.g., "G-XXXXXXXXXX"). Enables gtag callback instead of cookie polling. */
|
|
10
12
|
ga4MeasurementId?: string;
|
|
11
13
|
}
|
|
@@ -41,8 +43,10 @@ interface TrackingParams {
|
|
|
41
43
|
* Session data returned by getSession().
|
|
42
44
|
*/
|
|
43
45
|
interface TrackingSession {
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
+
/** Signed session token */
|
|
47
|
+
sessionToken: string;
|
|
48
|
+
/** Lease ID for deterministic number assignment */
|
|
49
|
+
leaseId: string | null;
|
|
46
50
|
/** Assigned phone number, or null if no number available */
|
|
47
51
|
phoneNumber: string | null;
|
|
48
52
|
/** Location data, or null if location could not be resolved */
|
|
@@ -52,7 +56,8 @@ interface TrackingSession {
|
|
|
52
56
|
* Internal API response format (includes expiresAt).
|
|
53
57
|
*/
|
|
54
58
|
interface ApiResponse {
|
|
55
|
-
|
|
59
|
+
sessionToken: string;
|
|
60
|
+
leaseId: string | null;
|
|
56
61
|
phoneNumber: string | null;
|
|
57
62
|
location: {
|
|
58
63
|
city: string;
|
|
@@ -61,6 +66,14 @@ interface ApiResponse {
|
|
|
61
66
|
} | null;
|
|
62
67
|
expiresAt: number;
|
|
63
68
|
}
|
|
69
|
+
interface CallIntentResponse {
|
|
70
|
+
callIntentToken: string;
|
|
71
|
+
intentId: string;
|
|
72
|
+
sessionId: string;
|
|
73
|
+
leaseId: string | null;
|
|
74
|
+
expiresAt: string;
|
|
75
|
+
attributionVersion: 'v1';
|
|
76
|
+
}
|
|
64
77
|
/**
|
|
65
78
|
* Callback function for onReady subscription.
|
|
66
79
|
*/
|
|
@@ -80,8 +93,6 @@ declare class CallForge {
|
|
|
80
93
|
private readonly cache;
|
|
81
94
|
private sessionPromise;
|
|
82
95
|
private customParams;
|
|
83
|
-
private sessionId;
|
|
84
|
-
private sessionCreated;
|
|
85
96
|
private constructor();
|
|
86
97
|
/**
|
|
87
98
|
* Initialize the CallForge tracking client.
|
|
@@ -92,22 +103,23 @@ declare class CallForge {
|
|
|
92
103
|
* Returns cached data if valid, otherwise fetches from API.
|
|
93
104
|
*/
|
|
94
105
|
getSession(): Promise<TrackingSession>;
|
|
106
|
+
/**
|
|
107
|
+
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
108
|
+
*/
|
|
109
|
+
createCallIntent(): Promise<CallIntentResponse>;
|
|
95
110
|
/**
|
|
96
111
|
* Subscribe to session ready event.
|
|
97
112
|
* Callback is called once session data is available.
|
|
98
113
|
*/
|
|
99
114
|
onReady(callback: ReadyCallback): void;
|
|
100
115
|
/**
|
|
101
|
-
* Set custom tracking parameters.
|
|
102
|
-
* If session already exists, sends PATCH to update.
|
|
103
|
-
* Otherwise queues params for next session request.
|
|
116
|
+
* Set custom tracking parameters for the next session fetch.
|
|
104
117
|
*/
|
|
105
118
|
setParams(params: Record<string, string>): Promise<void>;
|
|
106
119
|
/**
|
|
107
120
|
* Get the currently queued custom params.
|
|
108
121
|
*/
|
|
109
122
|
getQueuedParams(): TrackingParams;
|
|
110
|
-
private patchSession;
|
|
111
123
|
/**
|
|
112
124
|
* Capture the GA4 client ID using gtag callback.
|
|
113
125
|
* Only runs if ga4MeasurementId is configured.
|
|
@@ -126,18 +138,7 @@ declare class CallForge {
|
|
|
126
138
|
/**
|
|
127
139
|
* Generate HTML snippet for preloading tracking data.
|
|
128
140
|
* Add this to the <head> of your HTML for optimal performance.
|
|
129
|
-
*
|
|
130
|
-
* The generated snippet:
|
|
131
|
-
* - Checks for loc_physical_ms URL parameter
|
|
132
|
-
* - Checks localStorage cache for valid session
|
|
133
|
-
* - If cache valid, resolves Promise.resolve(cached) immediately
|
|
134
|
-
* - If cache expired but same location, includes sessionId for refresh
|
|
135
|
-
* - Otherwise fetches fresh session data
|
|
136
|
-
* - Sets window.__cfTracking with the session promise
|
|
137
|
-
*
|
|
138
|
-
* @throws {Error} If categoryId contains invalid characters
|
|
139
|
-
* @throws {Error} If endpoint is not a valid HTTPS URL
|
|
140
141
|
*/
|
|
141
142
|
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
142
143
|
|
|
143
|
-
export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
|
144
|
+
export { CallForge, type CallForgeConfig, type CallIntentResponse, type ReadyCallback, type TrackingLocation, type TrackingParams, type TrackingSession, getPreloadSnippet };
|
package/dist/index.js
CHANGED
|
@@ -42,10 +42,11 @@ module.exports = __toCommonJS(index_exports);
|
|
|
42
42
|
// src/cache.ts
|
|
43
43
|
var EXPIRY_BUFFER_MS = 3e4;
|
|
44
44
|
var TrackingCache = class {
|
|
45
|
-
constructor(categoryId) {
|
|
45
|
+
constructor(categoryId, siteKey) {
|
|
46
46
|
this.memoryCache = null;
|
|
47
47
|
this.useMemory = false;
|
|
48
|
-
|
|
48
|
+
const resolvedSiteKey = siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
49
|
+
this.key = `cf_tracking_v1_${resolvedSiteKey}_${categoryId}`;
|
|
49
50
|
this.useMemory = !this.isLocalStorageAvailable();
|
|
50
51
|
}
|
|
51
52
|
isLocalStorageAvailable() {
|
|
@@ -78,17 +79,15 @@ var TrackingCache = class {
|
|
|
78
79
|
return cached;
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
|
-
* Get
|
|
82
|
-
* Used when cache is expired but location is same - send
|
|
83
|
-
*
|
|
84
|
-
* Only returns sessionId if locationId is provided AND matches cached locId.
|
|
85
|
-
* (IP-based sessions don't send sessionId for refresh since location may differ)
|
|
82
|
+
* Get sessionToken for refresh if location matches (regardless of expiry).
|
|
83
|
+
* Used when cache is expired but location is same - send token to server.
|
|
86
84
|
*/
|
|
87
|
-
|
|
85
|
+
getSessionToken(locationId) {
|
|
88
86
|
const cached = this.read();
|
|
89
87
|
if (!cached) return null;
|
|
90
|
-
if (!locationId
|
|
91
|
-
|
|
88
|
+
if (!locationId) return cached.sessionToken;
|
|
89
|
+
if (cached.locId !== locationId) return null;
|
|
90
|
+
return cached.sessionToken;
|
|
92
91
|
}
|
|
93
92
|
/**
|
|
94
93
|
* Get cached params (for merging with new params).
|
|
@@ -142,19 +141,19 @@ var TrackingCache = class {
|
|
|
142
141
|
// src/client.ts
|
|
143
142
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
144
143
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
144
|
+
var CALL_INTENT_TIMEOUT_MS = 8e3;
|
|
145
145
|
var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
|
|
146
146
|
var CallForge = class _CallForge {
|
|
147
147
|
constructor(config) {
|
|
148
148
|
this.sessionPromise = null;
|
|
149
149
|
this.customParams = {};
|
|
150
|
-
this.sessionId = null;
|
|
151
|
-
this.sessionCreated = false;
|
|
152
150
|
this.config = {
|
|
153
151
|
categoryId: config.categoryId,
|
|
154
152
|
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
|
155
|
-
ga4MeasurementId: config.ga4MeasurementId
|
|
153
|
+
ga4MeasurementId: config.ga4MeasurementId,
|
|
154
|
+
siteKey: config.siteKey
|
|
156
155
|
};
|
|
157
|
-
this.cache = new TrackingCache(config.categoryId);
|
|
156
|
+
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
158
157
|
this.captureGA4ClientId();
|
|
159
158
|
}
|
|
160
159
|
/**
|
|
@@ -174,6 +173,33 @@ var CallForge = class _CallForge {
|
|
|
174
173
|
this.sessionPromise = this.fetchSession();
|
|
175
174
|
return this.sessionPromise;
|
|
176
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
178
|
+
*/
|
|
179
|
+
async createCallIntent() {
|
|
180
|
+
const session = await this.getSession();
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutId = setTimeout(() => controller.abort(), CALL_INTENT_TIMEOUT_MS);
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${this.config.endpoint}/v1/tracking/call-intent`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"Content-Type": "application/json"
|
|
188
|
+
},
|
|
189
|
+
credentials: "omit",
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
sessionToken: session.sessionToken
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw new Error(`Call intent API error: ${response.status} ${response.statusText}`);
|
|
197
|
+
}
|
|
198
|
+
return await response.json();
|
|
199
|
+
} finally {
|
|
200
|
+
clearTimeout(timeoutId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
177
203
|
/**
|
|
178
204
|
* Subscribe to session ready event.
|
|
179
205
|
* Callback is called once session data is available.
|
|
@@ -183,15 +209,10 @@ var CallForge = class _CallForge {
|
|
|
183
209
|
});
|
|
184
210
|
}
|
|
185
211
|
/**
|
|
186
|
-
* Set custom tracking parameters.
|
|
187
|
-
* If session already exists, sends PATCH to update.
|
|
188
|
-
* Otherwise queues params for next session request.
|
|
212
|
+
* Set custom tracking parameters for the next session fetch.
|
|
189
213
|
*/
|
|
190
214
|
async setParams(params) {
|
|
191
215
|
this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
|
|
192
|
-
if (this.sessionCreated && this.sessionId) {
|
|
193
|
-
await this.patchSession(params);
|
|
194
|
-
}
|
|
195
216
|
}
|
|
196
217
|
/**
|
|
197
218
|
* Get the currently queued custom params.
|
|
@@ -199,23 +220,6 @@ var CallForge = class _CallForge {
|
|
|
199
220
|
getQueuedParams() {
|
|
200
221
|
return __spreadValues({}, this.customParams);
|
|
201
222
|
}
|
|
202
|
-
async patchSession(params) {
|
|
203
|
-
try {
|
|
204
|
-
const response = await fetch(
|
|
205
|
-
`${this.config.endpoint}/v1/tracking/session/${this.sessionId}`,
|
|
206
|
-
{
|
|
207
|
-
method: "PATCH",
|
|
208
|
-
headers: { "Content-Type": "application/json" },
|
|
209
|
-
body: JSON.stringify(params)
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
if (!response.ok) {
|
|
213
|
-
console.warn("[CallForge] Failed to patch session:", response.status);
|
|
214
|
-
}
|
|
215
|
-
} catch (error) {
|
|
216
|
-
console.warn("[CallForge] Failed to patch session:", error);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
223
|
/**
|
|
220
224
|
* Capture the GA4 client ID using gtag callback.
|
|
221
225
|
* Only runs if ga4MeasurementId is configured.
|
|
@@ -244,26 +248,20 @@ var CallForge = class _CallForge {
|
|
|
244
248
|
const autoParams2 = this.getAutoParams();
|
|
245
249
|
const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
|
|
246
250
|
this.saveToCache(effectiveLocId, data2, params2);
|
|
247
|
-
this.sessionId = data2.sessionId;
|
|
248
|
-
this.sessionCreated = true;
|
|
249
251
|
return this.formatApiResponse(data2);
|
|
250
252
|
} catch (e) {
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
const cached = this.cache.get(locationId);
|
|
254
256
|
if (cached) {
|
|
255
|
-
this.sessionId = cached.sessionId;
|
|
256
|
-
this.sessionCreated = true;
|
|
257
257
|
return this.formatSession(cached);
|
|
258
258
|
}
|
|
259
259
|
const autoParams = this.getAutoParams();
|
|
260
260
|
const cachedParams = this.cache.getParams();
|
|
261
261
|
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
262
|
-
const
|
|
263
|
-
const data = await this.fetchFromApi(locationId,
|
|
262
|
+
const cachedSessionToken = this.cache.getSessionToken(locationId);
|
|
263
|
+
const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
|
|
264
264
|
this.saveToCache(locationId, data, params);
|
|
265
|
-
this.sessionId = data.sessionId;
|
|
266
|
-
this.sessionCreated = true;
|
|
267
265
|
return this.formatApiResponse(data);
|
|
268
266
|
}
|
|
269
267
|
getLocationId() {
|
|
@@ -283,8 +281,8 @@ var CallForge = class _CallForge {
|
|
|
283
281
|
}
|
|
284
282
|
return params;
|
|
285
283
|
}
|
|
286
|
-
async fetchFromApi(locationId,
|
|
287
|
-
const url = this.buildUrl(locationId,
|
|
284
|
+
async fetchFromApi(locationId, sessionToken, params) {
|
|
285
|
+
const url = this.buildUrl(locationId, sessionToken, params);
|
|
288
286
|
const controller = new AbortController();
|
|
289
287
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
290
288
|
try {
|
|
@@ -303,29 +301,33 @@ var CallForge = class _CallForge {
|
|
|
303
301
|
saveToCache(locationId, data, params) {
|
|
304
302
|
const cached = {
|
|
305
303
|
locId: locationId != null ? locationId : null,
|
|
306
|
-
|
|
304
|
+
sessionToken: data.sessionToken,
|
|
305
|
+
leaseId: data.leaseId,
|
|
307
306
|
phoneNumber: data.phoneNumber,
|
|
308
307
|
location: data.location,
|
|
309
308
|
expiresAt: data.expiresAt,
|
|
309
|
+
tokenVersion: "v1",
|
|
310
310
|
params
|
|
311
311
|
};
|
|
312
312
|
this.cache.set(cached);
|
|
313
313
|
}
|
|
314
314
|
formatSession(cached) {
|
|
315
315
|
return {
|
|
316
|
-
|
|
316
|
+
sessionToken: cached.sessionToken,
|
|
317
|
+
leaseId: cached.leaseId,
|
|
317
318
|
phoneNumber: cached.phoneNumber,
|
|
318
319
|
location: cached.location
|
|
319
320
|
};
|
|
320
321
|
}
|
|
321
322
|
formatApiResponse(data) {
|
|
322
323
|
return {
|
|
323
|
-
|
|
324
|
+
sessionToken: data.sessionToken,
|
|
325
|
+
leaseId: data.leaseId,
|
|
324
326
|
phoneNumber: data.phoneNumber,
|
|
325
327
|
location: data.location
|
|
326
328
|
};
|
|
327
329
|
}
|
|
328
|
-
buildUrl(locationId,
|
|
330
|
+
buildUrl(locationId, sessionToken, params) {
|
|
329
331
|
const { categoryId, endpoint } = this.config;
|
|
330
332
|
const queryParams = {
|
|
331
333
|
categoryId
|
|
@@ -333,8 +335,8 @@ var CallForge = class _CallForge {
|
|
|
333
335
|
if (locationId) {
|
|
334
336
|
queryParams.loc_physical_ms = locationId;
|
|
335
337
|
}
|
|
336
|
-
if (
|
|
337
|
-
queryParams.
|
|
338
|
+
if (sessionToken) {
|
|
339
|
+
queryParams.sessionToken = sessionToken;
|
|
338
340
|
}
|
|
339
341
|
for (const [key, value] of Object.entries(params)) {
|
|
340
342
|
if (value !== void 0) {
|
|
@@ -364,29 +366,29 @@ function getPreloadSnippet(config) {
|
|
|
364
366
|
`Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
|
|
365
367
|
);
|
|
366
368
|
}
|
|
367
|
-
const cacheKey = `cf_tracking_${categoryId}`;
|
|
368
369
|
const script = `(function(){
|
|
369
370
|
var u=new URLSearchParams(location.search);
|
|
370
371
|
var loc=u.get('loc_physical_ms');
|
|
371
372
|
var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
|
|
372
373
|
var p={};
|
|
373
374
|
for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
374
|
-
var
|
|
375
|
-
var
|
|
375
|
+
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
376
|
+
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
377
|
+
var token=null;
|
|
376
378
|
try{
|
|
377
379
|
var c=JSON.parse(localStorage.getItem(key));
|
|
378
380
|
if(c&&c.expiresAt>Date.now()+30000){
|
|
379
381
|
if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
|
|
380
|
-
|
|
382
|
+
token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
381
383
|
var cp=c.params||{};
|
|
382
384
|
p=Object.assign({},cp,p);
|
|
383
385
|
}}catch(e){}
|
|
384
386
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
385
387
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
386
|
-
if(
|
|
388
|
+
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
387
389
|
var ks=Object.keys(p).sort();
|
|
388
390
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
389
|
-
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()}).then(function(d){d.params=p;try{localStorage.setItem(key,JSON.stringify({locId:loc,
|
|
391
|
+
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,location:d.location,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
390
392
|
})();`.replace(/\n/g, "");
|
|
391
393
|
return `<link rel="preconnect" href="${endpoint}">
|
|
392
394
|
<script>${script}</script>`;
|
package/dist/index.mjs
CHANGED
|
@@ -18,10 +18,11 @@ var __spreadValues = (a, b) => {
|
|
|
18
18
|
// src/cache.ts
|
|
19
19
|
var EXPIRY_BUFFER_MS = 3e4;
|
|
20
20
|
var TrackingCache = class {
|
|
21
|
-
constructor(categoryId) {
|
|
21
|
+
constructor(categoryId, siteKey) {
|
|
22
22
|
this.memoryCache = null;
|
|
23
23
|
this.useMemory = false;
|
|
24
|
-
|
|
24
|
+
const resolvedSiteKey = siteKey || (typeof window !== "undefined" ? window.location.hostname : "unknown-site");
|
|
25
|
+
this.key = `cf_tracking_v1_${resolvedSiteKey}_${categoryId}`;
|
|
25
26
|
this.useMemory = !this.isLocalStorageAvailable();
|
|
26
27
|
}
|
|
27
28
|
isLocalStorageAvailable() {
|
|
@@ -54,17 +55,15 @@ var TrackingCache = class {
|
|
|
54
55
|
return cached;
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
|
-
* Get
|
|
58
|
-
* Used when cache is expired but location is same - send
|
|
59
|
-
*
|
|
60
|
-
* Only returns sessionId if locationId is provided AND matches cached locId.
|
|
61
|
-
* (IP-based sessions don't send sessionId for refresh since location may differ)
|
|
58
|
+
* Get sessionToken for refresh if location matches (regardless of expiry).
|
|
59
|
+
* Used when cache is expired but location is same - send token to server.
|
|
62
60
|
*/
|
|
63
|
-
|
|
61
|
+
getSessionToken(locationId) {
|
|
64
62
|
const cached = this.read();
|
|
65
63
|
if (!cached) return null;
|
|
66
|
-
if (!locationId
|
|
67
|
-
|
|
64
|
+
if (!locationId) return cached.sessionToken;
|
|
65
|
+
if (cached.locId !== locationId) return null;
|
|
66
|
+
return cached.sessionToken;
|
|
68
67
|
}
|
|
69
68
|
/**
|
|
70
69
|
* Get cached params (for merging with new params).
|
|
@@ -118,19 +117,19 @@ var TrackingCache = class {
|
|
|
118
117
|
// src/client.ts
|
|
119
118
|
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
120
119
|
var FETCH_TIMEOUT_MS = 1e4;
|
|
120
|
+
var CALL_INTENT_TIMEOUT_MS = 8e3;
|
|
121
121
|
var AUTO_PARAMS = ["gclid", "gbraid", "wbraid", "msclkid", "fbclid", "gad_campaignid", "gad_source"];
|
|
122
122
|
var CallForge = class _CallForge {
|
|
123
123
|
constructor(config) {
|
|
124
124
|
this.sessionPromise = null;
|
|
125
125
|
this.customParams = {};
|
|
126
|
-
this.sessionId = null;
|
|
127
|
-
this.sessionCreated = false;
|
|
128
126
|
this.config = {
|
|
129
127
|
categoryId: config.categoryId,
|
|
130
128
|
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
|
131
|
-
ga4MeasurementId: config.ga4MeasurementId
|
|
129
|
+
ga4MeasurementId: config.ga4MeasurementId,
|
|
130
|
+
siteKey: config.siteKey
|
|
132
131
|
};
|
|
133
|
-
this.cache = new TrackingCache(config.categoryId);
|
|
132
|
+
this.cache = new TrackingCache(config.categoryId, config.siteKey);
|
|
134
133
|
this.captureGA4ClientId();
|
|
135
134
|
}
|
|
136
135
|
/**
|
|
@@ -150,6 +149,33 @@ var CallForge = class _CallForge {
|
|
|
150
149
|
this.sessionPromise = this.fetchSession();
|
|
151
150
|
return this.sessionPromise;
|
|
152
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Create a short-lived call intent token for click/callback deterministic attribution.
|
|
154
|
+
*/
|
|
155
|
+
async createCallIntent() {
|
|
156
|
+
const session = await this.getSession();
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
const timeoutId = setTimeout(() => controller.abort(), CALL_INTENT_TIMEOUT_MS);
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch(`${this.config.endpoint}/v1/tracking/call-intent`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "application/json"
|
|
164
|
+
},
|
|
165
|
+
credentials: "omit",
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
sessionToken: session.sessionToken
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(`Call intent API error: ${response.status} ${response.statusText}`);
|
|
173
|
+
}
|
|
174
|
+
return await response.json();
|
|
175
|
+
} finally {
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
153
179
|
/**
|
|
154
180
|
* Subscribe to session ready event.
|
|
155
181
|
* Callback is called once session data is available.
|
|
@@ -159,15 +185,10 @@ var CallForge = class _CallForge {
|
|
|
159
185
|
});
|
|
160
186
|
}
|
|
161
187
|
/**
|
|
162
|
-
* Set custom tracking parameters.
|
|
163
|
-
* If session already exists, sends PATCH to update.
|
|
164
|
-
* Otherwise queues params for next session request.
|
|
188
|
+
* Set custom tracking parameters for the next session fetch.
|
|
165
189
|
*/
|
|
166
190
|
async setParams(params) {
|
|
167
191
|
this.customParams = __spreadValues(__spreadValues({}, this.customParams), params);
|
|
168
|
-
if (this.sessionCreated && this.sessionId) {
|
|
169
|
-
await this.patchSession(params);
|
|
170
|
-
}
|
|
171
192
|
}
|
|
172
193
|
/**
|
|
173
194
|
* Get the currently queued custom params.
|
|
@@ -175,23 +196,6 @@ var CallForge = class _CallForge {
|
|
|
175
196
|
getQueuedParams() {
|
|
176
197
|
return __spreadValues({}, this.customParams);
|
|
177
198
|
}
|
|
178
|
-
async patchSession(params) {
|
|
179
|
-
try {
|
|
180
|
-
const response = await fetch(
|
|
181
|
-
`${this.config.endpoint}/v1/tracking/session/${this.sessionId}`,
|
|
182
|
-
{
|
|
183
|
-
method: "PATCH",
|
|
184
|
-
headers: { "Content-Type": "application/json" },
|
|
185
|
-
body: JSON.stringify(params)
|
|
186
|
-
}
|
|
187
|
-
);
|
|
188
|
-
if (!response.ok) {
|
|
189
|
-
console.warn("[CallForge] Failed to patch session:", response.status);
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
console.warn("[CallForge] Failed to patch session:", error);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
199
|
/**
|
|
196
200
|
* Capture the GA4 client ID using gtag callback.
|
|
197
201
|
* Only runs if ga4MeasurementId is configured.
|
|
@@ -220,26 +224,20 @@ var CallForge = class _CallForge {
|
|
|
220
224
|
const autoParams2 = this.getAutoParams();
|
|
221
225
|
const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
|
|
222
226
|
this.saveToCache(effectiveLocId, data2, params2);
|
|
223
|
-
this.sessionId = data2.sessionId;
|
|
224
|
-
this.sessionCreated = true;
|
|
225
227
|
return this.formatApiResponse(data2);
|
|
226
228
|
} catch (e) {
|
|
227
229
|
}
|
|
228
230
|
}
|
|
229
231
|
const cached = this.cache.get(locationId);
|
|
230
232
|
if (cached) {
|
|
231
|
-
this.sessionId = cached.sessionId;
|
|
232
|
-
this.sessionCreated = true;
|
|
233
233
|
return this.formatSession(cached);
|
|
234
234
|
}
|
|
235
235
|
const autoParams = this.getAutoParams();
|
|
236
236
|
const cachedParams = this.cache.getParams();
|
|
237
237
|
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
238
|
-
const
|
|
239
|
-
const data = await this.fetchFromApi(locationId,
|
|
238
|
+
const cachedSessionToken = this.cache.getSessionToken(locationId);
|
|
239
|
+
const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
|
|
240
240
|
this.saveToCache(locationId, data, params);
|
|
241
|
-
this.sessionId = data.sessionId;
|
|
242
|
-
this.sessionCreated = true;
|
|
243
241
|
return this.formatApiResponse(data);
|
|
244
242
|
}
|
|
245
243
|
getLocationId() {
|
|
@@ -259,8 +257,8 @@ var CallForge = class _CallForge {
|
|
|
259
257
|
}
|
|
260
258
|
return params;
|
|
261
259
|
}
|
|
262
|
-
async fetchFromApi(locationId,
|
|
263
|
-
const url = this.buildUrl(locationId,
|
|
260
|
+
async fetchFromApi(locationId, sessionToken, params) {
|
|
261
|
+
const url = this.buildUrl(locationId, sessionToken, params);
|
|
264
262
|
const controller = new AbortController();
|
|
265
263
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
266
264
|
try {
|
|
@@ -279,29 +277,33 @@ var CallForge = class _CallForge {
|
|
|
279
277
|
saveToCache(locationId, data, params) {
|
|
280
278
|
const cached = {
|
|
281
279
|
locId: locationId != null ? locationId : null,
|
|
282
|
-
|
|
280
|
+
sessionToken: data.sessionToken,
|
|
281
|
+
leaseId: data.leaseId,
|
|
283
282
|
phoneNumber: data.phoneNumber,
|
|
284
283
|
location: data.location,
|
|
285
284
|
expiresAt: data.expiresAt,
|
|
285
|
+
tokenVersion: "v1",
|
|
286
286
|
params
|
|
287
287
|
};
|
|
288
288
|
this.cache.set(cached);
|
|
289
289
|
}
|
|
290
290
|
formatSession(cached) {
|
|
291
291
|
return {
|
|
292
|
-
|
|
292
|
+
sessionToken: cached.sessionToken,
|
|
293
|
+
leaseId: cached.leaseId,
|
|
293
294
|
phoneNumber: cached.phoneNumber,
|
|
294
295
|
location: cached.location
|
|
295
296
|
};
|
|
296
297
|
}
|
|
297
298
|
formatApiResponse(data) {
|
|
298
299
|
return {
|
|
299
|
-
|
|
300
|
+
sessionToken: data.sessionToken,
|
|
301
|
+
leaseId: data.leaseId,
|
|
300
302
|
phoneNumber: data.phoneNumber,
|
|
301
303
|
location: data.location
|
|
302
304
|
};
|
|
303
305
|
}
|
|
304
|
-
buildUrl(locationId,
|
|
306
|
+
buildUrl(locationId, sessionToken, params) {
|
|
305
307
|
const { categoryId, endpoint } = this.config;
|
|
306
308
|
const queryParams = {
|
|
307
309
|
categoryId
|
|
@@ -309,8 +311,8 @@ var CallForge = class _CallForge {
|
|
|
309
311
|
if (locationId) {
|
|
310
312
|
queryParams.loc_physical_ms = locationId;
|
|
311
313
|
}
|
|
312
|
-
if (
|
|
313
|
-
queryParams.
|
|
314
|
+
if (sessionToken) {
|
|
315
|
+
queryParams.sessionToken = sessionToken;
|
|
314
316
|
}
|
|
315
317
|
for (const [key, value] of Object.entries(params)) {
|
|
316
318
|
if (value !== void 0) {
|
|
@@ -340,29 +342,29 @@ function getPreloadSnippet(config) {
|
|
|
340
342
|
`Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
|
|
341
343
|
);
|
|
342
344
|
}
|
|
343
|
-
const cacheKey = `cf_tracking_${categoryId}`;
|
|
344
345
|
const script = `(function(){
|
|
345
346
|
var u=new URLSearchParams(location.search);
|
|
346
347
|
var loc=u.get('loc_physical_ms');
|
|
347
348
|
var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
|
|
348
349
|
var p={};
|
|
349
350
|
for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
350
|
-
var
|
|
351
|
-
var
|
|
351
|
+
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
352
|
+
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
353
|
+
var token=null;
|
|
352
354
|
try{
|
|
353
355
|
var c=JSON.parse(localStorage.getItem(key));
|
|
354
356
|
if(c&&c.expiresAt>Date.now()+30000){
|
|
355
357
|
if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
|
|
356
|
-
|
|
358
|
+
token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
357
359
|
var cp=c.params||{};
|
|
358
360
|
p=Object.assign({},cp,p);
|
|
359
361
|
}}catch(e){}
|
|
360
362
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
361
363
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
362
|
-
if(
|
|
364
|
+
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
363
365
|
var ks=Object.keys(p).sort();
|
|
364
366
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
365
|
-
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()}).then(function(d){d.params=p;try{localStorage.setItem(key,JSON.stringify({locId:loc,
|
|
367
|
+
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,location:d.location,expiresAt:d.expiresAt,tokenVersion:'v1',params:p}))}catch(e){}return d});
|
|
366
368
|
})();`.replace(/\n/g, "");
|
|
367
369
|
return `<link rel="preconnect" href="${endpoint}">
|
|
368
370
|
<script>${script}</script>`;
|
package/package.json
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@callforge/tracking-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
|
-
"module": "dist/index.
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
|
-
"import": "./dist/index.
|
|
11
|
-
"require": "./dist/index.
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
14
17
|
"files": [
|
|
15
18
|
"dist"
|
|
16
19
|
],
|
|
17
|
-
"devDependencies": {
|
|
18
|
-
"jsdom": "^27.4.0",
|
|
19
|
-
"tsup": "^8.0.0",
|
|
20
|
-
"typescript": "^5.3.0",
|
|
21
|
-
"vitest": "^1.6.0",
|
|
22
|
-
"@callforge/tsconfig": "0.0.0"
|
|
23
|
-
},
|
|
24
20
|
"scripts": {
|
|
25
21
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
26
22
|
"clean": "rm -rf dist",
|
|
27
23
|
"test": "vitest run",
|
|
28
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"
|
|
29
32
|
}
|
|
30
|
-
}
|
|
33
|
+
}
|