@flemo/devtools 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 +40 -0
- package/dist/__tests__/noop.test.d.ts +1 -0
- package/dist/noop.d.ts +13 -0
- package/dist/noop.mjs +356 -0
- package/package.json +15 -2
package/README.md
CHANGED
|
@@ -13,6 +13,46 @@ leave a toggle set?").
|
|
|
13
13
|
Zero dependencies. No imports from `@flemo/core` or `@flemo/react`; attaching
|
|
14
14
|
the recorder never changes the motion it measures.
|
|
15
15
|
|
|
16
|
+
## Production safety
|
|
17
|
+
|
|
18
|
+
`@flemo/devtools` resolves to an inert entry when your bundler builds for
|
|
19
|
+
production, so the ordinary import is the safe one:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { attachFlightRecorder } from "@flemo/devtools";
|
|
23
|
+
|
|
24
|
+
// Development: the real recorder. Production: a no-op that records nothing
|
|
25
|
+
// and whose implementation never enters the bundle.
|
|
26
|
+
const { detach } = attachFlightRecorder({ log: true });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This is done with the `development` / `production` export conditions rather
|
|
30
|
+
than left to the caller, because the failure is silent: a normal import of a
|
|
31
|
+
dev-time tool builds clean, warns about nothing, and ships to every visitor.
|
|
32
|
+
It happened to this project — the recorder's strings were found in a
|
|
33
|
+
production chunk of flemo.dev and had to be removed.
|
|
34
|
+
|
|
35
|
+
Two things to know:
|
|
36
|
+
|
|
37
|
+
- **Not every bundler sets those conditions.** Vite and Next do. If yours does
|
|
38
|
+
not, keep the module behind a dynamic import guarded by a build-time
|
|
39
|
+
constant (`process.env.NODE_ENV !== "production"`, Vite's
|
|
40
|
+
`import.meta.env.DEV`) so the branch — and the module behind it — is
|
|
41
|
+
eliminated. There is no automatic fallback for that case, and the shape of
|
|
42
|
+
this package is why: `@tanstack/react-query-devtools` gets one by importing
|
|
43
|
+
its implementation statically and swapping it on `process.env.NODE_ENV`, so
|
|
44
|
+
the bundler folds the constant and drops the now-unreferenced module. That
|
|
45
|
+
works because their implementation stays a separate module file. Ours cannot
|
|
46
|
+
— `dist/index.mjs` is self-contained on purpose, so it can be loaded
|
|
47
|
+
directly in a page — and measured with esbuild (which sets neither
|
|
48
|
+
condition), the same swap strips nothing: production and development bundles
|
|
49
|
+
came out byte-identical in size with the recorder present in both. The guard
|
|
50
|
+
is the answer there, not a trick inside the package.
|
|
51
|
+
- **`@flemo/devtools/force` is the escape hatch.** It resolves to the real
|
|
52
|
+
tool whatever the build mode, for when you deliberately want the recorder in
|
|
53
|
+
a production build (a staging deploy, an e2e suite that must run against a
|
|
54
|
+
production build). Import it dynamically behind your own opt-in flag.
|
|
55
|
+
|
|
16
56
|
## Keeping it out of your production bundle
|
|
17
57
|
|
|
18
58
|
Install it as a **devDependency** — and know that this alone is not enough.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/noop.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DevtoolsPanelHandle, DevtoolsPanelOptions } from './panel';
|
|
2
|
+
import { FlightRecorderHandle, FlightRecorderOptions } from './types';
|
|
3
|
+
export * from './anomalies';
|
|
4
|
+
export * from './blindSpots';
|
|
5
|
+
export * from './environment';
|
|
6
|
+
export * from './overrides';
|
|
7
|
+
export * from './sampling';
|
|
8
|
+
export declare const REPORT_SCHEMA_VERSION = "2";
|
|
9
|
+
export declare const attachFlightRecorder: (_options?: FlightRecorderOptions) => FlightRecorderHandle;
|
|
10
|
+
export declare const attachDevtoolsPanel: (_options?: DevtoolsPanelOptions) => DevtoolsPanelHandle;
|
|
11
|
+
export type { FlemoGlobal } from './recorder';
|
|
12
|
+
export type { DevtoolsPanelHandle, DevtoolsPanelOptions } from './panel';
|
|
13
|
+
export type { DriverPolicySection, EnvironmentFingerprint, FlemoReport, FlightDriver, FlightHolds, FlightKind, FlightParticipants, FlightRecord, FlightRecorderHandle, FlightRecorderOptions, FlightTimestamp, FramePhaseStats, FrameSampleStats, LandingAudit, LongTaskSpan, ObservationCapabilities, OverridesSection, PlayerGapStats, UaBrand } from './types';
|
package/dist/noop.mjs
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
//#region src/anomalies.ts
|
|
2
|
+
var e = 30, t = 1e4, n = 50, r = 120, i = 100, a = 48, o = (e) => {
|
|
3
|
+
let n = [], { t0Ms: r, driver: i, frameSamples: a, playerGaps: o, longTasks: s, holdLongTasks: c, landing: l } = e, u = r + (e.releasedAtMs ?? 0);
|
|
4
|
+
if (o && o.over30Count > 0 && n.push(`player frame gap up to ${o.maxMs}ms ×${o.over30Count} during flight (the rAF player missed frames — convergence jank on this navigation)`), a.released.over30Count > 0) {
|
|
5
|
+
let e = i === "compiled" ? " (compiled/compositor flights can still present cleanly through main-thread gaps)" : "";
|
|
6
|
+
n.push(`main-thread rAF gap up to ${a.released.maxGapMs}ms ×${a.released.over30Count} during visible motion${e}`);
|
|
7
|
+
}
|
|
8
|
+
for (let e of s) e.startMs <= u + 120 && e.startMs + e.durationMs >= u - 50 ? n.push(`long task ${Math.round(e.durationMs)}ms overlapped the visible-motion start (opening-swallow risk: the first presented frames of the transition may have been lost)`) : e.durationMs >= 100 && n.push(`long task ${Math.round(e.durationMs)}ms mid-flight`);
|
|
9
|
+
for (let e of c) e.durationMs >= 100 && n.push(`long task ${Math.round(e.durationMs)}ms absorbed by the hold (screen posed, not yet moving — the hold doing its job, not user-visible jank)`);
|
|
10
|
+
let { motion: d, images: f } = e;
|
|
11
|
+
return d.holdReassertedAtMs !== null && n.push(`hold re-asserted ${d.holdReassertedAtMs}ms into the flight, after it had already released (an interleaved commit wrote the stale paused hold attribute over a running flight — the flemo 2026-08-18 release-race signature; the motion pauses while every timing metric stays clean)`), d.pausedAfterRelease && n.push("compiled animation reported playState=paused after its release (the flight was posed and then stopped mid-motion, not merely starved of frames)"), d.longestStallMs >= 48 && n.push(`motion stalled ${d.longestStallMs}ms mid-flight (${d.stalledFrames}/${d.sampledFrames} released frames advanced neither the animation clock nor the pose — freeze, or freeze-then-leap if the flight still landed on time)`), d.sampledFrames === 0 && e.releasedAtMs !== null && n.push("no released frames were sampled (the flight ended at or before its own hold release — the visible motion, if any, was never observed)"), f.completedUnheld > 0 && n.push(`${f.completedUnheld} image(s) finished loading mid-flight without a hold (${f.completedDuringFlight} completed, ${f.heldDuringFlight} held; ${f.loadingAtStart} were loading at t0, ${f.addedDuringFlight} arrived mid-flight) — each decode rasters on the moving layer and costs a present; this is the warm-side image-hold regression`), l.orphanedHolds.length > 0 && n.push(`hold markers left on the page at rest: ${l.orphanedHolds.join("; ")} (whatever they hide has no owner left to reveal it — the permanently-blank-avatar class)`), l.residualInlineTransforms.length > 0 && n.push(`residual inline style after COMPLETED: ${l.residualInlineTransforms.join("; ")} (landing cleanup failure — the landed scope belongs to the compiled rest rules)`), l.offViewportAtRest && n.push("screen resting at from-pose while COMPLETED+active (blank-viewport signature — the flemo PR #259 class: a residual pose left the landed screen parked off-viewport)"), l.stuckStatuses.length > 0 && n.push(`transitional status stuck >${t / 1e3}s: ${l.stuckStatuses.join(", ")} (navigation queue lock or missed animationend — later navigations will be swallowed)`), i === "unknown" && n.push("driver could not be classified (no running flemo-* CSSAnimation and no player inline-style signature observed — zero-duration flight, or the sampler attached after motion ended)"), n;
|
|
12
|
+
}, s = (e) => {
|
|
13
|
+
let n = [];
|
|
14
|
+
if (e.forcePin !== null && n.push(`active force pin flemo:motion-driver-force=${e.forcePin} — every transition this session is pinned to one driver (A/B residue? pins expire after 24h; clear the key before judging behavior)`), e.clearedForcePin !== null && e.forcePin === null && n.push(`force pin flemo:motion-driver-force=${e.clearedForcePin} was present at attach and has since been cleared (the library strips malformed/expired pins on sight) — early driver decisions in this session may still have been shaped by it`), e.legacyLocalForcePin !== null && n.push(`legacy localStorage driver pin present (flemo:motion-driver-force=${e.legacyLocalForcePin}) — never honored, but it marks A/B residue on this profile`), e.emulationSuspected) {
|
|
15
|
+
let t = /Win/i.test(e.platform) ? " (Windows touch hardware makes this signal ambiguous — confirm the DevTools device toolbar state)" : "";
|
|
16
|
+
n.push("DevTools device emulation suspected — the page composites to a rescaled surface, so visual reports from this session are untrustworthy; judge motion in a plain window or on a real device" + t);
|
|
17
|
+
}
|
|
18
|
+
return e.stuckFlightOpen && n.push(`a flight is still transitional after ${t / 1e3}s — the navigation queue is likely locked; subsequent navigations will be ignored`), e.flightAnomalies.some((e) => e.some((e) => e.includes("blank-viewport"))) && n.push("at least one flight landed with the blank-viewport signature (see that flight's anomalies)"), n;
|
|
19
|
+
}, c = [
|
|
20
|
+
"macOS Chrome present-pipeline pacing: on 120Hz ProMotion displays Chrome can present frames unevenly even when rAF cadence and this report read perfectly clean (Chromium issues 40062488 / 345275139; proven with a no-script pure-CSS control page that still juddered). Not diagnosable or fixable from page code.",
|
|
21
|
+
"Display-hardware effects: local dimming, backlight modulation, and panel overdrive alter perceived motion and are invisible to every web API.",
|
|
22
|
+
"Compositor-internal present skips: a frame can be produced on time (clean rAF, clean player gaps) yet dropped or delayed inside the compositor's present queue; only platform tools (about:tracing, Instruments) see that layer.",
|
|
23
|
+
"DevTools device emulation composites the page to a rescaled surface: every in-page instrument reads the pre-scale surface and reports clean while the eye watches the post-scale one, so emulated sessions grow shimmer/flash artifacts that do not exist in a plain window or on a real device."
|
|
24
|
+
], l = () => {
|
|
25
|
+
if (typeof navigator > "u") return null;
|
|
26
|
+
let e = navigator.userAgentData;
|
|
27
|
+
return !e || !Array.isArray(e.brands) ? null : e.brands.map((e) => ({
|
|
28
|
+
brand: String(e.brand ?? ""),
|
|
29
|
+
version: String(e.version ?? "")
|
|
30
|
+
}));
|
|
31
|
+
}, u = () => {
|
|
32
|
+
if (typeof navigator > "u") return "unknown";
|
|
33
|
+
let e = l();
|
|
34
|
+
if (e?.some((e) => /Chromium/i.test(e.brand))) return "blink";
|
|
35
|
+
let t = navigator.userAgent ?? "";
|
|
36
|
+
return /Firefox|FxiOS/i.test(t) ? "gecko" : e || /AppleWebKit/i.test(t) && !/Chrome|Chromium|Android/i.test(t) ? "webkit" : /Android/i.test(t) ? "blink" : "unknown";
|
|
37
|
+
}, d = () => {
|
|
38
|
+
if (typeof navigator > "u" || u() !== "blink") return !1;
|
|
39
|
+
let e = navigator.platform ?? "";
|
|
40
|
+
return /Mac|Win|Linux/i.test(e) ? /Android/i.test(navigator.userAgent ?? "") && !/Mac|Win/i.test(e) ? /Linux (x86_64|i686)/i.test(e) : (navigator.maxTouchPoints ?? 0) > 0 : !1;
|
|
41
|
+
}, f = (e = 20) => new Promise((t) => {
|
|
42
|
+
let n = Number.isFinite(e) ? Math.max(1, Math.floor(e)) : 20;
|
|
43
|
+
if (typeof requestAnimationFrame != "function" || typeof performance > "u") {
|
|
44
|
+
t({
|
|
45
|
+
medianGapMs: null,
|
|
46
|
+
sampleCount: 0
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let r = [], i = null, a = () => {
|
|
51
|
+
let e = performance.now();
|
|
52
|
+
if (i !== null && r.push(e - i), i = e, r.length >= n) {
|
|
53
|
+
let e = [...r].sort((e, t) => e - t), n = e[Math.floor(e.length / 2)];
|
|
54
|
+
t({
|
|
55
|
+
medianGapMs: Math.round(n * 100) / 100,
|
|
56
|
+
sampleCount: r.length
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
requestAnimationFrame(a);
|
|
61
|
+
};
|
|
62
|
+
requestAnimationFrame(a);
|
|
63
|
+
}), p = () => {
|
|
64
|
+
try {
|
|
65
|
+
return typeof PerformanceObserver < "u" && Array.isArray(PerformanceObserver.supportedEntryTypes) && PerformanceObserver.supportedEntryTypes.includes("longtask");
|
|
66
|
+
} catch {
|
|
67
|
+
return !1;
|
|
68
|
+
}
|
|
69
|
+
}, m = (e) => {
|
|
70
|
+
let t = typeof navigator < "u" ? navigator : null, n = typeof window < "u" ? window : null, r = !1;
|
|
71
|
+
try {
|
|
72
|
+
r = n?.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? !1;
|
|
73
|
+
} catch {
|
|
74
|
+
r = !1;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
userAgent: t?.userAgent ?? "",
|
|
78
|
+
uaBrands: l(),
|
|
79
|
+
engine: u(),
|
|
80
|
+
platform: t?.platform ?? "",
|
|
81
|
+
maxTouchPoints: t?.maxTouchPoints ?? 0,
|
|
82
|
+
devicePixelRatio: n?.devicePixelRatio ?? 1,
|
|
83
|
+
screen: {
|
|
84
|
+
width: n?.screen?.width ?? 0,
|
|
85
|
+
height: n?.screen?.height ?? 0
|
|
86
|
+
},
|
|
87
|
+
viewport: {
|
|
88
|
+
width: n?.innerWidth ?? 0,
|
|
89
|
+
height: n?.innerHeight ?? 0
|
|
90
|
+
},
|
|
91
|
+
visualViewportScale: n?.visualViewport?.scale ?? null,
|
|
92
|
+
rafCadence: e,
|
|
93
|
+
reducedMotion: r,
|
|
94
|
+
emulationSuspected: d(),
|
|
95
|
+
observation: {
|
|
96
|
+
longTasks: p(),
|
|
97
|
+
elementAnimations: typeof Element < "u" && typeof Element.prototype.getAnimations == "function",
|
|
98
|
+
playerGapMirror: Array.isArray(n?.__flemoPlayerGaps)
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}, h = [
|
|
102
|
+
{
|
|
103
|
+
key: "flemo:motion-driver",
|
|
104
|
+
storage: "local",
|
|
105
|
+
kind: "production-state",
|
|
106
|
+
description: "learned driver ledger ('css' = player demoted on this device, 'raf' = clean probe)"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "flemo:motion-driver-force",
|
|
110
|
+
storage: "session",
|
|
111
|
+
kind: "opt-in-diagnostic",
|
|
112
|
+
description: "hard driver pin ('css@<epoch-ms>' / 'raf@<epoch-ms>', 24h TTL) — pins EVERY transition"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "flemo:lpm",
|
|
116
|
+
storage: "session",
|
|
117
|
+
kind: "production-state",
|
|
118
|
+
description: "low-power-mode cadence seed (learned)"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "flemo:sixty",
|
|
122
|
+
storage: "session",
|
|
123
|
+
kind: "production-state",
|
|
124
|
+
description: "steady-60 display verdict seed ('high' latches, a count seeds the streak)"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: "flemo:lat",
|
|
128
|
+
storage: "session",
|
|
129
|
+
kind: "production-state",
|
|
130
|
+
description: "LPM release-latency seed (learned)"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "flemo:landing-snap",
|
|
134
|
+
storage: "session",
|
|
135
|
+
kind: "opt-in-diagnostic",
|
|
136
|
+
description: "Blink landing pixel-snap easing A/B"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: "flemo:imghold",
|
|
140
|
+
storage: "session",
|
|
141
|
+
kind: "opt-in-diagnostic",
|
|
142
|
+
description: "flight-scoped <img> reveal hold"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "flemo:settle-gate",
|
|
146
|
+
storage: "session",
|
|
147
|
+
kind: "production-default-with-override",
|
|
148
|
+
description: "render-settle entry gate override"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "flemo:handoff",
|
|
152
|
+
storage: "session",
|
|
153
|
+
kind: "opt-in-diagnostic",
|
|
154
|
+
description: "anchored-opening handoff (POP-scoped)"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: "flemo:handoffms",
|
|
158
|
+
storage: "session",
|
|
159
|
+
kind: "opt-in-diagnostic",
|
|
160
|
+
description: "handoff point override (ms)"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "flemo:apply",
|
|
164
|
+
storage: "session",
|
|
165
|
+
kind: "opt-in-diagnostic",
|
|
166
|
+
description: "force the scrub-WAAPI value-application tier"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: "flemo:snap",
|
|
170
|
+
storage: "session",
|
|
171
|
+
kind: "production-default-with-override",
|
|
172
|
+
description: "player device-pixel snap policy override"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: "flemo:snapband",
|
|
176
|
+
storage: "session",
|
|
177
|
+
kind: "opt-in-diagnostic",
|
|
178
|
+
description: "'hybrid' snap jitter-band width (device px)"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: "flemo:layers",
|
|
182
|
+
storage: "session",
|
|
183
|
+
kind: "opt-in-diagnostic",
|
|
184
|
+
description: "resident screen layers at rest (armed by ?flemo-layers=)"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "flemo:freeze",
|
|
188
|
+
storage: "session",
|
|
189
|
+
kind: "opt-in-diagnostic",
|
|
190
|
+
description: "keep the direct prev screen live (armed by ?flemo-freeze=)"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
key: "flemo:preraster",
|
|
194
|
+
storage: "session",
|
|
195
|
+
kind: "opt-in-diagnostic",
|
|
196
|
+
description: "promote the entering content layer through the hold"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
key: "flemo:imgoffload",
|
|
200
|
+
storage: "session",
|
|
201
|
+
kind: "production-default-with-override",
|
|
202
|
+
description: "image decode offloader override"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
key: "flemo:devtools",
|
|
206
|
+
storage: "session",
|
|
207
|
+
kind: "opt-in-diagnostic",
|
|
208
|
+
description: "arms this flight recorder in the playground (?devtools=on)"
|
|
209
|
+
}
|
|
210
|
+
], g = "flemo:motion-driver-force", _ = `${g} (localStorage — legacy location)`, v = (e) => {
|
|
211
|
+
try {
|
|
212
|
+
let t = e === "session" ? sessionStorage : localStorage;
|
|
213
|
+
return t.length, t;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}, y = (e, t) => {
|
|
218
|
+
try {
|
|
219
|
+
return e ? e.getItem(t) : null;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}, b = new Set(h.map((e) => e.key)), x = () => {
|
|
224
|
+
let e = {}, t = v("session"), n = v("local");
|
|
225
|
+
for (let r of h) {
|
|
226
|
+
let i = y(r.storage === "session" ? t : n, r.key);
|
|
227
|
+
i !== null && (e[r.key] = i);
|
|
228
|
+
}
|
|
229
|
+
let r = y(n, g);
|
|
230
|
+
r !== null && (e[_] = r);
|
|
231
|
+
for (let [r, i] of [[t, "sessionStorage"], [n, "localStorage"]]) if (r) try {
|
|
232
|
+
for (let t = 0; t < r.length; t += 1) {
|
|
233
|
+
let n = r.key(t);
|
|
234
|
+
!n || !n.startsWith("flemo:") || b.has(n) || (e[`${n} (${i}, unknown key)`] = r.getItem(n) ?? "");
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
return e;
|
|
238
|
+
}, S = (e) => {
|
|
239
|
+
let t = [];
|
|
240
|
+
for (let [n, r] of Object.entries(e)) {
|
|
241
|
+
if (n === g) {
|
|
242
|
+
t.push(`${n}=${r} — A DRIVER PIN IS ACTIVE: every transition this session is forced onto one driver, bypassing measurement and demotion. If you did not set it deliberately just now, it is A/B residue (mobile tab restoration resurrects sessionStorage across days; pins expire after 24h). This exact residue once burned a multi-day investigation — clear the key before judging any behavior.`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (n === _) {
|
|
246
|
+
t.push(`${n}=${r} — legacy localStorage driver pin present. The library removes it on sight and never honors it, but its existence means an old A/B session left residue on this profile.`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (n.startsWith(`${g} `)) {
|
|
250
|
+
t.push(`${n}=${r} — a driver force pin was observed this session (see marker in the key). Even a since-cleared pin may have shaped early driver decisions; treat this session's routing as suspect.`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
let e = h.find((e) => n === e.key || n.startsWith(`${e.key} `));
|
|
254
|
+
!e || e.key === "flemo:devtools" || (e.kind === "opt-in-diagnostic" ? t.push(`${n}=${r} — opt-in diagnostic active (${e.description}); behavior differs from stock. Possible left-over A/B toggle from an earlier debugging session.`) : e.kind === "production-default-with-override" && t.push(`${n}=${r} — production default overridden for this session (${e.description}).`));
|
|
255
|
+
}
|
|
256
|
+
return t;
|
|
257
|
+
}, C = (e) => Math.round(e * 10) / 10, w = (e) => {
|
|
258
|
+
if (e.length === 0) return {
|
|
259
|
+
count: 0,
|
|
260
|
+
medianGapMs: 0,
|
|
261
|
+
maxGapMs: 0,
|
|
262
|
+
over30Count: 0
|
|
263
|
+
};
|
|
264
|
+
let t = [...e].sort((e, t) => e - t);
|
|
265
|
+
return {
|
|
266
|
+
count: e.length,
|
|
267
|
+
medianGapMs: C(t[Math.floor(t.length / 2)]),
|
|
268
|
+
maxGapMs: C(t[t.length - 1]),
|
|
269
|
+
over30Count: e.filter((e) => e >= 30).length
|
|
270
|
+
};
|
|
271
|
+
}, T = (e, t = []) => {
|
|
272
|
+
let n = [...e, ...t];
|
|
273
|
+
return {
|
|
274
|
+
...n.length === 0 ? {
|
|
275
|
+
count: 0,
|
|
276
|
+
medianGapMs: 0,
|
|
277
|
+
maxGapMs: 0
|
|
278
|
+
} : (() => {
|
|
279
|
+
let e = [...n].sort((e, t) => e - t);
|
|
280
|
+
return {
|
|
281
|
+
count: n.length,
|
|
282
|
+
medianGapMs: C(e[Math.floor(e.length / 2)]),
|
|
283
|
+
maxGapMs: C(e[e.length - 1])
|
|
284
|
+
};
|
|
285
|
+
})(),
|
|
286
|
+
longGaps: n.filter((e) => e >= 30).map(C),
|
|
287
|
+
held: w(e),
|
|
288
|
+
released: w(t)
|
|
289
|
+
};
|
|
290
|
+
}, E = (e) => e.length === 0 ? null : {
|
|
291
|
+
maxMs: C(Math.max(...e)),
|
|
292
|
+
over30Count: e.filter((e) => e >= 30).length
|
|
293
|
+
}, D = (e) => {
|
|
294
|
+
let t = e.playerSuppression || e.playerAdvance;
|
|
295
|
+
return t && e.compiledAnimation ? "mixed" : t ? "player" : e.compiledAnimation ? "compiled" : "unknown";
|
|
296
|
+
}, O = (e) => e === "PUSHING" ? "PUSH" : e === "POPPING" ? "POP" : e === "REPLACING" ? "REPLACE" : null, k = (e) => {
|
|
297
|
+
let t = /^matrix3d\(([^)]+)\)$/.exec(e.trim());
|
|
298
|
+
if (t) {
|
|
299
|
+
let e = t[1].split(",").map((e) => Number(e.trim()));
|
|
300
|
+
return e.length === 16 && Number.isFinite(e[12]) ? e[12] : null;
|
|
301
|
+
}
|
|
302
|
+
let n = /^matrix\(([^)]+)\)$/.exec(e.trim());
|
|
303
|
+
if (n) {
|
|
304
|
+
let e = n[1].split(",").map((e) => Number(e.trim()));
|
|
305
|
+
return e.length === 6 && Number.isFinite(e[4]) ? e[4] : null;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}, A = "2", j = "@flemo/devtools resolved to its production entry: nothing was recorded. Run a development build to collect a real report.", M = () => {}, N = () => ({
|
|
309
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
310
|
+
version: "inert",
|
|
311
|
+
environment: {
|
|
312
|
+
userAgent: "",
|
|
313
|
+
uaBrands: null,
|
|
314
|
+
engine: "unknown",
|
|
315
|
+
platform: "",
|
|
316
|
+
maxTouchPoints: 0,
|
|
317
|
+
devicePixelRatio: 0,
|
|
318
|
+
screen: {
|
|
319
|
+
width: 0,
|
|
320
|
+
height: 0
|
|
321
|
+
},
|
|
322
|
+
viewport: {
|
|
323
|
+
width: 0,
|
|
324
|
+
height: 0
|
|
325
|
+
},
|
|
326
|
+
visualViewportScale: null,
|
|
327
|
+
rafCadence: {
|
|
328
|
+
medianGapMs: null,
|
|
329
|
+
sampleCount: 0
|
|
330
|
+
},
|
|
331
|
+
reducedMotion: !1,
|
|
332
|
+
emulationSuspected: !1,
|
|
333
|
+
observation: {
|
|
334
|
+
longTasks: !1,
|
|
335
|
+
elementAnimations: !1,
|
|
336
|
+
playerGapMirror: !1
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
overrides: {
|
|
340
|
+
active: {},
|
|
341
|
+
warnings: [j]
|
|
342
|
+
},
|
|
343
|
+
driverPolicy: {
|
|
344
|
+
demotion: null,
|
|
345
|
+
forcePin: null
|
|
346
|
+
},
|
|
347
|
+
flights: [],
|
|
348
|
+
anomalies: [j],
|
|
349
|
+
blindSpots: [],
|
|
350
|
+
judgingProtocol: [j]
|
|
351
|
+
}), P = (e) => ({
|
|
352
|
+
detach: M,
|
|
353
|
+
report: N
|
|
354
|
+
}), F = (e) => ({ detach: M });
|
|
355
|
+
//#endregion
|
|
356
|
+
export { c as BLIND_SPOTS, h as FLAG_REGISTRY, _ as LEGACY_LOCAL_PIN_KEY, e as LONG_GAP_MS, i as MID_FLIGHT_TASK_MS, n as OPENING_WINDOW_LEAD_MS, r as OPENING_WINDOW_TAIL_MS, A as REPORT_SCHEMA_VERSION, a as STALL_MS, t as STUCK_STATUS_MS, F as attachDevtoolsPanel, P as attachFlightRecorder, m as captureEnvironment, D as classifyDriver, T as computeFrameStats, w as computePhaseStats, E as computePlayerGapStats, o as deriveFlightAnomalies, S as deriveOverrideWarnings, s as deriveReportAnomalies, u as detectEngine, d as isEmulationSuspected, O as kindFromStatus, k as parseTranslateX, f as sampleRafCadence, x as snapshotOverrides };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flemo/devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Zero-config flight recorder for flemo screen transitions: captures per-flight driver routing, frame pacing, long tasks, landing residues, active debug overrides, and environment/observation-trap fingerprints into one JSON report readable by humans and coding agents.",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -48,8 +48,21 @@
|
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
|
+
"exports": {
|
|
52
|
+
".": {
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"development": "./dist/index.mjs",
|
|
55
|
+
"production": "./dist/noop.mjs",
|
|
56
|
+
"default": "./dist/index.mjs"
|
|
57
|
+
},
|
|
58
|
+
"./force": {
|
|
59
|
+
"types": "./dist/index.d.ts",
|
|
60
|
+
"default": "./dist/index.mjs"
|
|
61
|
+
},
|
|
62
|
+
"./package.json": "./package.json"
|
|
63
|
+
},
|
|
51
64
|
"scripts": {
|
|
52
|
-
"build": "vite build",
|
|
65
|
+
"build": "vite build && DEVTOOLS_ENTRY=noop vite build",
|
|
53
66
|
"watch": "vite build --watch",
|
|
54
67
|
"dev": "vite build --watch",
|
|
55
68
|
"lint": "eslint \"**/*.{js,mjs,ts,jsx,tsx,mts}\"",
|