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..b32effbff3b8 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
@@ -7,4 +7,11 @@ Sentry.init({
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
trackComponents: true,
+ trackPinia: {
+ 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/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts
index 6ba29752a308..5b714968d3ca 100644
--- a/packages/nuxt/src/common/types.ts
+++ b/packages/nuxt/src/common/types.ts
@@ -1,10 +1,21 @@
import type { init as initNode } from '@sentry/node';
import type { SentryRollupPluginOptions } from '@sentry/rollup-plugin';
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
-import type { init as initVue } from '@sentry/vue';
+import type { createSentryPiniaPlugin, init as initVue } from '@sentry/vue';
// Omitting 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this)
-export type SentryNuxtClientOptions = Omit[0] & object, 'app'>;
+export type SentryNuxtClientOptions = Omit[0] & object, 'app'> & {
+ /**
+ * Control if an existing Pinia store should be monitored.
+ * Set this to `true` to track with default options or provide your custom Pinia plugin options.
+ *
+ * This only works if "@pinia/nuxt" is added to the `modules` array.
+ *
+ * @default false
+ */
+ trackPinia?: true | Parameters[0];
+};
+
export type SentryNuxtServerOptions = Omit[0] & object, 'app'>;
type SourceMapsOptions = {
diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts
index b89a2fa87a8d..a8b15b937d53 100644
--- a/packages/nuxt/src/runtime/plugins/sentry.client.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts
@@ -1,5 +1,6 @@
import { getClient } from '@sentry/core';
-import { browserTracingIntegration, vueIntegration } from '@sentry/vue';
+import { consoleSandbox } from '@sentry/utils';
+import { browserTracingIntegration, createSentryPiniaPlugin, vueIntegration } from '@sentry/vue';
import { defineNuxtPlugin } from 'nuxt/app';
import { reportNuxtError } from '../utils';
@@ -34,11 +35,12 @@ export default defineNuxtPlugin({
name: 'sentry-client-integrations',
dependsOn: ['sentry-client-config'],
async setup(nuxtApp) {
+ const sentryClient = getClient();
+ const clientOptions = sentryClient && sentryClient.getOptions();
+
// This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside
// will get tree-shaken away
if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) {
- const sentryClient = getClient();
-
if (sentryClient && '$router' in nuxtApp) {
sentryClient.addIntegration(
browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }),
@@ -46,6 +48,23 @@ export default defineNuxtPlugin({
}
}
+ if (clientOptions && 'trackPinia' in clientOptions && clientOptions.trackPinia) {
+ if ('$pinia' in nuxtApp) {
+ (nuxtApp.$pinia as { use: (plugin: unknown) => void }).use(
+ // `trackPinia` is an object with custom options or `true` (pass `undefined` to use default options)
+ createSentryPiniaPlugin(clientOptions.trackPinia === true ? undefined : clientOptions.trackPinia),
+ );
+ } else {
+ clientOptions.debug &&
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn(
+ '[Sentry] You set `trackPinia`, but the Pinia module was not found. Make sure to add `"@pinia/nuxt"` to your modules array.',
+ );
+ });
+ }
+ }
+
nuxtApp.hook('app:created', vueApp => {
const sentryClient = getClient();