Skip to content

Commit 5eaaa98

Browse files
committed
fix(e2e): tighten vitals/frames coverage, retract dead WebKit plan
- drop the vacuous "non-empty method" frames test; the fallback it guards makes it unfailable - vitals spec now asserts values, not just attribute names: finite and non-negative for every reported vital, strictly positive for ttfb/fcp/lcp (not cls/inp, which can legitimately be zero) - label the CHROMIUM_ONLY_VITALS pin as tied to today's Playwright browser builds, not a client guarantee - move the WebKit-race retraction to the top of its design doc section, reword both headings so they stop asserting a client bug, and correct the Goal so it no longer implies CI re-proves this automatically - document hover-preload's effect on SvelteKit traces in the package README, not just the internal spec
1 parent 3cc6caa commit 5eaaa98

5 files changed

Lines changed: 61 additions & 22 deletions

File tree

docs/superpowers/specs/2026-08-11-cross-engine-e2e-design.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ labelled as such.
1111

1212
## Goal
1313

14-
Prove the JavaScript client behaves correctly on all three browser engines, and keep proving it.
15-
Tracing is the part that has to be right.
14+
Prove the JavaScript client behaves correctly on all three browser engines, and make that provable on
15+
demand. Tracing is the part that has to be right. The engine axis is opt-in and defaults to Chromium;
16+
wiring it into continuous integration so it re-proves itself automatically is not done (see "Out of
17+
scope").
1618

1719
## How this started, and why the instrument changed
1820

@@ -53,7 +55,7 @@ traceparent propagation, pageload and navigation roots, span nesting, parameteri
5355
component trees, span errors, aborted XHR handling and the keepalive flush on unload all work on all
5456
three engines, in every framework integration.
5557

56-
### The one bug: SvelteKit load fetch nests under the wrong root on WebKit
58+
### The WebKit failure (not a client bug — retracted)
5759

5860
> **Retracted 2026-08-11.** This is not a client defect. See "Spike result" below: the fetch is a
5961
> hover-triggered SvelteKit preload fired by Playwright's own pointer movement, before the navigation
@@ -165,7 +167,13 @@ preserved when the projects become generated rather than literal.
165167

166168
Contributors need `npx playwright install firefox webkit` once. Document that next to the new script.
167169

168-
## Part two: fix the WebKit race
170+
## Part two: the WebKit failure (retracted — see below)
171+
172+
> **Retracted 2026-08-11.** Neither shape below was built. The spike in "Spike result (2026-08-11)"
173+
> found the premise wrong: `beforeNavigate` is not the earliest signal available, because SvelteKit's
174+
> hover-preload fires a `load` fetch before any navigation-lifecycle hook runs at all. There is no
175+
> client bug to fix here; see "The WebKit failure (not a client bug — retracted)" above. The prose
176+
> below is kept as the record of what was considered before the spike ruled it out.
169177
170178
Chosen direction: **an optional companion for the root layout.**
171179

e2e/specs/engines.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
/** Reported by every engine. Measured 2026-08-11 on Chromium, Firefox 150.0.2 and WebKit 26.4. */
55
export const UNIVERSAL_VITALS = ['ttfb', 'fcp', 'lcp', 'inp'] as const;
66

7-
/** Firefox and WebKit implement no layout-shift observer, so CLS never arrives there. */
7+
/**
8+
* Firefox and WebKit implement no layout-shift observer, so CLS never arrives there. This pins the
9+
* browser builds Playwright ships today. If a future Playwright Firefox or WebKit build adds a
10+
* layout-shift observer, this test starts failing because the engine gained support, not because the
11+
* client broke — update this list, don't go hunting for a client bug.
12+
*/
813
export const CHROMIUM_ONLY_VITALS = ['cls'] as const;
914

1015
export const expectedVitals = (browserName: string): string[] =>

e2e/specs/frames.spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,4 @@ test.describe('stack frames', () => {
4343
// fetched, which is Vite's transformed output, and that is double-quoted today.
4444
expect(top.codeSnippet[String(top.lineNumber)]).toMatch(/throw new Error\(['"]sync-throw['"]\)/);
4545
});
46-
47-
// Every engine has to yield a usable name, whether the engine supplied one or createStackTrace fell
48-
// back. An empty method would reach the Flare interface as a blank row.
49-
test('every frame carries a non-empty method', async ({ page, fakeFlare }) => {
50-
await page.goto('/broken');
51-
await page.waitForLoadState('networkidle');
52-
await page.getByTestId(testIds.brokenTrigger('sync-throw')).click();
53-
54-
const report = await fakeFlare.waitForReport({ timeout: 9000, predicate: isSyncThrow });
55-
const frames = (report.bodyJson as { stacktrace?: Frame[] }).stacktrace ?? [];
56-
57-
expect(frames.length).toBeGreaterThan(0);
58-
for (const frame of frames) {
59-
expect(frame.method.length).toBeGreaterThan(0);
60-
}
61-
});
6246
});

e2e/specs/vitals.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { testIds } from '../../playgrounds/shared/src';
44
import type { FakeFlare } from '../fixtures/fake-flare';
55
import { expect, test } from '../fixtures/fake-flare';
66
import { expectedVitals } from './engines';
7-
import { attributeKeys, spansOf } from './otlp';
7+
import { attr, attributeKeys, spansOf } from './otlp';
88

99
const PREFIX = 'browser.web_vital.';
1010

@@ -23,6 +23,25 @@ const vitalsReported = async (fakeFlare: FakeFlare): Promise<string[]> => {
2323
return [...names].toSorted();
2424
};
2525

26+
/** Latest value seen for each reported vital, across every envelope captured so far. */
27+
const vitalValues = async (fakeFlare: FakeFlare): Promise<Record<string, number>> => {
28+
const values: Record<string, number> = {};
29+
for (const record of await fakeFlare.traces()) {
30+
for (const span of spansOf(record.bodyJson)) {
31+
for (const key of attributeKeys(span)) {
32+
if (key.startsWith(PREFIX)) {
33+
const value = attr(span, key) as { intValue?: number; doubleValue?: number } | undefined;
34+
const number = value?.intValue ?? value?.doubleValue;
35+
if (typeof number === 'number') {
36+
values[key.slice(PREFIX.length)] = number;
37+
}
38+
}
39+
}
40+
}
41+
}
42+
return values;
43+
};
44+
2645
/**
2746
* A full page life: load, interact, leave. The click has to happen before any navigation or INP is
2847
* never recorded, and pagehide is what flushes the late vitals span.
@@ -60,5 +79,19 @@ test.describe('web vitals', () => {
6079
await expect.poll(() => vitalsReported(fakeFlare), { timeout: 9000 }).toEqual(expect.arrayContaining(expected));
6180

6281
expect(await vitalsReported(fakeFlare)).toEqual(expected);
82+
83+
// This catches a client emitting 0, NaN or a non-number for a reported vital. It does not
84+
// catch a unit change (milliseconds vs. seconds), since both are plausible positive numbers.
85+
const values = await vitalValues(fakeFlare);
86+
for (const name of expected) {
87+
const value = values[name];
88+
expect(Number.isFinite(value)).toBe(true);
89+
expect(value).toBeGreaterThanOrEqual(0);
90+
}
91+
// Only these three are guaranteed strictly positive for a real page load. cls can be exactly 0
92+
// (a perfect score, no layout shift) and inp can legitimately round to 0 for a fast interaction.
93+
for (const name of ['ttfb', 'fcp', 'lcp']) {
94+
expect(values[name]).toBeGreaterThan(0);
95+
}
6396
});
6497
});

packages/sveltekit/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ flare.setUser({ id: 123, email: 'jane@example.com', fullName: 'Jane Doe' });
6363

6464
See the [JavaScript identifying-users docs](https://flareapp.io/docs/javascript/data-collection/identifying-users) for the full field list. Pass `null` to clear.
6565

66+
## Hover-preloaded fetches in traces
67+
68+
SvelteKit's recommended default, `data-sveltekit-preload-data="hover"`, starts a route's `load`
69+
function, including its `fetch`, as soon as a visitor hovers a link — before they click it. If they
70+
never click, a trace for the page they were on can still show a fetch for the route they hovered but
71+
never opened. That is a correct record of what the browser did, not a bug, but it can read as wrong
72+
data in a waterfall. Setting `data-sveltekit-preload-data` to a less eager value (for example `"tap"`)
73+
avoids it.
74+
6675
## Documentation
6776

6877
Full documentation on `handleErrorWithFlare`, `captureError`, `trackRouteContext`, lifecycle callbacks, and more is

0 commit comments

Comments
 (0)