feat: SpeedCurve LUX#782
Conversation
Add e2e test suite for useScriptSpeedCurve covering: - Primer injection and snippetVersion initialization - SPA auto-tracking: startSoftNavigation and markLoadTime flow - Navigation timing and sequencing - Label generation (default Nuxt route names) - Back navigation multi-calls - Canceled/blocked navigation with luxNavFailed tag - SPA mode disabled (no calls) - Custom label function (labelFor) - Label persistence when labelFor is false - Manual tracking mode (no auto calls) Uses fixture pages pre-installed with spy object on window.LUX that captures method calls before composable hooks fire during hydration.
Adds the missing `speedcurve` key to `scriptMeta` in `script-meta.ts`, satisfying the `Record<BuiltInRegistryScriptKey, ScriptMeta>` constraint that was failing the unit typecheck.
- sort-keys: alphabetize @speedcurve/lux in package.json and pnpm-workspace.yaml - perfectionist/sort-imports + antfu/if-newline + style/arrow-parens: fix speedcurve.ts - style/max-statements-per-line: split inline rAF mock bodies in test files - style/quote-props: fix test file quote style - test/prefer-lowercase-title: restore SPA capitalization with eslint-disable tags - harlanzw/ai-deslop-passive-voice: disable for "is injected" in docs (technical term)
…solidate e2e timeouts
- Restore SPA capitalization in e2e test names (test/prefer-lowercase-title
was auto-lowercasing 'SPA' to 'sPA'); add eslint-disable-next-line tags
- Consolidate per-test timeouts into describe-level { timeout: 15000 };
only back-navigation test keeps its own 20000ms override
- Add eslint-disable for passive voice on "is injected" in docs
- Fix style/max-statements-per-line in afterNextPaint tests
- Apply auto-fixed style issues (sort-keys, sort-imports, if-newline, etc.)
…derive forwarded keys from schema SpeedCurveInput now intersects UserConfig (= Partial<ConfigObject>) from @speedcurve/lux so the TypeScript type is linked to the upstream source of truth rather than duplicating it. LUX_USER_CONFIG_KEYS is derived from SpeedCurveOptions.entries at module-load time (filtering out composable-only keys id and autoTrackSpaNavigations), so the schema is the single source of truth for which fields get forwarded to window.LUX.
…e2e test The autoTrackSpaNavigations=false test is last in the describe block and runs after 10 other tests (some with 20s timeouts), making it the most susceptible to accumulated fixture-server latency. Give it the same 20s override as the back-nav test.
- Add composableName: 'useScriptSpeedCurve' to registry def so auto-import resolves the correct casing (fixes 500 on all speedcurve fixture routes) - Add setup callback to createPage helper for pre-navigation Playwright config - Use scPage wrapper to block cdn.speedcurve.com so the CDN script never overwrites the window.LUX spy set up by test fixtures - Guard window._luxCalls init with !window._luxCalls so back-navigation remounts don't reset the accumulated call log - Wait for initial page:finish markLoadTime before clearing _luxCalls in the timing-order test (rAF fires between evaluate and click otherwise)
Three isolated chains (spa-auto, no-spa, manual-spa), each with pages A/B/C linked only within their own scenario. manual-spa exposes LUX API buttons for startSoftNavigation, markLoadTime, and addData.
- Merge labelFor into label: accepts string | (to) => string | false - Remove auto from schema (incompatible with spaMode; accessible via proxy) - applyConfig skips label when non-string (functions handled by installAutoTracker) - Add function_/literal(false) union to schema for docs table + dev validation - Add function_() support to generate-registry-types.ts AST resolver - Add unit tests for all three label variants in applyConfig and installAutoTracker
|
@zizzfizzix is attempting to deploy a commit to the Nuxt Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR adds complete SpeedCurve LUX real-user monitoring (RUM) integration to Nuxt Scripts. The implementation includes a valibot schema defining configuration options (id, SPA mode, auto-tracking, label customization), a composable that injects the Lux primer, applies configuration, and automatically instruments Vue Router for soft navigation tracking on SPA navigations. The runtime includes an Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/e2e/basic.test.ts (1)
15-19:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSeparate
setupfrom Playwright page options.
setupis test-helper metadata for configuring the page after creation, not a PlaywrightnewPageoption. Passing it directly tobrowser.newPage(options)would forward an unknown property to Playwright's API. Extract it before passing tonewPage.Proposed fix
async function createPage(path: string, options?: any) { const logs: { text: string, location: string }[] = [] const browser = await getBrowser() - const page = await browser.newPage(options) + const { setup: setupPage, ...pageOptions } = options ?? {} + const page = await browser.newPage(pageOptions)- if (options?.setup) - await options.setup(page) + if (setupPage) + await setupPage(page)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/e2e/basic.test.ts` around lines 15 - 19, The createPage helper is passing test-helper metadata named "setup" into Playwright via browser.newPage(options); extract and remove the setup property before calling browser.newPage so only valid Playwright options are forwarded. In the createPage function (reference symbols: createPage, getBrowser, browser.newPage, setup) read/setup = options?.setup (or destructure { setup, ...playwrightOptions } = options), call browser.newPage with playwrightOptions, then apply the setup callback to the created page afterwards (e.g., await setup(page)) and keep the logging and event wiring as-is.
🧹 Nitpick comments (4)
playground/pages/third-parties/speed-curve/spa-auto/a.vue (1)
2-2: 💤 Low valueConsider consistent import style.
Line 2 uses explicit imports for Nuxt composables, but lines 13–14 rely on auto-imports for Vue's
refandonMounted. While Nuxt auto-imports both by default, mixing explicit and auto-imports reduces consistency.♻️ Option: Add explicit imports for consistency
-import { useHead, useScriptSpeedCurve } from '`#imports`' +import { onMounted, ref, useHead, useScriptSpeedCurve } from '`#imports`'Also applies to: 13-16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playground/pages/third-parties/speed-curve/spa-auto/a.vue` at line 2, The file mixes explicit imports (useHead, useScriptSpeedCurve) with relying on Nuxt auto-imports for Vue refs/onMounted (ref, onMounted), so make imports consistent: add explicit imports for ref and onMounted alongside useHead and useScriptSpeedCurve (or conversely remove explicit Nuxt composable imports and rely on auto-imports for all), updating the import statement that currently references useHead and useScriptSpeedCurve to also import ref and onMounted to keep styling uniform and avoid ambiguity.playground/pages/third-parties/speed-curve/spa-auto/b.vue (1)
2-2: 💤 Low valueConsider consistent import style.
Line 2 uses explicit imports for Nuxt composables, but lines 13–14 rely on auto-imports for Vue's
refandonMounted. While Nuxt auto-imports both by default, mixing explicit and auto-imports reduces consistency.♻️ Option: Add explicit imports for consistency
-import { useHead, useScriptSpeedCurve } from '`#imports`' +import { onMounted, ref, useHead, useScriptSpeedCurve } from '`#imports`'Also applies to: 13-16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@playground/pages/third-parties/speed-curve/spa-auto/b.vue` at line 2, The file mixes explicit Nuxt imports (useHead, useScriptSpeedCurve) with auto-imported Vue APIs (ref, onMounted); make the import style consistent by adding explicit imports for Vue's ref and onMounted into the top import statement (update the line importing useHead and useScriptSpeedCurve to also import ref and onMounted) so references to ref and onMounted in this component use the same explicit import pattern as useHead/useScriptSpeedCurve.test/unit/speedcurve-auto-tracker.test.ts (1)
157-168: ⚡ Quick winAdd a regression test for
labelcallback returningfalse.Current tests validate function labels that return strings, but not the skip-update branch (
false), which is a key route-labeling edge case.Suggested test case
+ it('does not update label when label function returns false', async () => { + const lux = { startSoftNavigation: vi.fn(), markLoadTime: vi.fn(), addData: vi.fn(), label: 'keep-me' } + Object.defineProperty(window, 'LUX', { value: lux, writable: true, configurable: true }) + + const { installAutoTracker } = await import('../../packages/script/src/runtime/registry/speedcurve') + installAutoTracker({ id: '123', label: () => false }) + + const handler = mockBeforeEach.mock.calls[0][0] as (to: any) => void + handler({ name: 'about', path: '/about' }) + + expect(lux.label).toBe('keep-me') + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/unit/speedcurve-auto-tracker.test.ts` around lines 157 - 168, Add a regression test to cover the branch where the provided label callback returns false so route labels are skipped: in the existing test that imports installAutoTracker from '../../packages/script/src/runtime/registry/speedcurve', create a mock LUX (window.LUX) as in the current test, call installAutoTracker({ id: '123', label: () => false }), invoke the captured navigation handler from mockBeforeEach (mockBeforeEach.mock.calls[0][0]) with a route object (e.g., { name: 'about', path: '/about' }), and assert that lux.label was not updated (remains initial empty string or unchanged) to verify the skip-update branch is exercised.test/e2e/speedcurve.test.ts (1)
8-14: ⚡ Quick winConsider aligning async setup with Vitest best practices.
While Vitest supports async
describecallbacks, the recommended approach is to usebeforeAllhooks or test fixtures for asynchronous setup. This keeps test registration clean and defers setup execution to the test execution phase.+import { beforeAll } from 'vitest' + describe('speedcurve', { timeout: 15000 }, () => { + beforeAll(async () => { + await setup({ + rootDir: resolve('../fixtures/speedcurve'), + browser: true, + }) + }) defineSpeedCurveSuite() })Note: This pattern is used elsewhere in the codebase (e.g.,
_linkedin-insight-suite.ts), though several E2E tests currently use asyncdescribe. Consider whether a broader refactoring of E2E test setup is needed for consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/e2e/speedcurve.test.ts` around lines 8 - 14, The describe callback should not be async; move the await setup into a beforeAll hook so setup runs during test execution rather than during registration: change describe('speedcurve', { timeout: 15000 }, async () => { ... }) to a non-async describe and add beforeAll(async () => { await setup({ rootDir: resolve('../fixtures/speedcurve'), browser: true }) }) and then call defineSpeedCurveSuite() (or call it after the beforeAll if it must run post-setup), keeping the same timeout option; use the existing setup(), resolve(), and defineSpeedCurveSuite() symbols to locate and update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/content/scripts/speedcurve.md`:
- Around line 86-90: The fenced code block containing the CSP directives (the
triple-backtick block with lines "script-src cdn.speedcurve.com;", "img-src
lux.speedcurve.com;", "connect-src lux.speedcurve.com beacon.speedcurve.com;")
needs a language specified to satisfy MD040; update the opening backticks from
``` to ```text (or another appropriate language) so the block becomes a fenced
code block with a language tag.
In `@packages/script/src/registry-types.json`:
- Around line 969-980: The registry-types.json SpeedCurve contract is stale: the
exported SpeedCurveOptions const currently lists auto and a simple string label,
but the runtime schema's SpeedCurveOptions allows label to be string | function
| false and does not include auto; regenerate or update registry-types.json from
the runtime schema source so the exported SpeedCurveOptions and SpeedCurveApi
match exactly the runtime schema (ensure SpeedCurveOptions' properties, types
and defaults mirror the schema, and remove or add fields as needed, then re-run
whatever generator produces registry-types.json).
In `@packages/script/src/runtime/registry/schemas.ts`:
- Line 1: The samplerate field's schema currently allows any number even though
docs require 0–100; update the schema definition for the "samplerate" property
(the object schema around lines 1036-1039) to constrain values to the 0–100
range—e.g. use valibot's range validators (import and apply min and max) or wrap
number() with pipe/custom to enforce >=0 and <=100; ensure you also update the
import list (add min and max or the custom helper) so the schema validation
rejects out-of-range samplerate values.
In `@packages/script/src/runtime/registry/speedcurve.ts`:
- Around line 24-26: The dynamic label callback type in SpeedCurveInput is
currently declared to return only string but the API allows returning false;
update the label type to include false (i.e. label?: string | ((to:
RouteLocationNormalized) => string | false) | false) and in the runtime where
the callback is invoked (the code that assigns the callback result to lux.label
in the block around the current assignment at/near lux.label) guard the
assignment so you only set lux.label when the callback result !== false (leave
lux.label unchanged when the callback returns false). Ensure you reference
SpeedCurveInput and the runtime assignment to lux.label so both the type and the
runtime behavior are consistent.
---
Outside diff comments:
In `@test/e2e/basic.test.ts`:
- Around line 15-19: The createPage helper is passing test-helper metadata named
"setup" into Playwright via browser.newPage(options); extract and remove the
setup property before calling browser.newPage so only valid Playwright options
are forwarded. In the createPage function (reference symbols: createPage,
getBrowser, browser.newPage, setup) read/setup = options?.setup (or destructure
{ setup, ...playwrightOptions } = options), call browser.newPage with
playwrightOptions, then apply the setup callback to the created page afterwards
(e.g., await setup(page)) and keep the logging and event wiring as-is.
---
Nitpick comments:
In `@playground/pages/third-parties/speed-curve/spa-auto/a.vue`:
- Line 2: The file mixes explicit imports (useHead, useScriptSpeedCurve) with
relying on Nuxt auto-imports for Vue refs/onMounted (ref, onMounted), so make
imports consistent: add explicit imports for ref and onMounted alongside useHead
and useScriptSpeedCurve (or conversely remove explicit Nuxt composable imports
and rely on auto-imports for all), updating the import statement that currently
references useHead and useScriptSpeedCurve to also import ref and onMounted to
keep styling uniform and avoid ambiguity.
In `@playground/pages/third-parties/speed-curve/spa-auto/b.vue`:
- Line 2: The file mixes explicit Nuxt imports (useHead, useScriptSpeedCurve)
with auto-imported Vue APIs (ref, onMounted); make the import style consistent
by adding explicit imports for Vue's ref and onMounted into the top import
statement (update the line importing useHead and useScriptSpeedCurve to also
import ref and onMounted) so references to ref and onMounted in this component
use the same explicit import pattern as useHead/useScriptSpeedCurve.
In `@test/e2e/speedcurve.test.ts`:
- Around line 8-14: The describe callback should not be async; move the await
setup into a beforeAll hook so setup runs during test execution rather than
during registration: change describe('speedcurve', { timeout: 15000 }, async ()
=> { ... }) to a non-async describe and add beforeAll(async () => { await
setup({ rootDir: resolve('../fixtures/speedcurve'), browser: true }) }) and then
call defineSpeedCurveSuite() (or call it after the beforeAll if it must run
post-setup), keeping the same timeout option; use the existing setup(),
resolve(), and defineSpeedCurveSuite() symbols to locate and update the code.
In `@test/unit/speedcurve-auto-tracker.test.ts`:
- Around line 157-168: Add a regression test to cover the branch where the
provided label callback returns false so route labels are skipped: in the
existing test that imports installAutoTracker from
'../../packages/script/src/runtime/registry/speedcurve', create a mock LUX
(window.LUX) as in the current test, call installAutoTracker({ id: '123', label:
() => false }), invoke the captured navigation handler from mockBeforeEach
(mockBeforeEach.mock.calls[0][0]) with a route object (e.g., { name: 'about',
path: '/about' }), and assert that lux.label was not updated (remains initial
empty string or unchanged) to verify the skip-update branch is exercised.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0a445647-7d04-4794-a71e-6d675713a253
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
FIRST_PARTY.mddocs/content/scripts/speedcurve.mdpackage.jsonpackages/script/package.jsonpackages/script/src/registry-logos.tspackages/script/src/registry-types.jsonpackages/script/src/registry.tspackages/script/src/runtime/registry/schemas.tspackages/script/src/runtime/registry/speedcurve.tspackages/script/src/runtime/types.tspackages/script/src/runtime/utils/after-next-paint.tspackages/script/src/script-meta.tsplayground/pages/third-parties/speed-curve/manual-spa/a.vueplayground/pages/third-parties/speed-curve/manual-spa/b.vueplayground/pages/third-parties/speed-curve/manual-spa/c.vueplayground/pages/third-parties/speed-curve/no-spa/a.vueplayground/pages/third-parties/speed-curve/no-spa/b.vueplayground/pages/third-parties/speed-curve/no-spa/c.vueplayground/pages/third-parties/speed-curve/spa-auto/a.vueplayground/pages/third-parties/speed-curve/spa-auto/b.vueplayground/pages/third-parties/speed-curve/spa-auto/c.vuepnpm-workspace.yamlscripts/generate-registry-types.tstest/e2e/_speedcurve-suite.tstest/e2e/basic.test.tstest/e2e/speedcurve.test.tstest/fixtures/speedcurve/app.vuetest/fixtures/speedcurve/nuxt.config.tstest/fixtures/speedcurve/package.jsontest/fixtures/speedcurve/pages/tpc/speedcurve-custom-label/destination.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-custom-label/index.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-label-off/destination.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-label-off/index.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-manual/destination.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-manual/index.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-no-spa/destination.vuetest/fixtures/speedcurve/pages/tpc/speedcurve-no-spa/index.vuetest/fixtures/speedcurve/pages/tpc/speedcurve/destination.vuetest/fixtures/speedcurve/pages/tpc/speedcurve/index.vuetest/fixtures/speedcurve/tsconfig.jsontest/unit/speedcurve-after-next-paint.test.tstest/unit/speedcurve-auto-tracker.test.tstest/unit/speedcurve-config.test.tstest/unit/speedcurve-primer.test.ts
commit: |
- Regenerate registry-types.json: remove stale `auto` field, update `label` to reflect union([string(), function_(), literal(false)]) - Add minValue(0)/maxValue(100) bounds to samplerate schema field - Allow label callback to return `false` per navigation to skip that navigation's label assignment (type + runtime guard) - Add `text` language to CSP fenced code block in docs (MD040)
- Add explicit imports for ref/onMounted in spa-auto/a.vue and b.vue to match the explicit import style used for useHead/useScriptSpeedCurve - Add regression test: label callback returning false must not update lux.label (covers the string|false return branch added in prior commit)
Extract the setup key from createPage options before passing to browser.newPage() so Playwright only receives valid BrowserNewPageOptions.
The setup callback was added when speedcurve e2e tests lived in basic.test.ts. They have since moved to test/e2e/_speedcurve-suite.ts which handles CDN blocking directly. basic.test.ts changes are out of scope for this PR.
labelFor was renamed to label when the two options were unified.
Summary
useScriptSpeedCurvecomposable with full LUX SDK integration — automatic page-view tracking, SPA navigation support, and manual modesamplerate,sendBeaconOnPageHidden,trackErrors,maxErrors,minMeasureTime,maxMeasureTime,newBeaconOnPageShow,trackHiddenPages,cookieDomain, and SPA-specificspaMode,autoTrackSpaNavigations,label)afterNextPaint, and SPA navigation scenarios (default label, custom label, label disabled, back navigation, cancelled navigation, manual mode)/third-parties/speed-curve/spa-auto,no-spa,manual-spa), and user-facing documentation indocs/scripts/speedcurve.mdKey design decisions
afterNextPaint(fn)utility schedulesmarkLoadTimeafter the nextrequestAnimationFrameflush so it runs after LUX's paint capture window closesautoTrackSpaNavigationsis enabled,vue-routerbeforeEachcallslux.startSoftNavigation()+ sets the label; thepage:finishNuxt hook callsafterNextPaint(() => lux.markLoadTime())to seal the beacon after render;afterEachseals cancelled-navigation phantom beacons withmarkLoadTime+addData('luxNavFailed', '1')labelacceptsstring | ((to: RouteLocationNormalized) => string) | false— functions are resolved per navigation inbeforeEach;falseskips setting the label entirely; default falls back toString(to.name ?? to.path)luxWiredmodule-level flag) prevents double-wiring the router hook during Vite hot reloadsTest plan
pnpm test— all unit tests pass (speedcurve primer, config, auto-tracker, afterNextPaint)pnpm test:e2e— all e2e tests pass (CDN-blocked LUX, all SPA scenario variants)/third-parties/speed-curve/— verifyspa-auto,no-spa, andmanual-spaA→B→C chains in browserLUX_SITE_IDto confirm beacons fire correctly