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);