@progen-labs/spectra 0.0.0-stage → 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -2
  3. package/bin/spectra.js +215 -0
  4. package/dist/assets/index-BvBsPok5.js +46 -0
  5. package/dist/assets/index-Da3Le5Px.css +1 -0
  6. package/dist/index.html +14 -0
  7. package/index.html +13 -0
  8. package/package.json +76 -4
  9. package/server/analysis/cpu.js +369 -0
  10. package/server/analysis/findings.js +551 -0
  11. package/server/analysis/locate.js +99 -0
  12. package/server/analysis/report.js +750 -0
  13. package/server/analysis/sources.js +249 -0
  14. package/server/analysis/trace.js +314 -0
  15. package/server/api.js +292 -0
  16. package/server/chrome.js +81 -0
  17. package/server/cli.js +94 -0
  18. package/server/config.js +59 -0
  19. package/server/dashboard.js +154 -0
  20. package/server/explore/boot.js +150 -0
  21. package/server/explore/classify.js +169 -0
  22. package/server/explore/discover.js +162 -0
  23. package/server/explore/evaluate-cli.js +101 -0
  24. package/server/explore/evaluate.js +165 -0
  25. package/server/explore/paths.js +105 -0
  26. package/server/explore/runner.js +94 -0
  27. package/server/explore/test-explore.js +139 -0
  28. package/server/explore/test-paths.js +75 -0
  29. package/server/framework.js +67 -0
  30. package/server/git.js +59 -0
  31. package/server/pdf/render.js +60 -0
  32. package/server/pdf/template.js +419 -0
  33. package/server/plugin.js +19 -0
  34. package/server/probe.browser.js +689 -0
  35. package/server/recorder.js +610 -0
  36. package/server/signin.js +27 -0
  37. package/server/test-analysis.js +424 -0
  38. package/server/test-compare.js +473 -0
  39. package/server/test-sources.js +136 -0
  40. package/shared/compare.js +977 -0
  41. package/shared/snapshot.js +77 -0
  42. package/src/App.jsx +361 -0
  43. package/src/api.js +51 -0
  44. package/src/components/AppearanceMenu.jsx +76 -0
  45. package/src/components/CodeRef.jsx +48 -0
  46. package/src/components/ComparePanel.jsx +143 -0
  47. package/src/components/CompareResults.jsx +473 -0
  48. package/src/components/ControlBar.jsx +336 -0
  49. package/src/components/CulpritSphere.jsx +218 -0
  50. package/src/components/Culprits.jsx +118 -0
  51. package/src/components/CulpritsHero.jsx +109 -0
  52. package/src/components/EvaluationPanel.jsx +160 -0
  53. package/src/components/FilePanel.jsx +225 -0
  54. package/src/components/FindingsList.jsx +381 -0
  55. package/src/components/HeatTrees.jsx +222 -0
  56. package/src/components/Icons.jsx +58 -0
  57. package/src/components/LongTasks.jsx +199 -0
  58. package/src/components/NetworkPanel.jsx +98 -0
  59. package/src/components/PageMap.jsx +329 -0
  60. package/src/components/Popover.jsx +51 -0
  61. package/src/components/ReactPanel.jsx +200 -0
  62. package/src/components/SidePicker.jsx +207 -0
  63. package/src/components/SnapshotButton.jsx +140 -0
  64. package/src/components/TimeBreakdown.jsx +45 -0
  65. package/src/components/TraceButton.jsx +76 -0
  66. package/src/components/TreeView.jsx +173 -0
  67. package/src/components/Vitals.jsx +163 -0
  68. package/src/culprits.js +127 -0
  69. package/src/download.js +18 -0
  70. package/src/format.js +26 -0
  71. package/src/heat.js +177 -0
  72. package/src/highlight.js +45 -0
  73. package/src/hooks/useCompare.js +187 -0
  74. package/src/hooks/useSession.js +73 -0
  75. package/src/layout.js +55 -0
  76. package/src/main.jsx +15 -0
  77. package/src/styles.css +2478 -0
  78. package/src/theme.js +75 -0
  79. package/src/themes.css +286 -0
  80. package/vite.config.js +8 -0
@@ -0,0 +1,689 @@
1
+ /*
2
+ * In-page probe, injected with Page.addScriptToEvaluateOnNewDocument so it
3
+ * runs before any application code. It is plain ES5-ish browser code: no
4
+ * imports, no build step.
5
+ *
6
+ * It collects what the trace cannot know:
7
+ * - React: per-component render counts, time, *why* each render happened
8
+ * and which props were recreated without changing (via the React DevTools
9
+ * global hook; React 16.9+ / 17 / 18 development builds).
10
+ * - Web vitals as the browser reported them: long tasks, long animation
11
+ * frames, layout shifts (with the shifting elements), slow interactions,
12
+ * LCP, FCP and resource timings.
13
+ */
14
+ (function () {
15
+ if (window.__PERF_PROBE__) return;
16
+ // Runtime hint injected by the profiler: { framework, bundler, isSSR }
17
+ var SPECTRA_FRAMEWORK = typeof window.__SPECTRA_FRAMEWORK__ !== 'undefined' ? window.__SPECTRA_FRAMEWORK__ : null;
18
+ var WANT_REACT = !SPECTRA_FRAMEWORK || SPECTRA_FRAMEWORK.framework === 'react' || SPECTRA_FRAMEWORK.framework === 'unknown';
19
+
20
+ var PERFORMED_WORK = 1;
21
+ var TAG_FUNCTION = 0;
22
+ var TAG_CLASS = 1;
23
+ var TAG_FORWARD_REF = 11;
24
+ var TAG_SIMPLE_MEMO = 15;
25
+ var TAG_HOST_COMPONENT = 5;
26
+ var TAG_HOST_PORTAL = 4;
27
+ var MAX_COMMITS = 3000;
28
+ var DEEP_COMPARE_LIMIT = 300;
29
+
30
+ var probe = {
31
+ active: false,
32
+ startedAt: 0,
33
+ renderers: 0,
34
+ sawTimings: false,
35
+ reactVersion: '',
36
+ stats: {},
37
+ typeFns: [],
38
+ typeIds: new WeakMap(),
39
+ commits: [],
40
+ commitCount: 0,
41
+ overheadMs: 0,
42
+ roots: new Set(),
43
+ };
44
+
45
+ var vitals = {
46
+ longtasks: [],
47
+ loafs: [],
48
+ shifts: [],
49
+ events: [],
50
+ lcp: null,
51
+ fcp: null,
52
+ };
53
+
54
+ // ---------------------------------------------------------------- helpers
55
+
56
+ function selectorOf(el) {
57
+ if (!el || el.nodeType !== 1) return '';
58
+ var parts = [];
59
+ for (
60
+ var n = el, i = 0;
61
+ n && n.nodeType === 1 && i < 4;
62
+ n = n.parentElement, i++
63
+ ) {
64
+ var s = n.tagName.toLowerCase();
65
+ if (n.id) {
66
+ parts.unshift(s + '#' + n.id);
67
+ break;
68
+ }
69
+ var testId = n.getAttribute && n.getAttribute('data-test');
70
+ if (testId) s += '[data-test="' + testId + '"]';
71
+ else if (n.classList && n.classList.length) {
72
+ s += '.' + Array.prototype.slice.call(n.classList, 0, 2).join('.');
73
+ }
74
+ parts.unshift(s);
75
+ }
76
+ return parts.join(' > ');
77
+ }
78
+
79
+ function looseEqual(a, b, depth) {
80
+ if (a === b) return true;
81
+ if (
82
+ depth <= 0 ||
83
+ !a ||
84
+ !b ||
85
+ typeof a !== 'object' ||
86
+ typeof b !== 'object'
87
+ )
88
+ return false;
89
+ if (a.$$typeof || b.$$typeof) return false;
90
+ var ka = Object.keys(a);
91
+ if (ka.length !== Object.keys(b).length || ka.length > 50) return false;
92
+ for (var i = 0; i < ka.length; i++) {
93
+ if (!looseEqual(a[ka[i]], b[ka[i]], depth - 1)) return false;
94
+ }
95
+ return true;
96
+ }
97
+
98
+ function sameFunctionSource(a, b) {
99
+ try {
100
+ return (
101
+ a.length === b.length &&
102
+ Function.prototype.toString.call(a) ===
103
+ Function.prototype.toString.call(b)
104
+ );
105
+ } catch (e) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ function componentFn(fiber) {
111
+ var t = fiber.type;
112
+ if (!t) return null;
113
+ if (typeof t === 'function') return t;
114
+ return t.render || t.type || null; // forwardRef / memo wrappers
115
+ }
116
+
117
+ function displayName(fiber, fn) {
118
+ var t = fiber.type;
119
+ return (
120
+ (t && t.displayName) ||
121
+ (fn && (fn.displayName || fn.name)) ||
122
+ (fiber.elementType && fiber.elementType.displayName) ||
123
+ 'Anonymous'
124
+ );
125
+ }
126
+
127
+ function typeIdOf(fn) {
128
+ var id = probe.typeIds.get(fn);
129
+ if (id === undefined) {
130
+ id = probe.typeFns.length;
131
+ probe.typeFns.push(fn);
132
+ probe.typeIds.set(fn, id);
133
+ }
134
+ return id;
135
+ }
136
+
137
+ // ------------------------------------------------- why did this render?
138
+
139
+ function changedPropKeys(prev, next) {
140
+ var keys = [];
141
+ if (prev === next) return keys;
142
+ if (!prev || !next) return ['(props)'];
143
+ var seen = {};
144
+ var k;
145
+ for (k in next) {
146
+ seen[k] = true;
147
+ if (prev[k] !== next[k]) keys.push(k);
148
+ }
149
+ for (k in prev) if (!seen[k] && next[k] !== prev[k]) keys.push(k);
150
+ return keys;
151
+ }
152
+
153
+ function hooksStateChanged(prev, next) {
154
+ for (var a = prev, b = next; a && b; a = a.next, b = b.next) {
155
+ // Only useState/useReducer hooks own a queue; memo/effect/ref hooks
156
+ // change their memoizedState on every render and would be noise.
157
+ if (b.queue && a.memoizedState !== b.memoizedState) return true;
158
+ }
159
+ return false;
160
+ }
161
+
162
+ function contextChanged(prevDeps, nextDeps) {
163
+ var a = prevDeps && prevDeps.firstContext;
164
+ var b = nextDeps && nextDeps.firstContext;
165
+ while (a && b) {
166
+ if (a.memoizedValue !== b.memoizedValue) return true;
167
+ a = a.next;
168
+ b = b.next;
169
+ }
170
+ return false;
171
+ }
172
+
173
+ function classifyPropChange(stat, key, prevValue, nextValue) {
174
+ var rec =
175
+ stat.props[key] ||
176
+ (stat.props[key] = { n: 0, sampled: 0, fn: 0, obj: 0 });
177
+ rec.n++;
178
+ if (stat.renders > DEEP_COMPARE_LIMIT) return;
179
+ rec.sampled++;
180
+ if (typeof prevValue === 'function' && typeof nextValue === 'function') {
181
+ if (sameFunctionSource(prevValue, nextValue)) rec.fn++;
182
+ } else if (
183
+ prevValue &&
184
+ nextValue &&
185
+ typeof prevValue === 'object' &&
186
+ typeof nextValue === 'object'
187
+ ) {
188
+ if (looseEqual(prevValue, nextValue, 3)) rec.obj++;
189
+ }
190
+ }
191
+
192
+ function childrenDuration(fiber) {
193
+ var sum = 0;
194
+ for (var c = fiber.child; c; c = c.sibling) sum += c.actualDuration || 0;
195
+ return sum;
196
+ }
197
+
198
+ function record(fiber, alt, parentName, commit) {
199
+ var fn = componentFn(fiber);
200
+ if (!fn) return;
201
+ var id = typeIdOf(fn);
202
+ var stat = probe.stats[id];
203
+ if (!stat) {
204
+ stat = probe.stats[id] = {
205
+ id: id,
206
+ name: displayName(fiber, fn),
207
+ kind: fiber.tag === TAG_CLASS ? 'class' : 'function',
208
+ renders: 0,
209
+ mounts: 0,
210
+ wasted: 0,
211
+ selfMs: 0,
212
+ totalMs: 0,
213
+ maxMs: 0,
214
+ reasons: { props: 0, state: 0, hooks: 0, context: 0, parent: 0 },
215
+ props: {},
216
+ parents: {},
217
+ };
218
+ }
219
+ stat.renders++;
220
+ commit.rendered++;
221
+
222
+ var total = fiber.actualDuration || 0;
223
+ var self = Math.max(0, total - childrenDuration(fiber));
224
+ stat.selfMs += self;
225
+ stat.totalMs += total;
226
+ if (self > stat.maxMs) stat.maxMs = self;
227
+ if (parentName)
228
+ stat.parents[parentName] = (stat.parents[parentName] || 0) + 1;
229
+
230
+ if (!alt) {
231
+ stat.mounts++;
232
+ return;
233
+ }
234
+ var keys = changedPropKeys(alt.memoizedProps, fiber.memoizedProps);
235
+ var stateChanged = false;
236
+ var hooksChanged = false;
237
+ if (fiber.tag === TAG_CLASS)
238
+ stateChanged = alt.memoizedState !== fiber.memoizedState;
239
+ else
240
+ hooksChanged = hooksStateChanged(alt.memoizedState, fiber.memoizedState);
241
+ var ctxChanged = contextChanged(alt.dependencies, fiber.dependencies);
242
+
243
+ if (keys.length) {
244
+ stat.reasons.props++;
245
+ for (var i = 0; i < keys.length; i++) {
246
+ classifyPropChange(
247
+ stat,
248
+ keys[i],
249
+ alt.memoizedProps && alt.memoizedProps[keys[i]],
250
+ fiber.memoizedProps && fiber.memoizedProps[keys[i]],
251
+ );
252
+ }
253
+ }
254
+ if (stateChanged) stat.reasons.state++;
255
+ if (hooksChanged) stat.reasons.hooks++;
256
+ if (ctxChanged) stat.reasons.context++;
257
+ if (!keys.length && !stateChanged && !hooksChanged && !ctxChanged) {
258
+ stat.wasted++;
259
+ stat.reasons.parent++;
260
+ commit.wasted++;
261
+ }
262
+ }
263
+
264
+ function isComposite(tag) {
265
+ return (
266
+ tag === TAG_FUNCTION ||
267
+ tag === TAG_CLASS ||
268
+ tag === TAG_FORWARD_REF ||
269
+ tag === TAG_SIMPLE_MEMO
270
+ );
271
+ }
272
+
273
+ function walkCommit(rootFiber, commit) {
274
+ var stack = [rootFiber, ''];
275
+ while (stack.length) {
276
+ var parentName = stack.pop();
277
+ var fiber = stack.pop();
278
+ var alt = fiber.alternate;
279
+ var flags = fiber.flags !== undefined ? fiber.flags : fiber.effectTag;
280
+ var name = parentName;
281
+ if (isComposite(fiber.tag) && (!alt || flags & PERFORMED_WORK)) {
282
+ record(fiber, alt, parentName, commit);
283
+ name = displayName(fiber, componentFn(fiber));
284
+ } else if (isComposite(fiber.tag)) {
285
+ name = displayName(fiber, componentFn(fiber));
286
+ }
287
+ // A bailed-out subtree shares its child pointer with the previous tree.
288
+ if (fiber.child && (!alt || fiber.child !== alt.child)) {
289
+ for (var c = fiber.child; c; c = c.sibling) stack.push(c, name);
290
+ }
291
+ }
292
+ }
293
+
294
+ function onCommit(root) {
295
+ probe.roots.add(root);
296
+ // Only development/profiling builds of React maintain actualDuration.
297
+ if (typeof (root.current && root.current.actualDuration) === 'number')
298
+ probe.sawTimings = true;
299
+ if (!probe.active) return;
300
+ var t0 = performance.now();
301
+ var commit = {
302
+ t: t0,
303
+ dur: (root.current && root.current.actualDuration) || 0,
304
+ rendered: 0,
305
+ wasted: 0,
306
+ };
307
+ try {
308
+ walkCommit(root.current, commit);
309
+ } catch (e) {
310
+ probe.lastError = String(e && e.message);
311
+ }
312
+ probe.commitCount++;
313
+ if (probe.commits.length < MAX_COMMITS) probe.commits.push(commit);
314
+ probe.overheadMs += performance.now() - t0;
315
+ }
316
+
317
+ // ------------------------------------------------ DevTools global hook
318
+
319
+ function installHook() {
320
+ var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
321
+ if (!hook) {
322
+ var nextId = 1;
323
+ hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
324
+ renderers: new Map(),
325
+ supportsFiber: true,
326
+ isDisabled: false,
327
+ inject: function (renderer) {
328
+ var id = nextId++;
329
+ hook.renderers.set(id, renderer);
330
+ return id;
331
+ },
332
+ onCommitFiberRoot: function () {},
333
+ onCommitFiberUnmount: function () {},
334
+ onScheduleFiberRoot: function () {},
335
+ onPostCommitFiberRoot: function () {},
336
+ checkDCE: function () {},
337
+ };
338
+ }
339
+ var originalInject = hook.inject;
340
+ hook.inject = function (renderer) {
341
+ probe.renderers++;
342
+ probe.reactVersion = (renderer && renderer.version) || probe.reactVersion;
343
+ return originalInject.apply(this, arguments);
344
+ };
345
+ var originalCommit = hook.onCommitFiberRoot;
346
+ hook.onCommitFiberRoot = function (id, root) {
347
+ onCommit(root);
348
+ return originalCommit && originalCommit.apply(this, arguments);
349
+ };
350
+ if (hook.renderers && hook.renderers.size) probe.renderers = hook.renderers.size;
351
+ }
352
+
353
+ // ------------------------------------------------------------ vitals
354
+
355
+ function observe(type, options, handler) {
356
+ try {
357
+ if (!PerformanceObserver.supportedEntryTypes.includes(type)) return;
358
+ var po = new PerformanceObserver(function (list) {
359
+ list.getEntries().forEach(handler);
360
+ });
361
+ options.type = type;
362
+ options.buffered = true;
363
+ po.observe(options);
364
+ } catch (e) {
365
+ /* unsupported entry type */
366
+ }
367
+ }
368
+
369
+ function installObservers() {
370
+ observe('longtask', {}, function (e) {
371
+ vitals.longtasks.push({ start: e.startTime, dur: e.duration });
372
+ });
373
+ observe('long-animation-frame', {}, function (e) {
374
+ vitals.loafs.push({
375
+ start: e.startTime,
376
+ dur: e.duration,
377
+ blocking: e.blockingDuration || 0,
378
+ style: e.styleAndLayoutStart
379
+ ? e.startTime + e.duration - e.styleAndLayoutStart
380
+ : 0,
381
+ scripts: (e.scripts || []).slice(0, 8).map(function (s) {
382
+ return {
383
+ url: s.sourceURL,
384
+ fn: s.sourceFunctionName,
385
+ pos: s.sourceCharPosition,
386
+ invoker: s.invoker,
387
+ type: s.invokerType,
388
+ dur: s.duration,
389
+ forced: s.forcedStyleAndLayoutDuration || 0,
390
+ };
391
+ }),
392
+ });
393
+ });
394
+ observe('layout-shift', {}, function (e) {
395
+ if (e.hadRecentInput) return;
396
+ vitals.shifts.push({
397
+ start: e.startTime,
398
+ value: e.value,
399
+ sources: (e.sources || []).slice(0, 4).map(function (s) {
400
+ return selectorOf(s.node);
401
+ }),
402
+ });
403
+ });
404
+ observe('largest-contentful-paint', {}, function (e) {
405
+ vitals.lcp = {
406
+ start: e.startTime,
407
+ size: e.size,
408
+ el: selectorOf(e.element),
409
+ url: e.url || '',
410
+ };
411
+ });
412
+ observe('paint', {}, function (e) {
413
+ if (e.name === 'first-contentful-paint') vitals.fcp = e.startTime;
414
+ });
415
+ observe('event', { durationThreshold: 40 }, function (e) {
416
+ vitals.events.push({
417
+ name: e.name,
418
+ start: e.startTime,
419
+ dur: e.duration,
420
+ input: e.processingStart - e.startTime,
421
+ processing: e.processingEnd - e.processingStart,
422
+ present: e.startTime + e.duration - e.processingEnd,
423
+ target: selectorOf(e.target),
424
+ interaction: e.interactionId || 0,
425
+ });
426
+ });
427
+ }
428
+
429
+ // --------------------------------------------------------------- API
430
+
431
+ probe.start = function () {
432
+ probe.stats = {};
433
+ probe.commits = [];
434
+ probe.commitCount = 0;
435
+ probe.overheadMs = 0;
436
+ probe.startedAt = performance.now();
437
+ probe.active = true;
438
+ return probe.startedAt;
439
+ };
440
+
441
+ probe.stop = function () {
442
+ probe.active = false;
443
+ };
444
+
445
+ // Outermost DOM nodes a component renders (stops at the first host element).
446
+ function hostRect(fiber, vw, vh) {
447
+ var left = Infinity;
448
+ var top = Infinity;
449
+ var right = -Infinity;
450
+ var bottom = -Infinity;
451
+ var stack = [];
452
+ for (var c = fiber.child; c; c = c.sibling) stack.push(c);
453
+ while (stack.length) {
454
+ var f = stack.pop();
455
+ if (f.tag === TAG_HOST_COMPONENT) {
456
+ var el = f.stateNode;
457
+ if (!el || !el.getBoundingClientRect) continue;
458
+ var r = el.getBoundingClientRect();
459
+ if (r.width <= 0 || r.height <= 0) continue;
460
+ left = Math.min(left, r.left);
461
+ top = Math.min(top, r.top);
462
+ right = Math.max(right, r.right);
463
+ bottom = Math.max(bottom, r.bottom);
464
+ } else if (f.tag !== TAG_HOST_PORTAL) {
465
+ for (var k = f.child; k; k = k.sibling) stack.push(k);
466
+ }
467
+ }
468
+ left = Math.max(0, left);
469
+ top = Math.max(0, top);
470
+ right = Math.min(vw, right);
471
+ bottom = Math.min(vh, bottom);
472
+ if (right - left < 16 || bottom - top < 16) return null;
473
+ return [
474
+ Math.round(left),
475
+ Math.round(top),
476
+ Math.round(right - left),
477
+ Math.round(bottom - top),
478
+ ];
479
+ }
480
+
481
+ // Where every component that rendered during the recording sits in the viewport.
482
+ probe.layout = function () {
483
+ var vw = window.innerWidth;
484
+ var vh = window.innerHeight;
485
+ var byId = {};
486
+ probe.roots.forEach(function (root) {
487
+ var stack = [root.current];
488
+ while (stack.length) {
489
+ var f = stack.pop();
490
+ if (isComposite(f.tag)) {
491
+ var fn = componentFn(f);
492
+ var id = fn ? probe.typeIds.get(fn) : undefined;
493
+ if (id !== undefined && probe.stats[id]) {
494
+ var rect = hostRect(f, vw, vh);
495
+ if (rect) (byId[id] = byId[id] || []).push(rect);
496
+ }
497
+ }
498
+ for (var c = f.child; c; c = c.sibling) stack.push(c);
499
+ }
500
+ });
501
+ var entries = Object.keys(byId)
502
+ .map(function (id) {
503
+ var rects = byId[id]
504
+ .sort(function (a, b) {
505
+ return b[2] * b[3] - a[2] * a[3];
506
+ })
507
+ .slice(0, 6);
508
+ return { id: Number(id), rects: rects, count: byId[id].length };
509
+ })
510
+ .sort(function (a, b) {
511
+ return probe.stats[b.id].selfMs - probe.stats[a.id].selfMs;
512
+ })
513
+ .slice(0, 300);
514
+ return {
515
+ width: vw,
516
+ height: vh,
517
+ scrollX: Math.round(window.scrollX),
518
+ scrollY: Math.round(window.scrollY),
519
+ entries: entries,
520
+ };
521
+ };
522
+
523
+ probe.info = function () {
524
+ return {
525
+ react: probe.renderers > 0,
526
+ version: probe.reactVersion,
527
+ active: probe.active,
528
+ timings: probe.sawTimings,
529
+ };
530
+ };
531
+
532
+ probe.snapshot = function () {
533
+ var since = probe.startedAt;
534
+ var inWindow = function (e) {
535
+ return e.start >= since;
536
+ };
537
+ var resources = performance.getEntriesByType('resource').map(function (r) {
538
+ return {
539
+ url: r.name,
540
+ type: r.initiatorType,
541
+ start: r.startTime,
542
+ dur: r.duration,
543
+ transfer: r.transferSize,
544
+ encoded: r.encodedBodySize,
545
+ decoded: r.decodedBodySize,
546
+ blocking: r.renderBlockingStatus || '',
547
+ };
548
+ });
549
+ var nav = performance.getEntriesByType('navigation')[0];
550
+ return {
551
+ info: probe.info(),
552
+ now: performance.now(),
553
+ since: since,
554
+ overheadMs: probe.overheadMs,
555
+ commitCount: probe.commitCount,
556
+ commits: probe.commits,
557
+ components: Object.keys(probe.stats).map(function (k) {
558
+ return probe.stats[k];
559
+ }),
560
+ vitals: {
561
+ longtasks: vitals.longtasks.filter(inWindow),
562
+ loafs: vitals.loafs.filter(inWindow),
563
+ shifts: vitals.shifts.filter(inWindow),
564
+ events: vitals.events.filter(inWindow),
565
+ lcp: vitals.lcp,
566
+ fcp: vitals.fcp,
567
+ },
568
+ resources: resources,
569
+ navigation: nav
570
+ ? {
571
+ ttfb: nav.responseStart,
572
+ domContentLoaded: nav.domContentLoadedEventEnd,
573
+ load: nav.loadEventEnd,
574
+ }
575
+ : null,
576
+ };
577
+ };
578
+
579
+ window.__PERF_PROBE__ = probe;
580
+ // Only attempt to hook into React when the runtime hint allows it.
581
+ try {
582
+ if (WANT_REACT) installHook();
583
+ } catch (e) {
584
+ // Fail silently: probe should still collect vitals even if hooking fails.
585
+ }
586
+
587
+ // ------------------------------------------------ Framework fallbacks
588
+ // Provide lightweight probes for Vue, Svelte and Angular when React is not detected.
589
+ function installFrameworkProbes() {
590
+ var fw = SPECTRA_FRAMEWORK && SPECTRA_FRAMEWORK.framework ? SPECTRA_FRAMEWORK.framework : null;
591
+ if (!fw) return;
592
+
593
+ // Shared storage on probe for framework-specific summaries
594
+ probe.framework = probe.framework || {
595
+ name: fw,
596
+ detected: false,
597
+ version: '',
598
+ commits: [],
599
+ components: [],
600
+ commitCount: 0,
601
+ };
602
+
603
+ // MutationObserver-based heuristic for UI frameworks (Vue, Svelte): treat DOM updates as "commits".
604
+ try {
605
+ var mo = new MutationObserver(function (mutations) {
606
+ if (!probe.active) return;
607
+ var t = performance.now();
608
+ var rendered = 0;
609
+ var seen = new Set();
610
+ for (var m of mutations) {
611
+ rendered += (m.addedNodes && m.addedNodes.length) || 0;
612
+ // detect Vue component instances on added nodes (Vue 2: __vue__, Vue 3: __vueParentComponent)
613
+ var nodes = m.addedNodes || [];
614
+ for (var i = 0; i < nodes.length; i++) {
615
+ var n = nodes[i];
616
+ try {
617
+ if (n && n.__vue__) seen.add(n.__vue__);
618
+ if (n && n.__vueParentComponent) seen.add(n.__vueParentComponent);
619
+ // Svelte: look for nodes with attributes starting with 'data-svelte'
620
+ if (n && n.attributes) {
621
+ for (var j = 0; j < n.attributes.length; j++) {
622
+ var an = n.attributes[j].name || '';
623
+ if (an.indexOf('data-svelte') === 0) seen.add(an + (n.localName || ''));
624
+ }
625
+ }
626
+ } catch (e) {}
627
+ }
628
+ }
629
+ var commit = { t: t, dur: 0, rendered: rendered, components: seen.size };
630
+ probe.commits.push(commit);
631
+ probe.commitCount = (probe.commitCount || 0) + 1;
632
+ probe.framework.detected = true;
633
+ probe.framework.commitCount = probe.commitCount;
634
+ });
635
+ mo.observe(document, { childList: true, subtree: true });
636
+ } catch (e) {
637
+ // ignore
638
+ }
639
+
640
+ // Angular: if Zone.js is available, instrument Zone.run to measure change-detection durations
641
+ try {
642
+ if (fw === 'angular' && window.Zone && window.Zone.prototype && !window.__SPECTRA_ANGULAR_ZONE_PATCHED__) {
643
+ window.__SPECTRA_ANGULAR_ZONE_PATCHED__ = true;
644
+ var _origRun = window.Zone.prototype.run;
645
+ window.Zone.prototype.run = function () {
646
+ var start = performance.now();
647
+ try {
648
+ return _origRun.apply(this, arguments);
649
+ } finally {
650
+ try {
651
+ if (probe.active) {
652
+ var dur = performance.now() - start;
653
+ probe.commits.push({ t: start, dur: dur, rendered: 0, components: 0 });
654
+ probe.commitCount = (probe.commitCount || 0) + 1;
655
+ probe.framework.detected = true;
656
+ probe.framework.commitCount = probe.commitCount;
657
+ probe.framework.version = window.Zone.version || probe.framework.version;
658
+ }
659
+ } catch (e) {}
660
+ }
661
+ };
662
+ }
663
+ } catch (e) {}
664
+
665
+ // Next.js: treat as React unless React missing; try to detect Next router navigations for commits
666
+ try {
667
+ if (fw === 'next') {
668
+ // Next is usually React-based; provide basic navigational commit hints
669
+ if (window.next && window.next.router && window.next.router.events) {
670
+ var ev = window.next.router.events;
671
+ if (ev && ev.on) {
672
+ ev.on('routeChangeComplete', function () {
673
+ if (!probe.active) return;
674
+ var t = performance.now();
675
+ probe.commits.push({ t: t, dur: 0, rendered: 0, components: 0 });
676
+ probe.commitCount = (probe.commitCount || 0) + 1;
677
+ probe.framework.detected = true;
678
+ });
679
+ }
680
+ }
681
+ }
682
+ } catch (e) {}
683
+ }
684
+
685
+ try {
686
+ if (!WANT_REACT) installFrameworkProbes();
687
+ } catch (e) {}
688
+ installObservers();
689
+ })();