Skip to content

test(nuxt): cover the client plugin's pages-router instrumentation - #25348

Open
61465 wants to merge 4 commits into
getsentry:developfrom
61465:fix/nuxt-client-plugin-tests
Open

61465 wants to merge 4 commits into
getsentry:developfrom
61465:fix/nuxt-client-plugin-tests

Conversation

@61465

@61465 61465 commented Oct 14, 2026

Copy link
Copy Markdown

Summary

Closes #13813.

Fills in the Nuxt client-plugin unit tests requested by @mydea. Picks up where the dormant draft #24583 (@vansh-nagar, inactive since 2026-02; pinged by @JPeer264 on 2026-10-12 with no reply within the stated window) left off and extends it to all five code paths in packages/nuxt/src/runtime/plugins/sentry.client.ts.

What this covers

  1. Vue route provider setup (_INTERNAL_createVueRouteProvider → setRouteProvider)
    • Installs when getClient() returns a client, $router is on the Nuxt app, and no provider is set
    • Respects a user-supplied provider (does not override)
    • Skips without an initialized client
    • Skips without a $router on the Nuxt app
  2. browserTracingIntegration
    • Adds with { router, routeLabel: 'path' } on the happy path
    • Skips without an initialized client
    • Skips without a $router
  3. app:created hook — Vue integration
    • Calls vueIntegration with attachErrorHandler: false and the live Vue app
    • Handles the client-torn-down case (second getClient() returns undefined)
  4. app:error hook — status-code filtering
    • Nuxt error + 4xx/3xx status → skip
    • Nuxt error + 5xx → report
    • Non-Nuxt error → report regardless of status shape
  5. vue:error hook
    • Forwards { error, instance, info } to reportNuxtError

Pattern

Mocks mirror packages/nuxt/test/runtime/hooks/captureErrorHook.test.ts:

  • vi.hoisted(() => vi.fn()) for cross-module mock refs
  • vi.mock(import('…'), async importOriginal => …) for @sentry/core, @sentry/vue, nuxt/app
  • vi.spyOn(utils, 'reportNuxtError') for the local ../utils module

Pure vitest, no native dependencies. Runs under the existing yarn test command in packages/nuxt/.

Local verification

Syntax parse via tsc --noEmit --skipLibCheck on the test file passes (only "Cannot find module" warnings remain for workspace packages, which CI resolves via yarn install). I was unable to materialise the Yarn/npm workspace node_modules locally on my Windows + Node 24 shell (ETARGET on whatwg-url@^17.2.0, same error reported upstream in jsdom's current chain), so I'm relying on your CI matrix to confirm the suite is green across the vitest + jsdom environment configured in packages/nuxt/vite.config.ts.

Notes

  • All five code paths were taken straight from the current src/runtime/plugins/sentry.client.ts; happy to re-shape the test file structure if the preferred house style here is one describe per public behaviour rather than per source-code region.
  • @vansh-nagar's draft is credited above; if they prefer to carry this over I'll close this PR and move my coverage there as review comments.

cc @mydea + @JPeer264 — thanks for the pre-blessing on #13813 and for keeping the issue open.

🤖 Used Claude Code for the test scaffolding and to cross-check the mock shapes against existing nuxt tests; every assertion was signed off by me on content.

@61465
61465 requested a review from a team as a code owner October 14, 2026 03:25
@61465
61465 requested review from nicohrubec and s1gr1d and removed request for a team October 14, 2026 03:25
@github-actions github-actions Bot added the external PR from an external contributor label Oct 14, 2026
@61465

61465 commented Oct 15, 2026

Copy link
Copy Markdown
Author

Just rebased onto develop and hit a conflict with #25341 (@mydea's vue:error 3xx/4xx skip, merged after this PR opened). Upstream now has packages/nuxt/test/runtime/plugins/client.test.ts covering vue:error. This PR adds the same filename with coverage for all 5 code paths (Vue route provider, browserTracingIntegration, app:created, app:error, vue:error).

Two shapes available — happy with either:

  1. Rename this PR's file to client-coverage.test.ts and drop the vue:error block so it only adds coverage for the 4 remaining paths. Keeps fix(nuxt): Skip 3xx/4xx Nuxt errors in the vue:error hook #25341 intact.
  2. Supersede: replace client.test.ts with this PR's 255-line comprehensive version, which still covers everything fix(nuxt): Skip 3xx/4xx Nuxt errors in the vue:error hook #25341 added (vue:error 3xx/4xx skip is preserved).

Shape 1 is lower churn. Shape 2 reduces test file count by one. Which do you prefer?

@github-actions

Copy link
Copy Markdown
Contributor

👋 @nicohrubec, @s1gr1d — Please review this PR when you get a chance!

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

👋 @nicohrubec, @s1gr1d — Please review this PR when you get a chance!

Fills in the Nuxt client-plugin unit tests requested in getsentry#13813.
Supersedes the dormant draft in getsentry#24583 (last activity 2026-02; author
pinged 2026-10-12 by @JPeer264 with no reply).

Covers the five code paths in `src/runtime/plugins/sentry.client.ts`:
  1. Vue route provider setup (installs, respects user provider, skips
     without client, skips without `$router`)
  2. `browserTracingIntegration` (adds with router+path label, skips on
     no client, skips on no router)
  3. `app:created` hook — `vueIntegration` with
     `attachErrorHandler: false`, including client-torn-down path
  4. `app:error` hook — 4xx/3xx status-code skip for Nuxt errors, 5xx
     and non-Nuxt errors still reported
  5. `vue:error` hook — forwards error + instance + info to
     `reportNuxtError`

Follows the mock pattern already used in
`captureErrorHook.test.ts` (vi.hoisted + vi.mock(importOriginal) +
spyOn on local utils). Pure vitest, no native deps.

Refs getsentry#13813.

Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
@61465
61465 force-pushed the fix/nuxt-client-plugin-tests branch from 49070f6 to be38a35 Compare October 22, 2026 11:23

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit be38a35. Configure here.

Comment thread packages/nuxt/test/runtime/plugins/client.test.ts
…#25348

Three concrete gaps against the suite this PR replaced:

- Nuxt-error status filter now also exercises 3xx (301), not just 4xx.
  Both `app:error` and `vue:error` use `it.each([404, 301])` to match
  upstream getsentry#25341's pattern.
- The "non-Nuxt error" case now carries a 4xx statusCode on the error,
  which is what actually exercises the `isNuxtError` guard — without
  the guard the error would be skipped as 4xx. Previous shape passed a
  plain Error with no statusCode, so the guard was not actually under
  test.
- Added the equivalent non-Nuxt-with-statusCode case for `vue:error`
  (previously only present under `app:error`).

No logic change in the plugin — just test coverage.

Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>

@logaretm logaretm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up, the tests do pass but I think we should change a few things before this goes in.

Comment on lines +70 to +73
expect(createVueRouteProvider).toHaveBeenCalledTimes(1);
// The provider is a lazy router getter — confirm it resolves to our router.
const routerGetter = createVueRouteProvider.mock.calls[0]![0] as () => unknown;
expect(routerGetter()).toBe(router);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use this here instead of reading mock.calls?

Suggested change
expect(createVueRouteProvider).toHaveBeenCalledTimes(1);
// The provider is a lazy router getter — confirm it resolves to our router.
const routerGetter = createVueRouteProvider.mock.calls[0]![0] as () => unknown;
expect(routerGetter()).toBe(router);
expect(createVueRouteProvider).toHaveBeenCalledWith(expect.any(Function));

I don't think we need to check that the getter returns the router.

Comment on lines +159 to +165
expect(vueIntegration).toHaveBeenCalledTimes(1);
const vueCallArgs = vueIntegration.mock.calls[0]![0] as {
app: unknown;
attachErrorHandler: boolean;
};
expect(vueCallArgs.app).toBe(vueApp);
expect(vueCallArgs.attachErrorHandler).toBe(false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here:

Suggested change
expect(vueIntegration).toHaveBeenCalledTimes(1);
const vueCallArgs = vueIntegration.mock.calls[0]![0] as {
app: unknown;
attachErrorHandler: boolean;
};
expect(vueCallArgs.app).toBe(vueApp);
expect(vueCallArgs.attachErrorHandler).toBe(false);
expect(vueIntegration).toHaveBeenCalledWith(expect.objectContaining({ app: vueApp, attachErrorHandler: false }));


vi.mock(import('@sentry/core'), async importOriginal => {
const mod = await importOriginal();
return { ...mod, getClient, GLOBAL_OBJ: {} as never };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since GLOBAL_OBJ is stubbed to {}, we never test that _sentryNuxtVueIntegrationOptions gets passed to vueIntegration. That's the Nuxt-specific part here, can you add a test for it?

it('reports errors that are not Nuxt errors from vue:error', async () => {
const hook = await setupHook('vue:error');
const error = Object.assign(new Error('boom'), { statusCode: 404 });
describe('vue:error hook', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are mostly copies of the app:error tests. Can we go back to it.each(['app:error', 'vue:error']) like before and only keep the instance/info test separate?

Four review items from the client.test.ts pass:

1. Replace mock.calls read with an expect.toHaveBeenCalledWith(expect.any(Function))
   assertion for the createVueRouteProvider router-getter case.
2. Mock GLOBAL_OBJ with _sentryNuxtVueIntegrationOptions so the Nuxt-specific
   forwarding path into vueIntegration is actually covered. Previously
   GLOBAL_OBJ was stubbed to {} and the merge was invisible.
3. Replace vueIntegration mock.calls read with
   expect.toHaveBeenCalledWith(expect.objectContaining({...})), and assert
   the GLOBAL_OBJ options flow through.
4. Collapse the duplicated status-code tests on app:error and vue:error
   into one describe.each(['app:error', 'vue:error']) via a small fireHook
   helper. Keep the vue:error-specific instance/info passthrough test as
   its own describe.

Local run (packages/nuxt): 18/18 passed, sentry.client.ts coverage 95.83%.

Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
@61465

61465 commented Oct 27, 2026

Copy link
Copy Markdown
Author

@logaretm — thanks for the pass. Pushed 29e7fce42a.

  1. L73 (createVueRouteProvider): dropped the mock.calls[0] read, now expect.toHaveBeenCalledWith(expect.any(Function)).
  2. L15 (GLOBAL_OBJ): hoisted a _sentryNuxtVueIntegrationOptions object onto GLOBAL_OBJ, and the Vue-integration test now asserts those options flow through vueIntegration() alongside app and attachErrorHandler: false — closes the Nuxt-specific coverage gap you flagged.
  3. L165 (vueIntegration): same expect.objectContaining({...}) shortcut, together with Better non-signing implementation #2.
  4. L237 (app:error/vue:error dedup): collapsed the duplicated status-code tests into describe.each(['app:error', 'vue:error']) with a small fireHook helper. Kept the vue:error-specific instance/info passthrough as its own describe.

Local: 18/18 pass, sentry.client.ts v8 coverage 95.83%.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has CRLF line endings, please format the PR first (also fails in CI).

Comment on lines +210 to +219
it.each([404, 301])('skips reporting when a Nuxt error carries a %d status code', async statusCode => {
const { isNuxtError } = await import('nuxt/app');
const reportSpy = vi.spyOn(utils, 'reportNuxtError').mockImplementation(() => {});
vi.mocked(isNuxtError).mockReturnValue(true);
getClient.mockReturnValue(makeClient());
getRouteProvider.mockReturnValue(undefined);

const nuxtApp = makeNuxtApp();
await (clientPlugin as NuxtPluginShape).setup(nuxtApp);
fireHook(nuxtApp, { statusCode });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isNuxtError checks both status and statusCode. Make sure to test both.

e.g. with this:

it.each([
  { status: 404 },
  { status: 301 },
  { statusCode: 404 },
  { statusCode: 301 },
])('skips reporting when a Nuxt error carries %o', ...)

…elds

Two review items from s1gr1d:

1. The client.test.ts file I pushed had CRLF line endings from a
   Windows checkout with core.autocrlf=true. Converted to LF and
   staged with core.autocrlf=false so the file stays consistent with
   the rest of packages/nuxt.
2. isNuxtError on a Nuxt error can reach the plugin's skip guard via
   either error.status or error.statusCode (see
   sentry.client.ts:45). The previous it.each only covered the
   statusCode branch. Expanded to four cases:
   {status: 404} / {status: 301} / {statusCode: 404} / {statusCode: 301}
   using %o formatting so the test name reflects the shape under test.

Local run (packages/nuxt): 22/22 passed, sentry.client.ts coverage
unchanged at 95.83%.

Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
@61465

61465 commented Oct 27, 2026

Copy link
Copy Markdown
Author

@s1gr1d — thanks for the pass. Pushed bc8a3ff5e3.

  1. CRLF: converted the file to LF and committed with core.autocrlf=false so Git doesn't re-flip it on Windows checkouts. Verified the staged + pushed blob is pure LF.
  2. status / statusCode: the plugin reads error?.status || error?.statusCode at sentry.client.ts:45, so expanded the it.each from [404, 301] to [{status: 404}, {status: 301}, {statusCode: 404}, {statusCode: 301}] using %o so the test name reflects the shape under test.

Local: 22/22 pass, sentry.client.ts coverage still 95.83%.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external PR from an external contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Nuxt pages router unit tests

3 participants