@callforge/tracking-client 0.0.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 +154 -0
- package/dist/index.d.mts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +256 -0
- package/dist/index.mjs +228 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @callforge/tracking-client
|
|
2
|
+
|
|
3
|
+
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @callforge/tracking-client
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @callforge/tracking-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Add preload snippet to `<head>` (optional but recommended)
|
|
16
|
+
|
|
17
|
+
For optimal performance on static sites, add this snippet to your HTML `<head>`:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { getPreloadSnippet } from '@callforge/tracking-client';
|
|
21
|
+
|
|
22
|
+
const snippet = getPreloadSnippet({ categoryId: 'your-category-id' });
|
|
23
|
+
// Add snippet to your HTML <head>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Generated HTML:
|
|
27
|
+
```html
|
|
28
|
+
<link rel="preconnect" href="https://tracking.callforge.io">
|
|
29
|
+
<script>/* preload script */</script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Initialize and use the client
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { CallForge } from '@callforge/tracking-client';
|
|
36
|
+
|
|
37
|
+
const client = CallForge.init({
|
|
38
|
+
categoryId: 'your-category-id',
|
|
39
|
+
// endpoint: 'https://tracking-dev.callforge.io', // Optional: override for dev
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Promise style
|
|
43
|
+
const session = await client.getSession();
|
|
44
|
+
console.log(session.phoneNumber); // "+17705550000" or null
|
|
45
|
+
console.log(session.location); // { city: "Woodstock", state: "Georgia", stateCode: "GA" } or null
|
|
46
|
+
|
|
47
|
+
// Subscription style
|
|
48
|
+
client.onReady((session) => {
|
|
49
|
+
document.getElementById('phone').textContent = session.phoneNumber;
|
|
50
|
+
document.getElementById('city').textContent = session.location?.city;
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API Reference
|
|
55
|
+
|
|
56
|
+
### `CallForge.init(config)`
|
|
57
|
+
|
|
58
|
+
Initialize the tracking client.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
interface CallForgeConfig {
|
|
62
|
+
categoryId: string; // Required - which number pool to use
|
|
63
|
+
endpoint?: string; // Optional - defaults to 'https://tracking.callforge.io'
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `client.getSession()`
|
|
68
|
+
|
|
69
|
+
Get tracking session data. Returns cached data if valid, otherwise fetches from API.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interface TrackingSession {
|
|
73
|
+
sessionId: string | null;
|
|
74
|
+
phoneNumber: string | null;
|
|
75
|
+
location: {
|
|
76
|
+
city: string;
|
|
77
|
+
state: string; // Full name: "Georgia"
|
|
78
|
+
stateCode: string; // Abbreviation: "GA"
|
|
79
|
+
} | null;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Behavior:**
|
|
84
|
+
- Returns `null` values immediately if no `loc_physical_ms` in URL (no API call)
|
|
85
|
+
- Returns cached data if valid (same location, not expired)
|
|
86
|
+
- Fetches fresh data if cache expired or location changed
|
|
87
|
+
- Throws on network errors or API errors
|
|
88
|
+
|
|
89
|
+
### `client.onReady(callback)`
|
|
90
|
+
|
|
91
|
+
Subscribe to session ready event. Callback is called once session data is available.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
client.onReady((session) => {
|
|
95
|
+
// session is the same TrackingSession object
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `getPreloadSnippet(config)`
|
|
100
|
+
|
|
101
|
+
Generate HTML snippet for preloading tracking data.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { getPreloadSnippet } from '@callforge/tracking-client';
|
|
105
|
+
|
|
106
|
+
const html = getPreloadSnippet({
|
|
107
|
+
categoryId: 'your-category-id',
|
|
108
|
+
endpoint: 'https://tracking.callforge.io', // Optional
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Caching Behavior
|
|
113
|
+
|
|
114
|
+
- **Cache key:** `loc_physical_ms` parameter from URL
|
|
115
|
+
- **TTL:** 30 minutes (controlled by server)
|
|
116
|
+
- **Storage:** localStorage (falls back to memory if unavailable)
|
|
117
|
+
- **Invalidation:** Cache clears when location changes
|
|
118
|
+
|
|
119
|
+
## Error Handling
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
try {
|
|
123
|
+
const session = await client.getSession();
|
|
124
|
+
if (session.phoneNumber) {
|
|
125
|
+
// Use phone number
|
|
126
|
+
} else {
|
|
127
|
+
// No phone number available for this location
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// Network error or API error
|
|
131
|
+
console.error('Failed to get tracking session:', err);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## TypeScript
|
|
136
|
+
|
|
137
|
+
Full type definitions are included:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import type {
|
|
141
|
+
CallForgeConfig,
|
|
142
|
+
TrackingSession,
|
|
143
|
+
TrackingLocation,
|
|
144
|
+
ReadyCallback,
|
|
145
|
+
} from '@callforge/tracking-client';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Environment URLs
|
|
149
|
+
|
|
150
|
+
| Environment | Endpoint |
|
|
151
|
+
|-------------|----------|
|
|
152
|
+
| Production | `https://tracking.callforge.io` (default) |
|
|
153
|
+
| Staging | `https://tracking-staging.callforge.io` |
|
|
154
|
+
| Dev | `https://tracking-dev.callforge.io` |
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the CallForge tracking client.
|
|
3
|
+
*/
|
|
4
|
+
interface CallForgeConfig {
|
|
5
|
+
/** Required - which number pool to use */
|
|
6
|
+
categoryId: string;
|
|
7
|
+
/** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Location data returned by the tracking API.
|
|
12
|
+
*/
|
|
13
|
+
interface TrackingLocation {
|
|
14
|
+
/** City name */
|
|
15
|
+
city: string;
|
|
16
|
+
/** Full state name (e.g., "Georgia") */
|
|
17
|
+
state: string;
|
|
18
|
+
/** State abbreviation (e.g., "GA") */
|
|
19
|
+
stateCode: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Session data returned by getSession().
|
|
23
|
+
*/
|
|
24
|
+
interface TrackingSession {
|
|
25
|
+
/** Session ID from the tracking API */
|
|
26
|
+
sessionId: string | null;
|
|
27
|
+
/** Assigned phone number, or null if no number available */
|
|
28
|
+
phoneNumber: string | null;
|
|
29
|
+
/** Location data, or null if location could not be resolved */
|
|
30
|
+
location: TrackingLocation | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Internal API response format (includes expiresAt).
|
|
34
|
+
*/
|
|
35
|
+
interface ApiResponse {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
phoneNumber: string | null;
|
|
38
|
+
location: {
|
|
39
|
+
city: string;
|
|
40
|
+
state: string;
|
|
41
|
+
stateCode: string;
|
|
42
|
+
} | null;
|
|
43
|
+
expiresAt: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Callback function for onReady subscription.
|
|
47
|
+
*/
|
|
48
|
+
type ReadyCallback = (session: TrackingSession) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Global window extension for preload promise.
|
|
51
|
+
*/
|
|
52
|
+
declare global {
|
|
53
|
+
interface Window {
|
|
54
|
+
__cfTracking?: Promise<ApiResponse>;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare class CallForge {
|
|
59
|
+
private readonly config;
|
|
60
|
+
private readonly cache;
|
|
61
|
+
private sessionPromise;
|
|
62
|
+
private constructor();
|
|
63
|
+
/**
|
|
64
|
+
* Initialize the CallForge tracking client.
|
|
65
|
+
*/
|
|
66
|
+
static init(config: CallForgeConfig): CallForge;
|
|
67
|
+
/**
|
|
68
|
+
* Get tracking session data.
|
|
69
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
70
|
+
*/
|
|
71
|
+
getSession(): Promise<TrackingSession>;
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to session ready event.
|
|
74
|
+
* Callback is called once session data is available.
|
|
75
|
+
*/
|
|
76
|
+
onReady(callback: ReadyCallback): void;
|
|
77
|
+
private fetchSession;
|
|
78
|
+
private getLocationId;
|
|
79
|
+
private fetchFromApi;
|
|
80
|
+
private saveToCache;
|
|
81
|
+
private formatSession;
|
|
82
|
+
private formatApiResponse;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate HTML snippet for preloading tracking data.
|
|
87
|
+
* Add this to the <head> of your HTML for optimal performance.
|
|
88
|
+
*
|
|
89
|
+
* The generated snippet:
|
|
90
|
+
* - Checks for loc_physical_ms URL parameter
|
|
91
|
+
* - Checks localStorage cache for valid session
|
|
92
|
+
* - If cache valid, resolves Promise.resolve(cached) immediately
|
|
93
|
+
* - If cache expired but same location, includes sessionId for refresh
|
|
94
|
+
* - Otherwise fetches fresh session data
|
|
95
|
+
* - Sets window.__cfTracking with the session promise
|
|
96
|
+
*
|
|
97
|
+
* @throws {Error} If categoryId contains invalid characters
|
|
98
|
+
* @throws {Error} If endpoint is not a valid HTTPS URL
|
|
99
|
+
*/
|
|
100
|
+
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
101
|
+
|
|
102
|
+
export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingSession, getPreloadSnippet };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the CallForge tracking client.
|
|
3
|
+
*/
|
|
4
|
+
interface CallForgeConfig {
|
|
5
|
+
/** Required - which number pool to use */
|
|
6
|
+
categoryId: string;
|
|
7
|
+
/** Optional - API endpoint. Defaults to 'https://tracking.callforge.io' */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Location data returned by the tracking API.
|
|
12
|
+
*/
|
|
13
|
+
interface TrackingLocation {
|
|
14
|
+
/** City name */
|
|
15
|
+
city: string;
|
|
16
|
+
/** Full state name (e.g., "Georgia") */
|
|
17
|
+
state: string;
|
|
18
|
+
/** State abbreviation (e.g., "GA") */
|
|
19
|
+
stateCode: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Session data returned by getSession().
|
|
23
|
+
*/
|
|
24
|
+
interface TrackingSession {
|
|
25
|
+
/** Session ID from the tracking API */
|
|
26
|
+
sessionId: string | null;
|
|
27
|
+
/** Assigned phone number, or null if no number available */
|
|
28
|
+
phoneNumber: string | null;
|
|
29
|
+
/** Location data, or null if location could not be resolved */
|
|
30
|
+
location: TrackingLocation | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Internal API response format (includes expiresAt).
|
|
34
|
+
*/
|
|
35
|
+
interface ApiResponse {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
phoneNumber: string | null;
|
|
38
|
+
location: {
|
|
39
|
+
city: string;
|
|
40
|
+
state: string;
|
|
41
|
+
stateCode: string;
|
|
42
|
+
} | null;
|
|
43
|
+
expiresAt: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Callback function for onReady subscription.
|
|
47
|
+
*/
|
|
48
|
+
type ReadyCallback = (session: TrackingSession) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Global window extension for preload promise.
|
|
51
|
+
*/
|
|
52
|
+
declare global {
|
|
53
|
+
interface Window {
|
|
54
|
+
__cfTracking?: Promise<ApiResponse>;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare class CallForge {
|
|
59
|
+
private readonly config;
|
|
60
|
+
private readonly cache;
|
|
61
|
+
private sessionPromise;
|
|
62
|
+
private constructor();
|
|
63
|
+
/**
|
|
64
|
+
* Initialize the CallForge tracking client.
|
|
65
|
+
*/
|
|
66
|
+
static init(config: CallForgeConfig): CallForge;
|
|
67
|
+
/**
|
|
68
|
+
* Get tracking session data.
|
|
69
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
70
|
+
*/
|
|
71
|
+
getSession(): Promise<TrackingSession>;
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to session ready event.
|
|
74
|
+
* Callback is called once session data is available.
|
|
75
|
+
*/
|
|
76
|
+
onReady(callback: ReadyCallback): void;
|
|
77
|
+
private fetchSession;
|
|
78
|
+
private getLocationId;
|
|
79
|
+
private fetchFromApi;
|
|
80
|
+
private saveToCache;
|
|
81
|
+
private formatSession;
|
|
82
|
+
private formatApiResponse;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate HTML snippet for preloading tracking data.
|
|
87
|
+
* Add this to the <head> of your HTML for optimal performance.
|
|
88
|
+
*
|
|
89
|
+
* The generated snippet:
|
|
90
|
+
* - Checks for loc_physical_ms URL parameter
|
|
91
|
+
* - Checks localStorage cache for valid session
|
|
92
|
+
* - If cache valid, resolves Promise.resolve(cached) immediately
|
|
93
|
+
* - If cache expired but same location, includes sessionId for refresh
|
|
94
|
+
* - Otherwise fetches fresh session data
|
|
95
|
+
* - Sets window.__cfTracking with the session promise
|
|
96
|
+
*
|
|
97
|
+
* @throws {Error} If categoryId contains invalid characters
|
|
98
|
+
* @throws {Error} If endpoint is not a valid HTTPS URL
|
|
99
|
+
*/
|
|
100
|
+
declare function getPreloadSnippet(config: CallForgeConfig): string;
|
|
101
|
+
|
|
102
|
+
export { CallForge, type CallForgeConfig, type ReadyCallback, type TrackingLocation, type TrackingSession, getPreloadSnippet };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CallForge: () => CallForge,
|
|
24
|
+
getPreloadSnippet: () => getPreloadSnippet
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/cache.ts
|
|
29
|
+
var EXPIRY_BUFFER_MS = 3e4;
|
|
30
|
+
var TrackingCache = class {
|
|
31
|
+
constructor(categoryId) {
|
|
32
|
+
this.memoryCache = null;
|
|
33
|
+
this.useMemory = false;
|
|
34
|
+
this.key = `cf_tracking_${categoryId}`;
|
|
35
|
+
this.useMemory = !this.isLocalStorageAvailable();
|
|
36
|
+
}
|
|
37
|
+
isLocalStorageAvailable() {
|
|
38
|
+
try {
|
|
39
|
+
localStorage.setItem("__cf_test__", "1");
|
|
40
|
+
localStorage.removeItem("__cf_test__");
|
|
41
|
+
return true;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get cached session if valid (same locationId and not expired).
|
|
48
|
+
* Returns null if cache miss, location changed, or expired.
|
|
49
|
+
*/
|
|
50
|
+
get(locationId) {
|
|
51
|
+
const cached = this.read();
|
|
52
|
+
if (!cached) return null;
|
|
53
|
+
if (cached.locId !== locationId) return null;
|
|
54
|
+
if (cached.expiresAt - EXPIRY_BUFFER_MS <= Date.now()) return null;
|
|
55
|
+
return cached;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get sessionId for refresh if location matches (regardless of expiry).
|
|
59
|
+
* Used when cache is expired but location is same - send sessionId to server.
|
|
60
|
+
*/
|
|
61
|
+
getSessionId(locationId) {
|
|
62
|
+
const cached = this.read();
|
|
63
|
+
if (!cached) return null;
|
|
64
|
+
if (cached.locId !== locationId) return null;
|
|
65
|
+
return cached.sessionId;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Store session data in cache.
|
|
69
|
+
*/
|
|
70
|
+
set(session) {
|
|
71
|
+
if (this.useMemory) {
|
|
72
|
+
this.memoryCache = session;
|
|
73
|
+
} else {
|
|
74
|
+
try {
|
|
75
|
+
localStorage.setItem(this.key, JSON.stringify(session));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
this.memoryCache = session;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear cached data.
|
|
83
|
+
*/
|
|
84
|
+
clear() {
|
|
85
|
+
this.memoryCache = null;
|
|
86
|
+
if (!this.useMemory) {
|
|
87
|
+
try {
|
|
88
|
+
localStorage.removeItem(this.key);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
read() {
|
|
94
|
+
if (this.useMemory) {
|
|
95
|
+
return this.memoryCache;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const raw = localStorage.getItem(this.key);
|
|
99
|
+
if (!raw) return null;
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
return this.memoryCache;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/client.ts
|
|
108
|
+
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
109
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
110
|
+
var CallForge = class _CallForge {
|
|
111
|
+
constructor(config) {
|
|
112
|
+
this.sessionPromise = null;
|
|
113
|
+
this.config = {
|
|
114
|
+
categoryId: config.categoryId,
|
|
115
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT
|
|
116
|
+
};
|
|
117
|
+
this.cache = new TrackingCache(config.categoryId);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Initialize the CallForge tracking client.
|
|
121
|
+
*/
|
|
122
|
+
static init(config) {
|
|
123
|
+
return new _CallForge(config);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get tracking session data.
|
|
127
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
128
|
+
*/
|
|
129
|
+
async getSession() {
|
|
130
|
+
if (this.sessionPromise) {
|
|
131
|
+
return this.sessionPromise;
|
|
132
|
+
}
|
|
133
|
+
this.sessionPromise = this.fetchSession();
|
|
134
|
+
return this.sessionPromise;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Subscribe to session ready event.
|
|
138
|
+
* Callback is called once session data is available.
|
|
139
|
+
*/
|
|
140
|
+
onReady(callback) {
|
|
141
|
+
this.getSession().then(callback).catch(() => {
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async fetchSession() {
|
|
145
|
+
const locationId = this.getLocationId();
|
|
146
|
+
if (!locationId) {
|
|
147
|
+
return { sessionId: null, phoneNumber: null, location: null };
|
|
148
|
+
}
|
|
149
|
+
const cached = this.cache.get(locationId);
|
|
150
|
+
if (cached) {
|
|
151
|
+
return this.formatSession(cached);
|
|
152
|
+
}
|
|
153
|
+
if (typeof window !== "undefined" && window.__cfTracking) {
|
|
154
|
+
try {
|
|
155
|
+
const data2 = await window.__cfTracking;
|
|
156
|
+
this.saveToCache(locationId, data2);
|
|
157
|
+
return this.formatApiResponse(data2);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const sessionId = this.cache.getSessionId(locationId);
|
|
162
|
+
const data = await this.fetchFromApi(locationId, sessionId);
|
|
163
|
+
this.saveToCache(locationId, data);
|
|
164
|
+
return this.formatApiResponse(data);
|
|
165
|
+
}
|
|
166
|
+
getLocationId() {
|
|
167
|
+
if (typeof window === "undefined") return null;
|
|
168
|
+
const params = new URLSearchParams(window.location.search);
|
|
169
|
+
return params.get("loc_physical_ms");
|
|
170
|
+
}
|
|
171
|
+
async fetchFromApi(locationId, sessionId) {
|
|
172
|
+
const { categoryId, endpoint } = this.config;
|
|
173
|
+
let url = `${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms=${locationId}`;
|
|
174
|
+
if (sessionId) {
|
|
175
|
+
url += `&sessionId=${sessionId}`;
|
|
176
|
+
}
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(url, {
|
|
181
|
+
credentials: "omit",
|
|
182
|
+
signal: controller.signal
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
186
|
+
}
|
|
187
|
+
return await response.json();
|
|
188
|
+
} finally {
|
|
189
|
+
clearTimeout(timeoutId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
saveToCache(locationId, data) {
|
|
193
|
+
const cached = {
|
|
194
|
+
locId: locationId,
|
|
195
|
+
sessionId: data.sessionId,
|
|
196
|
+
phoneNumber: data.phoneNumber,
|
|
197
|
+
location: data.location,
|
|
198
|
+
expiresAt: data.expiresAt
|
|
199
|
+
};
|
|
200
|
+
this.cache.set(cached);
|
|
201
|
+
}
|
|
202
|
+
formatSession(cached) {
|
|
203
|
+
return {
|
|
204
|
+
sessionId: cached.sessionId,
|
|
205
|
+
phoneNumber: cached.phoneNumber,
|
|
206
|
+
location: cached.location
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
formatApiResponse(data) {
|
|
210
|
+
return {
|
|
211
|
+
sessionId: data.sessionId,
|
|
212
|
+
phoneNumber: data.phoneNumber,
|
|
213
|
+
location: data.location
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/preload.ts
|
|
219
|
+
var DEFAULT_ENDPOINT2 = "https://tracking.callforge.io";
|
|
220
|
+
var CATEGORY_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
221
|
+
var HTTPS_URL_PATTERN = /^https:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$/;
|
|
222
|
+
function getPreloadSnippet(config) {
|
|
223
|
+
const endpoint = config.endpoint || DEFAULT_ENDPOINT2;
|
|
224
|
+
const { categoryId } = config;
|
|
225
|
+
if (!CATEGORY_ID_PATTERN.test(categoryId)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Invalid categoryId: "${categoryId}". Must match pattern ${CATEGORY_ID_PATTERN}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (!HTTPS_URL_PATTERN.test(endpoint)) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const cacheKey = `cf_tracking_${categoryId}`;
|
|
236
|
+
const script = `(function(){
|
|
237
|
+
var loc=new URLSearchParams(location.search).get('loc_physical_ms');
|
|
238
|
+
if(!loc)return;
|
|
239
|
+
var key='${cacheKey}';
|
|
240
|
+
try{
|
|
241
|
+
var c=JSON.parse(localStorage.getItem(key));
|
|
242
|
+
if(c&&c.locId===loc&&c.expiresAt>Date.now()+30000){window.__cfTracking=Promise.resolve(c);return}
|
|
243
|
+
var sid=(c&&c.locId===loc)?c.sessionId:null;
|
|
244
|
+
}catch(e){}
|
|
245
|
+
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms='+loc;
|
|
246
|
+
if(sid)url+='&sessionId='+sid;
|
|
247
|
+
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()});
|
|
248
|
+
})();`.replace(/\n/g, "");
|
|
249
|
+
return `<link rel="preconnect" href="${endpoint}">
|
|
250
|
+
<script>${script}</script>`;
|
|
251
|
+
}
|
|
252
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
253
|
+
0 && (module.exports = {
|
|
254
|
+
CallForge,
|
|
255
|
+
getPreloadSnippet
|
|
256
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var EXPIRY_BUFFER_MS = 3e4;
|
|
3
|
+
var TrackingCache = class {
|
|
4
|
+
constructor(categoryId) {
|
|
5
|
+
this.memoryCache = null;
|
|
6
|
+
this.useMemory = false;
|
|
7
|
+
this.key = `cf_tracking_${categoryId}`;
|
|
8
|
+
this.useMemory = !this.isLocalStorageAvailable();
|
|
9
|
+
}
|
|
10
|
+
isLocalStorageAvailable() {
|
|
11
|
+
try {
|
|
12
|
+
localStorage.setItem("__cf_test__", "1");
|
|
13
|
+
localStorage.removeItem("__cf_test__");
|
|
14
|
+
return true;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get cached session if valid (same locationId and not expired).
|
|
21
|
+
* Returns null if cache miss, location changed, or expired.
|
|
22
|
+
*/
|
|
23
|
+
get(locationId) {
|
|
24
|
+
const cached = this.read();
|
|
25
|
+
if (!cached) return null;
|
|
26
|
+
if (cached.locId !== locationId) return null;
|
|
27
|
+
if (cached.expiresAt - EXPIRY_BUFFER_MS <= Date.now()) return null;
|
|
28
|
+
return cached;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get sessionId for refresh if location matches (regardless of expiry).
|
|
32
|
+
* Used when cache is expired but location is same - send sessionId to server.
|
|
33
|
+
*/
|
|
34
|
+
getSessionId(locationId) {
|
|
35
|
+
const cached = this.read();
|
|
36
|
+
if (!cached) return null;
|
|
37
|
+
if (cached.locId !== locationId) return null;
|
|
38
|
+
return cached.sessionId;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Store session data in cache.
|
|
42
|
+
*/
|
|
43
|
+
set(session) {
|
|
44
|
+
if (this.useMemory) {
|
|
45
|
+
this.memoryCache = session;
|
|
46
|
+
} else {
|
|
47
|
+
try {
|
|
48
|
+
localStorage.setItem(this.key, JSON.stringify(session));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
this.memoryCache = session;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Clear cached data.
|
|
56
|
+
*/
|
|
57
|
+
clear() {
|
|
58
|
+
this.memoryCache = null;
|
|
59
|
+
if (!this.useMemory) {
|
|
60
|
+
try {
|
|
61
|
+
localStorage.removeItem(this.key);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
read() {
|
|
67
|
+
if (this.useMemory) {
|
|
68
|
+
return this.memoryCache;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const raw = localStorage.getItem(this.key);
|
|
72
|
+
if (!raw) return null;
|
|
73
|
+
return JSON.parse(raw);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return this.memoryCache;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/client.ts
|
|
81
|
+
var DEFAULT_ENDPOINT = "https://tracking.callforge.io";
|
|
82
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
83
|
+
var CallForge = class _CallForge {
|
|
84
|
+
constructor(config) {
|
|
85
|
+
this.sessionPromise = null;
|
|
86
|
+
this.config = {
|
|
87
|
+
categoryId: config.categoryId,
|
|
88
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT
|
|
89
|
+
};
|
|
90
|
+
this.cache = new TrackingCache(config.categoryId);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Initialize the CallForge tracking client.
|
|
94
|
+
*/
|
|
95
|
+
static init(config) {
|
|
96
|
+
return new _CallForge(config);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get tracking session data.
|
|
100
|
+
* Returns cached data if valid, otherwise fetches from API.
|
|
101
|
+
*/
|
|
102
|
+
async getSession() {
|
|
103
|
+
if (this.sessionPromise) {
|
|
104
|
+
return this.sessionPromise;
|
|
105
|
+
}
|
|
106
|
+
this.sessionPromise = this.fetchSession();
|
|
107
|
+
return this.sessionPromise;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to session ready event.
|
|
111
|
+
* Callback is called once session data is available.
|
|
112
|
+
*/
|
|
113
|
+
onReady(callback) {
|
|
114
|
+
this.getSession().then(callback).catch(() => {
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async fetchSession() {
|
|
118
|
+
const locationId = this.getLocationId();
|
|
119
|
+
if (!locationId) {
|
|
120
|
+
return { sessionId: null, phoneNumber: null, location: null };
|
|
121
|
+
}
|
|
122
|
+
const cached = this.cache.get(locationId);
|
|
123
|
+
if (cached) {
|
|
124
|
+
return this.formatSession(cached);
|
|
125
|
+
}
|
|
126
|
+
if (typeof window !== "undefined" && window.__cfTracking) {
|
|
127
|
+
try {
|
|
128
|
+
const data2 = await window.__cfTracking;
|
|
129
|
+
this.saveToCache(locationId, data2);
|
|
130
|
+
return this.formatApiResponse(data2);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const sessionId = this.cache.getSessionId(locationId);
|
|
135
|
+
const data = await this.fetchFromApi(locationId, sessionId);
|
|
136
|
+
this.saveToCache(locationId, data);
|
|
137
|
+
return this.formatApiResponse(data);
|
|
138
|
+
}
|
|
139
|
+
getLocationId() {
|
|
140
|
+
if (typeof window === "undefined") return null;
|
|
141
|
+
const params = new URLSearchParams(window.location.search);
|
|
142
|
+
return params.get("loc_physical_ms");
|
|
143
|
+
}
|
|
144
|
+
async fetchFromApi(locationId, sessionId) {
|
|
145
|
+
const { categoryId, endpoint } = this.config;
|
|
146
|
+
let url = `${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms=${locationId}`;
|
|
147
|
+
if (sessionId) {
|
|
148
|
+
url += `&sessionId=${sessionId}`;
|
|
149
|
+
}
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
credentials: "omit",
|
|
155
|
+
signal: controller.signal
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
159
|
+
}
|
|
160
|
+
return await response.json();
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
saveToCache(locationId, data) {
|
|
166
|
+
const cached = {
|
|
167
|
+
locId: locationId,
|
|
168
|
+
sessionId: data.sessionId,
|
|
169
|
+
phoneNumber: data.phoneNumber,
|
|
170
|
+
location: data.location,
|
|
171
|
+
expiresAt: data.expiresAt
|
|
172
|
+
};
|
|
173
|
+
this.cache.set(cached);
|
|
174
|
+
}
|
|
175
|
+
formatSession(cached) {
|
|
176
|
+
return {
|
|
177
|
+
sessionId: cached.sessionId,
|
|
178
|
+
phoneNumber: cached.phoneNumber,
|
|
179
|
+
location: cached.location
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
formatApiResponse(data) {
|
|
183
|
+
return {
|
|
184
|
+
sessionId: data.sessionId,
|
|
185
|
+
phoneNumber: data.phoneNumber,
|
|
186
|
+
location: data.location
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/preload.ts
|
|
192
|
+
var DEFAULT_ENDPOINT2 = "https://tracking.callforge.io";
|
|
193
|
+
var CATEGORY_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
194
|
+
var HTTPS_URL_PATTERN = /^https:\/\/[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$/;
|
|
195
|
+
function getPreloadSnippet(config) {
|
|
196
|
+
const endpoint = config.endpoint || DEFAULT_ENDPOINT2;
|
|
197
|
+
const { categoryId } = config;
|
|
198
|
+
if (!CATEGORY_ID_PATTERN.test(categoryId)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Invalid categoryId: "${categoryId}". Must match pattern ${CATEGORY_ID_PATTERN}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!HTTPS_URL_PATTERN.test(endpoint)) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Invalid endpoint: "${endpoint}". Must be a valid HTTPS URL`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const cacheKey = `cf_tracking_${categoryId}`;
|
|
209
|
+
const script = `(function(){
|
|
210
|
+
var loc=new URLSearchParams(location.search).get('loc_physical_ms');
|
|
211
|
+
if(!loc)return;
|
|
212
|
+
var key='${cacheKey}';
|
|
213
|
+
try{
|
|
214
|
+
var c=JSON.parse(localStorage.getItem(key));
|
|
215
|
+
if(c&&c.locId===loc&&c.expiresAt>Date.now()+30000){window.__cfTracking=Promise.resolve(c);return}
|
|
216
|
+
var sid=(c&&c.locId===loc)?c.sessionId:null;
|
|
217
|
+
}catch(e){}
|
|
218
|
+
var url='${endpoint}/v1/tracking/session?categoryId=${categoryId}&loc_physical_ms='+loc;
|
|
219
|
+
if(sid)url+='&sessionId='+sid;
|
|
220
|
+
window.__cfTracking=fetch(url,{credentials:'omit'}).then(function(r){return r.json()});
|
|
221
|
+
})();`.replace(/\n/g, "");
|
|
222
|
+
return `<link rel="preconnect" href="${endpoint}">
|
|
223
|
+
<script>${script}</script>`;
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
CallForge,
|
|
227
|
+
getPreloadSnippet
|
|
228
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@callforge/tracking-client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"module": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@callforge/tsconfig": "workspace:*",
|
|
25
|
+
"jsdom": "^27.4.0",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.3.0",
|
|
28
|
+
"vitest": "^1.6.0"
|
|
29
|
+
}
|
|
30
|
+
}
|