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