@flemo/devtools 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.
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/__tests__/anomalies.test.d.ts +1 -0
- package/dist/__tests__/environment.test.d.ts +1 -0
- package/dist/__tests__/overrides.test.d.ts +1 -0
- package/dist/__tests__/panel.test.d.ts +1 -0
- package/dist/__tests__/recorder.branches.test.d.ts +1 -0
- package/dist/__tests__/recorder.guards.test.d.ts +1 -0
- package/dist/__tests__/recorder.regressions.test.d.ts +1 -0
- package/dist/__tests__/recorder.test.d.ts +1 -0
- package/dist/__tests__/sampling.test.d.ts +1 -0
- package/dist/anomalies.d.ts +45 -0
- package/dist/blindSpots.d.ts +1 -0
- package/dist/environment.d.ts +31 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +1056 -0
- package/dist/judging.d.ts +1 -0
- package/dist/overrides.d.ts +22 -0
- package/dist/panel/dom.d.ts +7 -0
- package/dist/panel/format.d.ts +29 -0
- package/dist/panel/index.d.ts +25 -0
- package/dist/panel/styles.d.ts +1 -0
- package/dist/panel/view.d.ts +12 -0
- package/dist/recorder.d.ts +17 -0
- package/dist/sampling.d.ts +26 -0
- package/dist/types.d.ts +280 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 김종혁
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# @flemo/devtools
|
|
2
|
+
|
|
3
|
+
Zero-config flight recorder for [flemo](https://flemo.dev) screen transitions.
|
|
4
|
+
|
|
5
|
+
It watches the DOM surfaces flemo already exposes — `data-flemo-*` attributes,
|
|
6
|
+
`window.__flemoPlayerGaps`, the `flemo:*` storage registry — plus standard
|
|
7
|
+
observers (`MutationObserver`, `PerformanceObserver("longtask")`, rAF), and
|
|
8
|
+
condenses every navigation into one structured JSON report. The goal: a coding
|
|
9
|
+
agent can diagnose a transition problem from the report alone, without
|
|
10
|
+
round-tripping through the user ("which browser? was DevTools open? did you
|
|
11
|
+
leave a toggle set?").
|
|
12
|
+
|
|
13
|
+
Zero dependencies. No imports from `@flemo/core` or `@flemo/react`; attaching
|
|
14
|
+
the recorder never changes the motion it measures.
|
|
15
|
+
|
|
16
|
+
## Keeping it out of your production bundle
|
|
17
|
+
|
|
18
|
+
Install it as a **devDependency** — and know that this alone is not enough.
|
|
19
|
+
`devDependencies` decides what gets INSTALLED (consumers of _your_ package do
|
|
20
|
+
not receive it); it does not decide what gets BUNDLED. A plain top-level import
|
|
21
|
+
of a package you call at runtime ships to every visitor regardless of which
|
|
22
|
+
dependency field it sits in. We measured exactly that on the flemo docs site
|
|
23
|
+
before fixing it.
|
|
24
|
+
|
|
25
|
+
What actually removes it is a **dynamic import behind a build-time constant**,
|
|
26
|
+
which bundlers replace before dead-code elimination:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// Vite
|
|
30
|
+
if (import.meta.env.DEV) {
|
|
31
|
+
const { attachFlightRecorder } = await import("@flemo/devtools");
|
|
32
|
+
attachFlightRecorder({ log: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Next.js / webpack
|
|
36
|
+
if (process.env.NODE_ENV !== "production") {
|
|
37
|
+
const { attachFlightRecorder } = await import("@flemo/devtools");
|
|
38
|
+
attachFlightRecorder({ log: true });
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package ships `"sideEffects": false` and has no import-time side effects,
|
|
43
|
+
so nothing is pulled in by the import statement itself — but a binding you
|
|
44
|
+
actually call cannot be shaken out. Verify with a production build: the string
|
|
45
|
+
`present-pipeline pacing` (from the blind-spot list) must not appear in it.
|
|
46
|
+
|
|
47
|
+
`apps/web/app/[lang]/playground/_hooks/useDevtoolsRecorder` in this repo is a
|
|
48
|
+
working reference.
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { attachFlightRecorder } from "@flemo/devtools";
|
|
54
|
+
|
|
55
|
+
const recorder = attachFlightRecorder({ log: true });
|
|
56
|
+
|
|
57
|
+
// ...navigate around...
|
|
58
|
+
|
|
59
|
+
const report = recorder.report(); // JSON-serializable FlemoReport
|
|
60
|
+
recorder.detach();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The recorder also installs `window.flemo` (unless something else already owns
|
|
64
|
+
that name, or you pass `installGlobal: false`):
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
copy(JSON.stringify(window.flemo.report(), null, 2)); // DevTools console
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
In the flemo playground it is pre-wired: visit `/playground?devtools=on` (the
|
|
71
|
+
toggle is stored in `sessionStorage` under `flemo:devtools`, so it survives
|
|
72
|
+
navigation; `?devtools=off` disarms it).
|
|
73
|
+
|
|
74
|
+
`attachFlightRecorder()` is idempotent — while a recorder is attached, further
|
|
75
|
+
calls return the same handle. In non-DOM environments (SSR) it returns an
|
|
76
|
+
inert handle.
|
|
77
|
+
|
|
78
|
+
## Visual panel
|
|
79
|
+
|
|
80
|
+
The same data, on screen. `attachDevtoolsPanel()` mounts a floating `flemo`
|
|
81
|
+
toggle (flight count, plus a red dot when any flight carries an anomaly) and a
|
|
82
|
+
bottom drawer with the flight list, the per-flight detail, the active
|
|
83
|
+
overrides, and the blind-spot list.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { attachDevtoolsPanel } from "@flemo/devtools";
|
|
87
|
+
|
|
88
|
+
const panel = attachDevtoolsPanel(); // reads window.flemo, or attaches its own
|
|
89
|
+
// panel.detach();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The panel is opt-in and mounted by the consumer: nothing in flemo attaches it
|
|
93
|
+
for you, and the flemo playground deliberately arms the recorder only (the
|
|
94
|
+
site should not ship a debug UI). Mount it behind your own dev-only flag.
|
|
95
|
+
|
|
96
|
+
Options: `recorder` (a handle from `attachFlightRecorder`; defaults to this
|
|
97
|
+
package's `window.flemo`, otherwise the panel attaches — and owns — one),
|
|
98
|
+
`initialOpen` (default false), `position` (`"bottom-right"` default, or
|
|
99
|
+
`"bottom-left"`). Zero dependencies, no framework: vanilla DOM inside an open
|
|
100
|
+
shadow root, so no consumer CSS reaches in and none of the panel's reaches
|
|
101
|
+
out. The host is `position: fixed`, zero-sized, marked
|
|
102
|
+
`data-flemo-devtools-panel`, and carries no `data-flemo-*` screen attributes —
|
|
103
|
+
the recorder never sees the panel as a flight participant. Drag the drawer's
|
|
104
|
+
top edge to resize; the height persists in `sessionStorage` under
|
|
105
|
+
`flemo:devtools-panel-height`.
|
|
106
|
+
|
|
107
|
+
**It never repaints during a flight.** That is the design constraint, not a
|
|
108
|
+
nicety: this project once spent weeks chasing stutter that turned out to be
|
|
109
|
+
_DevTools being open_, and a measurement surface
|
|
110
|
+
that repaints mid-transition reproduces that artifact and then reports it as a
|
|
111
|
+
finding. So the panel
|
|
112
|
+
|
|
113
|
+
- refreshes on a `setTimeout` chain (~3×/s open, once per 2s closed) — never a
|
|
114
|
+
persistent `requestAnimationFrame` loop competing with the motion,
|
|
115
|
+
- **skips any refresh while a screen carries a transitional
|
|
116
|
+
`data-flemo-status`** and retries on the next tick — deferred user actions
|
|
117
|
+
(open/close, row selection) included,
|
|
118
|
+
- renders only the toggle button while closed,
|
|
119
|
+
- ships no CSS transitions and no keyframes at all.
|
|
120
|
+
|
|
121
|
+
Do not turn it into a live-updating dashboard.
|
|
122
|
+
|
|
123
|
+
## Report schema (version "2")
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
{
|
|
127
|
+
"generatedAt": "2026-08-17T09:00:00.000Z",
|
|
128
|
+
"version": "2",
|
|
129
|
+
"environment": {
|
|
130
|
+
"userAgent": "…",
|
|
131
|
+
"uaBrands": [{ "brand": "Chromium", "version": "126" }],
|
|
132
|
+
"engine": "blink", // blink | webkit | gecko | unknown
|
|
133
|
+
"platform": "MacIntel",
|
|
134
|
+
"maxTouchPoints": 0,
|
|
135
|
+
"devicePixelRatio": 2,
|
|
136
|
+
"screen": { "width": 1728, "height": 1117 },
|
|
137
|
+
"viewport": { "width": 1280, "height": 720 },
|
|
138
|
+
"visualViewportScale": 1,
|
|
139
|
+
"rafCadence": { "medianGapMs": 16.67, "sampleCount": 20 }, // idle sample at attach
|
|
140
|
+
"reducedMotion": false,
|
|
141
|
+
"emulationSuspected": false, // DevTools device-toolbar signature
|
|
142
|
+
"observation": { "longTasks": true, "elementAnimations": true, "playerGapMirror": false }
|
|
143
|
+
},
|
|
144
|
+
"overrides": {
|
|
145
|
+
// Every flemo:* storage key currently set (both storages), including
|
|
146
|
+
// unknown keys and keys present at attach but cleared since (marked).
|
|
147
|
+
"active": { "flemo:apply": "scrub" },
|
|
148
|
+
// Read these FIRST. A non-empty warnings list means the session does not
|
|
149
|
+
// run stock behavior — especially the driver force pin, which pins EVERY
|
|
150
|
+
// transition and once burned a multi-day investigation as residue.
|
|
151
|
+
"warnings": ["flemo:apply=scrub — opt-in diagnostic active (…)"]
|
|
152
|
+
},
|
|
153
|
+
"driverPolicy": {
|
|
154
|
+
"demotion": null, // localStorage flemo:motion-driver ("css" = player demoted)
|
|
155
|
+
"forcePin": null // sessionStorage flemo:motion-driver-force — non-null = PIN ACTIVE
|
|
156
|
+
},
|
|
157
|
+
"flights": [
|
|
158
|
+
{
|
|
159
|
+
"id": "flight-1",
|
|
160
|
+
"routerId": "…", // when the screen stamps data-flemo-router
|
|
161
|
+
"kind": "PUSH", // PUSH | POP | REPLACE
|
|
162
|
+
"t0": { "ms": 1234.5, "iso": "…" }, // performance.now + wall clock
|
|
163
|
+
"t1": { "ms": 1834.5, "iso": "…" },
|
|
164
|
+
"durationMs": 600,
|
|
165
|
+
// player | compiled | mixed | unknown — detected per flight from the
|
|
166
|
+
// DOM signature (inline animation suppression + advancing inline pose
|
|
167
|
+
// = player; a running flemo-* CSSAnimation = compiled). Never assume a
|
|
168
|
+
// platform always routes one tier; routing policies evolve.
|
|
169
|
+
"driver": "compiled",
|
|
170
|
+
"participants": { "screens": 2, "bars": 0, "decorators": 1, "parts": 0 },
|
|
171
|
+
// data-flemo-anim-hold: releasedAtMs = when the LAST hold released,
|
|
172
|
+
// relative to t0. The engine absorbs heavy commits INTO the hold (the
|
|
173
|
+
// screen is posed, not moving), so everything below is segmented on
|
|
174
|
+
// this boundary.
|
|
175
|
+
"holds": { "kind": "park-under", "releasedAtMs": 120 },
|
|
176
|
+
"frameSamples": {
|
|
177
|
+
"count": 36,
|
|
178
|
+
"medianGapMs": 16.7,
|
|
179
|
+
"maxGapMs": 17.2,
|
|
180
|
+
"longGaps": [],
|
|
181
|
+
// held-phase gaps are absorbed by design — no anomaly ever fires on them;
|
|
182
|
+
"held": { "count": 7, "medianGapMs": 18.1, "maxGapMs": 45.0, "over30Count": 1 },
|
|
183
|
+
// released-phase gaps are visible motion — the anomaly rules key on these.
|
|
184
|
+
"released": { "count": 29, "medianGapMs": 16.7, "maxGapMs": 17.2, "over30Count": 0 }
|
|
185
|
+
},
|
|
186
|
+
// Did it MOVE — a different question from "did frames arrive". A
|
|
187
|
+
// compiled flight is read off its own animation clock, a player flight
|
|
188
|
+
// off the inline pose it writes; neither forces a style flush.
|
|
189
|
+
"motion": {
|
|
190
|
+
"sampledFrames": 29,
|
|
191
|
+
"stalledFrames": 0, // released frames where neither clock nor pose moved
|
|
192
|
+
"longestStallMs": 0, // >= 48ms raises an anomaly
|
|
193
|
+
"pausedAfterRelease": false, // playState went "paused" mid-motion
|
|
194
|
+
"holdReassertedAtMs": null // a hold went back ON after releasing
|
|
195
|
+
},
|
|
196
|
+
// Images inside the participants. One still-loading <img> completing
|
|
197
|
+
// mid-flight costs one skipped present (glass-measured 1:1), which is
|
|
198
|
+
// why the engine holds them; an unheld completion is that regression.
|
|
199
|
+
// completedUnheld is the number that matters, counted PER IMAGE: a
|
|
200
|
+
// held-but-still-loading image must not cancel out an unheld completed
|
|
201
|
+
// one. addedDuringFlight covers images a data commit inserts mid-
|
|
202
|
+
// navigation, which is the case core's image hold also watches for.
|
|
203
|
+
"images": {
|
|
204
|
+
"loadingAtStart": 12,
|
|
205
|
+
"addedDuringFlight": 0,
|
|
206
|
+
"completedDuringFlight": 0,
|
|
207
|
+
"heldDuringFlight": 12,
|
|
208
|
+
"completedUnheld": 0
|
|
209
|
+
},
|
|
210
|
+
"playerGaps": { "maxMs": 42.3, "over30Count": 1 }, // only if the player mirror grew
|
|
211
|
+
"longTasks": [{ "startMs": 1200.0, "durationMs": 180.0 }], // intersecting visible motion
|
|
212
|
+
"holdLongTasks": [], // fully absorbed by the hold — engine working as designed
|
|
213
|
+
"landing": {
|
|
214
|
+
// Audited 2 rAF after COMPLETED:
|
|
215
|
+
"residualInlineTransforms": [], // inline transform/opacity leftovers
|
|
216
|
+
"offViewportAtRest": false, // the blank-viewport (PR #259) signature
|
|
217
|
+
"stuckStatuses": [], // transitional statuses >10s old
|
|
218
|
+
// Hold markers still on the page at rest. Skipped (left empty) when
|
|
219
|
+
// another flight is already running: two frames after a landing, a
|
|
220
|
+
// fast back-to-back navigation legitimately owns holds of its own.
|
|
221
|
+
"orphanedHolds": []
|
|
222
|
+
},
|
|
223
|
+
"anomalies": ["long task 180ms overlapped flight start (opening-swallow risk)"]
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"anomalies": [], // session-level: active pins, emulation, stuck flights
|
|
227
|
+
"blindSpots": ["…"], // constant — see below
|
|
228
|
+
"judgingProtocol": ["…"] // constant — the preconditions a verdict needs
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## What it is built to catch
|
|
233
|
+
|
|
234
|
+
Every rule below exists because the matching defect actually shipped, was
|
|
235
|
+
lived with, and cost days. They share one property: **frame timing was clean
|
|
236
|
+
through all of them.** A recorder that only measured gaps would have called
|
|
237
|
+
each of these sessions healthy.
|
|
238
|
+
|
|
239
|
+
| Signature in `anomalies` | The defect it guards against |
|
|
240
|
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
241
|
+
| `hold re-asserted …ms into the flight` | An interleaved commit writing the stale paused hold attribute over a running flight — froze motion ~250ms, intermittently |
|
|
242
|
+
| `motion stalled …ms mid-flight` | Any freeze / freeze-then-leap: the pose stopped advancing while rAF kept ticking |
|
|
243
|
+
| `playState=paused` | The flight was posed and then stopped, rather than starved of frames |
|
|
244
|
+
| `image(s) finished loading mid-flight without a hold` | Warm-side image decode rastering on the sliding layer — one skipped present per decode |
|
|
245
|
+
| `hold markers left on the page at rest` | Orphaned image/arrival holds: content hidden with no owner left to reveal it (the permanently-blank-avatar class) |
|
|
246
|
+
| `screen resting at from-pose while COMPLETED+active` | The blank-viewport landing (PR #259) |
|
|
247
|
+
| `long task …ms overlapped the visible-motion start` | The swallowed opening (씹힘) |
|
|
248
|
+
| `transitional status stuck >10s` | A locked navigation queue — every later navigation is silently ignored |
|
|
249
|
+
| `active force pin flemo:motion-driver-force=…` | A/B residue pinning the whole session to one driver (this one burned a multi-day investigation) |
|
|
250
|
+
|
|
251
|
+
## The judging protocol
|
|
252
|
+
|
|
253
|
+
Every report also carries `judgingProtocol`. It is not derived — the page
|
|
254
|
+
cannot check it — so it is stated instead:
|
|
255
|
+
|
|
256
|
+
- **DevTools closed.** The 2026-08 campaign's entire residual "stutter" was
|
|
257
|
+
the open inspector, verified bidirectionally on the reporting machine. A
|
|
258
|
+
clean report from a DevTools-open session proves nothing.
|
|
259
|
+
- **No screen capture running.** A capture client forces the compositor to a
|
|
260
|
+
steady cadence and _suppresses_ the symptom.
|
|
261
|
+
- **Real input.** Synthetic dispatch never fires `pointerdown`, so it bypasses
|
|
262
|
+
the gesture machinery a real navigation goes through.
|
|
263
|
+
- **Known viewing configuration.** Emulation off; which display, refresh rate,
|
|
264
|
+
HiDPI scaling, Low Power Mode.
|
|
265
|
+
|
|
266
|
+
## Blind spots
|
|
267
|
+
|
|
268
|
+
Every report ends with a constant `blindSpots` list: the layers **no in-page
|
|
269
|
+
instrument can see**, each of which once consumed a real investigation:
|
|
270
|
+
|
|
271
|
+
- macOS Chrome present-pipeline pacing (Chromium issues 40062488/345275139) —
|
|
272
|
+
frames judder on 120Hz ProMotion while every in-page metric reads clean;
|
|
273
|
+
proven with a no-script pure-CSS control page.
|
|
274
|
+
- Display-hardware effects (local dimming, backlight modulation).
|
|
275
|
+
- Compositor-internal present skips invisible to rAF.
|
|
276
|
+
- DevTools device emulation composites to a rescaled surface — instruments
|
|
277
|
+
read the pre-scale surface, the eye watches the post-scale one.
|
|
278
|
+
|
|
279
|
+
If the report is clean and the user still sees jank, the cause lives in one of
|
|
280
|
+
these. Do not chase them with in-page tooling.
|
|
281
|
+
|
|
282
|
+
## Handing a report to an agent
|
|
283
|
+
|
|
284
|
+
1. Reproduce the problem once with the recorder attached (playground:
|
|
285
|
+
`/playground?devtools=on`).
|
|
286
|
+
2. `copy(JSON.stringify(window.flemo.report(), null, 2))` in the console.
|
|
287
|
+
3. Paste the JSON into the issue/conversation.
|
|
288
|
+
|
|
289
|
+
Reading order for the agent: `overrides.warnings` (is this session even
|
|
290
|
+
stock?) → `environment.emulationSuspected` + `rafCadence` (is the observation
|
|
291
|
+
trustworthy? what display cadence?) → per-flight `driver` + `anomalies`
|
|
292
|
+
(which tier ran, what went wrong, when) → `blindSpots` (what not to chase).
|
|
293
|
+
All anomaly strings are stable, grep-friendly signatures.
|
|
294
|
+
|
|
295
|
+
## API
|
|
296
|
+
|
|
297
|
+
- `attachFlightRecorder(options?)` → `{ report(), detach() }` — options:
|
|
298
|
+
`maxFlights` (default 50), `log` (default false), `installGlobal`
|
|
299
|
+
(default true).
|
|
300
|
+
- `attachDevtoolsPanel(options?)` → `{ detach() }` — options: `recorder`,
|
|
301
|
+
`initialOpen` (default false), `position` (default `"bottom-right"`).
|
|
302
|
+
Idempotent while mounted; inert without a DOM.
|
|
303
|
+
- Pure helpers (unit-testable, no DOM): `deriveFlightAnomalies`,
|
|
304
|
+
`deriveReportAnomalies`, `deriveOverrideWarnings`, `classifyDriver`,
|
|
305
|
+
`computeFrameStats`, `computePlayerGapStats`, `parseTranslateX`,
|
|
306
|
+
`kindFromStatus`.
|
|
307
|
+
- Constants/registries: `BLIND_SPOTS`, `FLAG_REGISTRY`, `LONG_GAP_MS`,
|
|
308
|
+
`STUCK_STATUS_MS`, `REPORT_SCHEMA_VERSION`.
|
|
309
|
+
- Environment probes: `captureEnvironment`, `detectEngine`,
|
|
310
|
+
`isEmulationSuspected`, `sampleRafCadence`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FlightDriver, FrameSampleStats, ImageActivity, LandingAudit, LongTaskSpan, MotionProgress, PlayerGapStats } from './types';
|
|
2
|
+
/** A gap at/over this is at least one missed 60Hz frame (mirrors core). */
|
|
3
|
+
export declare const LONG_GAP_MS = 30;
|
|
4
|
+
/** A transitional status older than this is a stuck flight. */
|
|
5
|
+
export declare const STUCK_STATUS_MS = 10000;
|
|
6
|
+
/** Long tasks intersecting [t0 - lead, t0 + tail] threaten the opening. */
|
|
7
|
+
export declare const OPENING_WINDOW_LEAD_MS = 50;
|
|
8
|
+
export declare const OPENING_WINDOW_TAIL_MS = 120;
|
|
9
|
+
/** Mid-flight long tasks at/over this get their own anomaly line. */
|
|
10
|
+
export declare const MID_FLIGHT_TASK_MS = 100;
|
|
11
|
+
/**
|
|
12
|
+
* A stall this long is user-visible. Two dropped 60Hz frames is the smallest
|
|
13
|
+
* run the eye reliably catches on a tracked slide; the release race that
|
|
14
|
+
* motivated this rule froze flights for ~250ms.
|
|
15
|
+
*/
|
|
16
|
+
export declare const STALL_MS = 48;
|
|
17
|
+
export interface FlightAnomalyInput {
|
|
18
|
+
t0Ms: number;
|
|
19
|
+
t1Ms: number;
|
|
20
|
+
driver: FlightDriver;
|
|
21
|
+
frameSamples: FrameSampleStats;
|
|
22
|
+
playerGaps: PlayerGapStats | null;
|
|
23
|
+
/** Long tasks intersecting the RELEASED (visible-motion) phase. */
|
|
24
|
+
longTasks: LongTaskSpan[];
|
|
25
|
+
/** Long tasks fully absorbed by the hold phase (informational only). */
|
|
26
|
+
holdLongTasks: LongTaskSpan[];
|
|
27
|
+
/** Hold release offset from t0 — the start of visible motion (null: no hold). */
|
|
28
|
+
releasedAtMs: number | null;
|
|
29
|
+
landing: LandingAudit;
|
|
30
|
+
motion: MotionProgress;
|
|
31
|
+
images: ImageActivity;
|
|
32
|
+
}
|
|
33
|
+
export declare const deriveFlightAnomalies: (input: FlightAnomalyInput) => string[];
|
|
34
|
+
export interface ReportAnomalyInput {
|
|
35
|
+
forcePin: string | null;
|
|
36
|
+
legacyLocalForcePin: string | null;
|
|
37
|
+
/** Force-pin value seen at attach but cleared by report time, if any. */
|
|
38
|
+
clearedForcePin: string | null;
|
|
39
|
+
emulationSuspected: boolean;
|
|
40
|
+
platform: string;
|
|
41
|
+
/** True when a flight is still transitional past STUCK_STATUS_MS. */
|
|
42
|
+
stuckFlightOpen: boolean;
|
|
43
|
+
flightAnomalies: string[][];
|
|
44
|
+
}
|
|
45
|
+
export declare const deriveReportAnomalies: (input: ReportAnomalyInput) => string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BLIND_SPOTS: readonly string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EnvironmentFingerprint } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Engine classification. Chromium brand in UA-CH is the only reliable Blink
|
|
4
|
+
* signal (mere presence of userAgentData is not — WebKit shipped it in 2025).
|
|
5
|
+
* iOS Chrome is WebKit underneath and ships no Chromium brand, so it
|
|
6
|
+
* correctly lands in "webkit".
|
|
7
|
+
*/
|
|
8
|
+
export declare const detectEngine: () => "blink" | "webkit" | "gecko" | "unknown";
|
|
9
|
+
/**
|
|
10
|
+
* DevTools device-emulation signature: the device toolbar force-enables touch
|
|
11
|
+
* emulation, so Blink + a DESKTOP platform + touch points is its fingerprint.
|
|
12
|
+
* Duplicated from packages/core/src/core/engine/emulationNotice.ts (a zero-
|
|
13
|
+
* dependency package cannot import it); core's console notice restricts
|
|
14
|
+
* itself to Mac, where the signal is unambiguous — here Win/Linux are also
|
|
15
|
+
* flagged as SUSPECTED because the report carries the platform string, so an
|
|
16
|
+
* agent can weigh the Windows-touch-laptop ambiguity itself.
|
|
17
|
+
*/
|
|
18
|
+
export declare const isEmulationSuspected: () => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Sample the idle rAF cadence: median gap over ~`frames` frames. Run at
|
|
21
|
+
* attach, while the page is quiet, so the report can distinguish a 60Hz
|
|
22
|
+
* display from 120Hz ProMotion or an LPM-capped ~30Hz clock.
|
|
23
|
+
*/
|
|
24
|
+
export declare const sampleRafCadence: (frames?: number) => Promise<{
|
|
25
|
+
medianGapMs: number | null;
|
|
26
|
+
sampleCount: number;
|
|
27
|
+
}>;
|
|
28
|
+
export declare const captureEnvironment: (rafCadence: {
|
|
29
|
+
medianGapMs: number | null;
|
|
30
|
+
sampleCount: number;
|
|
31
|
+
}) => EnvironmentFingerprint;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { attachFlightRecorder, REPORT_SCHEMA_VERSION } from './recorder';
|
|
2
|
+
export type { FlemoGlobal } from './recorder';
|
|
3
|
+
export { deriveFlightAnomalies, deriveReportAnomalies, LONG_GAP_MS, STUCK_STATUS_MS, OPENING_WINDOW_LEAD_MS, OPENING_WINDOW_TAIL_MS, MID_FLIGHT_TASK_MS } from './anomalies';
|
|
4
|
+
export type { FlightAnomalyInput, ReportAnomalyInput } from './anomalies';
|
|
5
|
+
export { BLIND_SPOTS } from './blindSpots';
|
|
6
|
+
export { attachDevtoolsPanel } from './panel';
|
|
7
|
+
export type { DevtoolsPanelHandle, DevtoolsPanelOptions } from './panel';
|
|
8
|
+
export { captureEnvironment, detectEngine, isEmulationSuspected, sampleRafCadence } from './environment';
|
|
9
|
+
export { deriveOverrideWarnings, FLAG_REGISTRY, LEGACY_LOCAL_PIN_KEY, snapshotOverrides } from './overrides';
|
|
10
|
+
export type { FlagClass, FlagDescriptor } from './overrides';
|
|
11
|
+
export { classifyDriver, computeFrameStats, computePhaseStats, computePlayerGapStats, kindFromStatus, parseTranslateX } from './sampling';
|
|
12
|
+
export type { DriverEvidence } from './sampling';
|
|
13
|
+
export type { DriverPolicySection, EnvironmentFingerprint, FlemoReport, FlightDriver, FlightHolds, FlightKind, FlightParticipants, FlightRecord, FlightRecorderHandle, FlightRecorderOptions, FlightTimestamp, FramePhaseStats, FrameSampleStats, LandingAudit, LongTaskSpan, ObservationCapabilities, OverridesSection, PlayerGapStats, UaBrand } from './types';
|