Files
lobehub__lobehub/index.mobile.html
Innei ea7c1dcc6f feat(boot-metrics): instrument SPA bootstrap timing beacon (#16305)
*  feat(boot-timing): add bootTiming collector for SPA bootstrap metrics

Implements a dependency-free module singleton at src/libs/bootTiming/index.ts
with mark, measure, span, recordSpan, and snapshot APIs. Resilient to SSR
environments (guards performance unavailability). Covered by 7 Vitest tests.

*  feat(boot-timing): instrument SPA bootstrap pipeline (LC2)

- Add spanSync<T>(name, fn) helper to bootTiming collector (records even on throw)
- Wrap import-settings and tool-surfaces steps with spanSync (sync, pre-render)
- Wrap core-init with span; plant app-ready mark after setAppReady flushSync
- Plant bundle-eval mark in all four SPA entries (web/desktop/mobile/popup)
- Plant __LOBE_BOOT_T_HTML__ mark as earliest inline script in index.html
- Declare Window.__LOBE_BOOT_T_HTML__?: number in global.d.ts

* 🐛 fix(boot-timing): add __LOBE_BOOT_T_HTML__ mark to mobile HTML template

Mirrors the earliest-script timing anchor from index.html into
index.mobile.html so mobile SPA builds capture the HTML parse time.

*  feat(boot-timing): plant first-paint mark and cache-hydration span

Add `bootTiming.mark('first-paint')` in CacheHydrationGate behind a
module-level flag so scope remounts after login never overwrite the
initial reveal timestamp.

Add `bootTiming.recordSpan('cache-hydration', ...)` in the SWR cache
provider's IDB hydration path, captured once per provider instance
(closure flag). Failed hydrations produce no span.

*  feat(boot-metrics): assemble, gate, and beacon bootstrap timing payload

- Pure builder `buildBootMetricsPayload` with full unit coverage (27 tests)
- Runtime glue in `finalize.ts`: cloud gate, telemetry opt-in, sampling, scheduleAfterFirstPaint, sendBeacon, pagehide fallback
- Wire `startBootMetricsFinalize()` into bootstrap.ts finally-block

* 🐛 fix(boot-metrics): drop mislabeled fetch:server-config span

lambdaClient is shared by many services; the first /trpc/lambda
resource timing entry cannot reliably identify the server-config
call. Remove getResourceTiming and the fetch:server-config derivation
from finalize.ts. The builder's generic resourceTimings input remains
intact and tested — we simply pass nothing from the glue now.

*  test(bootMetrics): cap spans to 64 + finalize gate coverage

- buildPayload: slice spans to 64 before returning (lockstep with server Zod .max(64))
- buildPayload.test: add two assertions for the cap (>64 truncated; ==64 unchanged)
- finalize.test: new focused test covering all gate predicates and the sent-flag double-send guard

* 🐛 fix(boot-metrics): round span/total ms to integers before beacon

performance.now() yields sub-millisecond floats; the develop-center metrics
columns (total_ms, start_ms, dur_ms) are integer, so the real client beacon
was rejected at insert ('invalid input syntax for type integer'). Round all
ms in buildPayload output. Found by running a real browser bootstrap locally.

*  feat(boot-metrics): ingest bootstrap metrics in oss

* 🧹 chore(boot-metrics): keep storage outside oss

* 🧹 chore(boot-metrics): drop dbml drift
2026-07-01 19:24:44 +08:00

74 lines
2.6 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<link rel="shortcut icon" href="/favicon-32x32.ico" />
<!--SEO_META-->
<style>
html body {
background: #f8f8f8;
}
html[data-theme='dark'] body {
background-color: #000;
}
</style>
<script>
window.__LOBE_BOOT_T_HTML__ = performance.now();
</script>
<script>
(function(){
var O=globalThis.Worker;
globalThis.Worker=function(u,o){
var h=typeof u==='string'?u:u instanceof URL?u.href:'';
if(h.startsWith('http')&&!h.startsWith(location.origin)){
var b=new Blob(['import "'+h+'";'],{type:'application/javascript'});
return new O(URL.createObjectURL(b),Object.assign({},o,{type:'module'}));
}return new O(u,o)};
globalThis.Worker.prototype=O.prototype;
})();
</script>
<script>
(function () {
var theme = 'system';
try {
theme = localStorage.getItem('theme') || 'system';
} catch (_) {}
var systemTheme =
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
var resolvedTheme = theme === 'system' ? systemTheme : theme;
if (resolvedTheme === 'dark' || resolvedTheme === 'light') {
document.documentElement.setAttribute('data-theme', resolvedTheme);
}
var hl = new URLSearchParams(location.search).get('hl');
var m = document.cookie.match(/(?:^|;\s*)LOBE_LOCALE=([^;]*)/);
var cookie = m ? decodeURIComponent(m[1]) : '';
var locale = hl || cookie || navigator.language || 'en-US';
if (locale === 'auto') locale = navigator.language || 'en-US';
if (hl && !cookie) {
document.cookie =
'LOBE_LOCALE=' + encodeURIComponent(hl) + ';path=/;max-age=7776000;SameSite=Lax';
}
document.documentElement.lang = locale;
var rtl = ['ar', 'arc', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ku', 'ps', 'ur', 'yi'];
document.documentElement.dir =
rtl.indexOf(locale.split('-')[0].toLowerCase()) >= 0 ? 'rtl' : 'ltr';
})();
</script>
<script>
window.__SERVER_CONFIG__ = undefined; /* SERVER_CONFIG */
</script>
</head>
<body>
<div id="root" style="height: 100%"></div>
<!--ANALYTICS_SCRIPTS-->
<script type="module" src="/src/spa/entry.mobile.tsx"></script>
</body>
</html>