catalyst-core 0.2.0-beta.2 → 0.3.0-beta.1

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 (184) hide show
  1. package/babel.native.config.cjs +8 -0
  2. package/bin/catalyst.js +49 -105
  3. package/changelog.md +8 -0
  4. package/dist/caching.js +1 -1
  5. package/dist/index.jsx +8 -0
  6. package/dist/logger.js +74 -3
  7. package/dist/native/androidProject/app/build.gradle.kts +1 -0
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +143 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +26 -6
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +6 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +26 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
  19. package/dist/native/androidSetup.js +300 -8
  20. package/dist/native/bridge/WebBridge.js +481 -59
  21. package/dist/native/bridge/constants/NativeInterfaces.js +290 -44
  22. package/dist/native/bridge/errors.js +250 -26
  23. package/dist/native/bridge/hooks/index.js +125 -1
  24. package/dist/native/bridge/hooks/useCamera.js +347 -9
  25. package/dist/native/bridge/hooks/useDataProtection.js +200 -2
  26. package/dist/native/bridge/hooks/useDeviceInfo.js +71 -2
  27. package/dist/native/bridge/hooks/useFilePicker.js +429 -2
  28. package/dist/native/bridge/hooks/useGoogleSignIn.js +81 -1
  29. package/dist/native/bridge/hooks/useHapticFeedback.js +196 -1
  30. package/dist/native/bridge/hooks/useIntent.js +95 -1
  31. package/dist/native/bridge/hooks/useNativeTransition.js +202 -27
  32. package/dist/native/bridge/hooks/useNetworkStatus.js +84 -3
  33. package/dist/native/bridge/hooks/useNotification.js +318 -2
  34. package/dist/native/bridge/hooks/useSafeArea.js +84 -2
  35. package/dist/native/bridge/hooks/useVideoStream.js +434 -6
  36. package/dist/native/bridge/hooks/utils.js +27 -1
  37. package/dist/native/bridge/hooks.js +16 -1
  38. package/dist/native/bridge/perf/collectors/ApiCollector.js +85 -0
  39. package/dist/native/bridge/perf/collectors/CacheCollector.js +215 -0
  40. package/dist/native/bridge/perf/collectors/HookCollector.js +165 -0
  41. package/dist/native/bridge/perf/collectors/InsightsCollector.js +505 -0
  42. package/dist/native/bridge/perf/collectors/InteractionCollector.js +273 -0
  43. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +124 -0
  44. package/dist/native/bridge/perf/collectors/NavigationCollector.js +294 -0
  45. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +183 -0
  46. package/dist/native/bridge/perf/collectors/RenderCollector.js +284 -0
  47. package/dist/native/bridge/perf/collectors/ScrollCollector.js +111 -0
  48. package/dist/native/bridge/perf/collectors/ThreadCollector.js +111 -0
  49. package/dist/native/bridge/perf/core/constants.js +126 -0
  50. package/dist/native/bridge/perf/core/measure.js +189 -0
  51. package/dist/native/bridge/perf/core/session.js +100 -0
  52. package/dist/native/bridge/perf/core/store.js +554 -0
  53. package/dist/native/bridge/perf/core/summary.js +174 -0
  54. package/dist/native/bridge/perf/core/traceExport.js +240 -0
  55. package/dist/native/bridge/perf/core/waterfalls.js +146 -0
  56. package/dist/native/bridge/perf/index.js +402 -0
  57. package/dist/native/bridge/safeArea.js +53 -5
  58. package/dist/native/bridge/standardizedHooks.js +595 -82
  59. package/dist/native/bridge/useBaseHook.js +274 -38
  60. package/dist/native/bridge/utils/CameraUtils.js +201 -56
  61. package/dist/native/bridge/utils/FileObjectConverter.js +129 -18
  62. package/dist/native/bridge/utils/NativeBridge.js +425 -141
  63. package/dist/native/buildAndroid/assets.js +510 -13
  64. package/dist/native/buildAndroid/build.js +296 -19
  65. package/dist/native/buildAndroid/config.js +87 -1
  66. package/dist/native/buildAndroid/index.js +374 -1
  67. package/dist/native/buildAppAndroid.js +34 -2
  68. package/dist/native/buildAppIos.js +62 -2
  69. package/dist/native/buildIos/assets.js +609 -8
  70. package/dist/native/buildIos/build.js +639 -20
  71. package/dist/native/buildIos/config.js +294 -12
  72. package/dist/native/buildIos/index.js +343 -6
  73. package/dist/native/buildIos/plugins.js +343 -16
  74. package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +43 -0
  75. package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +28 -0
  76. package/dist/native/internal-plugins/device-info-plugin/manifest.json +19 -0
  77. package/dist/native/internalPluginUtils.js +232 -7
  78. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +10 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +373 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +35 -1
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +126 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +75 -1
  89. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
  90. package/dist/native/package.json +3 -0
  91. package/dist/native/plugin-bridge/PluginBridge.js +227 -1
  92. package/dist/native/pluginComposerAndroid.js +407 -23
  93. package/dist/native/pluginComposerIos.js +435 -11
  94. package/dist/native/renameAndroidProject.js +1094 -88
  95. package/dist/native/setupEmulatorIos.js +289 -22
  96. package/dist/native/setupServer.js +160 -19
  97. package/dist/native/terminalProgress.js +199 -11
  98. package/dist/native/utils.js +193 -15
  99. package/dist/offline/catalyst-sw.js +145 -1
  100. package/dist/offline/registerServiceWorker.js +33 -1
  101. package/dist/otel.js +375 -46
  102. package/dist/router/ClientRouter.js +14 -2
  103. package/dist/router/ServerRouter.js +9 -1
  104. package/dist/router.js +1 -1
  105. package/dist/scripts/build.js +112 -3
  106. package/dist/scripts/checkVersion.js +3 -1
  107. package/dist/scripts/cleanCache.js +30 -1
  108. package/dist/scripts/generateOfflineManifest.js +116 -1
  109. package/dist/scripts/loadEnvironmentVariables.js +32 -6
  110. package/dist/scripts/plugins.js +309 -1
  111. package/dist/scripts/preServerInit.js +13 -0
  112. package/dist/scripts/scriptUtils.js +54 -3
  113. package/dist/scripts/serve.js +51 -4
  114. package/dist/scripts/start.js +47 -10
  115. package/dist/scripts/validator.js +149 -1
  116. package/dist/sentry.cjs +127 -0
  117. package/dist/sentry.js +125 -5
  118. package/dist/server/expressServer.js +250 -5
  119. package/dist/server/manifestCache.js +36 -0
  120. package/dist/server/renderer/ChunkExtractor.js +144 -0
  121. package/dist/server/renderer/document/Body.jsx +59 -0
  122. package/dist/server/renderer/document/FastRefresh.jsx +20 -0
  123. package/dist/server/renderer/document/Head.jsx +60 -0
  124. package/dist/server/renderer/document/index.jsx +2 -0
  125. package/dist/server/renderer/extract.js +149 -31
  126. package/dist/server/renderer/handler.jsx +433 -0
  127. package/dist/server/renderer/index.js +8 -1
  128. package/dist/server/renderer/render.js +47 -0
  129. package/dist/server/utils/botDetectionMiddleware.js +29 -0
  130. package/dist/server/utils/cjsRequire.js +10 -0
  131. package/dist/server/utils/userAgentUtil.js +87 -9
  132. package/dist/server/utils/validator.js +164 -1
  133. package/dist/vite/FastRefresh.jsx +20 -0
  134. package/dist/vite/inject-cache-key-plugin.js +276 -0
  135. package/dist/vite/loadCustomViteConfig.js +22 -0
  136. package/dist/vite/manifest-categorization-plugin.js +455 -0
  137. package/dist/vite/node-loader.mjs +458 -0
  138. package/dist/vite/scssParams.js +21 -0
  139. package/dist/vite/vite.config.client.js +130 -0
  140. package/dist/vite/vite.config.js +422 -0
  141. package/dist/vite/vite.config.server.js +84 -0
  142. package/dist/web-router/components/MetaTag.jsx +34 -0
  143. package/dist/web-router/components/RouterDataProvider.jsx +332 -0
  144. package/dist/web-router/components/Split.jsx +200 -0
  145. package/dist/web-router/components/SplitInview.jsx +106 -0
  146. package/dist/web-router/components/SsrRequestContext.jsx +14 -0
  147. package/dist/web-router/context.jsx +12 -0
  148. package/dist/web-router/hooks.jsx +72 -0
  149. package/dist/web-router/types/components/MetaTag.d.ts +2 -0
  150. package/dist/web-router/types/components/RouterDataProvider.d.ts +95 -0
  151. package/dist/web-router/types/context.d.ts +14 -0
  152. package/dist/web-router/types/hooks.d.ts +1 -0
  153. package/dist/web-router/types/index.d.ts +3 -0
  154. package/dist/web-router/types/utils/metaDataUtils.d.ts +24 -0
  155. package/dist/web-router/utils/metaDataUtils.jsx +119 -0
  156. package/mcp_v2/conversion-tasks.json +35 -17
  157. package/mcp_v2/knowledge-base.json +124 -13
  158. package/mcp_v2/lib/helpers.js +10 -0
  159. package/mcp_v2/mcp.js +4 -4
  160. package/mcp_v2/package.json +1 -0
  161. package/mcp_v2/tools/build.js +116 -20
  162. package/mcp_v2/tools/conversion.js +106 -5
  163. package/mcp_v2/tools/tasks.js +18 -14
  164. package/package.json +35 -68
  165. package/babel.config.js +0 -32
  166. package/dist/index.js +0 -1
  167. package/dist/scripts/devBuild.js +0 -3
  168. package/dist/scripts/devServe.js +0 -3
  169. package/dist/scripts/loadScriptsBeforeServerStarts.js +0 -4
  170. package/dist/scripts/registerAliases.js +0 -6
  171. package/dist/server/renderer/document/Body.js +0 -11
  172. package/dist/server/renderer/document/Head.js +0 -9
  173. package/dist/server/renderer/document/index.js +0 -1
  174. package/dist/server/renderer/handler.js +0 -47
  175. package/dist/server/startServer.js +0 -57
  176. package/dist/webpack/babel-plugins/remove-client.plugin.js +0 -5
  177. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +0 -5
  178. package/dist/webpack/babel.config.client.js +0 -1
  179. package/dist/webpack/babel.config.ssr.js +0 -1
  180. package/dist/webpack/base.babel.js +0 -7
  181. package/dist/webpack/development.client.babel.js +0 -10
  182. package/dist/webpack/production.client.babel.js +0 -4
  183. package/dist/webpack/production.ssr.babel.js +0 -8
  184. package/dist/webpack/scssParams.js +0 -9
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.InsightsCollector = void 0;
7
+ var _constants = require("../core/constants.js");
8
+ /**
9
+ * InsightsCollector.js
10
+ *
11
+ * Watches accumulated perf data and emits actionable warnings to the
12
+ * Catalyst > Insights track in Chrome DevTools.
13
+ *
14
+ * Each insight is a point-in-time span with:
15
+ * - severity: 'critical' | 'warning' | 'info'
16
+ * - rule: short machine-readable id (e.g. 'slow-page-load')
17
+ * - message: one-sentence human diagnosis
18
+ * - fix: immediate actionable recommendation
19
+ * - evidence: the numbers that triggered the rule
20
+ *
21
+ * Insights are emitted in two modes:
22
+ * 1. Reactive — emitted immediately when a threshold breach is detected
23
+ * (e.g. an fps-drop episode during interaction)
24
+ * 2. Summary — emitted once after SESSION_SUMMARY_DELAY_MS of inactivity
25
+ * to give a rolled-up view of what happened in the last session
26
+ *
27
+ * How to read this in DevTools:
28
+ * Performance tab → Catalyst > Insights track
29
+ * Click any span → Tooltip shows: severity, rule, message, fix, evidence
30
+ * Red spans = critical (user-visible jank/failure)
31
+ * Yellow (tertiary) = warning
32
+ * Blue (secondary) = info / optimization opportunity
33
+ */
34
+
35
+ const SESSION_SUMMARY_DELAY_MS = 3000;
36
+ function round(value) {
37
+ return Number.isFinite(value) ? Math.round(value) : value;
38
+ }
39
+ class InsightsCollector {
40
+ constructor(measure) {
41
+ this._measure = measure;
42
+ this._summaryTimer = null;
43
+ this._insightSeq = 0;
44
+
45
+ // Counters reset per session
46
+ this._fpsDrops = []; // [{ minFps, durationMs }]
47
+ this._slowLoads = []; // [{ url, durationMs }]
48
+ this._loafsDuringNav = 0;
49
+ this._slowInteractions = []; // [{ target, responseMs }]
50
+ this._highMemory = []; // [{ currentMb }]
51
+ this._layoutShifts = []; // [{ value }]
52
+ this._coldStartMs = null; // always attached to other insights once known
53
+ }
54
+ init() {
55
+ // No web observers — fed via notify*() methods from index.js
56
+ }
57
+ setStoreSource(fn) {
58
+ this._notifyStore = fn;
59
+ }
60
+
61
+ // ─── Feed methods — called by index.js after routing native events ─────────
62
+
63
+ notifyFpsDrop({
64
+ minFps,
65
+ avgFps,
66
+ durationMs,
67
+ duringInteraction,
68
+ deltaFps,
69
+ baselineFps,
70
+ startTime,
71
+ endTime,
72
+ interactionId
73
+ }) {
74
+ const source = {
75
+ startTime,
76
+ endTime,
77
+ related: {
78
+ interactionId
79
+ }
80
+ };
81
+ this._fpsDrops.push({
82
+ minFps,
83
+ durationMs
84
+ });
85
+ if (duringInteraction) {
86
+ this._emitReactive({
87
+ severity: "critical",
88
+ rule: "fps-drop-during-tap",
89
+ message: `FPS dropped to ${minFps.toFixed(1)} for ${Math.round(durationMs)}ms right after a tap`,
90
+ fix: "Heavy computation is blocking the render thread after interaction — compare spans with the same interaction ID",
91
+ evidence: {
92
+ minFps,
93
+ avgFps,
94
+ durationMs,
95
+ deltaFps,
96
+ baselineFps
97
+ },
98
+ ...source
99
+ });
100
+ } else if (minFps < 30) {
101
+ this._emitReactive({
102
+ severity: "warning",
103
+ rule: "severe-fps-drop",
104
+ message: `Severe FPS drop: ${minFps.toFixed(1)}fps min for ${Math.round(durationMs)}ms`,
105
+ fix: "Look for LoAF spans overlapping this window on the Render track — a long script or layout is stalling frames",
106
+ evidence: {
107
+ minFps,
108
+ avgFps,
109
+ durationMs,
110
+ deltaFps,
111
+ baselineFps
112
+ },
113
+ ...source
114
+ });
115
+ } else if (deltaFps != null && deltaFps >= 10) {
116
+ // Sudden spike: FPS fell sharply even if absolute value is not critical
117
+ this._emitReactive({
118
+ severity: "warning",
119
+ rule: "fps-spike-drop",
120
+ message: `Sudden FPS spike: dropped ${deltaFps.toFixed(1)}fps (${baselineFps?.toFixed(1)} → ${minFps.toFixed(1)}) in one frame window`,
121
+ fix: "A sudden spike drop — often a one-off heavy layout or image decode. Check Render track for a LoAF aligned with this episode.",
122
+ evidence: {
123
+ minFps,
124
+ avgFps,
125
+ durationMs,
126
+ deltaFps,
127
+ baselineFps
128
+ },
129
+ ...source
130
+ });
131
+ }
132
+ this._scheduleSummary();
133
+ }
134
+ notifySlowLoad({
135
+ url,
136
+ durationMs,
137
+ startTime,
138
+ endTime
139
+ }) {
140
+ this._slowLoads.push({
141
+ url,
142
+ durationMs
143
+ });
144
+ if (durationMs > 4000) {
145
+ this._emitReactive({
146
+ severity: "critical",
147
+ rule: "page-load-too-slow",
148
+ message: `Page load took ${Math.round(durationMs)}ms (${url.split("/").pop() || url})`,
149
+ fix: "Check Cache track for cache-miss-fetch bursts; consider preloading route chunks or warming the WebView cache",
150
+ evidence: {
151
+ url,
152
+ durationMs
153
+ },
154
+ startTime,
155
+ endTime
156
+ });
157
+ }
158
+ this._scheduleSummary();
159
+ }
160
+ notifyColdStart({
161
+ durationMs,
162
+ startTime,
163
+ endTime
164
+ }) {
165
+ this._coldStartMs = durationMs; // attach to all future insights automatically
166
+ const severity = durationMs > 3000 ? "critical" : durationMs > 1800 ? "warning" : "info";
167
+ const label = durationMs > 3000 ? "slow" : durationMs > 1800 ? "acceptable" : "fast";
168
+ this._emitReactive({
169
+ severity,
170
+ rule: "cold-start-time",
171
+ message: `Cold start: ${Math.round(durationMs)}ms (${label}) — app launched to first page finish`,
172
+ fix: durationMs > 3000 ? "Cold start >3s — check if WebView.loadUrl() is called before layout inflation completes; defer non-critical MainActivity init; consider prewarming WebView in Application.onCreate()" : durationMs > 1800 ? "Cold start >1.8s — consider moving WebView construction earlier (Application.onCreate vs MainActivity.onCreate); check if large local assets block onPageFinished" : "Cold start within target (≤1.8s). No action needed.",
173
+ evidence: {
174
+ durationMs,
175
+ targetMs: 1800,
176
+ acceptableMs: 3000
177
+ },
178
+ startTime,
179
+ endTime
180
+ });
181
+ }
182
+ notifyLoafDuringNav({
183
+ durationMs,
184
+ startTime,
185
+ endTime
186
+ }) {
187
+ this._loafsDuringNav++;
188
+ if (this._loafsDuringNav === 1) {
189
+ this._emitReactive({
190
+ severity: "warning",
191
+ rule: "loaf-blocking-nav",
192
+ message: `Long animation frame (${Math.round(durationMs)}ms) fired during navigation — may cause blank screen`,
193
+ fix: "Defer non-critical JS execution after page-load-end; split large bundles with dynamic import()",
194
+ evidence: {
195
+ durationMs,
196
+ loafsDuringNav: this._loafsDuringNav
197
+ },
198
+ startTime,
199
+ endTime
200
+ });
201
+ }
202
+ }
203
+ notifySlowInteraction({
204
+ target,
205
+ responseMs,
206
+ startTime,
207
+ endTime,
208
+ interactionId
209
+ }) {
210
+ this._slowInteractions.push({
211
+ target,
212
+ responseMs
213
+ });
214
+ if (responseMs > 300) {
215
+ this._emitReactive({
216
+ severity: "critical",
217
+ rule: "interaction-unresponsive",
218
+ message: `Tap on '${target}' took ${responseMs}ms to paint (threshold ${_constants.THRESHOLD.INTERACTION_SLOW_MS}ms)`,
219
+ fix: "A LoAF is delaying the paint — inspect work overlapping the interaction",
220
+ evidence: {
221
+ target,
222
+ responseMs,
223
+ thresholdMs: _constants.THRESHOLD.INTERACTION_SLOW_MS
224
+ },
225
+ startTime,
226
+ endTime,
227
+ related: {
228
+ interactionId
229
+ }
230
+ });
231
+ }
232
+ this._scheduleSummary();
233
+ }
234
+ notifyHighMemory({
235
+ currentMb,
236
+ startTime,
237
+ endTime,
238
+ memoryId
239
+ }) {
240
+ const source = {
241
+ startTime,
242
+ endTime,
243
+ related: {
244
+ memoryId
245
+ }
246
+ };
247
+ // currentMb is webviewMb (V8 + Blink + JNI PSS) — the WebView engine memory.
248
+ // A healthy WebView app idles at 80-120MB. Growth beyond that signals
249
+ // a real leak (un-released image bitmaps, accumulating JS closures, JNI objects).
250
+ // Thresholds are intentionally higher than total-PSS thresholds used previously
251
+ // because we now track the native slice, not the whole process.
252
+ this._highMemory.push({
253
+ currentMb
254
+ });
255
+ if (currentMb > 120) {
256
+ this._emitReactive({
257
+ severity: "critical",
258
+ rule: "memory-critical",
259
+ message: `WebView memory at ${Math.round(currentMb)}MB — above 120MB critical threshold (V8/Blink/JNI leak likely)`,
260
+ fix: "Check for un-released image bitmaps, accumulating JS closures, or JNI objects not returned to GC. Compare WebView memory growth across snapshots on the Memory track.",
261
+ evidence: {
262
+ webviewMb: currentMb
263
+ },
264
+ ...source
265
+ });
266
+ } else if (currentMb > 80) {
267
+ this._emitReactive({
268
+ severity: "warning",
269
+ rule: "memory-high",
270
+ message: `WebView memory at ${Math.round(currentMb)}MB — above 80MB warning threshold (healthy idle is <80MB)`,
271
+ fix: "Monitor WebView memory on the Memory track — sustained growth = leak. Check image cache eviction and dispose WebView listeners on route unmount.",
272
+ evidence: {
273
+ webviewMb: currentMb
274
+ },
275
+ ...source
276
+ });
277
+ }
278
+ }
279
+ notifyHwAccelDuringFrame({
280
+ durationMs,
281
+ thread,
282
+ trigger,
283
+ startTime,
284
+ endTime
285
+ }) {
286
+ this._emitReactive({
287
+ severity: "critical",
288
+ rule: "hw-accel-during-frame",
289
+ message: `Software rendering was active for ${Math.round(durationMs)}ms during a dropped frame (hw-accel disabled by ${trigger ?? "cache-serve"})`,
290
+ fix: "Cache serving on shouldInterceptRequest disables hardware acceleration — move cache I/O off the WebView thread or batch cache warming on app start to reduce mid-session hw-accel toggles",
291
+ evidence: {
292
+ durationMs,
293
+ thread: thread ?? "unknown",
294
+ trigger: trigger ?? "unknown"
295
+ },
296
+ startTime,
297
+ endTime
298
+ });
299
+ }
300
+ notifyLayoutShift({
301
+ value,
302
+ sessionContext,
303
+ startTime,
304
+ endTime
305
+ }) {
306
+ this._layoutShifts.push({
307
+ value
308
+ });
309
+ if (value > 0.25) {
310
+ this._emitReactive({
311
+ severity: "warning",
312
+ rule: "large-layout-shift",
313
+ message: `Large layout shift (CLS ${value.toFixed(3)}) during '${sessionContext}' phase`,
314
+ fix: "Reserve space for async-loaded content (images, ads, banners) using explicit height; avoid injecting DOM above visible content",
315
+ evidence: {
316
+ value,
317
+ sessionContext
318
+ },
319
+ startTime,
320
+ endTime
321
+ });
322
+ }
323
+ }
324
+ notifyScrollJank({
325
+ minFps,
326
+ frameDropCount,
327
+ startTime,
328
+ endTime
329
+ }) {
330
+ const severity = minFps < 30 ? "critical" : "warning";
331
+ const rule = minFps < 30 ? "scroll-jank-critical" : "scroll-jank-warning";
332
+ this._emitReactive({
333
+ severity,
334
+ rule,
335
+ message: `Scroll jank: ${Math.round(frameDropCount)} frame drop(s), min ${minFps.toFixed(1)}fps during scroll`,
336
+ fix: minFps < 30 ? "Severe scroll jank — check for layout thrash triggered during scroll. Look for LoAF spans on Render track overlapping the Input track scroll session." : "Scroll dropped below 55fps — common causes: sticky header recalc, cart badge updates, or image decode on scroll. Check Render track LoAF spans inside the scroll session.",
337
+ evidence: {
338
+ minFps,
339
+ frameDropCount
340
+ },
341
+ startTime,
342
+ endTime
343
+ });
344
+ this._scheduleSummary();
345
+ }
346
+ notifyCacheSummary({
347
+ hitRatePct,
348
+ total,
349
+ avgFetchMs,
350
+ startTime,
351
+ endTime
352
+ }) {
353
+ if (total < 5) return; // not enough data
354
+ if (hitRatePct < 50) {
355
+ this._emitReactive({
356
+ severity: "warning",
357
+ rule: "low-cache-hit-rate",
358
+ message: `Cache hit rate ${hitRatePct}% (${total} resources) — majority served via network`,
359
+ fix: "Add missed URLs to the allowedUrls cache list; check cache max-age and disk eviction policy",
360
+ evidence: {
361
+ hitRatePct,
362
+ total,
363
+ avgFetchMs
364
+ },
365
+ startTime,
366
+ endTime
367
+ });
368
+ }
369
+ }
370
+
371
+ // ─── Reactive insight emission ─────────────────────────────────────────────
372
+
373
+ _emitReactive({
374
+ severity,
375
+ rule,
376
+ message,
377
+ fix,
378
+ evidence = {},
379
+ startTime,
380
+ endTime,
381
+ related = {}
382
+ }) {
383
+ const start = Number.isFinite(startTime) ? startTime : performance.now();
384
+ const end = Number.isFinite(endTime) ? Math.max(start + 1, endTime) : start + 2;
385
+ const markName = `${_constants.PREFIX.INSIGHT}:${rule}:${Math.round(start)}:${++this._insightSeq}`;
386
+ const color = severity === "critical" ? "error" : severity === "warning" ? "tertiary" : "secondary";
387
+ const title = this._titleFor(rule, evidence, message);
388
+ const category = this._categoryFor(rule);
389
+ const detail = {
390
+ severity,
391
+ rule,
392
+ title,
393
+ category,
394
+ message,
395
+ fix,
396
+ ...related,
397
+ ...(this._coldStartMs != null ? {
398
+ "coldStart.ms": String(Math.round(this._coldStartMs))
399
+ } : {}),
400
+ ...Object.fromEntries(Object.entries(evidence).filter(([, value]) => value != null).map(([key, value]) => [`evidence.${key}`, String(value)]))
401
+ };
402
+ performance.mark(markName, {
403
+ startTime: start
404
+ });
405
+ this._notifyStore?.({
406
+ severity,
407
+ rule,
408
+ title,
409
+ category,
410
+ message,
411
+ fix,
412
+ evidence,
413
+ ...related,
414
+ startTime: start,
415
+ endTime: end,
416
+ durationMs: end - start
417
+ });
418
+ this._measure.emit(`${related.interactionId ? `[${related.interactionId}] ` : ""}Insight: ${title}`, markName, end, detail, _constants.TRACK.INSIGHTS, color);
419
+ }
420
+ _titleFor(rule, evidence, message) {
421
+ switch (rule) {
422
+ case "fps-drop-during-tap":
423
+ return `FPS drop after tap: ${round(evidence.minFps)}fps`;
424
+ case "severe-fps-drop":
425
+ return `FPS drop: ${round(evidence.minFps)}fps for ${round(evidence.durationMs)}ms`;
426
+ case "fps-spike-drop":
427
+ return `FPS spike drop: -${round(evidence.deltaFps)}fps`;
428
+ case "page-load-too-slow":
429
+ return `Page load slow: ${round(evidence.durationMs)}ms`;
430
+ case "cold-start-time":
431
+ return `Cold start: ${round(evidence.durationMs)}ms`;
432
+ case "loaf-blocking-nav":
433
+ return `Render blocked during navigation: ${round(evidence.durationMs)}ms`;
434
+ case "interaction-unresponsive":
435
+ return `Slow tap: ${evidence.target ?? "unknown"} ${round(evidence.responseMs)}ms`;
436
+ case "memory-critical":
437
+ case "memory-high":
438
+ return `Memory high: ${round(evidence.webviewMb)}MB`;
439
+ case "hw-accel-during-frame":
440
+ return `Software render window: ${round(evidence.durationMs)}ms`;
441
+ case "large-layout-shift":
442
+ return `Large layout shift: ${Number.isFinite(evidence.value) ? evidence.value.toFixed(3) : "unknown"}`;
443
+ case "scroll-jank-critical":
444
+ case "scroll-jank-warning":
445
+ return `Scroll jank: ${round(evidence.minFps)}fps`;
446
+ case "low-cache-hit-rate":
447
+ return `Cache hit rate low: ${evidence.hitRatePct}%`;
448
+ case "session-summary":
449
+ return "Session issues";
450
+ default:
451
+ return message ?? rule;
452
+ }
453
+ }
454
+ _categoryFor(rule) {
455
+ if (rule.includes("fps") || rule.includes("loaf") || rule.includes("layout")) return "render";
456
+ if (rule.includes("page-load") || rule.includes("cold-start")) return "navigation";
457
+ if (rule.includes("interaction") || rule.includes("tap")) return "interaction";
458
+ if (rule.includes("memory")) return "memory";
459
+ if (rule.includes("scroll")) return "scroll";
460
+ if (rule.includes("cache")) return "cache";
461
+ if (rule.includes("session")) return "session";
462
+ return "general";
463
+ }
464
+
465
+ // ─── Session summary ───────────────────────────────────────────────────────
466
+ // Emitted after SESSION_SUMMARY_DELAY_MS of quiet — a rolled-up health check.
467
+
468
+ _scheduleSummary() {
469
+ if (this._summaryTimer) clearTimeout(this._summaryTimer);
470
+ this._summaryTimer = setTimeout(() => this._emitSummary(), SESSION_SUMMARY_DELAY_MS);
471
+ }
472
+ _emitSummary() {
473
+ this._summaryTimer = null;
474
+ const problems = [];
475
+ if (this._fpsDrops.length > 0) problems.push(`${this._fpsDrops.length} fps-drop episode(s), min ${Math.min(...this._fpsDrops.map(f => f.minFps)).toFixed(0)}fps`);
476
+ if (this._slowLoads.length > 0) problems.push(`${this._slowLoads.length} slow page load(s)`);
477
+ if (this._slowInteractions.length > 0) problems.push(`${this._slowInteractions.length} slow tap(s)`);
478
+ if (this._highMemory.length > 0) problems.push(`peak ${Math.max(...this._highMemory.map(m => m.currentMb)).toFixed(0)}MB memory`);
479
+ const severity = this._fpsDrops.some(f => f.minFps < 20) ? "critical" : problems.length > 2 ? "warning" : "info";
480
+ const message = problems.length === 0 ? "Session clean — no threshold breaches detected" : `Session issues: ${problems.join(" · ")}`;
481
+ const fix = problems.length === 0 ? "No action needed" : "Expand each red/yellow span above for per-issue fixes";
482
+ this._emitReactive({
483
+ severity: severity === "info" ? "info" : severity,
484
+ rule: "session-summary",
485
+ message,
486
+ fix,
487
+ evidence: {
488
+ fpsDropEpisodes: this._fpsDrops.length,
489
+ slowPageLoads: this._slowLoads.length,
490
+ slowInteractions: this._slowInteractions.length,
491
+ highMemoryReadings: this._highMemory.length,
492
+ layoutShifts: this._layoutShifts.length
493
+ }
494
+ });
495
+
496
+ // Reset counters for next session
497
+ this._fpsDrops = [];
498
+ this._slowLoads = [];
499
+ this._loafsDuringNav = 0;
500
+ this._slowInteractions = [];
501
+ this._highMemory = [];
502
+ this._layoutShifts = [];
503
+ }
504
+ }
505
+ exports.InsightsCollector = InsightsCollector;