Skip to content

Commit 738870d

Browse files
authored
feat(nuxt): Add piniaIntegration (#14138)
After reverting #14134, this is a more bundle-size friendly way of adding monitoring for Pinia in Nuxt. 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(), { /* optional Pinia plugin options */ }), ], });
1 parent c579a45 commit 738870d

File tree

9 files changed

+220
-2
lines changed

9 files changed

+220
-2
lines changed

CHANGELOG.md

+17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13+
- **feat(nuxt): Add `piniaIntegration` ([#14138](https://github.com/getsentry/sentry-javascript/pull/14138))**
14+
15+
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:
16+
17+
```ts
18+
// sentry.client.config.ts
19+
import { usePinia } from '#imports';
20+
21+
Sentry.init({
22+
integrations: [
23+
Sentry.piniaIntegration(usePinia(), {
24+
/* optional Pinia plugin options */
25+
}),
26+
],
27+
});
28+
```
29+
1330
## 8.36.0
1431

1532
### Important Changes
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup lang="ts">
2+
import { ref } from '#imports'
3+
import { useCartStore } from '~~/stores/cart'
4+
5+
const cart = useCartStore()
6+
7+
const itemName = ref('')
8+
9+
function addItemToCart() {
10+
if (!itemName.value) return
11+
cart.addItem(itemName.value)
12+
itemName.value = ''
13+
}
14+
15+
function throwError() {
16+
throw new Error('This is an error')
17+
}
18+
19+
function clearCart() {
20+
if (window.confirm('Are you sure you want to clear the cart?')) {
21+
cart.rawItems = []
22+
}
23+
}
24+
</script>
25+
26+
<template>
27+
<Layout>
28+
<div>
29+
<div style="margin: 1rem 0;">
30+
<PiniaLogo />
31+
</div>
32+
33+
<form @submit.prevent="addItemToCart" data-testid="add-items">
34+
<input id="item-input" type="text" v-model="itemName" />
35+
<button id="item-add">Add</button>
36+
<button id="throw-error" @click="throwError">Throw error</button>
37+
</form>
38+
39+
<form>
40+
<ul data-testid="items">
41+
<li v-for="item in cart.items" :key="item.name">
42+
{{ item.name }} ({{ item.amount }})
43+
<button
44+
@click="cart.removeItem(item.name)"
45+
type="button"
46+
>X</button>
47+
</li>
48+
</ul>
49+
50+
<button
51+
:disabled="!cart.items.length"
52+
@click="clearCart"
53+
type="button"
54+
data-testid="clear"
55+
>Clear the cart</button>
56+
</form>
57+
</div>
58+
</Layout>
59+
</template>
60+
61+
62+
63+
<style scoped>
64+
img {
65+
width: 200px;
66+
}
67+
68+
button,
69+
input {
70+
margin-right: 0.5rem;
71+
margin-bottom: 0.5rem;
72+
}
73+
</style>

dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default defineNuxtConfig({
44
compatibilityDate: '2024-04-03',
55
imports: { autoImport: false },
66

7-
modules: ['@sentry/nuxt/module'],
7+
modules: ['@pinia/nuxt', '@sentry/nuxt/module'],
88

99
runtimeConfig: {
1010
public: {

dev-packages/e2e-tests/test-applications/nuxt-4/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"test:assert": "pnpm test"
1515
},
1616
"dependencies": {
17+
"@pinia/nuxt": "^0.5.5",
1718
"@sentry/nuxt": "latest || *",
1819
"nuxt": "^3.13.2"
1920
},
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import * as Sentry from '@sentry/nuxt';
2-
import { useRuntimeConfig } from '#imports';
2+
import { usePinia, useRuntimeConfig } from '#imports';
33

44
Sentry.init({
55
environment: 'qa', // dynamic sampling bias to keep transactions
66
dsn: useRuntimeConfig().public.sentry.dsn,
77
tunnel: `http://localhost:3031/`, // proxy server
88
tracesSampleRate: 1.0,
99
trackComponents: true,
10+
integrations: [
11+
Sentry.piniaIntegration(usePinia(), {
12+
actionTransformer: action => `Transformed: ${action}`,
13+
stateTransformer: state => ({
14+
transformed: true,
15+
...state,
16+
}),
17+
}),
18+
],
1019
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { acceptHMRUpdate, defineStore } from '#imports';
2+
3+
export const useCartStore = defineStore({
4+
id: 'cart',
5+
state: () => ({
6+
rawItems: [] as string[],
7+
}),
8+
getters: {
9+
items: (state): Array<{ name: string; amount: number }> =>
10+
state.rawItems.reduce(
11+
(items: any, item: any) => {
12+
const existingItem = items.find((it: any) => it.name === item);
13+
14+
if (!existingItem) {
15+
items.push({ name: item, amount: 1 });
16+
} else {
17+
existingItem.amount++;
18+
}
19+
20+
return items;
21+
},
22+
[] as Array<{ name: string; amount: number }>,
23+
),
24+
},
25+
actions: {
26+
addItem(name: string) {
27+
this.rawItems.push(name);
28+
},
29+
30+
removeItem(name: string) {
31+
const i = this.rawItems.lastIndexOf(name);
32+
if (i > -1) this.rawItems.splice(i, 1);
33+
},
34+
35+
throwError() {
36+
throw new Error('error');
37+
},
38+
},
39+
});
40+
41+
if (import.meta.hot) {
42+
import.meta.hot.accept(acceptHMRUpdate(useCartStore, import.meta.hot));
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('sends pinia action breadcrumbs and state context', async ({ page }) => {
5+
await page.goto('/pinia-cart');
6+
7+
await page.locator('#item-input').fill('item');
8+
await page.locator('#item-add').click();
9+
10+
const errorPromise = waitForError('nuxt-4', async errorEvent => {
11+
return errorEvent?.exception?.values?.[0].value === 'This is an error';
12+
});
13+
14+
await page.locator('#throw-error').click();
15+
16+
const error = await errorPromise;
17+
18+
expect(error).toBeTruthy();
19+
expect(error.breadcrumbs?.length).toBeGreaterThan(0);
20+
21+
const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'action');
22+
23+
expect(actionBreadcrumb).toBeDefined();
24+
expect(actionBreadcrumb?.message).toBe('Transformed: addItem');
25+
expect(actionBreadcrumb?.level).toBe('info');
26+
27+
const stateContext = error.contexts?.state?.state;
28+
29+
expect(stateContext).toBeDefined();
30+
expect(stateContext?.type).toBe('pinia');
31+
expect(stateContext?.value).toEqual({
32+
transformed: true,
33+
rawItems: ['item'],
34+
});
35+
});

packages/nuxt/src/client/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from '@sentry/vue';
22

33
export { init } from './sdk';
4+
export { piniaIntegration } from './piniaIntegration';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { defineIntegration } from '@sentry/core';
2+
import type { IntegrationFn } from '@sentry/types';
3+
4+
import { consoleSandbox } from '@sentry/utils';
5+
import { createSentryPiniaPlugin } from '@sentry/vue';
6+
7+
const INTEGRATION_NAME = 'Pinia';
8+
9+
type Pinia = { use: (plugin: ReturnType<typeof createSentryPiniaPlugin>) => void };
10+
11+
const _piniaIntegration = ((
12+
// `unknown` here as well because usePinia declares this type: `export declare const usePinia: () => unknown;`
13+
pinia: unknown | Pinia,
14+
options: Parameters<typeof createSentryPiniaPlugin>[0] = {},
15+
) => {
16+
return {
17+
name: INTEGRATION_NAME,
18+
setup() {
19+
if (!pinia || (typeof pinia === 'object' && !('use' in pinia))) {
20+
consoleSandbox(() => {
21+
// eslint-disable-next-line no-console
22+
console.warn(
23+
'[Sentry] The Pinia integration was added, but the passed parameter `pinia` has the wrong value. Make sure to enable Pinia by adding `"@pinia/nuxt"` to the Nuxt modules array and pass pinia to Sentry with `piniaIntegration(usePinia())`. Current value of `pinia`:',
24+
pinia,
25+
);
26+
});
27+
} else {
28+
(pinia as Pinia).use(createSentryPiniaPlugin(options));
29+
}
30+
},
31+
};
32+
}) satisfies IntegrationFn;
33+
34+
/**
35+
* Monitor an existing Pinia store.
36+
*
37+
* This only works if "@pinia/nuxt" is added to the `modules` array.
38+
*/
39+
export const piniaIntegration = defineIntegration(_piniaIntegration);

0 commit comments

Comments
 (0)