@callforge/tracking-client 0.5.0 → 0.6.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 +1 -1
- package/dist/index.d.mts +22 -21
- package/dist/index.d.ts +22 -21
- package/dist/index.js +75 -68
- package/dist/index.mjs +75 -68
- package/package.json +15 -12
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
|
|
4
4
|
|
|
5
|
-
**v0.
|
|
5
|
+
**v0.6.0** - Adds `createCallIntent()` helper for click/callback deterministic attribution and fixes ESM/CJS exports.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
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.
|
|
@@ -234,31 +238,30 @@ var CallForge = class _CallForge {
|
|
|
234
238
|
});
|
|
235
239
|
}
|
|
236
240
|
async fetchSession() {
|
|
241
|
+
var _a;
|
|
237
242
|
const locationId = this.getLocationId();
|
|
238
|
-
const cached = this.cache.get(locationId);
|
|
239
|
-
if (cached) {
|
|
240
|
-
this.sessionId = cached.sessionId;
|
|
241
|
-
this.sessionCreated = true;
|
|
242
|
-
return this.formatSession(cached);
|
|
243
|
-
}
|
|
244
|
-
const autoParams = this.getAutoParams();
|
|
245
|
-
const cachedParams = this.cache.getParams();
|
|
246
|
-
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
247
243
|
if (typeof window !== "undefined" && window.__cfTracking) {
|
|
248
244
|
try {
|
|
249
245
|
const data2 = await window.__cfTracking;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
const dataWithExtras = data2;
|
|
247
|
+
const effectiveLocId = (_a = dataWithExtras.locId) != null ? _a : locationId;
|
|
248
|
+
const autoParams2 = this.getAutoParams();
|
|
249
|
+
const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
|
|
250
|
+
this.saveToCache(effectiveLocId, data2, params2);
|
|
253
251
|
return this.formatApiResponse(data2);
|
|
254
252
|
} catch (e) {
|
|
255
253
|
}
|
|
256
254
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
255
|
+
const cached = this.cache.get(locationId);
|
|
256
|
+
if (cached) {
|
|
257
|
+
return this.formatSession(cached);
|
|
258
|
+
}
|
|
259
|
+
const autoParams = this.getAutoParams();
|
|
260
|
+
const cachedParams = this.cache.getParams();
|
|
261
|
+
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
262
|
+
const cachedSessionToken = this.cache.getSessionToken(locationId);
|
|
263
|
+
const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
|
|
259
264
|
this.saveToCache(locationId, data, params);
|
|
260
|
-
this.sessionId = data.sessionId;
|
|
261
|
-
this.sessionCreated = true;
|
|
262
265
|
return this.formatApiResponse(data);
|
|
263
266
|
}
|
|
264
267
|
getLocationId() {
|
|
@@ -278,8 +281,8 @@ var CallForge = class _CallForge {
|
|
|
278
281
|
}
|
|
279
282
|
return params;
|
|
280
283
|
}
|
|
281
|
-
async fetchFromApi(locationId,
|
|
282
|
-
const url = this.buildUrl(locationId,
|
|
284
|
+
async fetchFromApi(locationId, sessionToken, params) {
|
|
285
|
+
const url = this.buildUrl(locationId, sessionToken, params);
|
|
283
286
|
const controller = new AbortController();
|
|
284
287
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
285
288
|
try {
|
|
@@ -297,30 +300,34 @@ var CallForge = class _CallForge {
|
|
|
297
300
|
}
|
|
298
301
|
saveToCache(locationId, data, params) {
|
|
299
302
|
const cached = {
|
|
300
|
-
locId: locationId,
|
|
301
|
-
|
|
303
|
+
locId: locationId != null ? locationId : null,
|
|
304
|
+
sessionToken: data.sessionToken,
|
|
305
|
+
leaseId: data.leaseId,
|
|
302
306
|
phoneNumber: data.phoneNumber,
|
|
303
307
|
location: data.location,
|
|
304
308
|
expiresAt: data.expiresAt,
|
|
309
|
+
tokenVersion: "v1",
|
|
305
310
|
params
|
|
306
311
|
};
|
|
307
312
|
this.cache.set(cached);
|
|
308
313
|
}
|
|
309
314
|
formatSession(cached) {
|
|
310
315
|
return {
|
|
311
|
-
|
|
316
|
+
sessionToken: cached.sessionToken,
|
|
317
|
+
leaseId: cached.leaseId,
|
|
312
318
|
phoneNumber: cached.phoneNumber,
|
|
313
319
|
location: cached.location
|
|
314
320
|
};
|
|
315
321
|
}
|
|
316
322
|
formatApiResponse(data) {
|
|
317
323
|
return {
|
|
318
|
-
|
|
324
|
+
sessionToken: data.sessionToken,
|
|
325
|
+
leaseId: data.leaseId,
|
|
319
326
|
phoneNumber: data.phoneNumber,
|
|
320
327
|
location: data.location
|
|
321
328
|
};
|
|
322
329
|
}
|
|
323
|
-
buildUrl(locationId,
|
|
330
|
+
buildUrl(locationId, sessionToken, params) {
|
|
324
331
|
const { categoryId, endpoint } = this.config;
|
|
325
332
|
const queryParams = {
|
|
326
333
|
categoryId
|
|
@@ -328,8 +335,8 @@ var CallForge = class _CallForge {
|
|
|
328
335
|
if (locationId) {
|
|
329
336
|
queryParams.loc_physical_ms = locationId;
|
|
330
337
|
}
|
|
331
|
-
if (
|
|
332
|
-
queryParams.
|
|
338
|
+
if (sessionToken) {
|
|
339
|
+
queryParams.sessionToken = sessionToken;
|
|
333
340
|
}
|
|
334
341
|
for (const [key, value] of Object.entries(params)) {
|
|
335
342
|
if (value !== void 0) {
|
|
@@ -359,29 +366,29 @@ function getPreloadSnippet(config) {
|
|
|
359
366
|
`Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
|
|
360
367
|
);
|
|
361
368
|
}
|
|
362
|
-
const cacheKey = `cf_tracking_${categoryId}`;
|
|
363
369
|
const script = `(function(){
|
|
364
370
|
var u=new URLSearchParams(location.search);
|
|
365
371
|
var loc=u.get('loc_physical_ms');
|
|
366
372
|
var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
|
|
367
373
|
var p={};
|
|
368
374
|
for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
369
|
-
var
|
|
370
|
-
var
|
|
375
|
+
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
376
|
+
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
377
|
+
var token=null;
|
|
371
378
|
try{
|
|
372
379
|
var c=JSON.parse(localStorage.getItem(key));
|
|
373
380
|
if(c&&c.expiresAt>Date.now()+30000){
|
|
374
381
|
if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
|
|
375
|
-
|
|
382
|
+
token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
376
383
|
var cp=c.params||{};
|
|
377
384
|
p=Object.assign({},cp,p);
|
|
378
385
|
}}catch(e){}
|
|
379
386
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
380
387
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
381
|
-
if(
|
|
388
|
+
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
382
389
|
var ks=Object.keys(p).sort();
|
|
383
390
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
384
|
-
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});
|
|
385
392
|
})();`.replace(/\n/g, "");
|
|
386
393
|
return `<link rel="preconnect" href="${endpoint}">
|
|
387
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.
|
|
@@ -210,31 +214,30 @@ var CallForge = class _CallForge {
|
|
|
210
214
|
});
|
|
211
215
|
}
|
|
212
216
|
async fetchSession() {
|
|
217
|
+
var _a;
|
|
213
218
|
const locationId = this.getLocationId();
|
|
214
|
-
const cached = this.cache.get(locationId);
|
|
215
|
-
if (cached) {
|
|
216
|
-
this.sessionId = cached.sessionId;
|
|
217
|
-
this.sessionCreated = true;
|
|
218
|
-
return this.formatSession(cached);
|
|
219
|
-
}
|
|
220
|
-
const autoParams = this.getAutoParams();
|
|
221
|
-
const cachedParams = this.cache.getParams();
|
|
222
|
-
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
223
219
|
if (typeof window !== "undefined" && window.__cfTracking) {
|
|
224
220
|
try {
|
|
225
221
|
const data2 = await window.__cfTracking;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
222
|
+
const dataWithExtras = data2;
|
|
223
|
+
const effectiveLocId = (_a = dataWithExtras.locId) != null ? _a : locationId;
|
|
224
|
+
const autoParams2 = this.getAutoParams();
|
|
225
|
+
const params2 = __spreadValues(__spreadValues(__spreadValues({}, autoParams2), dataWithExtras.params), this.customParams);
|
|
226
|
+
this.saveToCache(effectiveLocId, data2, params2);
|
|
229
227
|
return this.formatApiResponse(data2);
|
|
230
228
|
} catch (e) {
|
|
231
229
|
}
|
|
232
230
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
231
|
+
const cached = this.cache.get(locationId);
|
|
232
|
+
if (cached) {
|
|
233
|
+
return this.formatSession(cached);
|
|
234
|
+
}
|
|
235
|
+
const autoParams = this.getAutoParams();
|
|
236
|
+
const cachedParams = this.cache.getParams();
|
|
237
|
+
const params = __spreadValues(__spreadValues(__spreadValues({}, autoParams), cachedParams), this.customParams);
|
|
238
|
+
const cachedSessionToken = this.cache.getSessionToken(locationId);
|
|
239
|
+
const data = await this.fetchFromApi(locationId, cachedSessionToken, params);
|
|
235
240
|
this.saveToCache(locationId, data, params);
|
|
236
|
-
this.sessionId = data.sessionId;
|
|
237
|
-
this.sessionCreated = true;
|
|
238
241
|
return this.formatApiResponse(data);
|
|
239
242
|
}
|
|
240
243
|
getLocationId() {
|
|
@@ -254,8 +257,8 @@ var CallForge = class _CallForge {
|
|
|
254
257
|
}
|
|
255
258
|
return params;
|
|
256
259
|
}
|
|
257
|
-
async fetchFromApi(locationId,
|
|
258
|
-
const url = this.buildUrl(locationId,
|
|
260
|
+
async fetchFromApi(locationId, sessionToken, params) {
|
|
261
|
+
const url = this.buildUrl(locationId, sessionToken, params);
|
|
259
262
|
const controller = new AbortController();
|
|
260
263
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
261
264
|
try {
|
|
@@ -273,30 +276,34 @@ var CallForge = class _CallForge {
|
|
|
273
276
|
}
|
|
274
277
|
saveToCache(locationId, data, params) {
|
|
275
278
|
const cached = {
|
|
276
|
-
locId: locationId,
|
|
277
|
-
|
|
279
|
+
locId: locationId != null ? locationId : null,
|
|
280
|
+
sessionToken: data.sessionToken,
|
|
281
|
+
leaseId: data.leaseId,
|
|
278
282
|
phoneNumber: data.phoneNumber,
|
|
279
283
|
location: data.location,
|
|
280
284
|
expiresAt: data.expiresAt,
|
|
285
|
+
tokenVersion: "v1",
|
|
281
286
|
params
|
|
282
287
|
};
|
|
283
288
|
this.cache.set(cached);
|
|
284
289
|
}
|
|
285
290
|
formatSession(cached) {
|
|
286
291
|
return {
|
|
287
|
-
|
|
292
|
+
sessionToken: cached.sessionToken,
|
|
293
|
+
leaseId: cached.leaseId,
|
|
288
294
|
phoneNumber: cached.phoneNumber,
|
|
289
295
|
location: cached.location
|
|
290
296
|
};
|
|
291
297
|
}
|
|
292
298
|
formatApiResponse(data) {
|
|
293
299
|
return {
|
|
294
|
-
|
|
300
|
+
sessionToken: data.sessionToken,
|
|
301
|
+
leaseId: data.leaseId,
|
|
295
302
|
phoneNumber: data.phoneNumber,
|
|
296
303
|
location: data.location
|
|
297
304
|
};
|
|
298
305
|
}
|
|
299
|
-
buildUrl(locationId,
|
|
306
|
+
buildUrl(locationId, sessionToken, params) {
|
|
300
307
|
const { categoryId, endpoint } = this.config;
|
|
301
308
|
const queryParams = {
|
|
302
309
|
categoryId
|
|
@@ -304,8 +311,8 @@ var CallForge = class _CallForge {
|
|
|
304
311
|
if (locationId) {
|
|
305
312
|
queryParams.loc_physical_ms = locationId;
|
|
306
313
|
}
|
|
307
|
-
if (
|
|
308
|
-
queryParams.
|
|
314
|
+
if (sessionToken) {
|
|
315
|
+
queryParams.sessionToken = sessionToken;
|
|
309
316
|
}
|
|
310
317
|
for (const [key, value] of Object.entries(params)) {
|
|
311
318
|
if (value !== void 0) {
|
|
@@ -335,29 +342,29 @@ function getPreloadSnippet(config) {
|
|
|
335
342
|
`Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
|
|
336
343
|
);
|
|
337
344
|
}
|
|
338
|
-
const cacheKey = `cf_tracking_${categoryId}`;
|
|
339
345
|
const script = `(function(){
|
|
340
346
|
var u=new URLSearchParams(location.search);
|
|
341
347
|
var loc=u.get('loc_physical_ms');
|
|
342
348
|
var ap=['gclid','gbraid','wbraid','msclkid','fbclid','gad_campaignid','gad_source'];
|
|
343
349
|
var p={};
|
|
344
350
|
for(var i=0;i<ap.length;i++){var v=u.get(ap[i]);if(v)p[ap[i]]=v}
|
|
345
|
-
var
|
|
346
|
-
var
|
|
351
|
+
var site='${config.siteKey || ""}'||location.hostname||'unknown-site';
|
|
352
|
+
var key='cf_tracking_v1_'+site+'_${categoryId}';
|
|
353
|
+
var token=null;
|
|
347
354
|
try{
|
|
348
355
|
var c=JSON.parse(localStorage.getItem(key));
|
|
349
356
|
if(c&&c.expiresAt>Date.now()+30000){
|
|
350
357
|
if(!loc||(loc&&c.locId===loc)){c.params=Object.assign({},c.params,p);window.__cfTracking=Promise.resolve(c);return}
|
|
351
|
-
|
|
358
|
+
token=(!loc||c.locId===loc)?c.sessionToken:null;
|
|
352
359
|
var cp=c.params||{};
|
|
353
360
|
p=Object.assign({},cp,p);
|
|
354
361
|
}}catch(e){}
|
|
355
362
|
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}';
|
|
356
363
|
if(loc)url+='&loc_physical_ms='+loc;
|
|
357
|
-
if(
|
|
364
|
+
if(token)url+='&sessionToken='+encodeURIComponent(token);
|
|
358
365
|
var ks=Object.keys(p).sort();
|
|
359
366
|
for(var j=0;j<ks.length;j++)url+='&'+ks[j]+'='+encodeURIComponent(p[ks[j]]);
|
|
360
|
-
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});
|
|
361
368
|
})();`.replace(/\n/g, "");
|
|
362
369
|
return `<link rel="preconnect" href="${endpoint}">
|
|
363
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.0",
|
|
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
|
+
}
|