@flotrace/runtime 0.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3998 +1,563 @@
1
+ // src/index.ts
2
+ export * from "@flotrace/runtime-core";
3
+
1
4
  // src/FloTraceProvider.tsx
2
5
  import React, { useCallback, useEffect, useRef, createContext, useContext, Profiler } from "react";
6
+ import {
7
+ DEFAULT_CONFIG,
8
+ getWebSocketClient,
9
+ disposeWebSocketClient,
10
+ serializeProps,
11
+ getChangedKeys,
12
+ installFiberTreeWalker,
13
+ uninstallFiberTreeWalker,
14
+ requestTreeSnapshot,
15
+ requestFullSnapshot,
16
+ getNodeProps,
17
+ getNodeHooks,
18
+ getNodeEffects,
19
+ getDetailedRenderReason,
20
+ installZustandTracker,
21
+ uninstallZustandTracker,
22
+ installReduxTracker,
23
+ uninstallReduxTracker,
24
+ installTanStackQueryTracker,
25
+ uninstallTanStackQueryTracker,
26
+ installTimelineTracker,
27
+ uninstallTimelineTracker,
28
+ getTimeline
29
+ } from "@flotrace/runtime-core";
3
30
 
4
- // src/types.ts
5
- var DEFAULT_CONFIG = {
6
- host: "127.0.0.1",
7
- port: 3457,
8
- appName: "React App",
9
- enabled: process.env.NODE_ENV === "development",
10
- autoReconnect: true,
11
- reconnectInterval: 2e3,
12
- trackAllRenders: true,
13
- includeProps: true,
14
- trackZustand: true,
15
- trackRedux: true,
16
- trackRouter: true,
17
- trackContext: true,
18
- trackTanstackQuery: true
19
- };
20
-
21
- // src/websocketClient.ts
22
- var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
23
- constructor(config = {}) {
24
- this.ws = null;
25
- this.messageQueue = [];
26
- this.flushTimeout = null;
27
- this.reconnectTimeout = null;
28
- this.isConnecting = false;
29
- this.reconnectAttempts = 0;
30
- // Prevent unbounded queue growth when disconnected
31
- this.messageHandlers = /* @__PURE__ */ new Set();
32
- this.connectionHandlers = /* @__PURE__ */ new Set();
33
- this.config = { ...DEFAULT_CONFIG, ...config };
31
+ // src/routerTracker.ts
32
+ var isInstalled = false;
33
+ var debounceTimer = null;
34
+ var client = null;
35
+ var originalPushState = null;
36
+ var originalReplaceState = null;
37
+ var popstateHandler = null;
38
+ var DEBOUNCE_MS = 200;
39
+ function installRouterTracker(wsClient) {
40
+ if (isInstalled) {
41
+ console.warn("[FloTrace] Router tracker already installed, reinstalling");
42
+ uninstallRouterTracker();
34
43
  }
35
- /**
36
- * Connect to the FloTrace WebSocket server
37
- */
38
- connect() {
39
- if (this.ws || this.isConnecting) {
40
- return;
41
- }
42
- if (!this.config.enabled) {
43
- console.log("[FloTrace] Runtime disabled, skipping connection");
44
- return;
45
- }
46
- if (typeof WebSocket === "undefined") {
47
- console.log("[FloTrace] WebSocket not available, skipping connection");
48
- return;
49
- }
50
- this.isConnecting = true;
51
- try {
52
- const url = `ws://${this.config.host}:${this.config.port}`;
53
- console.log(`[FloTrace] Connecting to ${url}...`);
54
- this.ws = new WebSocket(url);
55
- this.ws.onopen = () => {
56
- this.isConnecting = false;
57
- this.reconnectAttempts = 0;
58
- console.log("[FloTrace] Connected to FloTrace desktop");
59
- this.notifyConnectionChange(true);
60
- let appUrl;
61
- try {
62
- appUrl = typeof window !== "undefined" && window.location ? window.location.href : void 0;
63
- } catch {
64
- appUrl = void 0;
65
- }
66
- this.send({
67
- type: "runtime:ready",
68
- appName: this.config.appName,
69
- reactVersion: this.getReactVersion(),
70
- appUrl
71
- });
72
- this.flush();
73
- };
74
- this.ws.onmessage = (event) => {
75
- try {
76
- const message = JSON.parse(event.data);
77
- this.handleMessage(message);
78
- } catch (error) {
79
- console.error("[FloTrace] Failed to parse message:", error);
80
- }
81
- };
82
- this.ws.onclose = () => {
83
- this.isConnecting = false;
84
- this.ws = null;
85
- console.log("[FloTrace] Disconnected from VS Code extension");
86
- this.notifyConnectionChange(false);
87
- if (this.config.autoReconnect) {
88
- this.scheduleReconnect();
89
- }
90
- };
91
- this.ws.onerror = (error) => {
92
- this.isConnecting = false;
93
- console.error("[FloTrace] WebSocket error:", error);
94
- };
95
- } catch (error) {
96
- this.isConnecting = false;
97
- console.error("[FloTrace] Failed to connect:", error);
98
- if (this.config.autoReconnect) {
99
- this.scheduleReconnect();
100
- }
101
- }
44
+ if (typeof window === "undefined" || typeof history === "undefined") {
45
+ console.warn("[FloTrace] Router tracker requires a browser environment");
46
+ return;
102
47
  }
103
- /**
104
- * Disconnect from the server
105
- */
106
- disconnect() {
107
- if (this.reconnectTimeout) {
108
- clearTimeout(this.reconnectTimeout);
109
- this.reconnectTimeout = null;
110
- }
111
- if (this.flushTimeout) {
112
- clearTimeout(this.flushTimeout);
113
- this.flushTimeout = null;
114
- }
115
- if (this.ws) {
48
+ console.log("[FloTrace] Installing router tracker");
49
+ try {
50
+ isInstalled = true;
51
+ client = wsClient;
52
+ originalPushState = history.pushState.bind(history);
53
+ originalReplaceState = history.replaceState.bind(history);
54
+ history.pushState = function(data, unused, url) {
55
+ originalPushState(data, unused, url);
116
56
  try {
117
- this.send({ type: "runtime:disconnect", reason: "Client disconnect" });
57
+ scheduleRouterUpdate();
118
58
  } catch (error) {
119
- console.error("[FloTrace] Error sending disconnect message:", error);
120
- }
121
- this.ws.close();
122
- this.ws = null;
123
- }
124
- }
125
- /**
126
- * Send a message to the extension (queued and batched)
127
- */
128
- send(message) {
129
- if (!this.config.enabled) {
130
- return;
131
- }
132
- this.messageQueue.push(message);
133
- if (this.messageQueue.length > _FloTraceWebSocketClient.MAX_QUEUE_SIZE) {
134
- this.messageQueue = this.messageQueue.slice(-_FloTraceWebSocketClient.MAX_QUEUE_SIZE);
135
- }
136
- if (!this.flushTimeout) {
137
- this.flushTimeout = setTimeout(() => {
138
- this.flush();
139
- }, _FloTraceWebSocketClient.BATCH_FLUSH_MS);
140
- }
141
- if (this.messageQueue.length >= (this.config.trackAllRenders ? 50 : 10)) {
142
- this.flush();
143
- }
144
- }
145
- /**
146
- * Send a message immediately (not batched)
147
- */
148
- sendImmediate(message) {
149
- if (!this.config.enabled || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
150
- return;
151
- }
152
- try {
153
- this.ws.send(JSON.stringify(message));
154
- } catch (error) {
155
- console.error("[FloTrace] Failed to send message:", error);
156
- }
157
- }
158
- /**
159
- * Flush the message queue
160
- */
161
- flush() {
162
- if (this.flushTimeout) {
163
- clearTimeout(this.flushTimeout);
164
- this.flushTimeout = null;
165
- }
166
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN || this.messageQueue.length === 0) {
167
- return;
168
- }
169
- try {
170
- for (const message of this.messageQueue) {
171
- this.ws.send(JSON.stringify(message));
59
+ console.error("[FloTrace] Error in pushState handler:", error);
172
60
  }
173
- this.messageQueue = [];
174
- } catch (error) {
175
- console.error("[FloTrace] Failed to flush messages:", error);
176
- }
177
- }
178
- /**
179
- * Schedule a reconnection attempt
180
- */
181
- scheduleReconnect() {
182
- if (this.reconnectTimeout) {
183
- return;
184
- }
185
- if (this.reconnectAttempts >= _FloTraceWebSocketClient.MAX_RECONNECT_ATTEMPTS) {
186
- console.warn(
187
- `[FloTrace] Reconnection budget exhausted (${_FloTraceWebSocketClient.MAX_RECONNECT_ATTEMPTS} attempts). Reload the page or restart the extension to retry.`
188
- );
189
- return;
190
- }
191
- const baseDelay = this.config.reconnectInterval || 2e3;
192
- const delay = Math.min(
193
- baseDelay * Math.pow(2, this.reconnectAttempts),
194
- _FloTraceWebSocketClient.MAX_RECONNECT_INTERVAL
195
- );
196
- this.reconnectAttempts++;
197
- console.log(
198
- `[FloTrace] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${_FloTraceWebSocketClient.MAX_RECONNECT_ATTEMPTS})`
199
- );
200
- this.reconnectTimeout = setTimeout(() => {
201
- this.reconnectTimeout = null;
202
- this.connect();
203
- }, delay);
204
- }
205
- /**
206
- * Handle incoming message from extension
207
- */
208
- handleMessage(message) {
209
- for (const handler of this.messageHandlers) {
61
+ };
62
+ history.replaceState = function(data, unused, url) {
63
+ originalReplaceState(data, unused, url);
210
64
  try {
211
- handler(message);
65
+ scheduleRouterUpdate();
212
66
  } catch (error) {
213
- console.error("[FloTrace] Message handler error:", error);
67
+ console.error("[FloTrace] Error in replaceState handler:", error);
214
68
  }
215
- }
216
- }
217
- /**
218
- * Notify connection state change
219
- */
220
- notifyConnectionChange(connected) {
221
- for (const handler of this.connectionHandlers) {
69
+ };
70
+ popstateHandler = () => {
222
71
  try {
223
- handler(connected);
72
+ scheduleRouterUpdate();
224
73
  } catch (error) {
225
- console.error("[FloTrace] Connection handler error:", error);
226
- }
227
- }
228
- }
229
- /**
230
- * Add a message handler
231
- */
232
- onMessage(handler) {
233
- this.messageHandlers.add(handler);
234
- return () => this.messageHandlers.delete(handler);
235
- }
236
- /**
237
- * Add a connection state handler
238
- */
239
- onConnectionChange(handler) {
240
- this.connectionHandlers.add(handler);
241
- return () => this.connectionHandlers.delete(handler);
242
- }
243
- /**
244
- * Check if connected
245
- */
246
- get connected() {
247
- return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
248
- }
249
- /**
250
- * Get React version if available
251
- */
252
- getReactVersion() {
253
- try {
254
- if (typeof window !== "undefined") {
255
- const React2 = window.React;
256
- return React2?.version;
74
+ console.error("[FloTrace] Error in popstate handler:", error);
257
75
  }
258
- } catch {
259
- }
260
- return void 0;
261
- }
262
- };
263
- _FloTraceWebSocketClient.MAX_RECONNECT_ATTEMPTS = 10;
264
- _FloTraceWebSocketClient.MAX_RECONNECT_INTERVAL = 3e4;
265
- // 30s cap
266
- _FloTraceWebSocketClient.BATCH_FLUSH_MS = 100;
267
- // Flush batched messages every 100ms
268
- _FloTraceWebSocketClient.MAX_QUEUE_SIZE = 500;
269
- var FloTraceWebSocketClient = _FloTraceWebSocketClient;
270
- var clientInstance = null;
271
- function getWebSocketClient(config) {
272
- if (!clientInstance) {
273
- clientInstance = new FloTraceWebSocketClient(config);
274
- }
275
- return clientInstance;
276
- }
277
- function disposeWebSocketClient() {
278
- if (clientInstance) {
279
- clientInstance.disconnect();
280
- clientInstance = null;
281
- }
282
- }
283
-
284
- // src/serializer.ts
285
- var MAX_DEPTH = 5;
286
- var MAX_STRING_LENGTH = 500;
287
- var MAX_ARRAY_LENGTH = 50;
288
- var MAX_OBJECT_KEYS = 30;
289
- function serializeValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
290
- if (value === null) {
291
- return null;
292
- }
293
- if (value === void 0) {
294
- return { __type: "undefined" };
295
- }
296
- if (typeof value === "boolean") {
297
- return value;
298
- }
299
- if (typeof value === "number") {
300
- if (Number.isNaN(value)) return "NaN";
301
- if (!Number.isFinite(value)) return value > 0 ? "Infinity" : "-Infinity";
302
- return value;
303
- }
304
- if (typeof value === "string") {
305
- if (value.length > MAX_STRING_LENGTH) {
306
- return {
307
- __type: "truncated",
308
- originalType: "string",
309
- length: value.length
310
- };
311
- }
312
- return value;
313
- }
314
- if (typeof value === "symbol") {
315
- return {
316
- __type: "symbol",
317
- description: value.description
318
76
  };
319
- }
320
- if (typeof value === "function") {
321
- return {
322
- __type: "function",
323
- name: value.name || "anonymous"
324
- };
325
- }
326
- if (typeof value === "object") {
327
- if (seen.has(value)) {
328
- return { __type: "circular" };
329
- }
330
- if (depth >= MAX_DEPTH) {
331
- return {
332
- __type: "truncated",
333
- originalType: Array.isArray(value) ? "array" : "object"
334
- };
335
- }
336
- seen.add(value);
337
- if (Array.isArray(value)) {
338
- if (value.length > MAX_ARRAY_LENGTH) {
339
- const truncated = value.slice(0, MAX_ARRAY_LENGTH).map((item) => serializeValue(item, depth + 1, seen));
340
- return [
341
- ...truncated,
342
- {
343
- __type: "truncated",
344
- originalType: "array",
345
- length: value.length
346
- }
347
- ];
348
- }
349
- return value.map((item) => serializeValue(item, depth + 1, seen));
350
- }
351
- if (value instanceof Date) {
352
- return value.toISOString();
353
- }
354
- if (value instanceof Error) {
355
- return {
356
- name: value.name,
357
- message: value.message
358
- };
359
- }
360
- if (value instanceof Map) {
361
- const obj = {};
362
- let count = 0;
363
- for (const [k, v] of value.entries()) {
364
- if (count >= MAX_OBJECT_KEYS) {
365
- obj.__truncated = { __type: "truncated", originalType: "Map", length: value.size };
366
- break;
367
- }
368
- obj[String(k)] = serializeValue(v, depth + 1, seen);
369
- count++;
370
- }
371
- return obj;
372
- }
373
- if (value instanceof Set) {
374
- const arr = Array.from(value);
375
- if (arr.length > MAX_ARRAY_LENGTH) {
376
- return {
377
- __type: "truncated",
378
- originalType: "Set",
379
- length: arr.length
380
- };
381
- }
382
- return arr.map((item) => serializeValue(item, depth + 1, seen));
383
- }
384
- if (value instanceof RegExp) {
385
- return value.toString();
386
- }
387
- const keys = Object.keys(value);
388
- const result = {};
389
- for (let i = 0; i < Math.min(keys.length, MAX_OBJECT_KEYS); i++) {
390
- const key = keys[i];
391
- try {
392
- result[key] = serializeValue(
393
- value[key],
394
- depth + 1,
395
- seen
396
- );
397
- } catch {
398
- result[key] = { __type: "truncated", originalType: "error" };
399
- }
400
- }
401
- if (keys.length > MAX_OBJECT_KEYS) {
402
- result.__truncated = {
403
- __type: "truncated",
404
- originalType: "object",
405
- length: keys.length
406
- };
407
- }
408
- return result;
409
- }
410
- return { __type: "truncated", originalType: typeof value };
411
- }
412
- function serializeProps(props) {
413
- const result = {};
414
- for (const [key, value] of Object.entries(props)) {
415
- if (key === "children" || key === "key" || key === "ref") {
416
- continue;
417
- }
418
- if (key.startsWith("__")) {
419
- continue;
420
- }
421
- try {
422
- result[key] = serializeValue(value);
423
- } catch (error) {
424
- console.error(`[FloTrace] Error serializing prop "${key}":`, error);
425
- result[key] = { __type: "truncated", originalType: "error" };
426
- }
427
- }
428
- return result;
429
- }
430
- function getChangedKeys(prev, next) {
431
- if (!prev) {
432
- return Object.keys(next);
433
- }
434
- const changed = [];
435
- const allKeys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
436
- for (const key of allKeys) {
437
- if (!Object.is(prev[key], next[key])) {
438
- changed.push(key);
439
- }
440
- }
441
- return changed;
442
- }
443
-
444
- // src/fiberConstants.ts
445
- var HOOK_HAS_EFFECT = 1;
446
- var HOOK_INSERTION = 2;
447
- var HOOK_LAYOUT = 4;
448
- var HOOK_PASSIVE = 8;
449
- function collectCircularList(lastEffect) {
450
- const list = [];
451
- let effect = lastEffect.next;
452
- if (!effect) return list;
453
- do {
454
- list.push(effect);
455
- effect = effect.next;
456
- } while (effect && effect !== lastEffect.next);
457
- return list;
458
- }
459
-
460
- // src/hookInspector.ts
461
- function inspectHooks(fiber) {
462
- const hooks = [];
463
- let hookState = fiber.memoizedState;
464
- const effects = fiber.updateQueue?.lastEffect ? collectCircularList(fiber.updateQueue.lastEffect) : [];
465
- let effectIndex = 0;
466
- const debugTypes = fiber._debugHookTypes ?? null;
467
- let index = 0;
468
- while (hookState) {
77
+ window.addEventListener("popstate", popstateHandler);
78
+ sendRouterUpdate();
79
+ } catch (error) {
80
+ console.error("[FloTrace] Failed to install router tracker:", error);
469
81
  try {
470
- const debugLabel = debugTypes?.[index] ?? void 0;
471
- const hookInfo = classifyHook(hookState, index, effects, effectIndex, debugLabel);
472
- hooks.push(hookInfo);
473
- if (hookInfo.type === "useEffect" || hookInfo.type === "useLayoutEffect" || hookInfo.type === "useInsertionEffect") {
474
- effectIndex++;
475
- }
476
- } catch (error) {
477
- hooks.push({ index, type: "unknown", value: { __type: "truncated", originalType: "error" } });
82
+ uninstallRouterTracker();
83
+ } catch (_) {
478
84
  }
479
- hookState = hookState.next;
480
- index++;
481
85
  }
482
- return hooks;
483
86
  }
484
- function classifyHook(state, index, effects, effectIdx, debugLabel) {
485
- const ms = state.memoizedState;
486
- if (debugLabel) {
487
- return classifyFromDebugLabel(state, index, effects, effectIdx, debugLabel);
488
- }
489
- if (state.queue !== null) {
490
- const queue = state.queue;
491
- const isReducer = queue.lastRenderedReducer && typeof queue.lastRenderedReducer === "function" && queue.lastRenderedReducer.name !== "" && queue.lastRenderedReducer.name !== "basicStateReducer";
492
- return {
493
- index,
494
- type: isReducer ? "useReducer" : "useState",
495
- value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet())
496
- };
497
- }
498
- if (ms !== null && typeof ms === "object" && !Array.isArray(ms) && "current" in ms) {
499
- const keys = Object.keys(ms);
500
- if (keys.length === 1 && keys[0] === "current") {
501
- return {
502
- index,
503
- type: "useRef",
504
- value: serializeValue(ms.current, 0, /* @__PURE__ */ new WeakSet())
505
- };
506
- }
507
- }
508
- if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
509
- const isCallback = typeof ms[0] === "function";
510
- return {
511
- index,
512
- type: isCallback ? "useCallback" : "useMemo",
513
- value: serializeValue(ms[0], 0, /* @__PURE__ */ new WeakSet()),
514
- deps: ms[1].map((d) => serializeValue(d, 0, /* @__PURE__ */ new WeakSet()))
515
- };
516
- }
517
- if (effectIdx < effects.length) {
518
- const effect = effects[effectIdx];
519
- if (typeof ms === "number" || isEffectShape(ms)) {
520
- const type = (effect.tag & HOOK_PASSIVE) !== 0 ? "useEffect" : (effect.tag & HOOK_LAYOUT) !== 0 ? "useLayoutEffect" : (effect.tag & HOOK_INSERTION) !== 0 ? "useInsertionEffect" : "useEffect";
521
- return {
522
- index,
523
- type,
524
- value: { __type: "function", name: "effect" },
525
- deps: effect.deps ? effect.deps.map((d) => serializeValue(d, 0, /* @__PURE__ */ new WeakSet())) : void 0
526
- };
527
- }
528
- }
529
- if (Array.isArray(ms) && ms.length === 2 && typeof ms[0] === "boolean" && typeof ms[1] === "function") {
530
- return {
531
- index,
532
- type: "useTransition",
533
- value: serializeValue(ms[0], 0, /* @__PURE__ */ new WeakSet())
534
- };
535
- }
536
- if (typeof ms === "string" && ms.startsWith(":")) {
537
- return {
538
- index,
539
- type: "useId",
540
- value: ms
541
- };
87
+ function uninstallRouterTracker() {
88
+ if (!isInstalled) return;
89
+ if (debounceTimer) {
90
+ clearTimeout(debounceTimer);
91
+ debounceTimer = null;
542
92
  }
543
- return { index, type: "unknown", value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet()) };
544
- }
545
- function classifyFromDebugLabel(state, index, effects, effectIdx, debugLabel) {
546
- const ms = state.memoizedState;
547
- const normalizedLabel = debugLabel.toLowerCase().replace(/\s/g, "");
548
- const labelMap = {
549
- "usestate": "useState",
550
- "usereducer": "useReducer",
551
- "useref": "useRef",
552
- "usememo": "useMemo",
553
- "usecallback": "useCallback",
554
- "useeffect": "useEffect",
555
- "uselayouteffect": "useLayoutEffect",
556
- "useinsertioneffect": "useInsertionEffect",
557
- "usecontext": "useContext",
558
- "useimperativehandle": "useImperativeHandle",
559
- "usedebugvalue": "useDebugValue",
560
- "usetransition": "useTransition",
561
- "usedeferredvalue": "useDeferredValue",
562
- "useid": "useId",
563
- "usesyncexternalstore": "useSyncExternalStore",
564
- "useoptimistic": "useOptimistic",
565
- "useformstatus": "useFormStatus"
566
- };
567
- const hookType = labelMap[normalizedLabel] ?? "unknown";
568
- const base = { index, type: hookType, value: serializeValue(ms, 0, /* @__PURE__ */ new WeakSet()), debugLabel };
569
- if (hookType === "useEffect" || hookType === "useLayoutEffect" || hookType === "useInsertionEffect") {
570
- if (effectIdx < effects.length) {
571
- const effect = effects[effectIdx];
572
- base.value = { __type: "function", name: "effect" };
573
- base.deps = effect.deps ? effect.deps.map((d) => serializeValue(d, 0, /* @__PURE__ */ new WeakSet())) : void 0;
93
+ try {
94
+ if (originalPushState) {
95
+ history.pushState = originalPushState;
96
+ originalPushState = null;
574
97
  }
98
+ } catch (error) {
99
+ console.error("[FloTrace] Error restoring pushState:", error);
575
100
  }
576
- if ((hookType === "useMemo" || hookType === "useCallback") && Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
577
- base.value = serializeValue(ms[0], 0, /* @__PURE__ */ new WeakSet());
578
- base.deps = ms[1].map((d) => serializeValue(d, 0, /* @__PURE__ */ new WeakSet()));
579
- }
580
- if (hookType === "useRef" && ms !== null && typeof ms === "object" && "current" in ms) {
581
- base.value = serializeValue(ms.current, 0, /* @__PURE__ */ new WeakSet());
582
- }
583
- return base;
584
- }
585
- function isEffectShape(ms) {
586
- if (ms === null || ms === void 0) return false;
587
- if (typeof ms === "object" && ms !== null) {
588
- const obj = ms;
589
- return "tag" in obj && "create" in obj && "deps" in obj;
590
- }
591
- return false;
592
- }
593
-
594
- // src/effectInspector.ts
595
- function inspectEffects(fiber) {
596
- const results = [];
597
- const lastEffect = fiber.updateQueue?.lastEffect;
598
- if (!lastEffect) return results;
599
- const currEffects = collectCircularList(lastEffect);
600
- const prevEffects = fiber.alternate?.updateQueue?.lastEffect ? collectCircularList(fiber.alternate.updateQueue.lastEffect) : [];
601
- const hookIndexMap = buildEffectToHookIndexMap(fiber, currEffects);
602
- for (let i = 0; i < currEffects.length; i++) {
603
- try {
604
- const curr = currEffects[i];
605
- const prev = prevEffects[i] ?? null;
606
- const type = (curr.tag & HOOK_PASSIVE) !== 0 ? "useEffect" : (curr.tag & HOOK_LAYOUT) !== 0 ? "useLayoutEffect" : (curr.tag & HOOK_INSERTION) !== 0 ? "useInsertionEffect" : "useEffect";
607
- const willRun = (curr.tag & HOOK_HAS_EFFECT) !== 0;
608
- const changedDepIndices = diffDeps(prev?.deps ?? null, curr.deps);
609
- const hasCleanup = typeof curr.destroy === "function";
610
- results.push({
611
- index: i,
612
- hookIndex: hookIndexMap.get(i) ?? -1,
613
- type,
614
- deps: serializeDeps(curr.deps),
615
- prevDeps: prev ? serializeDeps(prev.deps) : null,
616
- changedDepIndices,
617
- willRun,
618
- hasCleanup
619
- });
620
- } catch (error) {
621
- results.push({
622
- index: i,
623
- hookIndex: -1,
624
- type: "useEffect",
625
- deps: null,
626
- prevDeps: null,
627
- changedDepIndices: [],
628
- willRun: false,
629
- hasCleanup: false
630
- });
101
+ try {
102
+ if (originalReplaceState) {
103
+ history.replaceState = originalReplaceState;
104
+ originalReplaceState = null;
631
105
  }
106
+ } catch (error) {
107
+ console.error("[FloTrace] Error restoring replaceState:", error);
632
108
  }
633
- return results;
634
- }
635
- function buildEffectToHookIndexMap(fiber, effects) {
636
- const map = /* @__PURE__ */ new Map();
637
- let hookState = fiber.memoizedState;
638
- let hookIndex = 0;
639
- let effectIndex = 0;
640
- while (hookState && effectIndex < effects.length) {
641
- const ms = hookState.memoizedState;
642
- if (isLikelyEffectHook(ms, hookState)) {
643
- map.set(effectIndex, hookIndex);
644
- effectIndex++;
109
+ try {
110
+ if (popstateHandler) {
111
+ window.removeEventListener("popstate", popstateHandler);
112
+ popstateHandler = null;
645
113
  }
646
- hookState = hookState.next;
647
- hookIndex++;
114
+ } catch (error) {
115
+ console.error("[FloTrace] Error removing popstate listener:", error);
648
116
  }
649
- return map;
117
+ client = null;
118
+ isInstalled = false;
119
+ console.log("[FloTrace] Router tracker uninstalled");
650
120
  }
651
- function isLikelyEffectHook(ms, state) {
652
- if (state.queue !== null) return false;
653
- if (ms !== null && typeof ms === "object") {
654
- const obj = ms;
655
- if ("tag" in obj && "create" in obj && "deps" in obj) return true;
656
- }
657
- return false;
121
+ function scheduleRouterUpdate() {
122
+ if (debounceTimer) clearTimeout(debounceTimer);
123
+ debounceTimer = setTimeout(() => {
124
+ debounceTimer = null;
125
+ sendRouterUpdate();
126
+ }, DEBOUNCE_MS);
658
127
  }
659
- function diffDeps(prev, curr) {
660
- if (!prev || !curr) return [];
661
- const changed = [];
662
- const len = Math.max(prev.length, curr.length);
663
- for (let i = 0; i < len; i++) {
664
- if (!Object.is(prev[i], curr[i])) {
665
- changed.push(i);
128
+ function sendRouterUpdate() {
129
+ try {
130
+ if (!client?.connected) return;
131
+ const pathname = window.location.pathname;
132
+ const searchParams = {};
133
+ const urlSearchParams = new URLSearchParams(window.location.search);
134
+ for (const [key, value] of urlSearchParams.entries()) {
135
+ searchParams[key] = value;
666
136
  }
137
+ client.sendImmediate({
138
+ type: "runtime:router",
139
+ pathname,
140
+ // Matched route params (e.g., :id) are not available from the History API.
141
+ // Future enhancement: extract from React Router's fiber context.
142
+ params: {},
143
+ searchParams,
144
+ timestamp: Date.now()
145
+ });
146
+ } catch (error) {
147
+ console.error("[FloTrace] Error sending router update:", error);
667
148
  }
668
- return changed;
669
- }
670
- function serializeDeps(deps) {
671
- if (deps === null) return null;
672
- return deps.map((d) => serializeValue(d, 0, /* @__PURE__ */ new WeakSet()));
673
149
  }
674
150
 
675
- // src/timelineTracker.ts
676
- var MAX_EVENTS_PER_COMPONENT = 100;
151
+ // src/networkTracker.ts
152
+ import {
153
+ getCurrentRenderingFiber,
154
+ getComponentNameFromFiber,
155
+ buildAncestorChain,
156
+ tagFetchData,
157
+ clearFetchOriginTags
158
+ } from "@flotrace/runtime-core";
159
+ import { findFetchOrigin, hasActiveTags } from "@flotrace/runtime-core";
160
+ var MAX_BATCH_SIZE = 50;
677
161
  var FLUSH_INTERVAL_MS = 500;
678
- var MAX_PENDING_EVENTS = 200;
679
- var timelines = /* @__PURE__ */ new Map();
680
- var pendingEvents = [];
681
- var client = null;
682
- var flushTimer = null;
683
- var isInstalled = false;
684
- function installTimelineTracker(wsClient) {
685
- if (isInstalled) return;
686
- client = wsClient;
687
- isInstalled = true;
688
- flushTimer = setInterval(flushPendingEvents, FLUSH_INTERVAL_MS);
689
- }
690
- function uninstallTimelineTracker() {
691
- if (!isInstalled) return;
692
- if (flushTimer) {
693
- clearInterval(flushTimer);
694
- flushTimer = null;
695
- }
696
- flushPendingEvents();
697
- timelines.clear();
698
- pendingEvents = [];
699
- client = null;
700
- isInstalled = false;
701
- }
702
- function recordTimelineEvent(nodeId, componentName, eventType, detail, duration) {
703
- if (!isInstalled) return;
704
- const event = {
705
- type: eventType,
706
- timestamp: Date.now(),
707
- duration,
708
- detail: detail !== void 0 ? serializeValue(detail, 0, /* @__PURE__ */ new WeakSet()) : void 0
709
- };
710
- let events = timelines.get(nodeId);
711
- if (!events) {
712
- events = [];
713
- timelines.set(nodeId, events);
714
- }
715
- events.push(event);
716
- if (events.length > MAX_EVENTS_PER_COMPONENT) {
717
- events.shift();
718
- }
719
- if (pendingEvents.length < MAX_PENDING_EVENTS) {
720
- pendingEvents.push({ nodeId, componentName, event });
721
- }
722
- }
723
- function getTimeline(nodeId) {
724
- return timelines.get(nodeId) ?? [];
725
- }
726
- function flushPendingEvents() {
727
- if (!client?.connected || pendingEvents.length === 0) return;
728
- for (const { nodeId, componentName, event } of pendingEvents) {
729
- client.send({
730
- type: "runtime:timelineEvent",
731
- nodeId,
732
- componentName,
733
- event
734
- });
735
- }
736
- pendingEvents = [];
737
- }
738
-
739
- // src/fiberUtils.ts
740
- function getFiberDisplayName(type) {
741
- if (!type) return "Unknown";
742
- if (typeof type === "function") {
743
- return type.displayName || type.name || "Anonymous";
744
- }
745
- if (typeof type === "object") {
746
- const t = type;
747
- return t.type?.displayName || t.type?.name || t.render?.name || t.displayName || t.name || "Unknown";
748
- }
749
- return "Unknown";
750
- }
751
-
752
- // src/dispatchWrapper.ts
753
- var MAX_TRIGGERS = 200;
754
- var triggerBuffer = [];
755
- var triggerSeq = 0;
756
- var wrappedDispatchers = /* @__PURE__ */ new WeakSet();
757
- var currentBatchId = null;
758
- function nextBatchId() {
759
- if (!currentBatchId) {
760
- currentBatchId = String(Date.now()) + "-" + (Math.random() * 65535 | 0).toString(16);
761
- queueMicrotask(() => {
762
- currentBatchId = null;
763
- });
764
- }
765
- return currentBatchId;
766
- }
767
- function nextTriggerId() {
768
- return "tr-" + (++triggerSeq).toString(36);
769
- }
770
- var STACK_DEPTH_LIMIT = 15;
771
- var NOISE_PATTERNS = [
772
- "node_modules",
773
- "react-dom",
774
- "react-reconciler",
775
- "@flotrace/runtime",
776
- "flotrace/runtime",
777
- "/runtime/src/",
778
- "webpack-internal",
779
- "webpack/bootstrap",
780
- "<anonymous>"
781
- ];
782
- function isUserCodeFrame(fileName) {
783
- if (!fileName) return false;
784
- for (const pattern of NOISE_PATTERNS) {
785
- if (fileName.includes(pattern)) return false;
786
- }
787
- return true;
788
- }
789
- function captureStack() {
790
- const frames = [];
791
- try {
792
- const originalPrepare = Error.prepareStackTrace;
793
- Error.prepareStackTrace = (_err, callSites) => {
794
- for (const site of callSites) {
795
- if (frames.length >= STACK_DEPTH_LIMIT) break;
796
- const fileName = site.getFileName();
797
- frames.push({
798
- functionName: site.getFunctionName() ?? site.getMethodName(),
799
- fileName,
800
- lineNumber: site.getLineNumber(),
801
- columnNumber: site.getColumnNumber(),
802
- isUserCode: isUserCodeFrame(fileName)
803
- });
804
- }
805
- return "";
806
- };
807
- const err = new Error();
808
- void err.stack;
809
- Error.prepareStackTrace = originalPrepare;
810
- } catch {
811
- try {
812
- const raw = new Error().stack ?? "";
813
- const lines = raw.split("\n").slice(1);
814
- for (const line of lines) {
815
- if (frames.length >= STACK_DEPTH_LIMIT) break;
816
- const match = line.match(/^\s+at (?:(.+?) \()?(.+?):(\d+):(\d+)\)?$/);
817
- if (match) {
818
- const fileName = match[2] ?? null;
819
- frames.push({
820
- functionName: match[1] ?? null,
821
- fileName,
822
- lineNumber: match[3] ? parseInt(match[3], 10) : null,
823
- columnNumber: match[4] ? parseInt(match[4], 10) : null,
824
- isUserCode: isUserCodeFrame(fileName)
825
- });
826
- }
827
- }
828
- } catch {
829
- }
830
- }
831
- return frames;
832
- }
833
- var FIBER_TAG_FUNCTION = 0;
834
- var FIBER_TAG_CLASS = 1;
835
- var FIBER_TAG_FORWARD = 11;
836
- var FIBER_TAG_MEMO = 14;
837
- var FIBER_TAG_SIMPLEMEMO = 15;
838
- function getComponentName(fiber) {
839
- return getFiberDisplayName(fiber.type);
840
- }
841
- function wrapFunctionComponentDispatchers(fiber) {
842
- let hookNode = fiber.memoizedState;
843
- let hookIndex = 0;
844
- while (hookNode && hookIndex < 100) {
845
- try {
846
- const queue = hookNode.queue;
847
- if (queue && typeof queue.dispatch === "function") {
848
- const original = queue.dispatch;
849
- if (!wrappedDispatchers.has(original)) {
850
- const componentName = getComponentName(fiber);
851
- const fiberId = getFiberId(fiber);
852
- const capturedHookIndex = hookIndex;
853
- const hookType = typeof queue.lastRenderedReducer === "function" && queue.lastRenderedReducer?.toString().includes("action") ? "reducer" : "state";
854
- const wrapped = function dispatchWithCapture(action) {
855
- try {
856
- const stack = captureStack();
857
- const record = {
858
- triggerId: nextTriggerId(),
859
- fiberId,
860
- componentName,
861
- hookIndex: capturedHookIndex,
862
- hookType,
863
- stack,
864
- timestamp: performance.now(),
865
- action: serializeValue(action, 2),
866
- batchId: nextBatchId()
867
- };
868
- addTrigger(record);
869
- } catch {
870
- }
871
- return original(action);
872
- };
873
- wrappedDispatchers.add(wrapped);
874
- queue.dispatch = wrapped;
875
- }
876
- }
877
- } catch {
878
- }
879
- hookNode = hookNode.next;
880
- hookIndex++;
881
- }
882
- }
883
- function wrapClassComponentInstance(fiber) {
884
- const instance = fiber.stateNode;
885
- if (!instance || instance.__ftWrapped) return;
886
- const componentName = getComponentName(fiber);
887
- const fiberId = getFiberId(fiber);
888
- if (typeof instance.setState === "function") {
889
- const origSetState = instance.setState;
890
- instance.setState = function wrappedSetState(updater, callback) {
891
- try {
892
- const stack = captureStack();
893
- addTrigger({
894
- triggerId: nextTriggerId(),
895
- fiberId,
896
- componentName,
897
- hookIndex: 0,
898
- hookType: "setState",
899
- stack,
900
- timestamp: performance.now(),
901
- action: serializeValue(updater, 2),
902
- batchId: nextBatchId()
903
- });
904
- } catch {
905
- }
906
- return origSetState.call(this, updater, callback);
907
- };
908
- }
909
- if (typeof instance.forceUpdate === "function") {
910
- const origForceUpdate = instance.forceUpdate;
911
- instance.forceUpdate = function wrappedForceUpdate(callback) {
912
- try {
913
- const stack = captureStack();
914
- addTrigger({
915
- triggerId: nextTriggerId(),
916
- fiberId,
917
- componentName,
918
- hookIndex: 0,
919
- hookType: "forceUpdate",
920
- stack,
921
- timestamp: performance.now(),
922
- action: null,
923
- batchId: nextBatchId()
924
- });
925
- } catch {
926
- }
927
- return origForceUpdate.call(this, callback);
928
- };
929
- }
930
- instance.__ftWrapped = true;
931
- }
932
- var fiberIds = /* @__PURE__ */ new WeakMap();
933
- var fiberIdSeq = 0;
934
- function getFiberId(fiber) {
935
- let id = fiberIds.get(fiber);
936
- if (!id) {
937
- id = getComponentName(fiber) + "-" + (++fiberIdSeq).toString(36);
938
- fiberIds.set(fiber, id);
939
- }
940
- return id;
941
- }
942
- function addTrigger(record) {
943
- if (triggerBuffer.length >= MAX_TRIGGERS) {
944
- triggerBuffer.shift();
945
- }
946
- triggerBuffer.push(record);
947
- }
948
- function wrapFiberDispatchers(root) {
949
- try {
950
- walkAndWrap(root.current);
951
- } catch {
952
- }
953
- }
954
- function walkAndWrap(rootFiber) {
955
- if (!rootFiber) return;
956
- const stack = [rootFiber];
957
- while (stack.length > 0) {
958
- const fiber = stack.pop();
959
- try {
960
- const tag = fiber.tag;
961
- if (tag === FIBER_TAG_FUNCTION || tag === FIBER_TAG_FORWARD || tag === FIBER_TAG_MEMO || tag === FIBER_TAG_SIMPLEMEMO) {
962
- wrapFunctionComponentDispatchers(fiber);
963
- } else if (tag === FIBER_TAG_CLASS) {
964
- wrapClassComponentInstance(fiber);
965
- }
966
- } catch {
967
- }
968
- if (fiber.sibling) stack.push(fiber.sibling);
969
- if (fiber.child) stack.push(fiber.child);
970
- }
971
- }
972
- function peekTriggers() {
973
- return triggerBuffer;
974
- }
975
- function clearTriggers() {
976
- triggerBuffer.length = 0;
977
- }
978
-
979
- // src/laneDetector.ts
980
- var SyncHydrationLane = 1;
981
- var SyncLane = 2;
982
- var InputContinuousHydrationLane = 4;
983
- var InputContinuousLane = 8;
984
- var DefaultHydrationLane = 16;
985
- var DefaultLane = 32;
986
- var TransitionLanes = 4194240;
987
- var RetryLanes = 62914560;
988
- var SelectiveHydrationLane = 67108864;
989
- var IdleHydrationLane = 134217728;
990
- var IdleLane = 268435456;
991
- var OffscreenLane = 536870912;
992
- function classifyLanes(lanes) {
993
- try {
994
- if (lanes & SyncHydrationLane || lanes & SyncLane) {
995
- return { priority: "sync", lanes, isTransition: false, isBlocking: true };
996
- }
997
- if (lanes & InputContinuousHydrationLane || lanes & InputContinuousLane) {
998
- return { priority: "discrete", lanes, isTransition: false, isBlocking: true };
999
- }
1000
- if (lanes & DefaultHydrationLane || lanes & DefaultLane) {
1001
- return { priority: "default", lanes, isTransition: false, isBlocking: false };
1002
- }
1003
- if (lanes & TransitionLanes) {
1004
- return { priority: "transition", lanes, isTransition: true, isBlocking: false };
1005
- }
1006
- if (lanes & RetryLanes || lanes & SelectiveHydrationLane) {
1007
- return { priority: "deferred", lanes, isTransition: false, isBlocking: false };
1008
- }
1009
- if (lanes & IdleHydrationLane || lanes & IdleLane) {
1010
- return { priority: "idle", lanes, isTransition: false, isBlocking: false };
1011
- }
1012
- if (lanes & OffscreenLane) {
1013
- return { priority: "offscreen", lanes, isTransition: false, isBlocking: false };
1014
- }
1015
- } catch {
1016
- }
1017
- return { priority: "default", lanes, isTransition: false, isBlocking: false };
1018
- }
1019
- function getFinishedLanes(root) {
1020
- try {
1021
- return root.finishedLanes ?? root.pendingLanes ?? 0;
1022
- } catch {
1023
- return 0;
1024
- }
1025
- }
1026
-
1027
- // src/cascadeAnalyzer.ts
1028
- var PerformedWork = 1;
1029
- var ForceUpdateFlag = 256;
1030
- var FunctionComponent = 0;
1031
- var ClassComponent = 1;
1032
- var ForwardRef = 11;
1033
- var MemoComponent = 14;
1034
- var SimpleMemoComponent = 15;
1035
- var USER_TAGS = /* @__PURE__ */ new Set([FunctionComponent, ClassComponent, ForwardRef, MemoComponent, SimpleMemoComponent]);
1036
- function isMemoizedFiber(fiber) {
1037
- return fiber.tag === MemoComponent || fiber.tag === SimpleMemoComponent;
1038
- }
1039
- function propsChanged(prev, next) {
1040
- if (prev === next) return false;
1041
- if (!prev || !next) return true;
1042
- const prevKeys = Object.keys(prev);
1043
- const nextKeys = Object.keys(next);
1044
- if (prevKeys.length !== nextKeys.length) return true;
1045
- for (const key of nextKeys) {
1046
- if (key === "children") continue;
1047
- if (prev[key] !== next[key]) return true;
1048
- }
1049
- return false;
1050
- }
1051
- function getChangedPropKeys(prev, next) {
1052
- if (!prev || !next) return [];
1053
- const changed = [];
1054
- const allKeys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
1055
- for (const key of allKeys) {
1056
- if (key === "children") continue;
1057
- if (prev[key] !== next[key]) changed.push(key);
1058
- }
1059
- return changed;
1060
- }
1061
- function hadOwnUpdate(fiber) {
1062
- try {
1063
- const uq = fiber.updateQueue;
1064
- if (!uq) return false;
1065
- if (uq.shared && uq.shared.pending != null) return true;
1066
- if (fiber.lanes !== 0) return true;
1067
- return false;
1068
- } catch {
1069
- return false;
1070
- }
1071
- }
1072
- function hadContextUpdate(fiber) {
1073
- try {
1074
- return !!fiber.dependencies?.firstContext;
1075
- } catch {
1076
- return false;
1077
- }
1078
- }
1079
- function classifyFiber(fiber, didRender, parentRerendered) {
1080
- if (!didRender) {
1081
- if (fiber.alternate && isMemoizedFiber(fiber)) return "bailed-out";
1082
- return null;
1083
- }
1084
- if (fiber.flags & ForceUpdateFlag) return "force-update";
1085
- if (hadContextUpdate(fiber)) return "context-update";
1086
- if (hadOwnUpdate(fiber)) return "state-update";
1087
- if (parentRerendered) {
1088
- const alt = fiber.alternate;
1089
- if (alt && propsChanged(alt.memoizedProps, fiber.memoizedProps)) {
1090
- return "props-changed";
1091
- }
1092
- return "parent-cascade";
1093
- }
1094
- return "state-update";
1095
- }
1096
- function computeSubtreeDuration(node) {
1097
- let total = node.renderDuration;
1098
- for (const child of node.children) {
1099
- total += computeSubtreeDuration(child);
1100
- }
1101
- node.subtreeDuration = total;
1102
- return total;
1103
- }
1104
- var commitIdSeq = 0;
1105
- function nextCommitId() {
1106
- return "c-" + (++commitIdSeq).toString(36) + "-" + (Date.now() % 1e5).toString(36);
1107
- }
1108
- function buildCascadeTree(rootFiber, triggers) {
1109
- const rootCauses = [];
1110
- let totalComponents = 0;
1111
- let avoidableCount = 0;
1112
- let avoidableDuration = 0;
1113
- const triggerByName = /* @__PURE__ */ new Map();
1114
- for (const t of triggers) {
1115
- if (!triggerByName.has(t.componentName)) {
1116
- triggerByName.set(t.componentName, t);
1117
- }
1118
- }
1119
- const stack = [{
1120
- fiber: rootFiber,
1121
- depth: 0,
1122
- parentRerendered: false,
1123
- parentNode: null,
1124
- isRoot: true
1125
- }];
1126
- while (stack.length > 0) {
1127
- const entry = stack.pop();
1128
- const { fiber, depth, parentRerendered, parentNode, isRoot } = entry;
1129
- if (!fiber) continue;
1130
- if (depth > 150) continue;
1131
- const didRender = !!(fiber.flags & PerformedWork);
1132
- const isNewMount = !fiber.alternate;
1133
- if (isNewMount && !didRender) {
1134
- let child2 = fiber.child;
1135
- while (child2) {
1136
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: false, parentNode, isRoot: false });
1137
- child2 = child2.sibling;
1138
- }
1139
- continue;
1140
- }
1141
- if (!USER_TAGS.has(fiber.tag)) {
1142
- let child2 = fiber.child;
1143
- while (child2) {
1144
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: didRender || parentRerendered, parentNode, isRoot: false });
1145
- child2 = child2.sibling;
1146
- }
1147
- continue;
1148
- }
1149
- const reason = classifyFiber(fiber, didRender, parentRerendered);
1150
- if (reason === null) {
1151
- let child2 = fiber.child;
1152
- while (child2) {
1153
- stack.push({ fiber: child2, depth: depth + 1, parentRerendered: false, parentNode, isRoot: false });
1154
- child2 = child2.sibling;
1155
- }
1156
- continue;
1157
- }
1158
- const componentName = getFiberDisplayName(fiber.type);
1159
- const renderDuration = fiber.actualDuration ?? 0;
1160
- let changedProps;
1161
- if (reason === "props-changed" && fiber.alternate) {
1162
- changedProps = getChangedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps);
1163
- }
1164
- let triggerId;
1165
- if (reason === "state-update" || reason === "context-update" || reason === "force-update") {
1166
- triggerId = triggerByName.get(componentName)?.triggerId;
1167
- }
1168
- const node = {
1169
- nodeId: componentName + "-" + depth + "-" + totalComponents,
1170
- componentName,
1171
- reason,
1172
- renderDuration,
1173
- subtreeDuration: renderDuration,
1174
- // will be updated from children
1175
- changedProps,
1176
- triggerId,
1177
- children: [],
1178
- depth,
1179
- isMemoized: isMemoizedFiber(fiber)
1180
- };
1181
- totalComponents++;
1182
- if (reason === "parent-cascade") {
1183
- avoidableCount++;
1184
- avoidableDuration += renderDuration;
1185
- }
1186
- if (parentNode) {
1187
- parentNode.children.push(node);
1188
- } else if (reason === "state-update" || reason === "context-update" || reason === "force-update" || isRoot) {
1189
- rootCauses.push(node);
1190
- } else if (parentRerendered) {
1191
- rootCauses.push(node);
1192
- }
1193
- let child = fiber.child;
1194
- while (child) {
1195
- stack.push({
1196
- fiber: child,
1197
- depth: depth + 1,
1198
- parentRerendered: didRender,
1199
- parentNode: reason !== "bailed-out" ? node : parentNode,
1200
- isRoot: false
1201
- });
1202
- child = child.sibling;
1203
- }
1204
- }
1205
- for (const root of rootCauses) computeSubtreeDuration(root);
1206
- return { rootCauses, totalComponents, avoidableCount, avoidableDuration };
1207
- }
1208
- function analyzeCascade(root, triggers) {
1209
- try {
1210
- const finishedLanes = getFinishedLanes(root);
1211
- const lane = classifyLanes(finishedLanes);
1212
- const { rootCauses, totalComponents, avoidableCount, avoidableDuration } = buildCascadeTree(root.current, triggers);
1213
- if (totalComponents === 0) return null;
1214
- const totalDuration = rootCauses.reduce((sum, n) => sum + n.subtreeDuration, 0);
1215
- const triggerIds = triggers.map((t) => t.triggerId);
1216
- return {
1217
- commitId: nextCommitId(),
1218
- timestamp: performance.now(),
1219
- totalDuration,
1220
- totalComponents,
1221
- avoidableCount,
1222
- avoidableDuration,
1223
- rootCauses,
1224
- lane,
1225
- triggerIds
1226
- };
1227
- } catch {
1228
- return null;
1229
- }
1230
- }
1231
-
1232
- // src/propDrillingAnalyzer.ts
1233
- var ANALYZE_INTERVAL_MS = 2e3;
1234
- var DRILLING_THRESHOLD = 3;
1235
- var EXCLUDED_PROP_NAMES = /* @__PURE__ */ new Set([
1236
- // React internals
1237
- "children",
1238
- "key",
1239
- "ref",
1240
- // Common HTML attributes
1241
- "className",
1242
- "style",
1243
- "id",
1244
- "name",
1245
- "type",
1246
- "value",
1247
- "placeholder",
1248
- "disabled",
1249
- "readOnly",
1250
- "required",
1251
- "autoFocus",
1252
- "tabIndex",
1253
- "role",
1254
- "aria-label",
1255
- "aria-describedby",
1256
- "aria-hidden",
1257
- "title",
1258
- "lang",
1259
- "dir",
1260
- "hidden",
1261
- // Common layout props
1262
- "width",
1263
- "height",
1264
- "size",
1265
- "variant",
1266
- "color",
1267
- "theme",
1268
- // Test IDs
1269
- "data-testid",
1270
- "testID"
1271
- ]);
1272
- function isExcluded(propName) {
1273
- return EXCLUDED_PROP_NAMES.has(propName) || propName.startsWith("on");
1274
- }
1275
- var analyzeTimer = null;
1276
- var lastAnalysisTime = 0;
1277
- function valueFingerprint(value, depth = 0) {
1278
- if (depth > 3) return "__deep__";
1279
- if (value === null || value === void 0) return "null";
1280
- if (typeof value === "function") return `fn:${value.name || "anon"}`;
1281
- if (typeof value !== "object") return `${typeof value}:${String(value)}`;
1282
- if (Array.isArray(value)) {
1283
- const arr = value;
1284
- return `arr:${arr.length}:${arr.slice(0, 5).map((v) => valueFingerprint(v, depth + 1)).join(",")}`;
1285
- }
1286
- const obj = value;
1287
- const keys = Object.keys(obj).sort();
1288
- return `obj:${keys.slice(0, 10).map((k) => `${k}=${valueFingerprint(obj[k], depth + 1)}`).join(",")}`;
1289
- }
1290
- function shouldFlagRename(value) {
1291
- if (value === null || value === void 0) return false;
1292
- if (typeof value !== "object") return false;
1293
- if (Array.isArray(value) && value.length === 0) return false;
1294
- if (!Array.isArray(value) && Object.keys(value).length === 0) return false;
1295
- return true;
1296
- }
1297
- function computePropIntersectionRatio(nodeProps, childrenProps) {
1298
- const nodeKeys = Object.keys(nodeProps).filter((k) => !isExcluded(k));
1299
- if (nodeKeys.length === 0) return 0;
1300
- let forwarded = 0;
1301
- for (const key of nodeKeys) {
1302
- const fp = valueFingerprint(nodeProps[key]);
1303
- const isForwarded = childrenProps.some(
1304
- (cp) => Object.values(cp).some((v) => valueFingerprint(v) === fp)
1305
- );
1306
- if (isForwarded) forwarded++;
1307
- }
1308
- return forwarded / nodeKeys.length;
1309
- }
1310
- function classifyNode(nodeId, drilledPropFp, parentNodeId, childNodeIds, getProps, hookCounts, contextFlags) {
1311
- if (!parentNodeId) return "source";
1312
- const parentProps = getProps(parentNodeId);
1313
- const parentHasProp = Object.values(parentProps).some(
1314
- (v) => valueFingerprint(v) === drilledPropFp
1315
- );
1316
- if (!parentHasProp) return "source";
1317
- const forwardsToChild = childNodeIds.some((cid) => {
1318
- const childProps = getProps(cid);
1319
- return Object.values(childProps).some((v) => valueFingerprint(v) === drilledPropFp);
1320
- });
1321
- if (!forwardsToChild) return "consumer";
1322
- const hookCount = hookCounts.get(nodeId) ?? 0;
1323
- const hasContext = contextFlags.get(nodeId) ?? false;
1324
- if (hookCount === 0) return "passthrough";
1325
- if (hasContext) return "consumer";
1326
- const nodeProps = getProps(nodeId);
1327
- const childrenProps = childNodeIds.map(getProps);
1328
- const intersectionRatio = computePropIntersectionRatio(nodeProps, childrenProps);
1329
- if (intersectionRatio > 0.7 && hookCount <= 1) return "passthrough";
1330
- return "consumer";
1331
- }
1332
- function calculateSeverity(depth, passthroughCount, consumerCount) {
1333
- if (depth >= 5) return "critical";
1334
- if (passthroughCount >= 3) return "critical";
1335
- if (consumerCount >= 3 && depth >= 4) return "critical";
1336
- if (depth >= 4) return "warning";
1337
- if (passthroughCount >= 2) return "warning";
1338
- if (consumerCount >= 2) return "warning";
1339
- return "info";
1340
- }
1341
- function makeChainId(sourceNodeId, fp, consumerNodeId) {
1342
- return `${sourceNodeId}::${fp.slice(0, 20)}::${consumerNodeId}`;
1343
- }
1344
- function flattenTree(node, parentId, parentMap, childrenMap, nodeMap) {
1345
- if (node.isFramework) {
1346
- for (const child of node.children) {
1347
- flattenTree(child, parentId, parentMap, childrenMap, nodeMap);
1348
- }
1349
- return;
1350
- }
1351
- nodeMap.set(node.id, node);
1352
- if (parentId) {
1353
- parentMap.set(node.id, parentId);
1354
- const siblings = childrenMap.get(parentId) ?? [];
1355
- siblings.push(node.id);
1356
- childrenMap.set(parentId, siblings);
1357
- }
1358
- if (!childrenMap.has(node.id)) {
1359
- childrenMap.set(node.id, []);
1360
- }
1361
- for (const child of node.children) {
1362
- flattenTree(child, node.id, parentMap, childrenMap, nodeMap);
1363
- }
1364
- }
1365
- function runAnalysis(tree, fiberRefMap2) {
1366
- const parentMap = /* @__PURE__ */ new Map();
1367
- const childrenMap = /* @__PURE__ */ new Map();
1368
- const nodeMap = /* @__PURE__ */ new Map();
1369
- flattenTree(tree, void 0, parentMap, childrenMap, nodeMap);
1370
- const allNodeIds = Array.from(nodeMap.keys());
1371
- function getProps(nodeId) {
1372
- try {
1373
- return fiberRefMap2.get(nodeId)?.memoizedProps ?? {};
1374
- } catch {
1375
- return {};
1376
- }
1377
- }
1378
- const hookCounts = /* @__PURE__ */ new Map();
1379
- const contextFlags = /* @__PURE__ */ new Map();
1380
- for (const nodeId of allNodeIds) {
1381
- const node = nodeMap.get(nodeId);
1382
- hookCounts.set(nodeId, node.hookCount ?? 0);
1383
- contextFlags.set(nodeId, node.hasContextHook ?? false);
1384
- }
1385
- const edges = [];
1386
- for (const nodeId of allNodeIds) {
1387
- const parentId = parentMap.get(nodeId);
1388
- if (!parentId) continue;
1389
- const parentProps = getProps(parentId);
1390
- const childProps = getProps(nodeId);
1391
- const childKeys = Object.keys(childProps).filter((k) => !isExcluded(k));
1392
- const parentKeys = Object.keys(parentProps).filter((k) => !isExcluded(k));
1393
- for (const childKey of childKeys) {
1394
- const childVal = childProps[childKey];
1395
- if (typeof childVal === "function") continue;
1396
- const childFp = valueFingerprint(childVal);
1397
- if (childFp === "null") continue;
1398
- for (const parentKey of parentKeys) {
1399
- const parentVal = parentProps[parentKey];
1400
- if (typeof parentVal === "function") continue;
1401
- const parentFp = valueFingerprint(parentVal);
1402
- if (parentFp === childFp) {
1403
- const isRename = parentKey !== childKey;
1404
- if (!isRename || shouldFlagRename(parentVal)) {
1405
- edges.push({
1406
- parentNodeId: parentId,
1407
- childNodeId: nodeId,
1408
- propKey: parentKey,
1409
- childPropKey: childKey,
1410
- fp: childFp
1411
- });
1412
- break;
1413
- }
1414
- }
1415
- }
1416
- }
1417
- }
1418
- const edgesByFp = /* @__PURE__ */ new Map();
1419
- for (const edge of edges) {
1420
- const group = edgesByFp.get(edge.fp) ?? [];
1421
- group.push(edge);
1422
- edgesByFp.set(edge.fp, group);
1423
- }
1424
- const chains = [];
1425
- const passthroughNodeIdSet = /* @__PURE__ */ new Set();
1426
- for (const [fp, fpEdges] of edgesByFp) {
1427
- const outEdges = /* @__PURE__ */ new Map();
1428
- const inNodes = /* @__PURE__ */ new Set();
1429
- for (const edge of fpEdges) {
1430
- const out = outEdges.get(edge.parentNodeId) ?? [];
1431
- out.push(edge);
1432
- outEdges.set(edge.parentNodeId, out);
1433
- inNodes.add(edge.childNodeId);
1434
- }
1435
- const sourceNodeIds = /* @__PURE__ */ new Set();
1436
- for (const edge of fpEdges) {
1437
- if (!inNodes.has(edge.parentNodeId)) {
1438
- sourceNodeIds.add(edge.parentNodeId);
1439
- }
1440
- }
1441
- for (const sourceId of sourceNodeIds) {
1442
- let dfs2 = function(currentId, currentPropKey, currentPath, visited) {
1443
- if (visited.has(currentId)) return;
1444
- visited.add(currentId);
1445
- const outgoing = outEdges.get(currentId);
1446
- if (!outgoing || outgoing.length === 0) {
1447
- if (currentPath.length >= DRILLING_THRESHOLD) {
1448
- allPaths.push([...currentPath]);
1449
- }
1450
- visited.delete(currentId);
1451
- return;
1452
- }
1453
- for (const edge of outgoing) {
1454
- const isRename = edge.propKey !== edge.childPropKey;
1455
- dfs2(
1456
- edge.childNodeId,
1457
- edge.childPropKey,
1458
- [...currentPath, { nodeId: edge.childNodeId, propKey: edge.childPropKey, isRename }],
1459
- new Set(visited)
1460
- );
1461
- }
1462
- visited.delete(currentId);
1463
- };
1464
- var dfs = dfs2;
1465
- const firstEdge = outEdges.get(sourceId)?.[0];
1466
- if (!firstEdge) continue;
1467
- const sourcePropName = firstEdge.propKey;
1468
- const allPaths = [];
1469
- dfs2(
1470
- sourceId,
1471
- sourcePropName,
1472
- [{ nodeId: sourceId, propKey: sourcePropName, isRename: false }],
1473
- /* @__PURE__ */ new Set()
1474
- );
1475
- if (allPaths.length === 0) continue;
1476
- for (const path of allPaths) {
1477
- if (path.length < DRILLING_THRESHOLD) continue;
1478
- const consumerNodeId = path[path.length - 1].nodeId;
1479
- const consumerNode = nodeMap.get(consumerNodeId);
1480
- if (!consumerNode) continue;
1481
- const chainNodes = path.map((p, i) => {
1482
- const parentIdForNode = i === 0 ? void 0 : path[i - 1].nodeId;
1483
- const childNodeIds = i < path.length - 1 ? [path[i + 1].nodeId] : [];
1484
- const role = classifyNode(
1485
- p.nodeId,
1486
- fp,
1487
- parentIdForNode,
1488
- childNodeIds,
1489
- getProps,
1490
- hookCounts,
1491
- contextFlags
1492
- );
1493
- if (role === "passthrough") {
1494
- passthroughNodeIdSet.add(p.nodeId);
1495
- }
1496
- const n = nodeMap.get(p.nodeId);
1497
- return {
1498
- nodeId: p.nodeId,
1499
- componentName: n?.name ?? p.nodeId,
1500
- propKey: p.propKey,
1501
- role,
1502
- hookCount: hookCounts.get(p.nodeId) ?? 0,
1503
- hasContextHook: contextFlags.get(p.nodeId) ?? false
1504
- };
1505
- });
1506
- const passthroughCount = chainNodes.filter((n) => n.role === "passthrough").length;
1507
- const sourceNode = nodeMap.get(sourceId);
1508
- const renames = path.flatMap(
1509
- (p, idx) => p.isRename ? [{ atNodeId: p.nodeId, fromKey: idx > 0 ? path[idx - 1].propKey : sourcePropName, toKey: p.propKey }] : []
1510
- );
1511
- chains.push({
1512
- chainId: makeChainId(sourceId, fp, consumerNodeId),
1513
- propName: sourcePropName,
1514
- sourceNodeId: sourceId,
1515
- sourceComponentName: sourceNode?.name ?? sourceId,
1516
- consumerNodeIds: [consumerNodeId],
1517
- consumerComponentNames: [consumerNode.name],
1518
- path: chainNodes,
1519
- depth: path.length,
1520
- passthroughCount,
1521
- severity: calculateSeverity(path.length, passthroughCount, 1),
1522
- renames
1523
- });
1524
- }
1525
- }
1526
- }
1527
- const seen = /* @__PURE__ */ new Set();
1528
- const dedupedChains = chains.filter((c) => {
1529
- if (seen.has(c.chainId)) return false;
1530
- seen.add(c.chainId);
1531
- return true;
1532
- });
1533
- const severityOrder = { critical: 0, warning: 1, info: 2 };
1534
- dedupedChains.sort((a, b) => {
1535
- const s = severityOrder[a.severity] - severityOrder[b.severity];
1536
- if (s !== 0) return s;
1537
- return b.depth - a.depth;
1538
- });
1539
- return {
1540
- chains: dedupedChains.slice(0, 50),
1541
- // cap at 50 chains
1542
- passthroughNodeIds: Array.from(passthroughNodeIdSet)
1543
- };
1544
- }
1545
- function schedulePropDrillingAnalysis(tree, fiberRefMap2, client4) {
1546
- if (analyzeTimer) clearTimeout(analyzeTimer);
1547
- const now = Date.now();
1548
- const elapsed = now - lastAnalysisTime;
1549
- const delay = elapsed >= ANALYZE_INTERVAL_MS ? 0 : ANALYZE_INTERVAL_MS - elapsed;
1550
- analyzeTimer = setTimeout(() => {
1551
- analyzeTimer = null;
1552
- if (!client4.connected) return;
1553
- try {
1554
- lastAnalysisTime = Date.now();
1555
- const { chains, passthroughNodeIds } = runAnalysis(tree, fiberRefMap2);
1556
- client4.sendImmediate({
1557
- type: "runtime:propDrilling",
1558
- payload: {
1559
- chains,
1560
- passthroughNodeIds,
1561
- analysisTimestamp: lastAnalysisTime,
1562
- treeSize: fiberRefMap2.size
1563
- }
1564
- });
1565
- } catch (err) {
1566
- if (typeof console !== "undefined") {
1567
- console.warn("[FloTrace] Prop drilling analysis error:", err);
1568
- }
1569
- }
1570
- }, delay);
1571
- }
1572
-
1573
- // src/compilerAnalyzer.ts
1574
- var MEMO_CACHE_SENTINEL = /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel");
1575
- var FUNCTION_COMPONENT = 0;
1576
- var SIMPLE_MEMO = 15;
1577
- function detectCompilerStatus(fiber) {
1578
- if (fiber.tag === SIMPLE_MEMO) return "manual";
1579
- if (fiber.tag !== FUNCTION_COMPONENT) return void 0;
1580
- const firstHook = fiber.memoizedState;
1581
- if (!firstHook) return "unoptimized";
1582
- const cache = firstHook.memoizedState;
1583
- if (!Array.isArray(cache) || cache.length === 0) return "unoptimized";
1584
- const hasSentinel = cache.some((v) => v === MEMO_CACHE_SENTINEL);
1585
- if (!hasSentinel) return "unoptimized";
1586
- const allSentinel = cache.every((v) => v === MEMO_CACHE_SENTINEL);
1587
- if (allSentinel && fiber.alternate != null) return "de-opted";
1588
- return "compiled";
1589
- }
1590
-
1591
- // src/nextjsDetector.ts
1592
- var SERVER_COMPONENT_PATTERNS = [
1593
- /\.server\.[jt]sx?$/,
1594
- // explicit .server.tsx convention
1595
- /[\\/]app[\\/].+[\\/]page\.[jt]sx?$/,
1596
- // Next.js app router page
1597
- /[\\/]app[\\/].+[\\/]layout\.[jt]sx?$/,
1598
- // Next.js app router layout
1599
- /[\\/]app[\\/].+[\\/]loading\.[jt]sx?$/,
1600
- // Next.js loading UI
1601
- /[\\/]app[\\/].+[\\/]error\.[jt]sx?$/
1602
- // Next.js error UI
1603
- ];
1604
- var SERVER_REFERENCE_PATTERNS = [
1605
- /_ServerReference$/,
1606
- /^RSC_/
1607
- ];
1608
- var detectionEmitted = false;
1609
- function maybeEmitNextjsContext(client4) {
1610
- if (detectionEmitted) return;
1611
- try {
1612
- const win = globalThis;
1613
- const hasNextData = "__NEXT_DATA__" in win;
1614
- const hasNextRouter = "__next_router_state_tree__" in win;
1615
- const hasNext = "next" in win && win.next !== null;
1616
- if (!hasNextData && !hasNextRouter && !hasNext) return;
1617
- detectionEmitted = true;
1618
- let version;
1619
- let isAppRouter = false;
1620
- let initialRoute;
1621
- try {
1622
- const nextData = win.__NEXT_DATA__;
1623
- if (nextData) {
1624
- version = typeof nextData.buildId === "string" ? nextData.buildId : void 0;
1625
- initialRoute = typeof nextData.page === "string" ? nextData.page : void 0;
1626
- }
1627
- isAppRouter = hasNextRouter || !!win.__next_router_state_tree__;
1628
- } catch {
1629
- }
1630
- client4.sendImmediate({
1631
- type: "runtime:nextjsContext",
1632
- detected: true,
1633
- version,
1634
- isAppRouter,
1635
- initialRoute,
1636
- timestamp: Date.now()
1637
- });
1638
- } catch {
1639
- }
1640
- }
1641
- function detectServerComponent(fiber) {
1642
- const type = fiber.type;
1643
- if (type) {
1644
- const name = type.displayName || type.name || "";
1645
- if (SERVER_REFERENCE_PATTERNS.some((p) => p.test(name))) return true;
1646
- }
1647
- const fileName = fiber._debugSource?.fileName;
1648
- if (fileName) {
1649
- if (SERVER_COMPONENT_PATTERNS.some((p) => p.test(fileName))) return true;
1650
- }
1651
- return false;
1652
- }
1653
- function resetNextjsDetection() {
1654
- detectionEmitted = false;
1655
- }
1656
-
1657
- // src/actionStateTracker.ts
1658
- var prevActionStateMap = /* @__PURE__ */ new Map();
1659
- var ACTION_STATE_HOOK_NAMES = /* @__PURE__ */ new Set(["useActionState"]);
1660
- var OPTIMISTIC_HOOK_NAMES = /* @__PURE__ */ new Set(["useOptimistic"]);
1661
- function extractActionEntries(fiber) {
1662
- const hookTypes = fiber._debugHookTypes;
1663
- if (!hookTypes) return null;
1664
- const entries = [];
1665
- let hookState = fiber.memoizedState;
1666
- let hookIdx = 0;
1667
- for (const hookType of hookTypes) {
1668
- if (!hookState) break;
1669
- if (ACTION_STATE_HOOK_NAMES.has(hookType)) {
1670
- const ms = hookState.memoizedState;
1671
- if (Array.isArray(ms) && ms.length >= 3) {
1672
- entries.push({
1673
- hookIndex: hookIdx,
1674
- hookKind: "action",
1675
- isPending: ms[2] === true,
1676
- state: serializeValue(ms[0])
1677
- });
1678
- }
1679
- } else if (OPTIMISTIC_HOOK_NAMES.has(hookType)) {
1680
- const ms = hookState.memoizedState;
1681
- if (Array.isArray(ms)) {
1682
- entries.push({
1683
- hookIndex: hookIdx,
1684
- hookKind: "optimistic",
1685
- isPending: false,
1686
- // optimistic values are "immediately applied"
1687
- state: serializeValue(ms[0])
1688
- });
1689
- }
1690
- }
1691
- hookState = hookState.next;
1692
- hookIdx++;
1693
- }
1694
- return entries.length > 0 ? entries : null;
1695
- }
1696
- function scanActionStateChanges(fiberRefMap2, client4) {
1697
- try {
1698
- for (const [nodeId, fiber] of fiberRefMap2) {
1699
- const entries = extractActionEntries(fiber);
1700
- if (!entries) continue;
1701
- const snapshot = JSON.stringify(entries.map((e) => ({ i: e.hookIndex, p: e.isPending, s: e.state })));
1702
- if (prevActionStateMap.get(nodeId) === snapshot) continue;
1703
- prevActionStateMap.set(nodeId, snapshot);
1704
- const componentName = nodeId.split("/").pop()?.replace(/-\d+$/, "") ?? "Unknown";
1705
- client4.send({
1706
- type: "runtime:actionState",
1707
- nodeId,
1708
- componentName,
1709
- actions: entries,
1710
- timestamp: Date.now()
1711
- });
1712
- }
1713
- } catch {
1714
- }
1715
- }
1716
- function clearActionStateCache() {
1717
- prevActionStateMap.clear();
1718
- }
1719
-
1720
- // src/rscPayloadInterceptor.ts
1721
- var RSC_URL_PATTERNS = [
1722
- /\?_rsc=/,
1723
- // App Router RSC param
1724
- /\?__RSC__=/,
1725
- // Older Next.js RSC param
1726
- /\/_next\/data\//,
1727
- // Pages Router getServerSideProps / getStaticProps
1728
- /\/__nextjs_original-stack-frame/
1729
- ];
1730
- function parseCacheStatus(headers) {
1731
- const raw = headers.get("x-nextjs-cache") || headers.get("x-vercel-cache") || "";
1732
- switch (raw.toUpperCase()) {
1733
- case "HIT":
1734
- return "HIT";
1735
- case "MISS":
1736
- return "MISS";
1737
- case "STALE":
1738
- return "STALE";
1739
- default:
1740
- return "unknown";
1741
- }
1742
- }
1743
- function extractRoute(url) {
1744
- try {
1745
- const u = new URL(url, globalThis.location?.href ?? "http://localhost");
1746
- return u.pathname;
1747
- } catch {
1748
- return url.split("?")[0] ?? url;
1749
- }
1750
- }
1751
- var originalFetch = null;
1752
- var interceptorClient = null;
1753
- var isInstalled2 = false;
1754
- function installRscPayloadInterceptor(client4) {
1755
- if (isInstalled2 || typeof globalThis.fetch !== "function") return;
1756
- isInstalled2 = true;
1757
- interceptorClient = client4;
1758
- originalFetch = globalThis.fetch;
1759
- globalThis.fetch = async function patchedFetch(input, init) {
1760
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1761
- const isRscRequest = RSC_URL_PATTERNS.some((p) => p.test(url));
1762
- const response = await originalFetch.call(globalThis, input, init);
1763
- if (isRscRequest && interceptorClient?.connected) {
1764
- try {
1765
- const sizeHeader = response.headers.get("content-length");
1766
- const payloadSizeBytes = sizeHeader ? parseInt(sizeHeader, 10) : 0;
1767
- interceptorClient.send({
1768
- type: "runtime:rscPayload",
1769
- route: extractRoute(url),
1770
- payloadSizeBytes: isNaN(payloadSizeBytes) ? 0 : payloadSizeBytes,
1771
- cacheStatus: parseCacheStatus(response.headers),
1772
- timestamp: Date.now()
1773
- });
1774
- } catch {
1775
- }
1776
- }
1777
- return response;
1778
- };
1779
- }
1780
- function uninstallRscPayloadInterceptor() {
1781
- if (!isInstalled2 || !originalFetch) return;
1782
- globalThis.fetch = originalFetch;
1783
- originalFetch = null;
1784
- interceptorClient = null;
1785
- isInstalled2 = false;
1786
- }
1787
-
1788
- // src/fiberAttribution.ts
1789
- function isFiberLike(val) {
1790
- if (!val || typeof val !== "object") return false;
1791
- const obj = val;
1792
- return typeof obj.tag === "number" && "type" in obj && "return" in obj && ("memoizedState" in obj || "stateNode" in obj);
1793
- }
1794
- function getCurrentRenderingFiber() {
1795
- try {
1796
- const win = window;
1797
- const secret = win.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
1798
- if (secret?.ReactCurrentOwner?.current) return secret.ReactCurrentOwner.current;
1799
- const client4 = win.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1800
- if (client4) {
1801
- for (const val of Object.values(client4)) {
1802
- if (isFiberLike(val)) return val;
1803
- }
1804
- }
1805
- return null;
1806
- } catch {
1807
- return null;
1808
- }
1809
- }
1810
- function getComponentNameFromFiber(fiber) {
1811
- const type = fiber.type;
1812
- if (!type) return null;
1813
- if (typeof type === "function") {
1814
- return type.displayName || type.name || null;
1815
- }
1816
- if (typeof type === "object" && type !== null) {
1817
- if (type.type) {
1818
- return type.type.displayName || type.type.name || null;
1819
- }
1820
- if (type.render) {
1821
- return type.render.displayName || type.render.name || null;
1822
- }
1823
- return type.displayName || type.name || null;
1824
- }
1825
- return null;
1826
- }
1827
- function buildAncestorChain(fiber) {
1828
- const chain = [];
1829
- let current = fiber;
1830
- const maxDepth = 10;
1831
- while (current && chain.length < maxDepth) {
1832
- const name = getComponentNameFromFiber(current);
1833
- if (name) {
1834
- chain.unshift(name);
1835
- }
1836
- current = current.return;
1837
- }
1838
- return chain;
1839
- }
1840
-
1841
- // src/networkTracker.ts
1842
- var MAX_BATCH_SIZE = 50;
1843
- var FLUSH_INTERVAL_MS2 = 500;
1844
- var MAX_BUFFER_SIZE = 300;
1845
- var DEDUPE_WINDOW_MS = 5e3;
1846
- var MAX_ANCESTOR_CHAIN = 3;
1847
- var NOISE_URL_PATTERNS = [
1848
- // Analytics & tracking
1849
- /google-analytics\.com/i,
1850
- /googletagmanager\.com/i,
1851
- /facebook\.com\/tr/i,
1852
- /segment\.io/i,
1853
- /mixpanel\.com/i,
1854
- /amplitude\.com/i,
1855
- /hotjar\.com/i,
1856
- /fullstory\.com/i,
1857
- /sentry\.io/i,
1858
- /bugsnag\.com/i,
1859
- /datadog/i,
1860
- /clarity\.ms/i,
1861
- /plausible\.io/i,
1862
- // Development tools
1863
- /webpack-dev-server/i,
1864
- /__webpack_hmr/i,
1865
- /\.hot-update\./i,
1866
- /\.map$/,
1867
- /sourcemap/i,
1868
- /__nextjs_original-stack-frame/i,
1869
- /__nextjs_launch-editor/i,
1870
- /on-demand-entries-ping/i,
1871
- // Browser resources
1872
- /favicon\.ico/i,
1873
- /robots\.txt/i,
1874
- /manifest\.json/i,
1875
- /service-worker/i,
1876
- /sw\.js/i,
1877
- // Static assets
1878
- /\/_next\/static\//i,
1879
- /\/_next\/image/i,
1880
- // FloTrace's own WebSocket
1881
- /127\.0\.0\.1:3457/,
1882
- // Chrome extensions
1883
- /chrome-extension:/i,
1884
- /moz-extension:/i
1885
- ];
1886
- var client2 = null;
1887
- var isInstalled3 = false;
1888
- var isPrewarmed = false;
1889
- var buffer = [];
1890
- var earlyBuffer = [];
1891
- var flushTimer2 = null;
1892
- var requestCounter = 0;
1893
- var requestIndexMap = /* @__PURE__ */ new Map();
1894
- var earlyRequestIndexMap = /* @__PURE__ */ new Map();
1895
- var previousFetch = null;
1896
- var originalXhrOpen = null;
1897
- var originalXhrSend = null;
1898
- var originalResponseJson = null;
1899
- var originalJsonParse = null;
1900
- var responseToRequestId = /* @__PURE__ */ new WeakMap();
1901
- var activeXhrRequestId = null;
1902
- var activeXhrResponseText = null;
1903
- var dedupeWindow = /* @__PURE__ */ new Map();
1904
- var fetchDataOrigin = /* @__PURE__ */ new WeakMap();
1905
- var requestTagTimestamps = /* @__PURE__ */ new Map();
1906
- var FETCH_ORIGIN_TTL_MS = 3e3;
1907
- function tagFetchData(obj, requestId, depth = 0) {
1908
- if (depth > 2 || obj === null || typeof obj !== "object") return;
1909
- fetchDataOrigin.set(obj, requestId);
1910
- if (depth === 0) requestTagTimestamps.set(requestId, Date.now());
1911
- if (Array.isArray(obj)) {
1912
- for (let i = 0; i < Math.min(obj.length, 50); i++) tagFetchData(obj[i], requestId, depth + 1);
1913
- } else {
1914
- for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
1915
- }
1916
- }
1917
- function hasActiveTags() {
1918
- return requestTagTimestamps.size > 0;
1919
- }
1920
- function findFetchOrigin(obj, depth = 0) {
1921
- if (depth > 2 || obj === null || typeof obj !== "object") return void 0;
1922
- const rid = fetchDataOrigin.get(obj);
1923
- if (rid) {
1924
- const tagTime = requestTagTimestamps.get(rid);
1925
- if (tagTime && Date.now() - tagTime <= FETCH_ORIGIN_TTL_MS) return rid;
1926
- requestTagTimestamps.delete(rid);
1927
- }
1928
- if (Array.isArray(obj)) {
1929
- for (let i = 0; i < Math.min(obj.length, 20); i++) {
1930
- const found = findFetchOrigin(obj[i], depth + 1);
1931
- if (found) return found;
1932
- }
1933
- } else {
1934
- for (const val of Object.values(obj)) {
1935
- const found = findFetchOrigin(val, depth + 1);
1936
- if (found) return found;
1937
- }
1938
- }
1939
- return void 0;
1940
- }
1941
- function installPatches() {
1942
- patchFetch();
1943
- patchXhr();
1944
- patchResponseJson();
1945
- patchJsonParse();
1946
- }
1947
- function prewarmNetworkTracker() {
1948
- if (isInstalled3 || isPrewarmed) return;
1949
- isPrewarmed = true;
1950
- installPatches();
1951
- }
1952
- function installNetworkTracker(wsClient) {
1953
- if (isInstalled3) return;
1954
- client2 = wsClient;
1955
- isInstalled3 = true;
1956
- if (!isPrewarmed) {
1957
- requestCounter = 0;
1958
- installPatches();
1959
- } else {
1960
- isPrewarmed = false;
1961
- if (earlyBuffer.length > 0) {
1962
- buffer = [...earlyBuffer, ...buffer];
1963
- rebuildRequestIndex();
1964
- earlyBuffer = [];
1965
- earlyRequestIndexMap.clear();
1966
- }
1967
- }
1968
- flushTimer2 = setInterval(flushBuffer, FLUSH_INTERVAL_MS2);
1969
- flushBuffer();
1970
- }
1971
- function uninstallNetworkTracker() {
1972
- if (!isInstalled3 && !isPrewarmed) return;
1973
- if (previousFetch) {
1974
- globalThis.fetch = previousFetch;
1975
- previousFetch = null;
1976
- }
1977
- if (originalXhrOpen) {
1978
- XMLHttpRequest.prototype.open = originalXhrOpen;
1979
- originalXhrOpen = null;
1980
- }
1981
- if (originalXhrSend) {
1982
- XMLHttpRequest.prototype.send = originalXhrSend;
1983
- originalXhrSend = null;
1984
- }
1985
- if (originalResponseJson) {
1986
- Response.prototype.json = originalResponseJson;
1987
- originalResponseJson = null;
1988
- }
1989
- if (originalJsonParse) {
1990
- JSON.parse = originalJsonParse;
1991
- originalJsonParse = null;
1992
- }
1993
- if (flushTimer2) {
1994
- clearInterval(flushTimer2);
1995
- flushTimer2 = null;
1996
- }
1997
- if (isInstalled3) flushBuffer();
1998
- buffer = [];
1999
- earlyBuffer = [];
2000
- requestIndexMap.clear();
2001
- earlyRequestIndexMap.clear();
2002
- dedupeWindow.clear();
2003
- requestTagTimestamps.clear();
2004
- activeXhrRequestId = null;
2005
- activeXhrResponseText = null;
2006
- client2 = null;
2007
- isInstalled3 = false;
2008
- isPrewarmed = false;
2009
- }
2010
- function patchFetch() {
2011
- if (typeof globalThis.fetch !== "function") return;
2012
- previousFetch = globalThis.fetch;
2013
- globalThis.fetch = async function trackedFetch(input, init) {
2014
- const url = extractUrl(input);
2015
- if (isNoiseUrl(url)) {
2016
- return previousFetch.call(globalThis, input, init);
2017
- }
2018
- const method = (init?.method ?? "GET").toUpperCase();
2019
- const parsedUrl = parseUrl(url);
2020
- const entry = createEntry(method, parsedUrl, init);
2021
- const startTime = performance.now();
2022
- if (init?.signal) {
2023
- init.signal.addEventListener("abort", () => {
2024
- entry.state = "aborted";
2025
- entry.durationMs = performance.now() - startTime;
2026
- pushEntry(entry);
2027
- }, { once: true });
2028
- }
2029
- pushEntry({ ...entry });
2030
- try {
2031
- const response = await previousFetch.call(globalThis, input, init);
2032
- if (entry.state !== "aborted") {
2033
- entry.state = response.ok ? "success" : "error";
2034
- entry.status = response.status;
2035
- entry.durationMs = performance.now() - startTime;
2036
- entry.responseSizeBytes = parseContentLength(response.headers);
2037
- if (!response.ok) {
2038
- entry.errorMessage = `${response.status} ${response.statusText}`;
2039
- }
2040
- pushEntry(entry);
2041
- responseToRequestId.set(response, entry.requestId);
2042
- }
2043
- return response;
2044
- } catch (err) {
2045
- if (entry.state !== "aborted") {
2046
- entry.state = "error";
2047
- entry.durationMs = performance.now() - startTime;
2048
- entry.errorMessage = err instanceof Error ? err.message : String(err);
2049
- pushEntry(entry);
2050
- }
2051
- throw err;
2052
- }
2053
- };
2054
- }
2055
- function patchXhr() {
2056
- if (typeof XMLHttpRequest === "undefined") return;
2057
- originalXhrOpen = XMLHttpRequest.prototype.open;
2058
- originalXhrSend = XMLHttpRequest.prototype.send;
2059
- XMLHttpRequest.prototype.open = function(method, url, ...rest) {
2060
- this.__ftMethod = method.toUpperCase();
2061
- this.__ftUrl = typeof url === "string" ? url : url.href;
2062
- const self = this;
2063
- this.addEventListener("load", function() {
2064
- const requestId = self.__ftRequestId;
2065
- if (!requestId) return;
2066
- if (self.responseType === "json" && self.response !== null && typeof self.response === "object") {
2067
- try {
2068
- tagFetchData(self.response, requestId, 0);
2069
- } catch {
2070
- }
2071
- return;
2072
- }
2073
- const text = self.responseText;
2074
- if (text) {
2075
- activeXhrRequestId = requestId;
2076
- activeXhrResponseText = text;
2077
- }
2078
- });
2079
- return originalXhrOpen.apply(this, [method, url, ...rest]);
2080
- };
2081
- XMLHttpRequest.prototype.send = function(body) {
2082
- const meta = this;
2083
- const url = meta.__ftUrl ?? "";
2084
- if (isNoiseUrl(url)) {
2085
- return originalXhrSend.call(this, body);
2086
- }
2087
- const method = meta.__ftMethod ?? "GET";
2088
- const parsedUrl = parseUrl(url);
2089
- const entry = createEntry(method, parsedUrl);
2090
- const startTime = performance.now();
2091
- this.__ftRequestId = entry.requestId;
2092
- pushEntry({ ...entry });
2093
- this.addEventListener("load", () => {
2094
- entry.state = this.status >= 400 ? "error" : "success";
2095
- entry.status = this.status;
2096
- entry.durationMs = performance.now() - startTime;
2097
- entry.responseSizeBytes = parseXhrContentLength(this);
2098
- if (this.status >= 400) {
2099
- entry.errorMessage = `${this.status} ${this.statusText}`;
2100
- }
2101
- pushEntry(entry);
2102
- });
2103
- this.addEventListener("error", () => {
2104
- entry.state = "error";
2105
- entry.durationMs = performance.now() - startTime;
2106
- entry.errorMessage = "Network error";
2107
- pushEntry(entry);
2108
- });
2109
- this.addEventListener("abort", () => {
2110
- entry.state = "aborted";
2111
- entry.durationMs = performance.now() - startTime;
2112
- pushEntry(entry);
2113
- });
2114
- return originalXhrSend.call(this, body);
2115
- };
2116
- }
2117
- function patchResponseJson() {
2118
- if (typeof Response === "undefined") return;
2119
- originalResponseJson = Response.prototype.json;
2120
- Response.prototype.json = async function() {
2121
- const data = await originalResponseJson.call(this);
2122
- const requestId = responseToRequestId.get(this);
2123
- if (requestId && data !== null && typeof data === "object") {
2124
- try {
2125
- tagFetchData(data, requestId, 0);
2126
- } catch {
2127
- }
2128
- }
2129
- return data;
2130
- };
2131
- }
2132
- function patchJsonParse() {
2133
- originalJsonParse = JSON.parse;
2134
- JSON.parse = function(text, reviver) {
2135
- const result = originalJsonParse.call(JSON, text, reviver);
2136
- if (activeXhrRequestId !== null && activeXhrResponseText !== null && text === activeXhrResponseText && result !== null && typeof result === "object") {
2137
- try {
2138
- tagFetchData(result, activeXhrRequestId, 0);
2139
- } catch {
2140
- }
2141
- activeXhrRequestId = null;
2142
- activeXhrResponseText = null;
2143
- }
2144
- return result;
2145
- };
2146
- }
2147
- function createEntry(method, parsedUrl, init) {
2148
- const requestId = String(++requestCounter);
2149
- const dedupeKey = `${method}:${parsedUrl.path}`;
2150
- const attribution = getAttribution();
2151
- const isServerAction = hasHeader(init, "Next-Action");
2152
- const isPrefetch = hasHeader(init, "Next-Router-Prefetch");
2153
- const now = Date.now();
2154
- const isDuplicate = checkDuplicate(dedupeKey, now);
2155
- return {
2156
- requestId,
2157
- method,
2158
- urlPath: parsedUrl.path,
2159
- urlHost: parsedUrl.host,
2160
- status: 0,
2161
- durationMs: null,
2162
- responseSizeBytes: null,
2163
- componentName: attribution.componentName,
2164
- ancestorChain: attribution.ancestorChain,
2165
- initiatedDuringRender: attribution.duringRender,
2166
- initiatedInEffect: attribution.inEffect,
2167
- state: "pending",
2168
- dedupeKey,
2169
- isDuplicate: isDuplicate || void 0,
2170
- isServerAction: isServerAction || void 0,
2171
- isPrefetch: isPrefetch || void 0,
2172
- timestamp: now
2173
- };
2174
- }
2175
- function getAttribution() {
2176
- const fiber = getCurrentRenderingFiber();
2177
- if (fiber) {
2178
- const name = getComponentNameFromFiber(fiber);
2179
- const ancestors = buildAncestorChain(fiber).slice(-MAX_ANCESTOR_CHAIN);
2180
- return {
2181
- componentName: name || void 0,
2182
- ancestorChain: ancestors.length > 0 ? ancestors : void 0,
2183
- duringRender: true,
2184
- inEffect: false
2185
- };
2186
- }
2187
- return { duringRender: false, inEffect: false };
2188
- }
2189
- function extractUrl(input) {
2190
- if (typeof input === "string") return input;
2191
- if (input instanceof URL) return input.href;
2192
- return input.url;
2193
- }
2194
- function parseUrl(url) {
2195
- try {
2196
- const u = new URL(url, globalThis.location?.href ?? "http://localhost");
2197
- return { path: u.pathname, host: u.host };
2198
- } catch {
2199
- return { path: url.split("?")[0] ?? url, host: "" };
2200
- }
2201
- }
2202
- var COMBINED_NOISE_PATTERN = new RegExp(
2203
- NOISE_URL_PATTERNS.map((r) => r.source).join("|"),
2204
- "i"
2205
- );
2206
- function isNoiseUrl(url) {
2207
- return COMBINED_NOISE_PATTERN.test(url);
2208
- }
2209
- function parseIntOrNull(value) {
2210
- if (!value) return null;
2211
- const n = parseInt(value, 10);
2212
- return isNaN(n) ? null : n;
2213
- }
2214
- function parseContentLength(headers) {
2215
- return parseIntOrNull(headers.get("content-length"));
2216
- }
2217
- function parseXhrContentLength(xhr) {
2218
- return parseIntOrNull(xhr.getResponseHeader("content-length"));
2219
- }
2220
- function hasHeader(init, name) {
2221
- if (!init?.headers) return false;
2222
- if (init.headers instanceof Headers) return init.headers.has(name);
2223
- if (Array.isArray(init.headers)) return init.headers.some(([k]) => k.toLowerCase() === name.toLowerCase());
2224
- if (typeof init.headers === "object") {
2225
- return Object.keys(init.headers).some((k) => k.toLowerCase() === name.toLowerCase());
2226
- }
2227
- return false;
2228
- }
2229
- function checkDuplicate(dedupeKey, now) {
2230
- for (const [key, ts] of dedupeWindow) {
2231
- if (now - ts > DEDUPE_WINDOW_MS) dedupeWindow.delete(key);
2232
- }
2233
- const isDup = dedupeWindow.has(dedupeKey);
2234
- dedupeWindow.set(dedupeKey, now);
2235
- return isDup;
2236
- }
2237
- function upsertAndPrune(entry, buf, idxMap, maxSize) {
2238
- const existingIdx = idxMap.get(entry.requestId);
2239
- if (existingIdx !== void 0 && existingIdx < buf.length && buf[existingIdx]?.requestId === entry.requestId) {
2240
- buf[existingIdx] = entry;
2241
- return buf;
2242
- }
2243
- idxMap.set(entry.requestId, buf.length);
2244
- buf.push(entry);
2245
- if (buf.length > maxSize) {
2246
- const pruned = buf.slice(-maxSize);
2247
- idxMap.clear();
2248
- for (let i = 0; i < pruned.length; i++) idxMap.set(pruned[i].requestId, i);
2249
- return pruned;
2250
- }
2251
- return buf;
2252
- }
2253
- function pushEntry(entry) {
2254
- if (client2 === null && isPrewarmed) {
2255
- earlyBuffer = upsertAndPrune(entry, earlyBuffer, earlyRequestIndexMap, MAX_BUFFER_SIZE);
2256
- return;
2257
- }
2258
- buffer = upsertAndPrune(entry, buffer, requestIndexMap, MAX_BUFFER_SIZE);
2259
- if (buffer.length >= MAX_BATCH_SIZE) flushBuffer();
2260
- }
2261
- function rebuildRequestIndex() {
2262
- requestIndexMap.clear();
2263
- for (let i = 0; i < buffer.length; i++) {
2264
- requestIndexMap.set(buffer[i].requestId, i);
2265
- }
2266
- }
2267
- function flushBuffer() {
2268
- if (buffer.length === 0 || !client2?.connected) return;
2269
- client2.send({
2270
- type: "runtime:networkRequest",
2271
- requests: [...buffer],
2272
- timestamp: Date.now()
2273
- });
2274
- buffer = [];
2275
- requestIndexMap.clear();
2276
- }
2277
-
2278
- // src/fiberTreeWalker.ts
2279
- var FIBER_TAGS = {
2280
- FunctionComponent: 0,
2281
- ClassComponent: 1,
2282
- HostRoot: 3,
2283
- // Root of a host tree (e.g., #root DOM node)
2284
- HostComponent: 5,
2285
- // DOM elements (div, span, etc.) - SKIP these
2286
- HostText: 6,
2287
- // Text nodes - SKIP these
2288
- Fragment: 7,
2289
- // React.Fragment - SKIP but traverse children
2290
- Mode: 8,
2291
- // React.StrictMode, ConcurrentMode - SKIP but traverse children
2292
- ContextConsumer: 9,
2293
- ContextProvider: 10,
2294
- ForwardRef: 11,
2295
- Profiler: 12,
2296
- // React.Profiler - SKIP but traverse children
2297
- SuspenseComponent: 13,
2298
- MemoComponent: 14,
2299
- SimpleMemoComponent: 15,
2300
- LazyComponent: 16,
2301
- OffscreenComponent: 22
2302
- // React 18 concurrent features - SKIP but traverse children
2303
- };
2304
- var USER_COMPONENT_TAGS = /* @__PURE__ */ new Set([
2305
- FIBER_TAGS.FunctionComponent,
2306
- FIBER_TAGS.ClassComponent,
2307
- FIBER_TAGS.ForwardRef,
2308
- FIBER_TAGS.MemoComponent,
2309
- FIBER_TAGS.SimpleMemoComponent
2310
- ]);
2311
- function isLikelyQueryObserver(obj) {
2312
- if (obj === null || typeof obj !== "object") return false;
2313
- const candidate = obj;
2314
- return typeof candidate.getCurrentResult === "function" && typeof candidate.subscribe === "function";
2315
- }
2316
- function getQueryHashFromObserver(observer) {
2317
- if (observer.options && typeof observer.options === "object") {
2318
- const opts = observer.options;
2319
- if (typeof opts.queryHash === "string") return opts.queryHash;
2320
- }
2321
- if (observer.currentQuery && typeof observer.currentQuery === "object") {
2322
- const q = observer.currentQuery;
2323
- if (typeof q.queryHash === "string") return q.queryHash;
2324
- }
2325
- if (typeof observer.queryHash === "string") return observer.queryHash;
2326
- return null;
2327
- }
2328
- function detectQueryObserverHashes(fiber) {
2329
- let hookState = fiber.memoizedState;
2330
- if (!hookState) return void 0;
2331
- const seen = /* @__PURE__ */ new Set();
2332
- let iterations = 0;
2333
- while (hookState && iterations < 100) {
2334
- iterations++;
2335
- try {
2336
- const ms = hookState.memoizedState;
2337
- if (isLikelyQueryObserver(ms)) {
2338
- const hash = getQueryHashFromObserver(ms);
2339
- if (hash) seen.add(hash);
2340
- } else if (ms !== null && typeof ms === "object" && !Array.isArray(ms)) {
2341
- const ref = ms.current;
2342
- if (isLikelyQueryObserver(ref)) {
2343
- const hash = getQueryHashFromObserver(ref);
2344
- if (hash) seen.add(hash);
2345
- }
2346
- }
2347
- } catch {
2348
- }
2349
- hookState = hookState.next;
2350
- }
2351
- return seen.size > 0 ? Array.from(seen) : void 0;
2352
- }
2353
- function countFiberHooks(fiber) {
2354
- if (fiber._debugHookTypes) return fiber._debugHookTypes.length;
2355
- let count = 0;
2356
- let state = fiber.memoizedState;
2357
- while (state && count < 100) {
2358
- count++;
2359
- state = state.next;
2360
- }
2361
- return count;
2362
- }
2363
- function hasFiberContextHook(fiber) {
2364
- if (fiber.dependencies?.firstContext) return true;
2365
- if (fiber._debugHookTypes?.includes("useContext")) return true;
2366
- return false;
2367
- }
2368
- function detectTransitionPending(fiber) {
2369
- let state = fiber.memoizedState;
2370
- let iterations = 0;
2371
- while (state && iterations < 100) {
2372
- iterations++;
2373
- const ms = state.memoizedState;
2374
- if (Array.isArray(ms) && ms.length === 2 && typeof ms[0] === "boolean" && typeof ms[1] === "function") {
2375
- if (ms[0] === true) return true;
2376
- }
2377
- state = state.next;
2378
- }
2379
- return false;
2380
- }
2381
- var MAX_TREE_DEPTH = 100;
2382
- var MAX_CHILDREN_PER_NODE = 300;
2383
- var throttleTimer = null;
2384
- var maxWaitTimer = null;
2385
- var INTERVAL_MS_SMALL = 200;
2386
- var INTERVAL_MS_MEDIUM = 500;
2387
- var INTERVAL_MS_LARGE = 1e3;
2388
- var snapshotIntervalMs = INTERVAL_MS_SMALL;
2389
- var cachedFiberRoot = null;
2390
- var isWalking = false;
2391
- var pendingLocalStateCorrelations = [];
2392
- var originalOnCommitFiberRoot = null;
2393
- var isInstalled4 = false;
2394
- var hookedRendererID = null;
2395
- var activeStrategy = null;
2396
- var lastSnapshotSentTime = 0;
2397
- var DEVTOOLS_STALE_THRESHOLD_MS = 2e3;
2398
- var debugEnabled = false;
2399
- try {
2400
- debugEnabled = !!globalThis.__FLOTRACE_DEBUG__;
2401
- } catch {
2402
- }
2403
- function debugLog(...args) {
2404
- if (debugEnabled) console.log(...args);
2405
- }
2406
- var fiberRefMap = /* @__PURE__ */ new Map();
2407
- function getComponentName2(fiber) {
2408
- const type = fiber.type;
2409
- if (!type) return "Unknown";
2410
- if (typeof type === "function") {
2411
- return type.displayName || type.name || "Anonymous";
2412
- }
2413
- if (typeof type === "object" && type !== null) {
2414
- const t = type;
2415
- if (t.type) {
2416
- return t.type.displayName || t.type.name || "Memo";
2417
- }
2418
- if (t.render) {
2419
- return t.render.displayName || t.render.name || "ForwardRef";
2420
- }
2421
- return t.displayName || t.name || "Unknown";
2422
- }
2423
- if (typeof type === "string") {
2424
- return type;
2425
- }
2426
- return "Unknown";
2427
- }
2428
- function isUserComponent(fiber) {
2429
- if (!USER_COMPONENT_TAGS.has(fiber.tag)) return false;
2430
- const name = getComponentName2(fiber);
2431
- if (name === "Anonymous" || name === "Unknown" || name === "ForwardRef" || name === "Memo")
2432
- return false;
2433
- if (name.startsWith("FloTrace")) return false;
2434
- if (name.startsWith("@") || name.includes("/")) return false;
2435
- if (/^[$_][A-Za-z0-9]{0,3}$/.test(name)) return false;
2436
- if (fiber._debugSource?.fileName?.includes("node_modules")) return false;
2437
- return true;
2438
- }
2439
- var FRAMEWORK_COMPONENT_NAMES = /* @__PURE__ */ new Set([
2440
- // Next.js App Router internals (Next.js 13–14)
2441
- "InnerLayoutRouter",
2442
- "OuterLayoutRouter",
2443
- "HotReload",
2444
- "RedirectBoundary",
2445
- "NotFoundBoundary",
2446
- "RenderFromTemplateContext",
2447
- "ScrollAndFocusHandler",
2448
- "AppRouter",
2449
- "ServerRoot",
2450
- "ReactDevOverlay",
2451
- "PathnameContextProviderAdapter",
2452
- "MetadataBoundary",
2453
- "ViewportBoundary",
2454
- "NotFoundErrorBoundary",
2455
- "RedirectErrorBoundary",
2456
- "InnerScrollAndFocusHandler",
2457
- "GlobalError",
2458
- // Next.js 15 / React 19 new internals
2459
- "ViewTransition",
2460
- // Next.js 15 shared-element transition wrapper
2461
- "ActionStateContext",
2462
- // Next.js 15 server action state context provider
2463
- "RequestCookiesProvider",
2464
- "DraftModeProvider",
2465
- // React Router v6 / v7
2466
- "Routes",
2467
- "Route",
2468
- "Router",
2469
- "BrowserRouter",
2470
- "HashRouter",
2471
- "MemoryRouter",
2472
- "Outlet",
2473
- "Navigate",
2474
- "RenderedRoute",
2475
- "RouterProvider",
2476
- // React 19 built-in primitives
2477
- "Activity",
2478
- // React 19: show/hide subtrees while preserving state (was <Offscreen>)
2479
- // Common library wrappers
2480
- "Suspense",
2481
- "ErrorBoundary",
2482
- "QueryClientProvider",
2483
- "PersistGate"
2484
- ]);
2485
- var FRAMEWORK_PATH_PATTERNS = [
2486
- // React core / Next.js
2487
- /next[\\/]dist/,
2488
- /react-dom/,
2489
- /[\\/]scheduler[\\/]/,
2490
- // React internal scheduler package
2491
- // Routing
2492
- /react-router/,
2493
- // React Router v6
2494
- /@react-router[\\/]/,
2495
- // React Router v7 (scoped package)
2496
- // State management
2497
- /@tanstack[\\/]/,
2498
- // TanStack Query / Table / Router / Form / Virtual
2499
- /react-redux/,
2500
- /zustand/,
2501
- /jotai/,
2502
- /recoil/,
2503
- // UI component libraries (for when source maps are available)
2504
- /@fortawesome[\\/]/,
2505
- // Font Awesome icons
2506
- /framer-motion/,
2507
- // Framer Motion (PresenceChild, AnimatePresence, etc.)
2508
- /sonner/,
2509
- // Sonner toast
2510
- /@radix-ui[\\/]/,
2511
- // Radix UI primitives
2512
- /@headlessui[\\/]/,
2513
- // Headless UI
2514
- /@mui[\\/]/,
2515
- // Material UI
2516
- /@chakra-ui[\\/]/,
2517
- // Chakra UI
2518
- /react-spring/,
2519
- // React Spring
2520
- /react-transition-group/,
2521
- // React Transition Group
2522
- /react-aria/,
2523
- // Adobe React Aria
2524
- /react-hook-form/,
2525
- /formik/
162
+ var MAX_BUFFER_SIZE = 300;
163
+ var DEDUPE_WINDOW_MS = 5e3;
164
+ var MAX_ANCESTOR_CHAIN = 3;
165
+ var NOISE_URL_PATTERNS = [
166
+ // Analytics & tracking
167
+ /google-analytics\.com/i,
168
+ /googletagmanager\.com/i,
169
+ /facebook\.com\/tr/i,
170
+ /segment\.io/i,
171
+ /mixpanel\.com/i,
172
+ /amplitude\.com/i,
173
+ /hotjar\.com/i,
174
+ /fullstory\.com/i,
175
+ /sentry\.io/i,
176
+ /bugsnag\.com/i,
177
+ /datadog/i,
178
+ /clarity\.ms/i,
179
+ /plausible\.io/i,
180
+ // Development tools
181
+ /webpack-dev-server/i,
182
+ /__webpack_hmr/i,
183
+ /\.hot-update\./i,
184
+ /\.map$/,
185
+ /sourcemap/i,
186
+ /__nextjs_original-stack-frame/i,
187
+ /__nextjs_launch-editor/i,
188
+ /on-demand-entries-ping/i,
189
+ // Browser resources
190
+ /favicon\.ico/i,
191
+ /robots\.txt/i,
192
+ /manifest\.json/i,
193
+ /service-worker/i,
194
+ /sw\.js/i,
195
+ // Static assets
196
+ /\/_next\/static\//i,
197
+ /\/_next\/image/i,
198
+ // FloTrace's own WebSocket
199
+ /127\.0\.0\.1:3457/,
200
+ // Chrome extensions
201
+ /chrome-extension:/i,
202
+ /moz-extension:/i
2526
203
  ];
2527
- function isFrameworkComponent(fiber, name) {
2528
- if (FRAMEWORK_COMPONENT_NAMES.has(name)) return true;
2529
- const filePath = fiber._debugSource?.fileName;
2530
- if (filePath) {
2531
- for (const pattern of FRAMEWORK_PATH_PATTERNS) {
2532
- if (pattern.test(filePath)) return true;
2533
- }
2534
- }
2535
- return false;
2536
- }
2537
- var KNOWN_LIBRARY_NAMES = /* @__PURE__ */ new Map([
2538
- // Font Awesome
2539
- ["FontAwesomeIcon", "fontawesome"],
2540
- ["FontAwesomeLayers", "fontawesome"],
2541
- ["FontAwesomeLayersText", "fontawesome"],
2542
- // Framer Motion
2543
- ["AnimatePresence", "framer"],
2544
- ["LazyMotion", "framer"],
2545
- ["MotionConfig", "framer"],
2546
- ["PresenceChild", "framer"],
2547
- ["LayoutGroupContext", "framer"],
2548
- // Lottie
2549
- ["Lottie", "lottie"],
2550
- ["LottiePlayer", "lottie"],
2551
- // Heroicons / Lucide exported icons sometimes appear as named functions
2552
- ["HeroIcon", "heroicons"]
2553
- ]);
2554
- function detectLibraryName(fiber, name) {
2555
- if (name.includes(".")) {
2556
- return name.split(".")[0].toLowerCase();
2557
- }
2558
- if (name.startsWith("__")) {
2559
- return "internal";
2560
- }
2561
- const known = KNOWN_LIBRARY_NAMES.get(name);
2562
- return known;
2563
- }
2564
- function buildNodeId(name, sameNameIndex, parentId) {
2565
- const segment = `${name}-${sameNameIndex}`;
2566
- return parentId ? `${parentId}/${segment}` : segment;
2567
- }
2568
- function shallowPropsChanged(prev, next) {
2569
- if (prev === next) return false;
2570
- if (!prev || !next) return true;
2571
- const prevKeys = Object.keys(prev);
2572
- const nextKeys = Object.keys(next);
2573
- if (prevKeys.length !== nextKeys.length) return true;
2574
- for (const key of nextKeys) {
2575
- if (key === "children") continue;
2576
- if (prev[key] !== next[key]) return true;
2577
- }
2578
- return false;
2579
- }
2580
- function detectRenderReason(fiber, renderPhase) {
2581
- if (renderPhase === "mount") return "mount";
2582
- const prev = fiber.alternate;
2583
- if (!prev) return "mount";
2584
- if (shallowPropsChanged(prev.memoizedProps, fiber.memoizedProps)) {
2585
- return "props-changed";
2586
- }
2587
- return "state-or-context";
2588
- }
2589
- function scanFiberStateForOrigin(fiber, componentName) {
2590
- let hook = fiber.memoizedState;
2591
- let hookIndex = 0;
2592
- while (hook !== null) {
2593
- try {
2594
- const ms = hook.memoizedState;
2595
- if (ms !== null && typeof ms === "object") {
2596
- const isEffect = "tag" in ms && "create" in ms;
2597
- if (!isEffect) {
2598
- const rid = findFetchOrigin(ms);
2599
- if (rid) {
2600
- pendingLocalStateCorrelations.push({ requestId: rid, componentName, hookIndex });
2601
- } else if (hook.queue !== null) {
2602
- const lastRendered = hook.queue.lastRenderedState;
2603
- if (lastRendered !== null && typeof lastRendered === "object") {
2604
- const rid2 = findFetchOrigin(lastRendered);
2605
- if (rid2) {
2606
- pendingLocalStateCorrelations.push({ requestId: rid2, componentName, hookIndex });
2607
- }
2608
- }
2609
- }
2610
- }
2611
- }
2612
- } catch {
2613
- }
2614
- hook = hook.next;
2615
- hookIndex++;
2616
- }
2617
- }
2618
- function walkFiber(fiber, parentId, sharedNameCountMap, depth = 0, inSuspenseFallback = false) {
2619
- if (!fiber) return [];
2620
- if (depth >= MAX_TREE_DEPTH) return [];
2621
- const nodes = [];
2622
- let current = fiber;
2623
- const nameCountMap = sharedNameCountMap || /* @__PURE__ */ new Map();
2624
- while (current) {
2625
- try {
2626
- const tag = current.tag;
2627
- if (isUserComponent(current)) {
2628
- const name = getComponentName2(current);
2629
- const nameCount = nameCountMap.get(name) || 0;
2630
- nameCountMap.set(name, nameCount + 1);
2631
- const nodeId = buildNodeId(name, nameCount, parentId);
2632
- fiberRefMap.set(nodeId, current);
2633
- const renderPhase = current.alternate ? "update" : "mount";
2634
- const renderReason = detectRenderReason(current, renderPhase);
2635
- recordTimelineEvent(
2636
- nodeId,
2637
- name,
2638
- renderPhase === "mount" ? "mount" : "render",
2639
- { reason: renderReason },
2640
- current.actualDuration
2641
- );
2642
- const children = walkFiber(
2643
- current.child,
2644
- nodeId,
2645
- void 0,
2646
- depth + 1,
2647
- inSuspenseFallback
2648
- );
2649
- const truncatedChildren = children.length > MAX_CHILDREN_PER_NODE ? children.slice(0, MAX_CHILDREN_PER_NODE) : children;
2650
- const framework = isFrameworkComponent(current, name) || void 0;
2651
- const queryHashes = detectQueryObserverHashes(current);
2652
- const isTransitionPending = detectTransitionPending(current) || void 0;
2653
- const compilerStatus = detectCompilerStatus(current);
2654
- const isServerComponent = detectServerComponent(current) || void 0;
2655
- const libraryName = framework ? void 0 : detectLibraryName(current, name);
2656
- nodes.push({
2657
- id: nodeId,
2658
- name,
2659
- children: truncatedChildren,
2660
- fiberTag: tag,
2661
- renderPhase,
2662
- renderReason,
2663
- renderDuration: current.actualDuration,
2664
- filePath: current._debugSource?.fileName,
2665
- lineNumber: current._debugSource?.lineNumber,
2666
- isFramework: framework,
2667
- reactKey: typeof current.key === "string" ? current.key : void 0,
2668
- queryHashes,
2669
- hookCount: countFiberHooks(current),
2670
- hasContextHook: hasFiberContextHook(current) || void 0,
2671
- isTransitionPending,
2672
- isSuspenseFallback: inSuspenseFallback || void 0,
2673
- compilerStatus,
2674
- isServerComponent,
2675
- isLibrary: libraryName !== void 0 ? true : void 0,
2676
- libraryName
2677
- });
2678
- if (hasActiveTags() && current.memoizedState !== null) {
2679
- scanFiberStateForOrigin(current, name);
2680
- }
2681
- } else if (tag === FIBER_TAGS.HostText) {
2682
- } else if (tag === FIBER_TAGS.SuspenseComponent) {
2683
- const primary = current.child;
2684
- if (current.memoizedState === null && primary) {
2685
- const childNodes = walkFiber(
2686
- primary.child,
2687
- parentId,
2688
- nameCountMap,
2689
- depth,
2690
- inSuspenseFallback
2691
- );
2692
- nodes.push(...childNodes);
2693
- } else if (primary?.sibling) {
2694
- const childNodes = walkFiber(
2695
- primary.sibling,
2696
- parentId,
2697
- nameCountMap,
2698
- depth,
2699
- true
2700
- // all nodes in the fallback branch get isSuspenseFallback
2701
- );
2702
- nodes.push(...childNodes);
2703
- } else {
2704
- debugLog("[FloTrace] SuspenseComponent has no walkable children");
2705
- }
2706
- } else if (tag === FIBER_TAGS.OffscreenComponent) {
2707
- if (current.memoizedState === null) {
2708
- const childNodes = walkFiber(
2709
- current.child,
2710
- parentId,
2711
- nameCountMap,
2712
- depth,
2713
- inSuspenseFallback
2714
- );
2715
- nodes.push(...childNodes);
2716
- } else {
2717
- debugLog("[FloTrace] Skipping hidden OffscreenComponent subtree");
2718
- }
2719
- } else {
2720
- const childNodes = walkFiber(
2721
- current.child,
2722
- parentId,
2723
- nameCountMap,
2724
- depth,
2725
- inSuspenseFallback
2726
- );
2727
- nodes.push(...childNodes);
2728
- }
2729
- } catch (error) {
2730
- console.error("[FloTrace] Error processing fiber node, skipping:", error);
2731
- }
2732
- current = current.sibling;
2733
- }
2734
- return nodes;
2735
- }
2736
- function buildTreeFromFiberRoot(root) {
2737
- const rootFiber = root.current;
2738
- if (!rootFiber || !rootFiber.child) {
2739
- console.warn("[FloTrace] No root fiber or no child:", {
2740
- hasRoot: !!rootFiber,
2741
- hasChild: !!rootFiber?.child
2742
- });
2743
- return null;
2744
- }
2745
- fiberRefMap.clear();
2746
- const topLevelNodes = walkFiber(rootFiber.child, "");
2747
- debugLog(
2748
- "[FloTrace] walkFiber found",
2749
- topLevelNodes.length,
2750
- "top-level nodes"
2751
- );
2752
- if (topLevelNodes.length === 1) {
2753
- return topLevelNodes[0];
2754
- }
2755
- if (topLevelNodes.length > 0) {
2756
- return {
2757
- id: "Root",
2758
- name: "Root",
2759
- children: topLevelNodes,
2760
- fiberTag: FIBER_TAGS.HostRoot
2761
- };
2762
- }
2763
- return null;
2764
- }
2765
- function findFiberRootFromDOM() {
2766
- try {
2767
- if (typeof document === "undefined") return null;
2768
- const selectors = ["#root", "#__next", "#app", "#__nuxt", "[data-reactroot]"];
2769
- for (const selector of selectors) {
2770
- const element = document.querySelector(selector);
2771
- if (!element) continue;
2772
- debugLog(
2773
- `[FloTrace] Trying selector "${selector}" \u2192 found element`,
2774
- element.tagName,
2775
- element.id
2776
- );
2777
- const reactKeys = Object.keys(element).filter(
2778
- (k) => k.startsWith("__react") || k.startsWith("_react")
2779
- );
2780
- debugLog(`[FloTrace] React keys on element:`, reactKeys);
2781
- const fiberRoot = getFiberRootFromElement(element);
2782
- if (fiberRoot) {
2783
- debugLog("[FloTrace] Found fiber root from selector:", selector);
2784
- return fiberRoot;
2785
- }
2786
- }
2787
- const allBodyChildren = document.body?.children;
2788
- if (allBodyChildren) {
2789
- debugLog(
2790
- "[FloTrace] Scanning all",
2791
- allBodyChildren.length,
2792
- "body children for React root..."
2793
- );
2794
- for (const child of Array.from(allBodyChildren)) {
2795
- const reactKeys = Object.keys(child).filter(
2796
- (k) => k.startsWith("__react") || k.startsWith("_react")
2797
- );
2798
- if (reactKeys.length > 0) {
2799
- debugLog(
2800
- "[FloTrace] React keys on",
2801
- child.tagName,
2802
- child.id || "(no id)",
2803
- ":",
2804
- reactKeys
2805
- );
2806
- }
2807
- const fiberRoot = getFiberRootFromElement(child);
2808
- if (fiberRoot) {
2809
- debugLog(
2810
- "[FloTrace] Found fiber root from body child scan:",
2811
- child.tagName,
2812
- child.id || "(no id)"
2813
- );
2814
- return fiberRoot;
2815
- }
2816
- }
2817
- }
2818
- console.warn(
2819
- "[FloTrace] Could not find React fiber root from any DOM element"
2820
- );
2821
- return null;
2822
- } catch (error) {
2823
- console.error("[FloTrace] Error finding fiber root from DOM:", error);
2824
- return null;
2825
- }
2826
- }
2827
- function getFiberRootFromElement(element) {
2828
- const keys = Object.keys(element);
2829
- const containerKey = keys.find((k) => k.startsWith("__reactContainer$"));
2830
- if (containerKey) {
2831
- const hostRootFiber = element[containerKey];
2832
- if (hostRootFiber?.stateNode) {
2833
- return hostRootFiber.stateNode;
2834
- }
2835
- }
2836
- const fiberKey = keys.find(
2837
- (k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
2838
- );
2839
- if (fiberKey) {
2840
- const fiber = element[fiberKey];
2841
- if (fiber) {
2842
- let current = fiber;
2843
- while (current?.return) {
2844
- current = current.return;
2845
- }
2846
- if (current && current.tag === FIBER_TAGS.HostRoot && current.stateNode) {
2847
- return current.stateNode;
2848
- }
2849
- }
2850
- }
2851
- const el = element;
2852
- if (el._reactRootContainer?._internalRoot) {
2853
- return el._reactRootContainer._internalRoot;
2854
- }
2855
- return null;
2856
- }
2857
- function adaptSnapshotInterval(nodeCount) {
2858
- if (nodeCount >= 200) {
2859
- snapshotIntervalMs = INTERVAL_MS_LARGE;
2860
- } else if (nodeCount >= 50) {
2861
- snapshotIntervalMs = INTERVAL_MS_MEDIUM;
2862
- } else {
2863
- snapshotIntervalMs = INTERVAL_MS_SMALL;
2864
- }
2865
- }
2866
- function executeSnapshot(root) {
2867
- if (isWalking) {
2868
- debugLog("[FloTrace] Skipped snapshot: already walking");
2869
- return;
2870
- }
2871
- isWalking = true;
2872
- try {
2873
- const tree = buildTreeFromFiberRoot(root);
2874
- if (!tree) {
2875
- console.warn("[FloTrace] buildTreeFromFiberRoot returned null");
2876
- return;
2877
- }
2878
- const nodeCount = fiberRefMap.size;
2879
- adaptSnapshotInterval(nodeCount);
2880
- const client4 = getWebSocketClient();
2881
- if (!client4.connected) {
2882
- console.warn(
2883
- "[FloTrace] WebSocket not connected, cannot send tree snapshot"
2884
- );
2885
- return;
2886
- }
2887
- const currentFlatTree = flattenTree2(tree);
2888
- const sendFull = previousFlatTree === null || snapshotCounter % FULL_SNAPSHOT_INTERVAL === 0;
2889
- if (sendFull) {
2890
- debugLog(
2891
- "[FloTrace] Sending FULL tree snapshot, root:",
2892
- tree.name,
2893
- "nodes:",
2894
- nodeCount,
2895
- "seq:",
2896
- snapshotCounter,
2897
- "nextInterval:",
2898
- snapshotIntervalMs + "ms"
2899
- );
2900
- client4.sendImmediate({
2901
- type: "runtime:treeSnapshot",
2902
- tree,
2903
- timestamp: Date.now()
2904
- });
2905
- lastSnapshotSentTime = Date.now();
2906
- diffSeq = 0;
2907
- } else {
2908
- const diff = computeTreeDiff(previousFlatTree, currentFlatTree);
2909
- if (diff) {
2910
- debugLog(
2911
- "[FloTrace] Sending tree diff, seq:",
2912
- diffSeq,
2913
- "added:",
2914
- diff.added.length,
2915
- "removed:",
2916
- diff.removed.length,
2917
- "updated:",
2918
- diff.updated.length
2919
- );
2920
- client4.sendImmediate({
2921
- type: "runtime:treeDiff",
2922
- seq: diffSeq,
2923
- added: diff.added,
2924
- removed: diff.removed,
2925
- updated: diff.updated,
2926
- timestamp: Date.now()
2927
- });
2928
- lastSnapshotSentTime = Date.now();
2929
- diffSeq++;
2930
- } else {
2931
- debugLog("[FloTrace] Tree unchanged, skipping diff");
2932
- }
2933
- }
2934
- previousFlatTree = currentFlatTree;
2935
- if (pendingLocalStateCorrelations.length > 0) {
2936
- const now = Date.now();
2937
- const toSend = pendingLocalStateCorrelations.splice(0);
2938
- for (const corr of toSend) {
2939
- try {
2940
- client4.sendImmediate({
2941
- type: "runtime:localStateCorrelation",
2942
- requestId: corr.requestId,
2943
- componentName: corr.componentName,
2944
- hookIndex: corr.hookIndex,
2945
- timestamp: now
2946
- });
2947
- } catch {
2948
- }
2949
- }
2950
- }
2951
- schedulePropDrillingAnalysis(tree, fiberRefMap, client4);
2952
- scanActionStateChanges(fiberRefMap, client4);
2953
- maybeEmitNextjsContext(client4);
2954
- snapshotCounter++;
2955
- } catch (error) {
2956
- console.error("[FloTrace] Error walking fiber tree:", error);
2957
- } finally {
2958
- isWalking = false;
2959
- }
2960
- }
2961
- function scheduleSnapshot(root) {
2962
- cachedFiberRoot = root;
2963
- if (throttleTimer) {
2964
- clearTimeout(throttleTimer);
2965
- }
2966
- throttleTimer = setTimeout(() => {
2967
- throttleTimer = null;
2968
- if (maxWaitTimer) {
2969
- clearTimeout(maxWaitTimer);
2970
- maxWaitTimer = null;
2971
- }
2972
- executeSnapshot(cachedFiberRoot);
2973
- }, snapshotIntervalMs);
2974
- if (!maxWaitTimer) {
2975
- maxWaitTimer = setTimeout(() => {
2976
- maxWaitTimer = null;
2977
- if (throttleTimer) {
2978
- clearTimeout(throttleTimer);
2979
- throttleTimer = null;
2980
- }
2981
- debugLog("[FloTrace] MaxWait forced snapshot (rapid commits detected)");
2982
- if (cachedFiberRoot) {
2983
- executeSnapshot(cachedFiberRoot);
2984
- }
2985
- }, snapshotIntervalMs * 2);
2986
- }
2987
- }
2988
- var previousFlatTree = null;
2989
- var diffSeq = 0;
2990
- var snapshotCounter = 0;
2991
- var FULL_SNAPSHOT_INTERVAL = 10;
2992
- function flattenTree2(root, out = /* @__PURE__ */ new Map()) {
2993
- out.set(root.id, root);
2994
- for (const child of root.children) {
2995
- flattenTree2(child, out);
2996
- }
2997
- return out;
2998
- }
2999
- function getParentId(nodeId) {
3000
- const lastSlash = nodeId.lastIndexOf("/");
3001
- return lastSlash === -1 ? "" : nodeId.substring(0, lastSlash);
3002
- }
3003
- function computeTreeDiff(prev, curr) {
3004
- const added = [];
3005
- const removed = [];
3006
- const updated = [];
3007
- for (const [id, currNode] of curr) {
3008
- const prevNode = prev.get(id);
3009
- if (!prevNode) {
3010
- added.push({ ...currNode, children: [], parentId: getParentId(id) });
3011
- } else {
3012
- if (prevNode.renderDuration !== currNode.renderDuration || prevNode.renderPhase !== currNode.renderPhase || prevNode.renderReason !== currNode.renderReason) {
3013
- updated.push({
3014
- id,
3015
- renderDuration: currNode.renderDuration,
3016
- renderPhase: currNode.renderPhase,
3017
- renderReason: currNode.renderReason
3018
- });
3019
- }
3020
- }
3021
- }
3022
- for (const id of prev.keys()) {
3023
- if (!curr.has(id)) {
3024
- removed.push(id);
3025
- }
3026
- }
3027
- if (added.length === 0 && removed.length === 0 && updated.length === 0) {
3028
- return null;
3029
- }
3030
- return { added, removed, updated };
3031
- }
3032
- function requestTreeSnapshot() {
3033
- if (!isInstalled4) {
3034
- return;
3035
- }
3036
- if (activeStrategy === "devtools") {
3037
- const elapsed = Date.now() - lastSnapshotSentTime;
3038
- if (elapsed < DEVTOOLS_STALE_THRESHOLD_MS) return;
3039
- debugLog("[FloTrace] DevTools hook stale (" + elapsed + "ms), falling back to DOM snapshot");
3040
- }
3041
- const root = findFiberRootFromDOM();
3042
- if (root) {
3043
- scheduleSnapshot(root);
3044
- }
204
+ var client2 = null;
205
+ var isInstalled2 = false;
206
+ var isPrewarmed = false;
207
+ var buffer = [];
208
+ var earlyBuffer = [];
209
+ var flushTimer = null;
210
+ var requestCounter = 0;
211
+ var requestIndexMap = /* @__PURE__ */ new Map();
212
+ var earlyRequestIndexMap = /* @__PURE__ */ new Map();
213
+ var previousFetch = null;
214
+ var originalXhrOpen = null;
215
+ var originalXhrSend = null;
216
+ var originalResponseJson = null;
217
+ var originalJsonParse = null;
218
+ var responseToRequestId = /* @__PURE__ */ new WeakMap();
219
+ var activeXhrRequestId = null;
220
+ var activeXhrResponseText = null;
221
+ var dedupeWindow = /* @__PURE__ */ new Map();
222
+ function installPatches() {
223
+ patchFetch();
224
+ patchXhr();
225
+ patchResponseJson();
226
+ patchJsonParse();
3045
227
  }
3046
- function requestFullSnapshot() {
3047
- previousFlatTree = null;
3048
- snapshotCounter = 0;
3049
- diffSeq = 0;
3050
- if (cachedFiberRoot) {
3051
- scheduleSnapshot(cachedFiberRoot);
3052
- }
228
+ function prewarmNetworkTracker() {
229
+ if (isInstalled2 || isPrewarmed) return;
230
+ isPrewarmed = true;
231
+ installPatches();
3053
232
  }
3054
- function installFiberTreeWalker() {
3055
- if (isInstalled4) {
3056
- console.warn("[FloTrace] Fiber tree walker already installed");
3057
- return () => uninstallFiberTreeWalker();
3058
- }
3059
- if (typeof window === "undefined") {
3060
- console.warn(
3061
- "[FloTrace] Not in browser environment, cannot install fiber tree walker"
3062
- );
3063
- return () => {
3064
- };
3065
- }
3066
- isInstalled4 = true;
3067
- try {
3068
- const client4 = getWebSocketClient();
3069
- installRscPayloadInterceptor(client4);
3070
- } catch {
3071
- }
3072
- const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3073
- if (hook && typeof hook.onCommitFiberRoot === "function") {
3074
- originalOnCommitFiberRoot = hook.onCommitFiberRoot;
3075
- hook.onCommitFiberRoot = (rendererID, root, priority) => {
3076
- if (originalOnCommitFiberRoot) {
3077
- try {
3078
- originalOnCommitFiberRoot(rendererID, root, priority);
3079
- } catch (error) {
3080
- console.error(
3081
- "[FloTrace] Error in original onCommitFiberRoot:",
3082
- error
3083
- );
3084
- }
3085
- }
3086
- if (hookedRendererID === null) {
3087
- hookedRendererID = rendererID;
3088
- }
3089
- if (rendererID !== hookedRendererID) return;
3090
- try {
3091
- const client4 = getWebSocketClient();
3092
- if (client4.connected) {
3093
- const triggers = peekTriggers();
3094
- for (const trigger of triggers) {
3095
- client4.sendImmediate({ type: "runtime:renderTrigger", trigger });
3096
- }
3097
- const cascade = analyzeCascade(root, triggers);
3098
- if (cascade) {
3099
- client4.sendImmediate({ type: "runtime:renderCascade", cascade });
3100
- }
3101
- wrapFiberDispatchers(root);
3102
- clearTriggers();
3103
- }
3104
- } catch {
3105
- }
3106
- scheduleSnapshot(root);
3107
- };
3108
- activeStrategy = "devtools";
3109
- console.log(
3110
- "[FloTrace] Fiber tree walker installed (DevTools hook strategy)"
3111
- );
3112
- setTimeout(() => {
3113
- try {
3114
- const root = findFiberRootFromDOM();
3115
- if (root) {
3116
- scheduleSnapshot(root);
3117
- }
3118
- } catch (error) {
3119
- console.error("[FloTrace] Error sending initial DevTools snapshot:", error);
3120
- }
3121
- }, 100);
233
+ function installNetworkTracker(wsClient) {
234
+ if (isInstalled2) return;
235
+ client2 = wsClient;
236
+ isInstalled2 = true;
237
+ if (!isPrewarmed) {
238
+ requestCounter = 0;
239
+ installPatches();
3122
240
  } else {
3123
- activeStrategy = "dom";
3124
- console.log(
3125
- "[FloTrace] Fiber tree walker installed (DOM fallback strategy)"
3126
- );
3127
- setTimeout(() => {
3128
- try {
3129
- const root = findFiberRootFromDOM();
3130
- if (root) {
3131
- scheduleSnapshot(root);
3132
- }
3133
- } catch (error) {
3134
- console.error("[FloTrace] Error sending initial DOM fallback snapshot:", error);
3135
- }
3136
- }, 100);
3137
- }
3138
- return () => uninstallFiberTreeWalker();
3139
- }
3140
- function getNodeProps(nodeId) {
3141
- const fiber = fiberRefMap.get(nodeId);
3142
- if (!fiber || !fiber.memoizedProps) {
3143
- return null;
3144
- }
3145
- try {
3146
- return serializeProps(fiber.memoizedProps);
3147
- } catch (error) {
3148
- console.error(`[FloTrace] Error serializing props for node "${nodeId}":`, error);
3149
- return null;
3150
- }
3151
- }
3152
- function detectDetailedRenderReason(fiber) {
3153
- if (!fiber.alternate) return { type: "mount" };
3154
- const prev = fiber.alternate;
3155
- if (shallowPropsChanged(prev.memoizedProps, fiber.memoizedProps)) {
3156
- const changedProps = diffProps(prev.memoizedProps, fiber.memoizedProps);
3157
- return { type: "props-changed", changedProps };
3158
- }
3159
- const changedHookIndices = diffHookStates(prev.memoizedState, fiber.memoizedState);
3160
- if (changedHookIndices.length > 0) {
3161
- return { type: "state-changed", changedHookIndices };
3162
- }
3163
- const changedContexts = detectContextChanges(fiber);
3164
- if (changedContexts.length > 0) {
3165
- return { type: "context-changed", contextNames: changedContexts };
3166
- }
3167
- const parentName = fiber.return ? getComponentName2(fiber.return) : void 0;
3168
- return { type: "parent-render", parentName };
3169
- }
3170
- function diffProps(prev, next) {
3171
- const changes = [];
3172
- if (!prev || !next) return changes;
3173
- const allKeys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
3174
- for (const key of allKeys) {
3175
- if (key === "children") continue;
3176
- if (!Object.is(prev[key], next[key])) {
3177
- changes.push({
3178
- key,
3179
- prev: serializeValue(prev[key], 0, /* @__PURE__ */ new WeakSet()),
3180
- next: serializeValue(next[key], 0, /* @__PURE__ */ new WeakSet())
3181
- });
3182
- }
3183
- }
3184
- return changes;
3185
- }
3186
- function diffHookStates(prev, next) {
3187
- const changed = [];
3188
- let prevHook = prev;
3189
- let nextHook = next;
3190
- let index = 0;
3191
- while (prevHook && nextHook) {
3192
- if (prevHook.queue !== null || nextHook.queue !== null) {
3193
- if (!Object.is(prevHook.memoizedState, nextHook.memoizedState)) {
3194
- changed.push(index);
3195
- }
3196
- }
3197
- prevHook = prevHook.next;
3198
- nextHook = nextHook.next;
3199
- index++;
3200
- }
3201
- return changed;
3202
- }
3203
- function detectContextChanges(fiber) {
3204
- const changed = [];
3205
- if (!fiber.dependencies?.firstContext) return changed;
3206
- let ctx = fiber.dependencies.firstContext;
3207
- while (ctx) {
3208
- try {
3209
- if (!Object.is(ctx.memoizedValue, ctx.context._currentValue)) {
3210
- const name = ctx.context.displayName || "UnknownContext";
3211
- changed.push(name);
3212
- }
3213
- } catch {
241
+ isPrewarmed = false;
242
+ if (earlyBuffer.length > 0) {
243
+ buffer = [...earlyBuffer, ...buffer];
244
+ rebuildRequestIndex();
245
+ earlyBuffer = [];
246
+ earlyRequestIndexMap.clear();
3214
247
  }
3215
- ctx = ctx.next;
3216
- }
3217
- return changed;
3218
- }
3219
- function getDetailedRenderReason(nodeId) {
3220
- const fiber = fiberRefMap.get(nodeId);
3221
- if (!fiber) return null;
3222
- try {
3223
- return detectDetailedRenderReason(fiber);
3224
- } catch (error) {
3225
- console.error(`[FloTrace] Error detecting render reason for "${nodeId}":`, error);
3226
- return null;
3227
248
  }
249
+ flushTimer = setInterval(flushBuffer, FLUSH_INTERVAL_MS);
250
+ flushBuffer();
3228
251
  }
3229
- function getNodeHooks(nodeId) {
3230
- const fiber = fiberRefMap.get(nodeId);
3231
- if (!fiber) return null;
3232
- try {
3233
- return inspectHooks(fiber);
3234
- } catch (error) {
3235
- console.error(`[FloTrace] Error inspecting hooks for node "${nodeId}":`, error);
3236
- return null;
252
+ function uninstallNetworkTracker() {
253
+ if (!isInstalled2 && !isPrewarmed) return;
254
+ if (previousFetch) {
255
+ globalThis.fetch = previousFetch;
256
+ previousFetch = null;
3237
257
  }
3238
- }
3239
- function getNodeEffects(nodeId) {
3240
- const fiber = fiberRefMap.get(nodeId);
3241
- if (!fiber) return null;
3242
- try {
3243
- return inspectEffects(fiber);
3244
- } catch (error) {
3245
- console.error(`[FloTrace] Error inspecting effects for node "${nodeId}":`, error);
3246
- return null;
258
+ if (originalXhrOpen) {
259
+ XMLHttpRequest.prototype.open = originalXhrOpen;
260
+ originalXhrOpen = null;
3247
261
  }
3248
- }
3249
- function getFiberRefMap() {
3250
- return fiberRefMap;
3251
- }
3252
- function uninstallFiberTreeWalker() {
3253
- if (!isInstalled4) return;
3254
- if (throttleTimer) {
3255
- clearTimeout(throttleTimer);
3256
- throttleTimer = null;
262
+ if (originalXhrSend) {
263
+ XMLHttpRequest.prototype.send = originalXhrSend;
264
+ originalXhrSend = null;
3257
265
  }
3258
- if (maxWaitTimer) {
3259
- clearTimeout(maxWaitTimer);
3260
- maxWaitTimer = null;
266
+ if (originalResponseJson) {
267
+ Response.prototype.json = originalResponseJson;
268
+ originalResponseJson = null;
3261
269
  }
3262
- cachedFiberRoot = null;
3263
- if (activeStrategy === "devtools" && typeof window !== "undefined") {
3264
- const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
3265
- if (hook) {
3266
- if (originalOnCommitFiberRoot) {
3267
- hook.onCommitFiberRoot = originalOnCommitFiberRoot;
3268
- } else {
3269
- delete hook.onCommitFiberRoot;
3270
- }
3271
- }
270
+ if (originalJsonParse) {
271
+ JSON.parse = originalJsonParse;
272
+ originalJsonParse = null;
3272
273
  }
3273
- originalOnCommitFiberRoot = null;
3274
- hookedRendererID = null;
3275
- activeStrategy = null;
3276
- fiberRefMap = /* @__PURE__ */ new Map();
3277
- previousFlatTree = null;
3278
- snapshotCounter = 0;
3279
- diffSeq = 0;
3280
- lastSnapshotSentTime = 0;
3281
- isInstalled4 = false;
3282
- try {
3283
- uninstallRscPayloadInterceptor();
3284
- } catch {
274
+ if (flushTimer) {
275
+ clearInterval(flushTimer);
276
+ flushTimer = null;
3285
277
  }
3286
- clearActionStateCache();
3287
- resetNextjsDetection();
3288
- console.log("[FloTrace] Fiber tree walker uninstalled");
278
+ if (isInstalled2) flushBuffer();
279
+ buffer = [];
280
+ earlyBuffer = [];
281
+ requestIndexMap.clear();
282
+ earlyRequestIndexMap.clear();
283
+ dedupeWindow.clear();
284
+ clearFetchOriginTags();
285
+ activeXhrRequestId = null;
286
+ activeXhrResponseText = null;
287
+ client2 = null;
288
+ isInstalled2 = false;
289
+ isPrewarmed = false;
3289
290
  }
3290
-
3291
- // src/storeUtils.ts
3292
- function serializeStoreState(state, logPrefix) {
3293
- const serialized = {};
3294
- for (const [key, value] of Object.entries(state)) {
3295
- try {
3296
- serialized[key] = serializeValue(value);
3297
- } catch (error) {
3298
- console.error(`[FloTrace] Error serializing ${logPrefix} key "${key}":`, error);
3299
- serialized[key] = { __type: "error", value: "Serialization failed" };
291
+ function patchFetch() {
292
+ if (typeof globalThis.fetch !== "function") return;
293
+ previousFetch = globalThis.fetch;
294
+ globalThis.fetch = async function trackedFetch(input, init) {
295
+ const url = extractUrl(input);
296
+ if (isNoiseUrl(url)) {
297
+ return previousFetch.call(globalThis, input, init);
3300
298
  }
3301
- }
3302
- return serialized;
3303
- }
3304
- function buildCorrelatedRequests(state, changedKeys) {
3305
- const byRequestId = /* @__PURE__ */ new Map();
3306
- for (const key of changedKeys) {
299
+ const method = (init?.method ?? "GET").toUpperCase();
300
+ const parsedUrl = parseUrl(url);
301
+ const entry = createEntry(method, parsedUrl, init);
302
+ const startTime = performance.now();
303
+ if (init?.signal) {
304
+ init.signal.addEventListener("abort", () => {
305
+ entry.state = "aborted";
306
+ entry.durationMs = performance.now() - startTime;
307
+ pushEntry(entry);
308
+ }, { once: true });
309
+ }
310
+ pushEntry({ ...entry });
3307
311
  try {
3308
- const rid = findFetchOrigin(state[key]);
3309
- if (rid) {
3310
- const keys = byRequestId.get(rid) ?? [];
3311
- keys.push(key);
3312
- byRequestId.set(rid, keys);
312
+ const response = await previousFetch.call(globalThis, input, init);
313
+ if (entry.state !== "aborted") {
314
+ entry.state = response.ok ? "success" : "error";
315
+ entry.status = response.status;
316
+ entry.durationMs = performance.now() - startTime;
317
+ entry.responseSizeBytes = parseContentLength(response.headers);
318
+ if (!response.ok) {
319
+ entry.errorMessage = `${response.status} ${response.statusText}`;
320
+ }
321
+ pushEntry(entry);
322
+ responseToRequestId.set(response, entry.requestId);
323
+ }
324
+ return response;
325
+ } catch (err) {
326
+ if (entry.state !== "aborted") {
327
+ entry.state = "error";
328
+ entry.durationMs = performance.now() - startTime;
329
+ entry.errorMessage = err instanceof Error ? err.message : String(err);
330
+ pushEntry(entry);
3313
331
  }
3314
- } catch {
332
+ throw err;
3315
333
  }
3316
- }
3317
- if (byRequestId.size === 0) return void 0;
3318
- return Array.from(byRequestId, ([requestId, storeKeys]) => ({ requestId, storeKeys }));
334
+ };
3319
335
  }
3320
-
3321
- // src/zustandTracker.ts
3322
- var activeUnsubscribers = [];
3323
- var isInstalled5 = false;
3324
- var debounceTimers = /* @__PURE__ */ new Map();
3325
- var DEBOUNCE_MS = 200;
3326
- function installZustandTracker(stores, client4) {
3327
- if (isInstalled5) {
3328
- console.warn("[FloTrace] Zustand tracker already installed, reinstalling");
3329
- uninstallZustandTracker();
3330
- }
3331
- isInstalled5 = true;
3332
- console.log("[FloTrace] Installing Zustand tracker for stores:", Object.keys(stores));
3333
- for (const [storeName, store] of Object.entries(stores)) {
3334
- if (!store || typeof store !== "object" && typeof store !== "function" || typeof store.getState !== "function" || typeof store.subscribe !== "function") {
3335
- console.warn(
3336
- `[FloTrace] Skipping "${storeName}" \u2014 not a valid Zustand store (missing getState/subscribe). Ensure you pass Zustand stores like: stores={{ myStore: useMyStore }}`
3337
- );
3338
- continue;
3339
- }
3340
- try {
3341
- const initialState = store.getState();
3342
- sendStoreUpdate(storeName, initialState, Object.keys(initialState), client4);
3343
- const unsubscribe = store.subscribe((newState, prevState) => {
336
+ function patchXhr() {
337
+ if (typeof XMLHttpRequest === "undefined") return;
338
+ originalXhrOpen = XMLHttpRequest.prototype.open;
339
+ originalXhrSend = XMLHttpRequest.prototype.send;
340
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
341
+ this.__ftMethod = method.toUpperCase();
342
+ this.__ftUrl = typeof url === "string" ? url : url.href;
343
+ const self = this;
344
+ this.addEventListener("load", function() {
345
+ const requestId = self.__ftRequestId;
346
+ if (!requestId) return;
347
+ if (self.responseType === "json" && self.response !== null && typeof self.response === "object") {
3344
348
  try {
3345
- scheduleStoreUpdate(storeName, prevState, newState, client4);
3346
- } catch (error) {
3347
- console.error(`[FloTrace] Error in Zustand subscribe callback for "${storeName}":`, error);
349
+ tagFetchData(self.response, requestId, 0);
350
+ } catch {
3348
351
  }
3349
- });
3350
- activeUnsubscribers.push(unsubscribe);
3351
- } catch (error) {
3352
- console.error(`[FloTrace] Failed to install tracker for Zustand store "${storeName}":`, error);
3353
- }
3354
- }
3355
- }
3356
- function uninstallZustandTracker() {
3357
- if (!isInstalled5) return;
3358
- for (const timer of debounceTimers.values()) {
3359
- clearTimeout(timer);
3360
- }
3361
- debounceTimers.clear();
3362
- for (const unsubscribe of activeUnsubscribers) {
3363
- try {
3364
- unsubscribe();
3365
- } catch (error) {
3366
- console.error("[FloTrace] Error unsubscribing from Zustand store:", error);
3367
- }
3368
- }
3369
- activeUnsubscribers = [];
3370
- isInstalled5 = false;
3371
- console.log("[FloTrace] Zustand tracker uninstalled");
3372
- }
3373
- function scheduleStoreUpdate(storeName, prevState, newState, client4) {
3374
- let changedKeys;
3375
- try {
3376
- changedKeys = getChangedKeys(prevState, newState);
3377
- } catch (error) {
3378
- console.error(`[FloTrace] Error diffing Zustand state for "${storeName}":`, error);
3379
- return;
3380
- }
3381
- if (changedKeys.length === 0) return;
3382
- const existing = debounceTimers.get(storeName);
3383
- if (existing) clearTimeout(existing);
3384
- debounceTimers.set(storeName, setTimeout(() => {
3385
- debounceTimers.delete(storeName);
3386
- sendStoreUpdate(storeName, newState, changedKeys, client4);
3387
- }, DEBOUNCE_MS));
3388
- }
3389
- function sendStoreUpdate(storeName, state, changedKeys, client4) {
3390
- try {
3391
- if (!client4.connected) return;
3392
- client4.sendImmediate({
3393
- type: "runtime:zustand",
3394
- storeName,
3395
- state: serializeStoreState(state, `Zustand "${storeName}"`),
3396
- changedKeys,
3397
- correlatedRequests: buildCorrelatedRequests(state, changedKeys),
3398
- timestamp: Date.now()
3399
- });
3400
- } catch (error) {
3401
- console.error(`[FloTrace] Error sending Zustand update for "${storeName}":`, error);
3402
- }
3403
- }
3404
-
3405
- // src/reduxTracker.ts
3406
- var activeUnsubscribe = null;
3407
- var isInstalled6 = false;
3408
- var debounceTimer = null;
3409
- var previousState = null;
3410
- var DEBOUNCE_MS2 = 200;
3411
- function isReduxStore(obj) {
3412
- return typeof obj === "object" && obj !== null && typeof obj.getState === "function" && typeof obj.subscribe === "function" && typeof obj.dispatch === "function";
3413
- }
3414
- function installReduxTracker(store, client4) {
3415
- if (isInstalled6) {
3416
- console.warn("[FloTrace] Redux tracker already installed, reinstalling");
3417
- uninstallReduxTracker();
3418
- }
3419
- isInstalled6 = true;
3420
- console.log("[FloTrace] Installing Redux tracker");
3421
- try {
3422
- const initialState = store.getState();
3423
- previousState = initialState;
3424
- sendReduxUpdate(initialState, Object.keys(initialState), client4);
3425
- activeUnsubscribe = store.subscribe(() => {
3426
- try {
3427
- const newState = store.getState();
3428
- scheduleReduxUpdate(newState, client4);
3429
- } catch (error) {
3430
- console.error("[FloTrace] Error in Redux subscribe callback:", error);
352
+ return;
353
+ }
354
+ const text = self.responseText;
355
+ if (text) {
356
+ activeXhrRequestId = requestId;
357
+ activeXhrResponseText = text;
3431
358
  }
3432
359
  });
3433
- } catch (error) {
3434
- console.error("[FloTrace] Failed to install Redux tracker:", error);
3435
- isInstalled6 = false;
3436
- }
3437
- }
3438
- function uninstallReduxTracker() {
3439
- if (!isInstalled6) return;
3440
- if (debounceTimer) {
3441
- clearTimeout(debounceTimer);
3442
- debounceTimer = null;
3443
- }
3444
- if (activeUnsubscribe) {
3445
- try {
3446
- activeUnsubscribe();
3447
- } catch (error) {
3448
- console.error("[FloTrace] Error unsubscribing from Redux store:", error);
360
+ return originalXhrOpen.apply(this, [method, url, ...rest]);
361
+ };
362
+ XMLHttpRequest.prototype.send = function(body) {
363
+ const meta = this;
364
+ const url = meta.__ftUrl ?? "";
365
+ if (isNoiseUrl(url)) {
366
+ return originalXhrSend.call(this, body);
3449
367
  }
3450
- activeUnsubscribe = null;
3451
- }
3452
- previousState = null;
3453
- isInstalled6 = false;
3454
- console.log("[FloTrace] Redux tracker uninstalled");
3455
- }
3456
- function scheduleReduxUpdate(newState, client4) {
3457
- let changedKeys;
3458
- try {
3459
- changedKeys = getChangedKeys(previousState ?? {}, newState);
3460
- } catch (error) {
3461
- console.error("[FloTrace] Error diffing Redux state:", error);
3462
- return;
3463
- }
3464
- if (changedKeys.length === 0) return;
3465
- previousState = newState;
3466
- if (debounceTimer) clearTimeout(debounceTimer);
3467
- debounceTimer = setTimeout(() => {
3468
- debounceTimer = null;
3469
- sendReduxUpdate(newState, changedKeys, client4);
3470
- }, DEBOUNCE_MS2);
3471
- }
3472
- function sendReduxUpdate(state, changedKeys, client4) {
3473
- try {
3474
- if (!client4.connected) return;
3475
- client4.sendImmediate({
3476
- type: "runtime:redux",
3477
- state: serializeStoreState(state, "Redux"),
3478
- changedKeys,
3479
- correlatedRequests: buildCorrelatedRequests(state, changedKeys),
3480
- timestamp: Date.now()
368
+ const method = meta.__ftMethod ?? "GET";
369
+ const parsedUrl = parseUrl(url);
370
+ const entry = createEntry(method, parsedUrl);
371
+ const startTime = performance.now();
372
+ this.__ftRequestId = entry.requestId;
373
+ pushEntry({ ...entry });
374
+ this.addEventListener("load", () => {
375
+ entry.state = this.status >= 400 ? "error" : "success";
376
+ entry.status = this.status;
377
+ entry.durationMs = performance.now() - startTime;
378
+ entry.responseSizeBytes = parseXhrContentLength(this);
379
+ if (this.status >= 400) {
380
+ entry.errorMessage = `${this.status} ${this.statusText}`;
381
+ }
382
+ pushEntry(entry);
3481
383
  });
3482
- } catch (error) {
3483
- console.error("[FloTrace] Error sending Redux update:", error);
3484
- }
3485
- }
3486
-
3487
- // src/tanstackQueryTracker.ts
3488
- var isInstalled7 = false;
3489
- var queryUnsubscribe = null;
3490
- var mutationUnsubscribe = null;
3491
- var debounceTimer2 = null;
3492
- var DEBOUNCE_MS3 = 300;
3493
- var MAX_EVENTS_PER_QUERY = 50;
3494
- var queryTracking = /* @__PURE__ */ new Map();
3495
- var CORRELATION_WINDOW_MS = 500;
3496
- var MAX_COMPLETED_CORRELATIONS = 20;
3497
- var correlationCounter = 0;
3498
- var pendingCorrelations = /* @__PURE__ */ new Map();
3499
- var completedCorrelations = [];
3500
- var mutationPrevStatus = /* @__PURE__ */ new Map();
3501
- var mutationCorrelationMap = /* @__PURE__ */ new Map();
3502
- function isTanStackQueryClient(obj) {
3503
- if (!obj || typeof obj !== "object") return false;
3504
- const candidate = obj;
3505
- return typeof candidate.getQueryCache === "function" && typeof candidate.getMutationCache === "function";
384
+ this.addEventListener("error", () => {
385
+ entry.state = "error";
386
+ entry.durationMs = performance.now() - startTime;
387
+ entry.errorMessage = "Network error";
388
+ pushEntry(entry);
389
+ });
390
+ this.addEventListener("abort", () => {
391
+ entry.state = "aborted";
392
+ entry.durationMs = performance.now() - startTime;
393
+ pushEntry(entry);
394
+ });
395
+ return originalXhrSend.call(this, body);
396
+ };
3506
397
  }
3507
- function installTanStackQueryTracker(queryClient, client4) {
3508
- if (isInstalled7) {
3509
- console.warn("[FloTrace] TanStack Query tracker already installed, reinstalling");
3510
- uninstallTanStackQueryTracker();
3511
- }
3512
- isInstalled7 = true;
3513
- console.log("[FloTrace] Installing TanStack Query tracker");
3514
- try {
3515
- const queryCache = queryClient.getQueryCache();
3516
- const mutationCache = queryClient.getMutationCache();
3517
- for (const query of queryCache.getAll()) {
3518
- if (!queryTracking.has(query.queryHash)) {
3519
- initQueryTracking(query);
3520
- }
3521
- }
3522
- for (const mutation of mutationCache.getAll()) {
3523
- mutationPrevStatus.set(mutation.mutationId, mutation.state.status);
3524
- }
3525
- sendSnapshot(queryCache, mutationCache, client4);
3526
- queryUnsubscribe = queryCache.subscribe((event) => {
398
+ function patchResponseJson() {
399
+ if (typeof Response === "undefined") return;
400
+ originalResponseJson = Response.prototype.json;
401
+ Response.prototype.json = async function() {
402
+ const data = await originalResponseJson.call(this);
403
+ const requestId = responseToRequestId.get(this);
404
+ if (requestId && data !== null && typeof data === "object") {
3527
405
  try {
3528
- if (event.type === "added" || event.type === "removed" || event.type === "updated") {
3529
- if (event.query) {
3530
- updateQueryTracking(event.query, event.type);
3531
- }
3532
- scheduleSnapshot2(queryCache, mutationCache, client4);
3533
- }
3534
- } catch (error) {
3535
- console.error("[FloTrace] Error in TanStack Query cache subscribe callback:", error);
406
+ tagFetchData(data, requestId, 0);
407
+ } catch {
3536
408
  }
3537
- });
3538
- mutationUnsubscribe = mutationCache.subscribe((event) => {
409
+ }
410
+ return data;
411
+ };
412
+ }
413
+ function patchJsonParse() {
414
+ originalJsonParse = JSON.parse;
415
+ JSON.parse = function(text, reviver) {
416
+ const result = originalJsonParse.call(JSON, text, reviver);
417
+ if (activeXhrRequestId !== null && activeXhrResponseText !== null && text === activeXhrResponseText && result !== null && typeof result === "object") {
3539
418
  try {
3540
- if (event.mutation) {
3541
- updateMutationTracking(event.mutation, queryCache, mutationCache, client4);
3542
- }
3543
- scheduleSnapshot2(queryCache, mutationCache, client4);
3544
- } catch (error) {
3545
- console.error("[FloTrace] Error in TanStack Mutation cache subscribe callback:", error);
419
+ tagFetchData(result, activeXhrRequestId, 0);
420
+ } catch {
3546
421
  }
3547
- });
3548
- } catch (error) {
3549
- console.error("[FloTrace] Failed to install TanStack Query tracker:", error);
3550
- isInstalled7 = false;
3551
- }
3552
- }
3553
- function uninstallTanStackQueryTracker() {
3554
- if (!isInstalled7) return;
3555
- if (debounceTimer2) {
3556
- clearTimeout(debounceTimer2);
3557
- debounceTimer2 = null;
3558
- }
3559
- if (queryUnsubscribe) {
3560
- try {
3561
- queryUnsubscribe();
3562
- } catch (e) {
3563
- console.error("[FloTrace] Error unsubscribing from QueryCache:", e);
3564
- }
3565
- queryUnsubscribe = null;
3566
- }
3567
- if (mutationUnsubscribe) {
3568
- try {
3569
- mutationUnsubscribe();
3570
- } catch (e) {
3571
- console.error("[FloTrace] Error unsubscribing from MutationCache:", e);
422
+ activeXhrRequestId = null;
423
+ activeXhrResponseText = null;
3572
424
  }
3573
- mutationUnsubscribe = null;
3574
- }
3575
- for (const pending of pendingCorrelations.values()) {
3576
- clearTimeout(pending.timeoutId);
3577
- }
3578
- pendingCorrelations.clear();
3579
- isInstalled7 = false;
3580
- console.log("[FloTrace] TanStack Query tracker uninstalled");
3581
- }
3582
- function computeDataHash(data) {
3583
- if (data === null || data === void 0) return "__null__";
3584
- try {
3585
- return JSON.stringify(data);
3586
- } catch {
3587
- return "__unhashable__";
3588
- }
425
+ return result;
426
+ };
3589
427
  }
3590
- function initQueryTracking(query) {
3591
- const state = {
3592
- lastDataHash: computeDataHash(query.state.data),
3593
- lastDataUpdatedAt: query.state.dataUpdatedAt,
3594
- prevStatus: query.state.status,
3595
- prevFetchStatus: query.state.fetchStatus,
3596
- totalFetchCount: 0,
3597
- wastedRefetchCount: 0,
3598
- events: []
428
+ function createEntry(method, parsedUrl, init) {
429
+ const requestId = String(++requestCounter);
430
+ const dedupeKey = `${method}:${parsedUrl.path}`;
431
+ const attribution = getAttribution();
432
+ const isServerAction = hasHeader(init, "Next-Action");
433
+ const isPrefetch = hasHeader(init, "Next-Router-Prefetch");
434
+ const now = Date.now();
435
+ const isDuplicate = checkDuplicate(dedupeKey, now);
436
+ return {
437
+ requestId,
438
+ method,
439
+ urlPath: parsedUrl.path,
440
+ urlHost: parsedUrl.host,
441
+ status: 0,
442
+ durationMs: null,
443
+ responseSizeBytes: null,
444
+ componentName: attribution.componentName,
445
+ ancestorChain: attribution.ancestorChain,
446
+ initiatedDuringRender: attribution.duringRender,
447
+ initiatedInEffect: attribution.inEffect,
448
+ state: "pending",
449
+ dedupeKey,
450
+ isDuplicate: isDuplicate || void 0,
451
+ isServerAction: isServerAction || void 0,
452
+ isPrefetch: isPrefetch || void 0,
453
+ timestamp: now
3599
454
  };
3600
- queryTracking.set(query.queryHash, state);
3601
- return state;
3602
455
  }
3603
- function updateQueryTracking(query, eventType) {
3604
- let tracking = queryTracking.get(query.queryHash);
3605
- if (eventType === "removed") {
3606
- queryTracking.delete(query.queryHash);
3607
- return;
3608
- }
3609
- if (!tracking) {
3610
- tracking = initQueryTracking(query);
3611
- }
3612
- const currentStatus = query.state.status;
3613
- const currentFetchStatus = query.state.fetchStatus;
3614
- const statusChanged = tracking.prevStatus !== currentStatus;
3615
- const fetchStatusChanged = tracking.prevFetchStatus !== currentFetchStatus;
3616
- if (statusChanged || fetchStatusChanged) {
3617
- const currentDataHash = computeDataHash(query.state.data);
3618
- const dataChanged = currentDataHash !== tracking.lastDataHash;
3619
- const event = {
3620
- timestamp: Date.now(),
3621
- fromStatus: tracking.prevStatus,
3622
- toStatus: currentStatus,
3623
- fromFetchStatus: tracking.prevFetchStatus,
3624
- toFetchStatus: currentFetchStatus,
3625
- dataChanged
456
+ function getAttribution() {
457
+ const fiber = getCurrentRenderingFiber();
458
+ if (fiber) {
459
+ const name = getComponentNameFromFiber(fiber);
460
+ const ancestors = buildAncestorChain(fiber).slice(-MAX_ANCESTOR_CHAIN);
461
+ return {
462
+ componentName: name || void 0,
463
+ ancestorChain: ancestors.length > 0 ? ancestors : void 0,
464
+ duringRender: true,
465
+ inEffect: false
3626
466
  };
3627
- tracking.events.push(event);
3628
- if (tracking.events.length > MAX_EVENTS_PER_QUERY) {
3629
- tracking.events.shift();
3630
- }
3631
- if (tracking.prevFetchStatus === "fetching" && currentFetchStatus === "idle" && currentStatus === "success") {
3632
- tracking.totalFetchCount++;
3633
- if (!dataChanged) {
3634
- tracking.wastedRefetchCount++;
3635
- }
3636
- tracking.lastDataHash = currentDataHash;
3637
- tracking.lastDataUpdatedAt = query.state.dataUpdatedAt;
3638
- if (query.state.data !== null && query.state.data !== void 0) {
3639
- const rid = findFetchOrigin(query.state.data);
3640
- if (rid) tracking.pendingCorrelationId = rid;
3641
- }
3642
- }
3643
- if (tracking.prevFetchStatus === "idle" && currentFetchStatus === "fetching") {
3644
- const now = Date.now();
3645
- for (const pending of pendingCorrelations.values()) {
3646
- if (pending.idleQueryHashes.has(query.queryHash)) {
3647
- pending.affectedQueries.set(query.queryHash, {
3648
- fetchStartedAt: now,
3649
- queryKey: query.queryKey
3650
- });
3651
- }
3652
- }
3653
- }
3654
- tracking.prevStatus = currentStatus;
3655
- tracking.prevFetchStatus = currentFetchStatus;
3656
467
  }
468
+ return { duringRender: false, inEffect: false };
3657
469
  }
3658
- function openCorrelationWindow(mutation, queryCache, mutationCache, client4) {
3659
- const correlationId = `corr-${++correlationCounter}`;
3660
- const now = Date.now();
3661
- const idleQueryHashes = /* @__PURE__ */ new Set();
3662
- for (const query of queryCache.getAll()) {
3663
- if (query.state.fetchStatus === "idle") {
3664
- idleQueryHashes.add(query.queryHash);
3665
- }
3666
- }
3667
- const timeoutId = setTimeout(() => {
3668
- resolveCorrelation(correlationId, queryCache, mutationCache, client4);
3669
- }, CORRELATION_WINDOW_MS);
3670
- pendingCorrelations.set(correlationId, {
3671
- correlationId,
3672
- mutationId: mutation.mutationId,
3673
- mutationKey: mutation.options.mutationKey,
3674
- completedAt: now,
3675
- idleQueryHashes,
3676
- affectedQueries: /* @__PURE__ */ new Map(),
3677
- timeoutId
3678
- });
3679
- mutationCorrelationMap.set(mutation.mutationId, correlationId);
470
+ function extractUrl(input) {
471
+ if (typeof input === "string") return input;
472
+ if (input instanceof URL) return input.href;
473
+ return input.url;
3680
474
  }
3681
- function resolveCorrelation(correlationId, queryCache, mutationCache, client4) {
3682
- const pending = pendingCorrelations.get(correlationId);
3683
- if (!pending) return;
3684
- pendingCorrelations.delete(correlationId);
3685
- if (pending.affectedQueries.size === 0) return;
3686
- const affectedQueries = [];
3687
- for (const [queryHash, info] of pending.affectedQueries) {
3688
- const tracking = queryTracking.get(queryHash);
3689
- let queryKeySerialized;
3690
- try {
3691
- queryKeySerialized = serializeValue(info.queryKey);
3692
- } catch {
3693
- queryKeySerialized = "[serialization failed]";
3694
- }
3695
- affectedQueries.push({
3696
- queryHash,
3697
- queryKey: queryKeySerialized,
3698
- fetchStartedAt: info.fetchStartedAt,
3699
- latencyMs: info.fetchStartedAt - pending.completedAt,
3700
- // dataChanged is resolved from the latest tracking state if the fetch completed
3701
- dataChanged: tracking?.events.length ? tracking.events[tracking.events.length - 1].dataChanged : void 0
3702
- });
3703
- }
3704
- let mutationKeySerialized;
3705
- if (pending.mutationKey) {
3706
- try {
3707
- mutationKeySerialized = serializeValue(pending.mutationKey);
3708
- } catch {
3709
- mutationKeySerialized = "[serialization failed]";
3710
- }
3711
- }
3712
- const correlation = {
3713
- correlationId,
3714
- mutationId: pending.mutationId,
3715
- mutationKey: mutationKeySerialized,
3716
- mutationCompletedAt: pending.completedAt,
3717
- affectedQueries,
3718
- resolvedAt: Date.now()
3719
- };
3720
- completedCorrelations.push(correlation);
3721
- if (completedCorrelations.length > MAX_COMPLETED_CORRELATIONS) {
3722
- completedCorrelations = completedCorrelations.slice(-MAX_COMPLETED_CORRELATIONS);
475
+ function parseUrl(url) {
476
+ try {
477
+ const u = new URL(url, globalThis.location?.href ?? "http://localhost");
478
+ return { path: u.pathname, host: u.host };
479
+ } catch {
480
+ return { path: url.split("?")[0] ?? url, host: "" };
3723
481
  }
3724
- scheduleSnapshot2(queryCache, mutationCache, client4);
3725
482
  }
3726
- function updateMutationTracking(mutation, queryCache, mutationCache, client4) {
3727
- const currentStatus = mutation.state.status;
3728
- const prevStatus = mutationPrevStatus.get(mutation.mutationId);
3729
- mutationPrevStatus.set(mutation.mutationId, currentStatus);
3730
- if (prevStatus && prevStatus !== "success" && currentStatus === "success") {
3731
- openCorrelationWindow(mutation, queryCache, mutationCache, client4);
3732
- }
483
+ var COMBINED_NOISE_PATTERN = new RegExp(
484
+ NOISE_URL_PATTERNS.map((r) => r.source).join("|"),
485
+ "i"
486
+ );
487
+ function isNoiseUrl(url) {
488
+ return COMBINED_NOISE_PATTERN.test(url);
3733
489
  }
3734
- function scheduleSnapshot2(queryCache, mutationCache, client4) {
3735
- if (debounceTimer2) clearTimeout(debounceTimer2);
3736
- debounceTimer2 = setTimeout(() => {
3737
- debounceTimer2 = null;
3738
- sendSnapshot(queryCache, mutationCache, client4);
3739
- }, DEBOUNCE_MS3);
490
+ function parseIntOrNull(value) {
491
+ if (!value) return null;
492
+ const n = parseInt(value, 10);
493
+ return isNaN(n) ? null : n;
3740
494
  }
3741
- function serializeQueryData(data) {
3742
- if (data === null || data === void 0) return null;
3743
- try {
3744
- return serializeValue(data);
3745
- } catch {
3746
- return { __type: "truncated", originalType: typeof data };
3747
- }
495
+ function parseContentLength(headers) {
496
+ return parseIntOrNull(headers.get("content-length"));
3748
497
  }
3749
- function extractErrorMessage(error) {
3750
- try {
3751
- return error instanceof Error ? error.message : String(error);
3752
- } catch {
3753
- return "Unknown error";
3754
- }
498
+ function parseXhrContentLength(xhr) {
499
+ return parseIntOrNull(xhr.getResponseHeader("content-length"));
3755
500
  }
3756
- function serializeQuery(query) {
3757
- let queryKeySerialized;
3758
- try {
3759
- queryKeySerialized = serializeValue(query.queryKey);
3760
- } catch {
3761
- queryKeySerialized = "[serialization failed]";
3762
- }
3763
- const errorMessage = query.state.error ? extractErrorMessage(query.state.error) : void 0;
3764
- const tracking = queryTracking.get(query.queryHash);
3765
- const correlatedRequestId = tracking?.pendingCorrelationId;
3766
- if (correlatedRequestId && tracking) {
3767
- tracking.pendingCorrelationId = void 0;
501
+ function hasHeader(init, name) {
502
+ if (!init?.headers) return false;
503
+ if (init.headers instanceof Headers) return init.headers.has(name);
504
+ if (Array.isArray(init.headers)) return init.headers.some(([k]) => k.toLowerCase() === name.toLowerCase());
505
+ if (typeof init.headers === "object") {
506
+ return Object.keys(init.headers).some((k) => k.toLowerCase() === name.toLowerCase());
3768
507
  }
3769
- return {
3770
- queryKey: queryKeySerialized,
3771
- queryHash: query.queryHash,
3772
- status: query.state.status,
3773
- fetchStatus: query.state.fetchStatus,
3774
- dataUpdatedAt: query.state.dataUpdatedAt,
3775
- errorUpdatedAt: query.state.errorUpdatedAt,
3776
- isInvalidated: query.state.isInvalidated,
3777
- isStale: safeCall(() => query.isStale(), false),
3778
- isActive: safeCall(() => query.isActive(), false),
3779
- isDisabled: safeCall(() => query.isDisabled(), false),
3780
- failureCount: query.state.fetchFailureCount,
3781
- errorMessage,
3782
- observerCount: safeCall(() => query.getObserversCount(), 0),
3783
- staleTime: query.options.staleTime,
3784
- gcTime: query.options.gcTime,
3785
- // Phase 1: additional config for health analysis
3786
- refetchInterval: query.options.refetchInterval,
3787
- refetchOnWindowFocus: query.options.refetchOnWindowFocus,
3788
- refetchOnMount: query.options.refetchOnMount,
3789
- refetchOnReconnect: query.options.refetchOnReconnect,
3790
- networkMode: query.options.networkMode,
3791
- enabled: query.options.enabled,
3792
- retry: query.options.retry,
3793
- dataShape: serializeQueryData(query.state.data),
3794
- // Phase 2: wasted refetch tracking
3795
- wastedRefetchCount: tracking?.wastedRefetchCount,
3796
- totalFetchCount: tracking?.totalFetchCount,
3797
- // Phase 3: query timeline
3798
- events: tracking?.events.length ? [...tracking.events] : void 0,
3799
- correlatedRequestId
3800
- };
508
+ return false;
3801
509
  }
3802
- function serializeMutation(mutation) {
3803
- const errorMessage = mutation.state.error ? extractErrorMessage(mutation.state.error) : void 0;
3804
- let mutationKey;
3805
- if (mutation.options.mutationKey) {
3806
- try {
3807
- mutationKey = serializeValue(mutation.options.mutationKey);
3808
- } catch {
3809
- mutationKey = "[serialization failed]";
3810
- }
510
+ function checkDuplicate(dedupeKey, now) {
511
+ for (const [key, ts] of dedupeWindow) {
512
+ if (now - ts > DEDUPE_WINDOW_MS) dedupeWindow.delete(key);
3811
513
  }
3812
- return {
3813
- mutationId: mutation.mutationId,
3814
- status: mutation.state.status,
3815
- isPaused: mutation.state.isPaused,
3816
- submittedAt: mutation.state.submittedAt,
3817
- failureCount: mutation.state.failureCount,
3818
- errorMessage,
3819
- mutationKey,
3820
- scope: mutation.options.scope?.id,
3821
- lastCorrelationId: mutationCorrelationMap.get(mutation.mutationId)
3822
- };
514
+ const isDup = dedupeWindow.has(dedupeKey);
515
+ dedupeWindow.set(dedupeKey, now);
516
+ return isDup;
3823
517
  }
3824
- function sendSnapshot(queryCache, mutationCache, client4) {
3825
- try {
3826
- if (!client4.connected) return;
3827
- const queries = [];
3828
- for (const query of queryCache.getAll()) {
3829
- try {
3830
- queries.push(serializeQuery(query));
3831
- } catch (error) {
3832
- console.error(`[FloTrace] Error serializing query "${query.queryHash}":`, error);
3833
- }
3834
- }
3835
- const mutations = [];
3836
- const activeMutationIds = /* @__PURE__ */ new Set();
3837
- for (const mutation of mutationCache.getAll()) {
3838
- try {
3839
- activeMutationIds.add(mutation.mutationId);
3840
- mutations.push(serializeMutation(mutation));
3841
- } catch (error) {
3842
- console.error(`[FloTrace] Error serializing mutation ${mutation.mutationId}:`, error);
3843
- }
3844
- }
3845
- for (const id of mutationPrevStatus.keys()) {
3846
- if (!activeMutationIds.has(id)) {
3847
- mutationPrevStatus.delete(id);
3848
- mutationCorrelationMap.delete(id);
3849
- }
3850
- }
3851
- const correlations = completedCorrelations.length > 0 ? [...completedCorrelations] : void 0;
3852
- if (correlations) {
3853
- completedCorrelations = [];
3854
- }
3855
- client4.sendImmediate({
3856
- type: "runtime:tanstackQuery",
3857
- queries,
3858
- mutations,
3859
- correlations,
3860
- timestamp: Date.now()
3861
- });
3862
- } catch (error) {
3863
- console.error("[FloTrace] Error sending TanStack Query snapshot:", error);
518
+ function upsertAndPrune(entry, buf, idxMap, maxSize) {
519
+ const existingIdx = idxMap.get(entry.requestId);
520
+ if (existingIdx !== void 0 && existingIdx < buf.length && buf[existingIdx]?.requestId === entry.requestId) {
521
+ buf[existingIdx] = entry;
522
+ return buf;
3864
523
  }
3865
- }
3866
- function safeCall(fn, fallback) {
3867
- try {
3868
- return fn();
3869
- } catch {
3870
- return fallback;
524
+ idxMap.set(entry.requestId, buf.length);
525
+ buf.push(entry);
526
+ if (buf.length > maxSize) {
527
+ const pruned = buf.slice(-maxSize);
528
+ idxMap.clear();
529
+ for (let i = 0; i < pruned.length; i++) idxMap.set(pruned[i].requestId, i);
530
+ return pruned;
3871
531
  }
532
+ return buf;
3872
533
  }
3873
-
3874
- // src/routerTracker.ts
3875
- var isInstalled8 = false;
3876
- var debounceTimer3 = null;
3877
- var client3 = null;
3878
- var originalPushState = null;
3879
- var originalReplaceState = null;
3880
- var popstateHandler = null;
3881
- var DEBOUNCE_MS4 = 200;
3882
- function installRouterTracker(wsClient) {
3883
- if (isInstalled8) {
3884
- console.warn("[FloTrace] Router tracker already installed, reinstalling");
3885
- uninstallRouterTracker();
3886
- }
3887
- if (typeof window === "undefined" || typeof history === "undefined") {
3888
- console.warn("[FloTrace] Router tracker requires a browser environment");
534
+ function pushEntry(entry) {
535
+ if (client2 === null && isPrewarmed) {
536
+ earlyBuffer = upsertAndPrune(entry, earlyBuffer, earlyRequestIndexMap, MAX_BUFFER_SIZE);
3889
537
  return;
3890
538
  }
3891
- console.log("[FloTrace] Installing router tracker");
3892
- try {
3893
- isInstalled8 = true;
3894
- client3 = wsClient;
3895
- originalPushState = history.pushState.bind(history);
3896
- originalReplaceState = history.replaceState.bind(history);
3897
- history.pushState = function(data, unused, url) {
3898
- originalPushState(data, unused, url);
3899
- try {
3900
- scheduleRouterUpdate();
3901
- } catch (error) {
3902
- console.error("[FloTrace] Error in pushState handler:", error);
3903
- }
3904
- };
3905
- history.replaceState = function(data, unused, url) {
3906
- originalReplaceState(data, unused, url);
3907
- try {
3908
- scheduleRouterUpdate();
3909
- } catch (error) {
3910
- console.error("[FloTrace] Error in replaceState handler:", error);
3911
- }
3912
- };
3913
- popstateHandler = () => {
3914
- try {
3915
- scheduleRouterUpdate();
3916
- } catch (error) {
3917
- console.error("[FloTrace] Error in popstate handler:", error);
3918
- }
3919
- };
3920
- window.addEventListener("popstate", popstateHandler);
3921
- sendRouterUpdate();
3922
- } catch (error) {
3923
- console.error("[FloTrace] Failed to install router tracker:", error);
3924
- try {
3925
- uninstallRouterTracker();
3926
- } catch (_) {
3927
- }
3928
- }
539
+ buffer = upsertAndPrune(entry, buffer, requestIndexMap, MAX_BUFFER_SIZE);
540
+ if (buffer.length >= MAX_BATCH_SIZE) flushBuffer();
3929
541
  }
3930
- function uninstallRouterTracker() {
3931
- if (!isInstalled8) return;
3932
- if (debounceTimer3) {
3933
- clearTimeout(debounceTimer3);
3934
- debounceTimer3 = null;
3935
- }
3936
- try {
3937
- if (originalPushState) {
3938
- history.pushState = originalPushState;
3939
- originalPushState = null;
3940
- }
3941
- } catch (error) {
3942
- console.error("[FloTrace] Error restoring pushState:", error);
3943
- }
3944
- try {
3945
- if (originalReplaceState) {
3946
- history.replaceState = originalReplaceState;
3947
- originalReplaceState = null;
3948
- }
3949
- } catch (error) {
3950
- console.error("[FloTrace] Error restoring replaceState:", error);
3951
- }
3952
- try {
3953
- if (popstateHandler) {
3954
- window.removeEventListener("popstate", popstateHandler);
3955
- popstateHandler = null;
3956
- }
3957
- } catch (error) {
3958
- console.error("[FloTrace] Error removing popstate listener:", error);
542
+ function rebuildRequestIndex() {
543
+ requestIndexMap.clear();
544
+ for (let i = 0; i < buffer.length; i++) {
545
+ requestIndexMap.set(buffer[i].requestId, i);
3959
546
  }
3960
- client3 = null;
3961
- isInstalled8 = false;
3962
- console.log("[FloTrace] Router tracker uninstalled");
3963
547
  }
3964
- function scheduleRouterUpdate() {
3965
- if (debounceTimer3) clearTimeout(debounceTimer3);
3966
- debounceTimer3 = setTimeout(() => {
3967
- debounceTimer3 = null;
3968
- sendRouterUpdate();
3969
- }, DEBOUNCE_MS4);
3970
- }
3971
- function sendRouterUpdate() {
3972
- try {
3973
- if (!client3?.connected) return;
3974
- const pathname = window.location.pathname;
3975
- const searchParams = {};
3976
- const urlSearchParams = new URLSearchParams(window.location.search);
3977
- for (const [key, value] of urlSearchParams.entries()) {
3978
- searchParams[key] = value;
3979
- }
3980
- client3.sendImmediate({
3981
- type: "runtime:router",
3982
- pathname,
3983
- // Matched route params (e.g., :id) are not available from the History API.
3984
- // Future enhancement: extract from React Router's fiber context.
3985
- params: {},
3986
- searchParams,
3987
- timestamp: Date.now()
3988
- });
3989
- } catch (error) {
3990
- console.error("[FloTrace] Error sending router update:", error);
3991
- }
548
+ function flushBuffer() {
549
+ if (buffer.length === 0 || !client2?.connected) return;
550
+ client2.send({
551
+ type: "runtime:networkRequest",
552
+ requests: [...buffer],
553
+ timestamp: Date.now()
554
+ });
555
+ buffer = [];
556
+ requestIndexMap.clear();
3992
557
  }
3993
558
 
3994
559
  // src/FloTraceProvider.tsx
3995
- import { jsx } from "react/jsx-runtime";
560
+ import { Fragment, jsx } from "react/jsx-runtime";
3996
561
  var pendingCleanupTimer = null;
3997
562
  function safeTrackerOp(name, op) {
3998
563
  try {
@@ -4006,7 +571,20 @@ function useFloTrace() {
4006
571
  return useContext(FloTraceContext);
4007
572
  }
4008
573
  function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClient }) {
4009
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
574
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
575
+ console.warn(
576
+ "[FloTrace] FloTraceProvider (from @flotrace/runtime) detected a React Native environment. Install @flotrace/runtime-native and use FloTraceProviderNative instead. Skipping attach."
577
+ );
578
+ return /* @__PURE__ */ jsx(Fragment, { children });
579
+ }
580
+ const mergedConfig = {
581
+ ...DEFAULT_CONFIG,
582
+ // Web default: expose the current page URL as the `appUrl` in runtime:ready.
583
+ // Runtime-core defaults this to undefined so it stays platform-agnostic.
584
+ getAppUrl: () => typeof window !== "undefined" ? window.location.href : void 0,
585
+ platform: "web",
586
+ ...config
587
+ };
4010
588
  const [connected, setConnected] = React.useState(false);
4011
589
  const trackingOptionsRef = useRef({});
4012
590
  const storesRef = useRef(stores);
@@ -4017,7 +595,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4017
595
  queryClientRef.current = queryClient;
4018
596
  const enabledRef = useRef(mergedConfig.enabled);
4019
597
  enabledRef.current = mergedConfig.enabled;
4020
- if (mergedConfig.enabled && typeof WebSocket !== "undefined") {
598
+ if (mergedConfig.enabled && typeof window !== "undefined") {
4021
599
  getWebSocketClient(mergedConfig);
4022
600
  installFiberTreeWalker();
4023
601
  prewarmNetworkTracker();
@@ -4030,37 +608,37 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4030
608
  clearTimeout(pendingCleanupTimer);
4031
609
  pendingCleanupTimer = null;
4032
610
  }
4033
- const client4 = getWebSocketClient();
4034
- const unsubConnection = client4.onConnectionChange((isConnected) => {
611
+ const client3 = getWebSocketClient();
612
+ const unsubConnection = client3.onConnectionChange((isConnected) => {
4035
613
  setConnected(isConnected);
4036
614
  if (isConnected) {
4037
615
  requestFullSnapshot();
4038
616
  }
4039
617
  });
4040
- const unsubMessage = client4.onMessage((message) => {
618
+ const unsubMessage = client3.onMessage((message) => {
4041
619
  try {
4042
620
  switch (message.type) {
4043
621
  case "ext:ping":
4044
- client4.sendImmediate({ type: "runtime:ready", appName: mergedConfig.appName });
622
+ client3.sendImmediate({ type: "runtime:ready", appName: mergedConfig.appName });
4045
623
  break;
4046
624
  case "ext:startTracking":
4047
625
  trackingOptionsRef.current = message.options || {};
4048
626
  if (message.options?.trackZustand && storesRef.current && Object.keys(storesRef.current).length > 0) {
4049
- safeTrackerOp("Zustand install", () => installZustandTracker(storesRef.current, client4));
627
+ safeTrackerOp("Zustand install", () => installZustandTracker(storesRef.current, client3));
4050
628
  }
4051
629
  if (message.options?.trackRedux && reduxStoreRef.current) {
4052
- safeTrackerOp("Redux install", () => installReduxTracker(reduxStoreRef.current, client4));
630
+ safeTrackerOp("Redux install", () => installReduxTracker(reduxStoreRef.current, client3));
4053
631
  }
4054
632
  if (message.options?.trackTanstackQuery && queryClientRef.current) {
4055
- safeTrackerOp("TanStack Query install", () => installTanStackQueryTracker(queryClientRef.current, client4));
633
+ safeTrackerOp("TanStack Query install", () => installTanStackQueryTracker(queryClientRef.current, client3));
4056
634
  }
4057
635
  if (message.options?.trackRouter) {
4058
- safeTrackerOp("Router install", () => installRouterTracker(client4));
636
+ safeTrackerOp("Router install", () => installRouterTracker(client3));
4059
637
  }
4060
638
  if (message.options?.trackNetwork) {
4061
- safeTrackerOp("Network install", () => installNetworkTracker(client4));
639
+ safeTrackerOp("Network install", () => installNetworkTracker(client3));
4062
640
  }
4063
- safeTrackerOp("Timeline install", () => installTimelineTracker(client4));
641
+ safeTrackerOp("Timeline install", () => installTimelineTracker(client3));
4064
642
  console.log("[FloTrace] Tracking started with options:", message.options);
4065
643
  break;
4066
644
  case "ext:stopTracking":
@@ -4082,7 +660,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4082
660
  break;
4083
661
  case "ext:requestNodeProps": {
4084
662
  const props = getNodeProps(message.nodeId);
4085
- client4.sendImmediate({
663
+ client3.sendImmediate({
4086
664
  type: "runtime:nodeProps",
4087
665
  nodeId: message.nodeId,
4088
666
  props: props || {},
@@ -4092,7 +670,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4092
670
  }
4093
671
  case "ext:requestNodeHooks": {
4094
672
  const hooks = getNodeHooks(message.nodeId);
4095
- client4.sendImmediate({
673
+ client3.sendImmediate({
4096
674
  type: "runtime:nodeHooks",
4097
675
  nodeId: message.nodeId,
4098
676
  hooks: hooks || [],
@@ -4102,7 +680,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4102
680
  }
4103
681
  case "ext:requestNodeEffects": {
4104
682
  const effects = getNodeEffects(message.nodeId);
4105
- client4.sendImmediate({
683
+ client3.sendImmediate({
4106
684
  type: "runtime:nodeEffects",
4107
685
  nodeId: message.nodeId,
4108
686
  effects: effects || [],
@@ -4113,7 +691,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4113
691
  case "ext:requestDetailedRenderReason": {
4114
692
  const reason = getDetailedRenderReason(message.nodeId);
4115
693
  if (reason) {
4116
- client4.sendImmediate({
694
+ client3.sendImmediate({
4117
695
  type: "runtime:detailedRenderReason",
4118
696
  nodeId: message.nodeId,
4119
697
  reason,
@@ -4130,7 +708,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4130
708
  const events = getTimeline(message.nodeId);
4131
709
  const componentName = message.nodeId.split("/").pop()?.replace(/-\d+$/, "") ?? "Unknown";
4132
710
  for (const event of events) {
4133
- client4.sendImmediate({
711
+ client3.sendImmediate({
4134
712
  type: "runtime:timelineEvent",
4135
713
  nodeId: message.nodeId,
4136
714
  componentName,
@@ -4140,34 +718,34 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4140
718
  break;
4141
719
  }
4142
720
  case "ext:startNetworkCapture":
4143
- safeTrackerOp("Network capture start", () => installNetworkTracker(client4));
721
+ safeTrackerOp("Network capture start", () => installNetworkTracker(client3));
4144
722
  break;
4145
723
  case "ext:stopNetworkCapture":
4146
724
  safeTrackerOp("Network capture stop", uninstallNetworkTracker);
4147
725
  break;
4148
726
  // --- Individual tracker start/stop (sidebar panel show/hide) ---
4149
727
  case "ext:startReduxTracking":
4150
- if (reduxStoreRef.current) safeTrackerOp("Redux install", () => installReduxTracker(reduxStoreRef.current, client4));
728
+ if (reduxStoreRef.current) safeTrackerOp("Redux install", () => installReduxTracker(reduxStoreRef.current, client3));
4151
729
  break;
4152
730
  case "ext:stopReduxTracking":
4153
731
  safeTrackerOp("Redux uninstall", uninstallReduxTracker);
4154
732
  break;
4155
733
  case "ext:startRouterTracking":
4156
- safeTrackerOp("Router install", () => installRouterTracker(client4));
734
+ safeTrackerOp("Router install", () => installRouterTracker(client3));
4157
735
  break;
4158
736
  case "ext:stopRouterTracking":
4159
737
  safeTrackerOp("Router uninstall", uninstallRouterTracker);
4160
738
  break;
4161
739
  case "ext:startZustandTracking":
4162
740
  if (storesRef.current && Object.keys(storesRef.current).length > 0) {
4163
- safeTrackerOp("Zustand install", () => installZustandTracker(storesRef.current, client4));
741
+ safeTrackerOp("Zustand install", () => installZustandTracker(storesRef.current, client3));
4164
742
  }
4165
743
  break;
4166
744
  case "ext:stopZustandTracking":
4167
745
  safeTrackerOp("Zustand uninstall", uninstallZustandTracker);
4168
746
  break;
4169
747
  case "ext:startTanstackTracking":
4170
- if (queryClientRef.current) safeTrackerOp("TanStack Query install", () => installTanStackQueryTracker(queryClientRef.current, client4));
748
+ if (queryClientRef.current) safeTrackerOp("TanStack Query install", () => installTanStackQueryTracker(queryClientRef.current, client3));
4171
749
  break;
4172
750
  case "ext:stopTanstackTracking":
4173
751
  safeTrackerOp("TanStack Query uninstall", uninstallTanStackQueryTracker);
@@ -4179,7 +757,7 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4179
757
  console.error(`[FloTrace] Error handling message type "${message.type}":`, error);
4180
758
  }
4181
759
  });
4182
- client4.connect();
760
+ client3.connect();
4183
761
  return () => {
4184
762
  unsubConnection();
4185
763
  unsubMessage();
@@ -4199,10 +777,10 @@ function FloTraceProvider({ children, config = {}, stores, reduxStore, queryClie
4199
777
  const onRenderCallback = useCallback((id, phase, actualDuration, baseDuration, _startTime, commitTime) => {
4200
778
  try {
4201
779
  if (!enabledRef.current) return;
4202
- const client4 = getWebSocketClient();
4203
- if (!client4.connected) return;
780
+ const client3 = getWebSocketClient();
781
+ if (!client3.connected) return;
4204
782
  const normalizedPhase = phase === "nested-update" ? "update" : phase;
4205
- client4.send({
783
+ client3.send({
4206
784
  type: "runtime:render",
4207
785
  componentName: id,
4208
786
  phase: normalizedPhase,
@@ -4231,12 +809,12 @@ function withFloTrace(Component, displayName) {
4231
809
  if (!floTrace?.enabled) {
4232
810
  return;
4233
811
  }
4234
- const client4 = getWebSocketClient();
4235
- if (!client4.connected) {
812
+ const client3 = getWebSocketClient();
813
+ if (!client3.connected) {
4236
814
  return;
4237
815
  }
4238
816
  const normalizedPhase = phase === "nested-update" ? "update" : phase;
4239
- client4.send({
817
+ client3.send({
4240
818
  type: "runtime:render",
4241
819
  componentName: id,
4242
820
  phase: normalizedPhase,
@@ -4245,7 +823,7 @@ function withFloTrace(Component, displayName) {
4245
823
  timestamp: commitTime
4246
824
  });
4247
825
  if (floTrace.config.includeProps) {
4248
- client4.send({
826
+ client3.send({
4249
827
  type: "runtime:props",
4250
828
  componentName: id,
4251
829
  props: serializeProps(props),
@@ -4269,13 +847,13 @@ function useTrackProps(componentName, props) {
4269
847
  if (!floTrace?.enabled || !floTrace.config.includeProps) {
4270
848
  return;
4271
849
  }
4272
- const client4 = getWebSocketClient();
4273
- if (!client4.connected) {
850
+ const client3 = getWebSocketClient();
851
+ if (!client3.connected) {
4274
852
  return;
4275
853
  }
4276
854
  const changedKeys = getChangedKeys(prevPropsRef.current, props);
4277
855
  if (changedKeys.length > 0) {
4278
- client4.send({
856
+ client3.send({
4279
857
  type: "runtime:props",
4280
858
  componentName,
4281
859
  props: serializeProps(props),
@@ -4291,38 +869,11 @@ function useTrackProps(componentName, props) {
4291
869
  }, [componentName, props, floTrace?.enabled, floTrace?.config.includeProps]);
4292
870
  }
4293
871
  export {
4294
- DEFAULT_CONFIG,
4295
872
  FloTraceProvider,
4296
- FloTraceWebSocketClient,
4297
- disposeWebSocketClient,
4298
- getDetailedRenderReason,
4299
- getFiberRefMap,
4300
- getNodeEffects,
4301
- getNodeHooks,
4302
- getTimeline,
4303
- getWebSocketClient,
4304
- inspectEffects,
4305
- inspectHooks,
4306
- installFiberTreeWalker,
4307
873
  installNetworkTracker,
4308
- installReduxTracker,
4309
874
  installRouterTracker,
4310
- installTanStackQueryTracker,
4311
- installTimelineTracker,
4312
- installZustandTracker,
4313
- isReduxStore,
4314
- isTanStackQueryClient,
4315
- recordTimelineEvent,
4316
- requestTreeSnapshot,
4317
- serializeProps,
4318
- serializeValue,
4319
- uninstallFiberTreeWalker,
4320
875
  uninstallNetworkTracker,
4321
- uninstallReduxTracker,
4322
876
  uninstallRouterTracker,
4323
- uninstallTanStackQueryTracker,
4324
- uninstallTimelineTracker,
4325
- uninstallZustandTracker,
4326
877
  useFloTrace,
4327
878
  useTrackProps,
4328
879
  withFloTrace