diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb9478862cc..87f6083752cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,73 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- **feat(nuxt): Add Sentry Pinia plugin ([#14138](https://github.com/getsentry/sentry-javascript/pull/14138))** + +The Nuxt SDK now allows you to track Pinia state for captured errors. To enable the Pinia plugin, add the `piniaIntegration` to your client config: + +```ts +// sentry.client.config.ts +import { usePinia } from '#imports'; + +Sentry.init({ + integrations: [ + Sentry.piniaIntegration(usePinia(), { + /* optinal Pinia plugin options */ + }), + ], +}); +``` + +## 8.36.0 + +### Important Changes + +- **feat(nuxt): Add Sentry Pinia plugin ([#14047](https://github.com/getsentry/sentry-javascript/pull/14047))** + +The Nuxt SDK now allows you to track Pinia state for captured errors. To enable the Pinia plugin, set the `trackPinia` option to `true` in your client config: + +```ts +// sentry.client.config.ts + +Sentry.init({ + trackPinia: true, +}); +``` + +Read more about the Pinia plugin in the [Sentry Pinia Documentation](https://docs.sentry.io/platforms/javascript/guides/nuxt/features/pinia/). + +- **feat(nextjs/vercel-edge/cloudflare): Switch to OTEL for performance monitoring ([#13889](https://github.com/getsentry/sentry-javascript/pull/13889))** + +With this release, the Sentry Next.js, and Cloudflare SDKs will now capture performance data based on OpenTelemetry. +Some exceptions apply in cases where Next.js captures inaccurate data itself. + +NOTE: You may experience minor differences in transaction names in Sentry. +Most importantly transactions for serverside pages router invocations will now be named `GET /[param]/my/route` instead of `/[param]/my/route`. +This means that those transactions are now better aligned with the OpenTelemetry semantic conventions. + +### Other Changes + +- deps: Bump bundler plugins and CLI to 2.22.6 and 2.37.0 respectively ([#14050](https://github.com/getsentry/sentry-javascript/pull/14050)) +- feat(deps): bump @opentelemetry/instrumentation-aws-sdk from 0.44.0 to 0.45.0 ([#14099](https://github.com/getsentry/sentry-javascript/pull/14099)) +- feat(deps): bump @opentelemetry/instrumentation-connect from 0.39.0 to 0.40.0 ([#14101](https://github.com/getsentry/sentry-javascript/pull/14101)) +- feat(deps): bump @opentelemetry/instrumentation-express from 0.43.0 to 0.44.0 ([#14102](https://github.com/getsentry/sentry-javascript/pull/14102)) +- feat(deps): bump @opentelemetry/instrumentation-fs from 0.15.0 to 0.16.0 ([#14098](https://github.com/getsentry/sentry-javascript/pull/14098)) +- feat(deps): bump @opentelemetry/instrumentation-kafkajs from 0.3.0 to 0.4.0 ([#14100](https://github.com/getsentry/sentry-javascript/pull/14100)) +- feat(nextjs): Add method and url to route handler request data ([#14084](https://github.com/getsentry/sentry-javascript/pull/14084)) +- feat(node): Add breadcrumbs for `child_process` and `worker_thread` ([#13896](https://github.com/getsentry/sentry-javascript/pull/13896)) +- fix(core): Ensure standalone spans are not sent if SDK is disabled ([#14088](https://github.com/getsentry/sentry-javascript/pull/14088)) +- fix(nextjs): Await flush in api handlers ([#14023](https://github.com/getsentry/sentry-javascript/pull/14023)) +- fix(nextjs): Don't leak webpack types into exports ([#14116](https://github.com/getsentry/sentry-javascript/pull/14116)) +- fix(nextjs): Fix matching logic for file convention type for root level components ([#14038](https://github.com/getsentry/sentry-javascript/pull/14038)) +- fix(nextjs): Respect directives in value injection loader ([#14083](https://github.com/getsentry/sentry-javascript/pull/14083)) +- fix(nuxt): Only wrap `.mjs` entry files in rollup ([#14060](https://github.com/getsentry/sentry-javascript/pull/14060)) +- fix(nuxt): Re-export all exported bindings ([#14086](https://github.com/getsentry/sentry-javascript/pull/14086)) +- fix(nuxt): Server-side setup in readme ([#14049](https://github.com/getsentry/sentry-javascript/pull/14049)) +- fix(profiling-node): Always warn when running on incompatible major version of Node.js ([#14043](https://github.com/getsentry/sentry-javascript/pull/14043)) +- fix(replay): Fix `onError` callback ([#14002](https://github.com/getsentry/sentry-javascript/pull/14002)) +- perf(otel): Only calculate current timestamp once ([#14094](https://github.com/getsentry/sentry-javascript/pull/14094)) +- test(browser-integration): Add sentry DSN route handler by default ([#14095](https://github.com/getsentry/sentry-javascript/pull/14095)) + ## 8.35.0 ### Beta release of the official Nuxt Sentry SDK diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue new file mode 100644 index 000000000000..3d210cf459de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue @@ -0,0 +1,73 @@ + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts index c00ba0d5d9ed..da988a9ee003 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts @@ -4,7 +4,7 @@ export default defineNuxtConfig({ compatibilityDate: '2024-04-03', imports: { autoImport: false }, - modules: ['@sentry/nuxt/module'], + modules: ['@pinia/nuxt', '@sentry/nuxt/module'], runtimeConfig: { public: { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index db56273a7493..178804768e87 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -14,6 +14,7 @@ "test:assert": "pnpm test" }, "dependencies": { + "@pinia/nuxt": "^0.5.5", "@sentry/nuxt": "latest || *", "nuxt": "^3.13.2" }, diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts index 7547bafa6618..dd2183162db9 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/nuxt'; -import { useRuntimeConfig } from '#imports'; +import { usePinia, useRuntimeConfig } from '#imports'; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions @@ -7,4 +7,13 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, trackComponents: true, + integrations: [ + Sentry.piniaIntegration(usePinia(), { + actionTransformer: action => `Transformed: ${action}`, + stateTransformer: state => ({ + transformed: true, + ...state, + }), + }), + ], }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts new file mode 100644 index 000000000000..cad52916ac25 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts @@ -0,0 +1,43 @@ +import { acceptHMRUpdate, defineStore } from '#imports'; + +export const useCartStore = defineStore({ + id: 'cart', + state: () => ({ + rawItems: [] as string[], + }), + getters: { + items: (state): Array<{ name: string; amount: number }> => + state.rawItems.reduce( + (items: any, item: any) => { + const existingItem = items.find((it: any) => it.name === item); + + if (!existingItem) { + items.push({ name: item, amount: 1 }); + } else { + existingItem.amount++; + } + + return items; + }, + [] as Array<{ name: string; amount: number }>, + ), + }, + actions: { + addItem(name: string) { + this.rawItems.push(name); + }, + + removeItem(name: string) { + const i = this.rawItems.lastIndexOf(name); + if (i > -1) this.rawItems.splice(i, 1); + }, + + throwError() { + throw new Error('error'); + }, + }, +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useCartStore, import.meta.hot)); +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts new file mode 100644 index 000000000000..44b057a29f15 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('sends pinia action breadcrumbs and state context', async ({ page }) => { + await page.goto('/pinia-cart'); + + await page.locator('#item-input').fill('item'); + await page.locator('#item-add').click(); + + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0].value === 'This is an error'; + }); + + await page.locator('#throw-error').click(); + + const error = await errorPromise; + + expect(error).toBeTruthy(); + expect(error.breadcrumbs?.length).toBeGreaterThan(0); + + const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'action'); + + expect(actionBreadcrumb).toBeDefined(); + expect(actionBreadcrumb?.message).toBe('Transformed: addItem'); + expect(actionBreadcrumb?.level).toBe('info'); + + const stateContext = error.contexts?.state?.state; + + expect(stateContext).toBeDefined(); + expect(stateContext?.type).toBe('pinia'); + expect(stateContext?.value).toEqual({ + transformed: true, + rawItems: ['item'], + }); +}); diff --git a/package.json b/package.json index 29a272efcc6c..bee335619d24 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,12 @@ "printWidth": 120, "proseWrap": "always", "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "overrides": [{ + "files": "CHANGELOG.md", + "options": { + "proseWrap": "preserve" + } + }] } } diff --git a/packages/nuxt/src/client/index.ts b/packages/nuxt/src/client/index.ts index 583643fa40f1..849c305a22e3 100644 --- a/packages/nuxt/src/client/index.ts +++ b/packages/nuxt/src/client/index.ts @@ -1,3 +1,4 @@ export * from '@sentry/vue'; export { init } from './sdk'; +export { piniaIntegration } from './piniaIntegration'; diff --git a/packages/nuxt/src/client/piniaIntegration.ts b/packages/nuxt/src/client/piniaIntegration.ts new file mode 100644 index 000000000000..bf81501298ba --- /dev/null +++ b/packages/nuxt/src/client/piniaIntegration.ts @@ -0,0 +1,39 @@ +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; + +import { consoleSandbox } from '@sentry/utils'; +import { createSentryPiniaPlugin } from '@sentry/vue'; + +const INTEGRATION_NAME = 'LinkedErrors'; + +type Pinia = { use: (plugin: ReturnType) => void }; + +const _piniaIntegration = (( + // `unknown` here as well because usePinia declares this type: `export declare const usePinia: () => unknown;` + pinia: unknown | Pinia, + options: Parameters[0] = {}, +) => { + return { + name: INTEGRATION_NAME, + setup() { + if (!pinia || (typeof pinia === 'object' && !('use' in pinia))) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] You added the Pinia integration, but the passed parameter `pinia` has the wrong value. Make sure to enable Pinia by adding `"@pinia/nuxt"` to your Nuxt modules array and pass pinia to Sentry with `piniaIntegration(usePinia())`. Current value of `pinia`: ', + pinia, + ); + }); + } else { + (pinia as Pinia).use(createSentryPiniaPlugin(options)); + } + }, + }; +}) satisfies IntegrationFn; + +/** + * Monitor an existing Pinia store. + * + * This only works if "@pinia/nuxt" is added to the `modules` array. + */ +export const piniaIntegration = defineIntegration(_piniaIntegration);