routeflow-browser 0.1.2 → 0.1.5
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/dist/client.d.ts +2 -2
- package/dist/client.js +6 -6
- package/dist/event_tracker.d.ts +50 -0
- package/dist/event_tracker.js +344 -0
- package/dist/external_spans.js +1 -1
- package/dist/fetch_patch.d.ts +8 -3
- package/dist/fetch_patch.js +82 -35
- package/dist/init.js +34 -21
- package/dist/route.d.ts +4 -0
- package/dist/route.js +15 -1
- package/dist/types.d.ts +35 -1
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { IngestPayload } from "./types";
|
|
5
5
|
/**
|
|
6
|
-
* Get the
|
|
6
|
+
* Get the RouteFlow backend URL (for telemetry ingestion).
|
|
7
7
|
*/
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function getRouteflowBackendUrl(): string;
|
|
9
9
|
/**
|
|
10
10
|
* Send events to Routeflow backend.
|
|
11
11
|
*/
|
package/dist/client.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP client for sending events to Routeflow backend.
|
|
3
3
|
*/
|
|
4
|
-
// Hardcoded backend URL - always use production
|
|
5
|
-
const
|
|
4
|
+
// Hardcoded RouteFlow backend URL - always use production
|
|
5
|
+
const ROUTEFLOW_BACKEND_URL = "https://routeflow-backend-production.up.railway.app";
|
|
6
6
|
const INGEST_ENDPOINT = "/v1/ingest";
|
|
7
7
|
/**
|
|
8
|
-
* Get the
|
|
8
|
+
* Get the RouteFlow backend URL (for telemetry ingestion).
|
|
9
9
|
*/
|
|
10
|
-
export function
|
|
11
|
-
return
|
|
10
|
+
export function getRouteflowBackendUrl() {
|
|
11
|
+
return ROUTEFLOW_BACKEND_URL;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Send events to Routeflow backend.
|
|
15
15
|
*/
|
|
16
16
|
export async function sendEvents(payload, ingestKey) {
|
|
17
17
|
try {
|
|
18
|
-
const url = `${
|
|
18
|
+
const url = `${ROUTEFLOW_BACKEND_URL}${INGEST_ENDPOINT}`;
|
|
19
19
|
const body = JSON.stringify(payload);
|
|
20
20
|
// Use fetch with keepalive for best reliability
|
|
21
21
|
const success = await fetch(url, {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified event tracking for all RouteFlow browser events.
|
|
3
|
+
*/
|
|
4
|
+
import { Environment } from "./types";
|
|
5
|
+
interface EventTrackerConfig {
|
|
6
|
+
environment: Environment;
|
|
7
|
+
allowlist: string[];
|
|
8
|
+
ingestKey?: string;
|
|
9
|
+
maxQueueSize: number;
|
|
10
|
+
flushIntervalMs: number;
|
|
11
|
+
sampleRate: number;
|
|
12
|
+
trackExternal: boolean;
|
|
13
|
+
trackBackend: boolean;
|
|
14
|
+
trackPageViews: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface EventStats {
|
|
17
|
+
queued: number;
|
|
18
|
+
sent: number;
|
|
19
|
+
dropped: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Initialize event tracking.
|
|
24
|
+
*/
|
|
25
|
+
export declare function initEventTracking(config: EventTrackerConfig): void;
|
|
26
|
+
/**
|
|
27
|
+
* Track a page view.
|
|
28
|
+
*/
|
|
29
|
+
export declare function trackPageView(route: string, referrer?: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Track a backend request.
|
|
32
|
+
*/
|
|
33
|
+
export declare function trackBackendRequest(url: string, init: RequestInit | undefined, startTime: number, endTime: number, response: Response | null, error: Error | null, traceId: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Track an external request.
|
|
36
|
+
*/
|
|
37
|
+
export declare function trackExternalRequest(url: string, init: RequestInit | undefined, startTime: number, endTime: number, response: Response | null, error: Error | null): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get event stats.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getEventStats(): EventStats;
|
|
42
|
+
/**
|
|
43
|
+
* Get queue size.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getEventQueueSize(): number;
|
|
46
|
+
/**
|
|
47
|
+
* Stop event tracking.
|
|
48
|
+
*/
|
|
49
|
+
export declare function stopEventTracking(): void;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified event tracking for all RouteFlow browser events.
|
|
3
|
+
*/
|
|
4
|
+
import { extractHostname, isHostAllowed, validateTargetHost, } from "./validate";
|
|
5
|
+
import { getCurrentFrontendRoute } from "./route";
|
|
6
|
+
import { getOrCreateSessionId, generateTraceId } from "./trace";
|
|
7
|
+
import { sendEvents } from "./client";
|
|
8
|
+
class EventTracker {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.queue = [];
|
|
11
|
+
this.stats = {
|
|
12
|
+
queued: 0,
|
|
13
|
+
sent: 0,
|
|
14
|
+
dropped: 0,
|
|
15
|
+
failed: 0,
|
|
16
|
+
};
|
|
17
|
+
this.flushTimer = null;
|
|
18
|
+
this.SDK_NAME = "@routeflow/browser";
|
|
19
|
+
this.SDK_VERSION = "0.1.5";
|
|
20
|
+
this.lastPageView = null;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.startFlushTimer();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Track a page view event.
|
|
26
|
+
*/
|
|
27
|
+
trackPageView(route, referrer) {
|
|
28
|
+
if (!this.config.trackPageViews) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Don't track duplicate page views
|
|
32
|
+
if (this.lastPageView === route) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.lastPageView = route;
|
|
36
|
+
try {
|
|
37
|
+
// Sample rate check
|
|
38
|
+
if (Math.random() > this.config.sampleRate) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const session_id = getOrCreateSessionId();
|
|
42
|
+
const event = {
|
|
43
|
+
type: "frontend_page_view",
|
|
44
|
+
timestamp: new Date().toISOString(),
|
|
45
|
+
session_id,
|
|
46
|
+
environment: this.config.environment,
|
|
47
|
+
frontend_route: route,
|
|
48
|
+
sdk: {
|
|
49
|
+
name: this.SDK_NAME,
|
|
50
|
+
version: this.SDK_VERSION,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
if (referrer) {
|
|
54
|
+
event.referrer = referrer;
|
|
55
|
+
}
|
|
56
|
+
// Add code version if available
|
|
57
|
+
if (typeof window !== "undefined" && window.__ROUTEFLOW_CODE_VERSION__) {
|
|
58
|
+
event.code_version = window.__ROUTEFLOW_CODE_VERSION__;
|
|
59
|
+
}
|
|
60
|
+
this.queueEvent(event);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Never throw
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Track a frontend-to-backend request.
|
|
68
|
+
*/
|
|
69
|
+
trackBackendRequest(url, init, startTime, endTime, response, error, traceId) {
|
|
70
|
+
if (!this.config.trackBackend) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// Sample rate check
|
|
75
|
+
if (Math.random() > this.config.sampleRate) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const method = (init?.method || "GET").toUpperCase();
|
|
79
|
+
const duration_ms = Math.round(endTime - startTime);
|
|
80
|
+
let outcome = "unknown";
|
|
81
|
+
let status_code;
|
|
82
|
+
if (error) {
|
|
83
|
+
outcome = "failure";
|
|
84
|
+
}
|
|
85
|
+
else if (response) {
|
|
86
|
+
status_code = response.status;
|
|
87
|
+
outcome = response.ok ? "success" : "failure";
|
|
88
|
+
}
|
|
89
|
+
const session_id = getOrCreateSessionId();
|
|
90
|
+
const frontend_route = getCurrentFrontendRoute();
|
|
91
|
+
const event = {
|
|
92
|
+
type: "frontend_backend_request",
|
|
93
|
+
timestamp: new Date(startTime).toISOString(),
|
|
94
|
+
trace_id: traceId,
|
|
95
|
+
session_id,
|
|
96
|
+
environment: this.config.environment,
|
|
97
|
+
frontend_route,
|
|
98
|
+
method,
|
|
99
|
+
backend_url: url,
|
|
100
|
+
duration_ms,
|
|
101
|
+
outcome,
|
|
102
|
+
sdk: {
|
|
103
|
+
name: this.SDK_NAME,
|
|
104
|
+
version: this.SDK_VERSION,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
if (status_code !== undefined) {
|
|
108
|
+
event.status_code = status_code;
|
|
109
|
+
}
|
|
110
|
+
// Add code version if available
|
|
111
|
+
if (typeof window !== "undefined" && window.__ROUTEFLOW_CODE_VERSION__) {
|
|
112
|
+
event.code_version = window.__ROUTEFLOW_CODE_VERSION__;
|
|
113
|
+
}
|
|
114
|
+
this.queueEvent(event);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Never throw
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Track an external dependency request.
|
|
122
|
+
*/
|
|
123
|
+
trackExternalRequest(url, init, startTime, endTime, response, error) {
|
|
124
|
+
if (!this.config.trackExternal) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
// Don't track requests to the Routeflow ingest endpoint (prevents feedback loop)
|
|
129
|
+
if (url.includes('/v1/ingest')) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Sample rate check
|
|
133
|
+
if (Math.random() > this.config.sampleRate) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Extract hostname
|
|
137
|
+
const hostname = extractHostname(url);
|
|
138
|
+
if (!hostname) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Check allowlist
|
|
142
|
+
if (!isHostAllowed(hostname, this.config.allowlist)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Validate hostname (no scheme, path, query)
|
|
146
|
+
if (!validateTargetHost(hostname)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const method = (init?.method || "GET").toUpperCase();
|
|
150
|
+
const duration_ms = Math.round(endTime - startTime);
|
|
151
|
+
let outcome = "unknown";
|
|
152
|
+
let status_code;
|
|
153
|
+
if (error) {
|
|
154
|
+
outcome = "failure";
|
|
155
|
+
}
|
|
156
|
+
else if (response) {
|
|
157
|
+
status_code = response.status;
|
|
158
|
+
outcome = response.ok ? "success" : "failure";
|
|
159
|
+
}
|
|
160
|
+
// Extract trace_id from request headers (same as sent to backend)
|
|
161
|
+
let trace_id;
|
|
162
|
+
const session_id = getOrCreateSessionId();
|
|
163
|
+
if (init && init.headers) {
|
|
164
|
+
const headers = new Headers(init.headers);
|
|
165
|
+
trace_id = headers.get("x-routeflow-trace-id") || undefined;
|
|
166
|
+
}
|
|
167
|
+
// Fallback: generate new trace_id if not found
|
|
168
|
+
if (!trace_id) {
|
|
169
|
+
trace_id = generateTraceId();
|
|
170
|
+
}
|
|
171
|
+
const frontend_route = getCurrentFrontendRoute();
|
|
172
|
+
const event = {
|
|
173
|
+
type: "frontend_external_request_observed",
|
|
174
|
+
timestamp: new Date(startTime).toISOString(),
|
|
175
|
+
trace_id,
|
|
176
|
+
session_id,
|
|
177
|
+
environment: this.config.environment,
|
|
178
|
+
frontend_route,
|
|
179
|
+
target_host: hostname,
|
|
180
|
+
method,
|
|
181
|
+
duration_ms,
|
|
182
|
+
outcome,
|
|
183
|
+
sdk: {
|
|
184
|
+
name: this.SDK_NAME,
|
|
185
|
+
version: this.SDK_VERSION,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
if (status_code !== undefined) {
|
|
189
|
+
event.status_code = status_code;
|
|
190
|
+
}
|
|
191
|
+
// Add code version if available
|
|
192
|
+
if (typeof window !== "undefined" && window.__ROUTEFLOW_CODE_VERSION__) {
|
|
193
|
+
event.code_version = window.__ROUTEFLOW_CODE_VERSION__;
|
|
194
|
+
}
|
|
195
|
+
this.queueEvent(event);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Never throw
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Queue an event for sending.
|
|
203
|
+
*/
|
|
204
|
+
queueEvent(event) {
|
|
205
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
206
|
+
this.stats.dropped++;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
this.queue.push(event);
|
|
210
|
+
this.stats.queued++;
|
|
211
|
+
// Flush if queue is getting large
|
|
212
|
+
if (this.queue.length >= 50) {
|
|
213
|
+
this.flush();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Flush queued events to backend.
|
|
218
|
+
*/
|
|
219
|
+
async flush() {
|
|
220
|
+
if (this.queue.length === 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Check for ingest key
|
|
224
|
+
if (!this.config.ingestKey) {
|
|
225
|
+
// Drop events silently
|
|
226
|
+
this.stats.dropped += this.queue.length;
|
|
227
|
+
this.queue = [];
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Take events from queue
|
|
231
|
+
const events = this.queue.splice(0, this.queue.length);
|
|
232
|
+
// Build payload
|
|
233
|
+
const payload = {
|
|
234
|
+
sent_at: new Date().toISOString(),
|
|
235
|
+
events,
|
|
236
|
+
};
|
|
237
|
+
// Send to backend
|
|
238
|
+
const success = await sendEvents(payload, this.config.ingestKey);
|
|
239
|
+
if (success) {
|
|
240
|
+
this.stats.sent += events.length;
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
this.stats.failed += events.length;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Start flush timer.
|
|
248
|
+
*/
|
|
249
|
+
startFlushTimer() {
|
|
250
|
+
if (this.flushTimer !== null) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.flushTimer = window.setInterval(() => {
|
|
254
|
+
this.flush();
|
|
255
|
+
}, this.config.flushIntervalMs);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Stop flush timer.
|
|
259
|
+
*/
|
|
260
|
+
stop() {
|
|
261
|
+
if (this.flushTimer !== null) {
|
|
262
|
+
clearInterval(this.flushTimer);
|
|
263
|
+
this.flushTimer = null;
|
|
264
|
+
}
|
|
265
|
+
// Final flush
|
|
266
|
+
this.flush();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get current stats.
|
|
270
|
+
*/
|
|
271
|
+
getStats() {
|
|
272
|
+
return { ...this.stats };
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get queue size.
|
|
276
|
+
*/
|
|
277
|
+
getQueueSize() {
|
|
278
|
+
return this.queue.length;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
let tracker = null;
|
|
282
|
+
/**
|
|
283
|
+
* Initialize event tracking.
|
|
284
|
+
*/
|
|
285
|
+
export function initEventTracking(config) {
|
|
286
|
+
if (tracker) {
|
|
287
|
+
tracker.stop();
|
|
288
|
+
}
|
|
289
|
+
tracker = new EventTracker(config);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Track a page view.
|
|
293
|
+
*/
|
|
294
|
+
export function trackPageView(route, referrer) {
|
|
295
|
+
if (!tracker) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
tracker.trackPageView(route, referrer);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Track a backend request.
|
|
302
|
+
*/
|
|
303
|
+
export function trackBackendRequest(url, init, startTime, endTime, response, error, traceId) {
|
|
304
|
+
if (!tracker) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
tracker.trackBackendRequest(url, init, startTime, endTime, response, error, traceId);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Track an external request.
|
|
311
|
+
*/
|
|
312
|
+
export function trackExternalRequest(url, init, startTime, endTime, response, error) {
|
|
313
|
+
if (!tracker) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
tracker.trackExternalRequest(url, init, startTime, endTime, response, error);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get event stats.
|
|
320
|
+
*/
|
|
321
|
+
export function getEventStats() {
|
|
322
|
+
if (!tracker) {
|
|
323
|
+
return { queued: 0, sent: 0, dropped: 0, failed: 0 };
|
|
324
|
+
}
|
|
325
|
+
return tracker.getStats();
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get queue size.
|
|
329
|
+
*/
|
|
330
|
+
export function getEventQueueSize() {
|
|
331
|
+
if (!tracker) {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
return tracker.getQueueSize();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Stop event tracking.
|
|
338
|
+
*/
|
|
339
|
+
export function stopEventTracking() {
|
|
340
|
+
if (tracker) {
|
|
341
|
+
tracker.stop();
|
|
342
|
+
tracker = null;
|
|
343
|
+
}
|
|
344
|
+
}
|
package/dist/external_spans.js
CHANGED
package/dist/fetch_patch.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fetch patching for correlation headers and
|
|
2
|
+
* Fetch patching for correlation headers and event tracking.
|
|
3
|
+
*
|
|
4
|
+
* This SDK patches fetch() to:
|
|
5
|
+
* 1. Add correlation headers to requests to user's app backend
|
|
6
|
+
* 2. Track backend requests and send telemetry to RouteFlow
|
|
7
|
+
* 3. Track external requests and send telemetry to RouteFlow
|
|
3
8
|
*/
|
|
4
9
|
/**
|
|
5
|
-
* Patch window.fetch to add correlation headers.
|
|
10
|
+
* Patch window.fetch to add correlation headers and track requests.
|
|
6
11
|
*/
|
|
7
|
-
export declare function patchFetch(enableExternalTracking: boolean): void;
|
|
12
|
+
export declare function patchFetch(enableExternalTracking: boolean, enableBackendTracking: boolean, backendUrl?: string): void;
|
|
8
13
|
/**
|
|
9
14
|
* Restore original fetch.
|
|
10
15
|
*/
|
package/dist/fetch_patch.js
CHANGED
|
@@ -1,36 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fetch patching for correlation headers and
|
|
2
|
+
* Fetch patching for correlation headers and event tracking.
|
|
3
|
+
*
|
|
4
|
+
* This SDK patches fetch() to:
|
|
5
|
+
* 1. Add correlation headers to requests to user's app backend
|
|
6
|
+
* 2. Track backend requests and send telemetry to RouteFlow
|
|
7
|
+
* 3. Track external requests and send telemetry to RouteFlow
|
|
3
8
|
*/
|
|
4
9
|
import { isSameOrigin } from "./validate";
|
|
5
10
|
import { getOrCreateSessionId, generateTraceId } from "./trace";
|
|
6
11
|
import { getCurrentFrontendRoute } from "./route";
|
|
7
|
-
import {
|
|
8
|
-
import { getBackendUrl } from "./client";
|
|
12
|
+
import { trackBackendRequest, trackExternalRequest } from "./event_tracker";
|
|
9
13
|
const TRACE_ID_HEADER = "x-routeflow-trace-id";
|
|
10
14
|
const SESSION_ID_HEADER = "x-routeflow-session-id";
|
|
11
15
|
const FRONTEND_ROUTE_HEADER = "x-routeflow-frontend-route";
|
|
12
16
|
let originalFetch;
|
|
13
17
|
let isPatched = false;
|
|
14
18
|
let trackExternal = false;
|
|
19
|
+
let trackBackend = false;
|
|
20
|
+
let userBackendUrl = ""; // User's app backend URL for header injection
|
|
15
21
|
/**
|
|
16
|
-
* Check if a URL
|
|
22
|
+
* Check if a URL should receive correlation headers.
|
|
23
|
+
* Returns true if:
|
|
24
|
+
* - URL matches the configured user backend URL, OR
|
|
25
|
+
* - No backend URL configured and request is same-origin
|
|
17
26
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
function shouldInjectHeaders(url) {
|
|
28
|
+
// If user explicitly configured their backend URL, match against it
|
|
29
|
+
if (userBackendUrl) {
|
|
30
|
+
try {
|
|
31
|
+
const requestUrl = new URL(url, window.location.href);
|
|
32
|
+
const backendUrlObj = new URL(userBackendUrl, window.location.href);
|
|
33
|
+
// Match by origin (protocol + hostname + port)
|
|
34
|
+
return requestUrl.origin === backendUrlObj.origin;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
28
39
|
}
|
|
40
|
+
// Fallback: inject headers for same-origin requests
|
|
41
|
+
return isSameOrigin(url);
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
|
-
* Patch window.fetch to add correlation headers.
|
|
44
|
+
* Patch window.fetch to add correlation headers and track requests.
|
|
32
45
|
*/
|
|
33
|
-
export function patchFetch(enableExternalTracking) {
|
|
46
|
+
export function patchFetch(enableExternalTracking, enableBackendTracking, backendUrl) {
|
|
34
47
|
if (isPatched) {
|
|
35
48
|
return;
|
|
36
49
|
}
|
|
@@ -39,6 +52,8 @@ export function patchFetch(enableExternalTracking) {
|
|
|
39
52
|
}
|
|
40
53
|
originalFetch = window.fetch;
|
|
41
54
|
trackExternal = enableExternalTracking;
|
|
55
|
+
trackBackend = enableBackendTracking;
|
|
56
|
+
userBackendUrl = backendUrl || "";
|
|
42
57
|
window.fetch = async function patchedFetch(input, init) {
|
|
43
58
|
// Convert input to URL string
|
|
44
59
|
const url = typeof input === "string"
|
|
@@ -46,34 +61,44 @@ export function patchFetch(enableExternalTracking) {
|
|
|
46
61
|
: input instanceof URL
|
|
47
62
|
? input.href
|
|
48
63
|
: input.url;
|
|
49
|
-
const
|
|
64
|
+
const shouldInject = shouldInjectHeaders(url);
|
|
50
65
|
const sameOrigin = isSameOrigin(url);
|
|
51
|
-
// Add correlation headers
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
// Add correlation headers to requests going to user's app backend
|
|
67
|
+
let traceId;
|
|
68
|
+
if (shouldInject) {
|
|
69
|
+
const result = addCorrelationHeaders(input, init);
|
|
70
|
+
init = result.init;
|
|
71
|
+
traceId = result.traceId;
|
|
72
|
+
}
|
|
73
|
+
// Track backend requests (to user's app)
|
|
74
|
+
if (trackBackend && shouldInject && traceId) {
|
|
75
|
+
return trackAndFetchBackend(url, init, traceId);
|
|
54
76
|
}
|
|
55
|
-
// Track external
|
|
56
|
-
// Note: We don't add correlation headers to external requests anymore
|
|
57
|
-
// unless they happen to be the backend
|
|
77
|
+
// Track external requests (to third-party APIs)
|
|
58
78
|
if (trackExternal && !sameOrigin) {
|
|
59
|
-
return
|
|
79
|
+
return trackAndFetchExternal(url, init);
|
|
60
80
|
}
|
|
61
|
-
// Regular fetch
|
|
81
|
+
// Regular fetch (no tracking)
|
|
62
82
|
return originalFetch(input, init);
|
|
63
83
|
};
|
|
64
84
|
isPatched = true;
|
|
65
85
|
}
|
|
66
86
|
/**
|
|
67
|
-
* Add correlation headers to
|
|
87
|
+
* Add RouteFlow correlation headers to requests going to user's app backend.
|
|
88
|
+
* Returns the modified init and the trace ID that was added.
|
|
68
89
|
*/
|
|
69
90
|
function addCorrelationHeaders(input, init) {
|
|
70
91
|
// Get or create headers
|
|
71
92
|
const headers = new Headers(init?.headers);
|
|
72
93
|
// Add trace ID if not already present (unique per request)
|
|
94
|
+
let traceId;
|
|
73
95
|
if (!headers.has(TRACE_ID_HEADER)) {
|
|
74
|
-
|
|
96
|
+
traceId = generateTraceId();
|
|
75
97
|
headers.set(TRACE_ID_HEADER, traceId);
|
|
76
98
|
}
|
|
99
|
+
else {
|
|
100
|
+
traceId = headers.get(TRACE_ID_HEADER);
|
|
101
|
+
}
|
|
77
102
|
// Add session ID if not already present (persists across requests)
|
|
78
103
|
if (!headers.has(SESSION_ID_HEADER)) {
|
|
79
104
|
const sessionId = getOrCreateSessionId();
|
|
@@ -85,20 +110,41 @@ function addCorrelationHeaders(input, init) {
|
|
|
85
110
|
headers.set(FRONTEND_ROUTE_HEADER, route);
|
|
86
111
|
}
|
|
87
112
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
init: {
|
|
114
|
+
...init,
|
|
115
|
+
headers,
|
|
116
|
+
},
|
|
117
|
+
traceId,
|
|
90
118
|
};
|
|
91
119
|
}
|
|
92
120
|
/**
|
|
93
|
-
* Track
|
|
94
|
-
|
|
121
|
+
* Track backend request and send telemetry to RouteFlow.
|
|
122
|
+
*/
|
|
123
|
+
async function trackAndFetchBackend(url, init, traceId) {
|
|
124
|
+
const startTime = performance.now();
|
|
125
|
+
let response = null;
|
|
126
|
+
let error = null;
|
|
127
|
+
try {
|
|
128
|
+
response = await originalFetch(url, init);
|
|
129
|
+
return response;
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
error = e;
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
const endTime = performance.now();
|
|
137
|
+
// Send telemetry about this backend request
|
|
138
|
+
trackBackendRequest(url, init, startTime, endTime, response, error, traceId);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Track external request and send telemetry to RouteFlow.
|
|
95
143
|
*/
|
|
96
|
-
async function
|
|
144
|
+
async function trackAndFetchExternal(url, init) {
|
|
97
145
|
const startTime = performance.now();
|
|
98
146
|
let response = null;
|
|
99
147
|
let error = null;
|
|
100
|
-
// Note: We don't add correlation headers here anymore
|
|
101
|
-
// Headers are only added if isBackendRequest() returned true (handled in patchedFetch)
|
|
102
148
|
try {
|
|
103
149
|
response = await originalFetch(url, init);
|
|
104
150
|
return response;
|
|
@@ -109,7 +155,8 @@ async function trackAndFetch(url, init) {
|
|
|
109
155
|
}
|
|
110
156
|
finally {
|
|
111
157
|
const endTime = performance.now();
|
|
112
|
-
|
|
158
|
+
// Send telemetry about this external request
|
|
159
|
+
trackExternalRequest(url, init, startTime, endTime, response, error);
|
|
113
160
|
}
|
|
114
161
|
}
|
|
115
162
|
/**
|
package/dist/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Initialization and configuration for Routeflow Browser SDK.
|
|
3
3
|
*/
|
|
4
|
-
import { initRouteTracking, getCurrentFrontendRoute } from "./route";
|
|
4
|
+
import { initRouteTracking, getCurrentFrontendRoute, setRouteChangeCallback } from "./route";
|
|
5
5
|
import { getOrCreateSessionId } from "./trace";
|
|
6
6
|
import { patchFetch, unpatchFetch } from "./fetch_patch";
|
|
7
|
-
import {
|
|
7
|
+
import { initEventTracking, getEventStats, getEventQueueSize, stopEventTracking, trackPageView, } from "./event_tracker";
|
|
8
8
|
let isInitialized = false;
|
|
9
9
|
let config = {
|
|
10
10
|
ingestKey: "",
|
|
@@ -15,6 +15,7 @@ let config = {
|
|
|
15
15
|
externalAllowlist: [],
|
|
16
16
|
flushIntervalMs: 1000,
|
|
17
17
|
maxQueueSize: 5000,
|
|
18
|
+
backendUrl: "",
|
|
18
19
|
};
|
|
19
20
|
/**
|
|
20
21
|
* Detect environment from deployment platform environment variables.
|
|
@@ -94,6 +95,7 @@ export function initRouteflow(opts) {
|
|
|
94
95
|
externalAllowlist: opts?.externalAllowlist || [],
|
|
95
96
|
flushIntervalMs: opts?.flushIntervalMs ?? 1000,
|
|
96
97
|
maxQueueSize: opts?.maxQueueSize ?? 5000,
|
|
98
|
+
backendUrl: opts?.backendUrl || "",
|
|
97
99
|
};
|
|
98
100
|
// Try to get ingest key from window if not provided
|
|
99
101
|
if (!config.ingestKey && typeof window !== "undefined") {
|
|
@@ -105,36 +107,47 @@ export function initRouteflow(opts) {
|
|
|
105
107
|
}
|
|
106
108
|
// Initialize route tracking
|
|
107
109
|
initRouteTracking();
|
|
110
|
+
// Set up route change callback to track page views on navigation
|
|
111
|
+
setRouteChangeCallback((route) => {
|
|
112
|
+
trackPageView(route);
|
|
113
|
+
});
|
|
108
114
|
// Initialize session ID (persists across page navigation within session)
|
|
109
115
|
getOrCreateSessionId();
|
|
110
|
-
// Patch fetch for correlation headers
|
|
111
|
-
patchFetch(config.trackExternal
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
// Patch fetch for correlation headers and tracking
|
|
117
|
+
patchFetch(config.trackExternal, // Track external API calls
|
|
118
|
+
true, // Always track backend requests
|
|
119
|
+
config.backendUrl);
|
|
120
|
+
// Initialize unified event tracking
|
|
121
|
+
initEventTracking({
|
|
122
|
+
environment: config.environment,
|
|
123
|
+
allowlist: config.externalAllowlist,
|
|
124
|
+
ingestKey: config.ingestKey || undefined,
|
|
125
|
+
maxQueueSize: config.maxQueueSize,
|
|
126
|
+
flushIntervalMs: config.flushIntervalMs,
|
|
127
|
+
sampleRate: config.sampleRate,
|
|
128
|
+
trackExternal: config.trackExternal,
|
|
129
|
+
trackBackend: true, // Always track backend requests
|
|
130
|
+
trackPageViews: true, // Always track page views
|
|
131
|
+
});
|
|
132
|
+
// Track initial page view
|
|
133
|
+
const currentRoute = getCurrentFrontendRoute();
|
|
134
|
+
const referrer = typeof document !== 'undefined' ? document.referrer : undefined;
|
|
135
|
+
trackPageView(currentRoute, referrer);
|
|
123
136
|
isInitialized = true;
|
|
124
137
|
}
|
|
125
138
|
/**
|
|
126
139
|
* Get Routeflow stats.
|
|
127
140
|
*/
|
|
128
141
|
export function getRouteflowStats() {
|
|
129
|
-
const
|
|
142
|
+
const eventStats = getEventStats();
|
|
130
143
|
return {
|
|
131
144
|
enabled: config.enabled,
|
|
132
145
|
session_id: getOrCreateSessionId(),
|
|
133
146
|
current_route: getCurrentFrontendRoute(),
|
|
134
|
-
events_queued:
|
|
135
|
-
events_sent:
|
|
136
|
-
events_dropped:
|
|
137
|
-
events_failed:
|
|
147
|
+
events_queued: getEventQueueSize(),
|
|
148
|
+
events_sent: eventStats.sent,
|
|
149
|
+
events_dropped: eventStats.dropped,
|
|
150
|
+
events_failed: eventStats.failed,
|
|
138
151
|
external_tracking_enabled: config.trackExternal,
|
|
139
152
|
has_ingest_key: !!config.ingestKey,
|
|
140
153
|
};
|
|
@@ -147,6 +160,6 @@ export function shutdownRouteflow() {
|
|
|
147
160
|
return;
|
|
148
161
|
}
|
|
149
162
|
unpatchFetch();
|
|
150
|
-
|
|
163
|
+
stopEventTracking();
|
|
151
164
|
isInitialized = false;
|
|
152
165
|
}
|
package/dist/route.d.ts
CHANGED
package/dist/route.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { normalizeFrontendRoute } from "./validate";
|
|
5
5
|
let currentRoute = "/";
|
|
6
|
+
let onRouteChange = null;
|
|
7
|
+
/**
|
|
8
|
+
* Set route change callback.
|
|
9
|
+
*/
|
|
10
|
+
export function setRouteChangeCallback(callback) {
|
|
11
|
+
onRouteChange = callback;
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
14
|
* Initialize route tracking by patching history API.
|
|
8
15
|
*/
|
|
@@ -32,7 +39,14 @@ export function initRouteTracking() {
|
|
|
32
39
|
* Update the current route from location.pathname.
|
|
33
40
|
*/
|
|
34
41
|
function updateCurrentRoute() {
|
|
35
|
-
|
|
42
|
+
const newRoute = normalizeFrontendRoute(window.location.pathname);
|
|
43
|
+
// Only trigger callback if route actually changed
|
|
44
|
+
if (newRoute !== currentRoute) {
|
|
45
|
+
currentRoute = newRoute;
|
|
46
|
+
if (onRouteChange) {
|
|
47
|
+
onRouteChange(newRoute);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
36
50
|
}
|
|
37
51
|
/**
|
|
38
52
|
* Get the current frontend route.
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface RouteflowInitOptions {
|
|
|
19
19
|
flushIntervalMs?: number;
|
|
20
20
|
/** Maximum queue size before dropping events (default: 5000) */
|
|
21
21
|
maxQueueSize?: number;
|
|
22
|
+
/** User's app backend URL for header injection (default: same-origin only) */
|
|
23
|
+
backendUrl?: string;
|
|
22
24
|
}
|
|
23
25
|
export interface ExternalSpanEvent {
|
|
24
26
|
type: "frontend_external_request_observed";
|
|
@@ -38,9 +40,41 @@ export interface ExternalSpanEvent {
|
|
|
38
40
|
};
|
|
39
41
|
code_version?: string;
|
|
40
42
|
}
|
|
43
|
+
export interface PageViewEvent {
|
|
44
|
+
type: "frontend_page_view";
|
|
45
|
+
timestamp: string;
|
|
46
|
+
session_id: string;
|
|
47
|
+
environment: Environment;
|
|
48
|
+
frontend_route: string;
|
|
49
|
+
referrer?: string;
|
|
50
|
+
sdk: {
|
|
51
|
+
name: string;
|
|
52
|
+
version: string;
|
|
53
|
+
};
|
|
54
|
+
code_version?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface BackendRequestEvent {
|
|
57
|
+
type: "frontend_backend_request";
|
|
58
|
+
timestamp: string;
|
|
59
|
+
trace_id: string;
|
|
60
|
+
session_id: string;
|
|
61
|
+
environment: Environment;
|
|
62
|
+
frontend_route: string;
|
|
63
|
+
method: string;
|
|
64
|
+
backend_url: string;
|
|
65
|
+
duration_ms: number;
|
|
66
|
+
outcome: "success" | "failure" | "unknown";
|
|
67
|
+
status_code?: number;
|
|
68
|
+
sdk: {
|
|
69
|
+
name: string;
|
|
70
|
+
version: string;
|
|
71
|
+
};
|
|
72
|
+
code_version?: string;
|
|
73
|
+
}
|
|
74
|
+
export type RouteflowEvent = ExternalSpanEvent | PageViewEvent | BackendRequestEvent;
|
|
41
75
|
export interface IngestPayload {
|
|
42
76
|
sent_at: string;
|
|
43
|
-
events:
|
|
77
|
+
events: RouteflowEvent[];
|
|
44
78
|
}
|
|
45
79
|
export interface RouteflowStats {
|
|
46
80
|
enabled: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routeflow-browser",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Browser SDK for Routeflow - Frontend
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Browser SDK for Routeflow - Frontend telemetry, correlation headers, and dependency tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|