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 CHANGED
@@ -3,9 +3,9 @@
3
3
  */
4
4
  import { IngestPayload } from "./types";
5
5
  /**
6
- * Get the current backend URL.
6
+ * Get the RouteFlow backend URL (for telemetry ingestion).
7
7
  */
8
- export declare function getBackendUrl(): string;
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 BACKEND_URL = "https://routeflow-backend-production.up.railway.app";
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 current backend URL.
8
+ * Get the RouteFlow backend URL (for telemetry ingestion).
9
9
  */
10
- export function getBackendUrl() {
11
- return BACKEND_URL;
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 = `${BACKEND_URL}${INGEST_ENDPOINT}`;
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
+ }
@@ -16,7 +16,7 @@ class ExternalSpanTracker {
16
16
  };
17
17
  this.flushTimer = null;
18
18
  this.SDK_NAME = "@routeflow/browser";
19
- this.SDK_VERSION = "0.1.2";
19
+ this.SDK_VERSION = "0.1.4";
20
20
  this.config = config;
21
21
  this.startFlushTimer();
22
22
  }
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Fetch patching for correlation headers and external span tracking.
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
  */
@@ -1,36 +1,49 @@
1
1
  /**
2
- * Fetch patching for correlation headers and external span tracking.
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 { trackExternalFetch } from "./external_spans";
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 is targeting the backend (where we should inject headers).
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 isBackendRequest(url) {
19
- try {
20
- const backendUrl = getBackendUrl();
21
- const urlObj = new URL(url, window.location.href);
22
- const backendObj = new URL(backendUrl);
23
- // Match by origin (protocol + hostname + port)
24
- return urlObj.origin === backendObj.origin;
25
- }
26
- catch {
27
- return false;
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 isBackend = isBackendRequest(url);
64
+ const shouldInject = shouldInjectHeaders(url);
50
65
  const sameOrigin = isSameOrigin(url);
51
- // Add correlation headers ONLY for backend requests
52
- if (isBackend) {
53
- init = addCorrelationHeaders(input, init);
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 spans if enabled and not same-origin
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 trackAndFetch(url, init);
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 same-origin requests.
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
- const traceId = generateTraceId();
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
- ...init,
89
- headers,
113
+ init: {
114
+ ...init,
115
+ headers,
116
+ },
117
+ traceId,
90
118
  };
91
119
  }
92
120
  /**
93
- * Track external fetch call and execute it.
94
- * Note: Headers are NOT added here - only if isBackendRequest() is true (already handled in patchedFetch).
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 trackAndFetch(url, init) {
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
- trackExternalFetch(url, init, startTime, endTime, response, error);
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 { initExternalSpanTracking, getExternalSpanStats, getExternalSpanQueueSize, stopExternalSpanTracking, } from "./external_spans";
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
- // Initialize external span tracking if enabled
113
- if (config.trackExternal) {
114
- initExternalSpanTracking({
115
- environment: config.environment,
116
- allowlist: config.externalAllowlist,
117
- ingestKey: config.ingestKey || undefined,
118
- maxQueueSize: config.maxQueueSize,
119
- flushIntervalMs: config.flushIntervalMs,
120
- sampleRate: config.sampleRate,
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 externalStats = getExternalSpanStats();
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: getExternalSpanQueueSize(),
135
- events_sent: externalStats.sent,
136
- events_dropped: externalStats.dropped,
137
- events_failed: externalStats.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
- stopExternalSpanTracking();
163
+ stopEventTracking();
151
164
  isInitialized = false;
152
165
  }
package/dist/route.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Frontend route tracking (framework-agnostic).
3
3
  */
4
+ /**
5
+ * Set route change callback.
6
+ */
7
+ export declare function setRouteChangeCallback(callback: (route: string) => void): void;
4
8
  /**
5
9
  * Initialize route tracking by patching history API.
6
10
  */
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
- currentRoute = normalizeFrontendRoute(window.location.pathname);
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: ExternalSpanEvent[];
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.2",
4
- "description": "Browser SDK for Routeflow - Frontend to backend correlation and external dependency tracking",
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": {