Skip to content

Commit

Permalink
feat(nuxt): Add piniaIntegration (#14138)
Browse files Browse the repository at this point in the history
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 */
    }),
  ],
});
  • Loading branch information
s1gr1d authored Nov 4, 2024
1 parent c579a45 commit 738870d
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 2 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@

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

- **feat(nuxt): Add `piniaIntegration` ([#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(), {
/* optional Pinia plugin options */
}),
],
});
```

## 8.36.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref } from '#imports'
import { useCartStore } from '~~/stores/cart'
const cart = useCartStore()
const itemName = ref('')
function addItemToCart() {
if (!itemName.value) return
cart.addItem(itemName.value)
itemName.value = ''
}
function throwError() {
throw new Error('This is an error')
}
function clearCart() {
if (window.confirm('Are you sure you want to clear the cart?')) {
cart.rawItems = []
}
}
</script>

<template>
<Layout>
<div>
<div style="margin: 1rem 0;">
<PiniaLogo />
</div>

<form @submit.prevent="addItemToCart" data-testid="add-items">
<input id="item-input" type="text" v-model="itemName" />
<button id="item-add">Add</button>
<button id="throw-error" @click="throwError">Throw error</button>
</form>

<form>
<ul data-testid="items">
<li v-for="item in cart.items" :key="item.name">
{{ item.name }} ({{ item.amount }})
<button
@click="cart.removeItem(item.name)"
type="button"
>X</button>
</li>
</ul>

<button
:disabled="!cart.items.length"
@click="clearCart"
type="button"
data-testid="clear"
>Clear the cart</button>
</form>
</div>
</Layout>
</template>



<style scoped>
img {
width: 200px;
}
button,
input {
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test:assert": "pnpm test"
},
"dependencies": {
"@pinia/nuxt": "^0.5.5",
"@sentry/nuxt": "latest || *",
"nuxt": "^3.13.2"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
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
dsn: useRuntimeConfig().public.sentry.dsn,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
trackComponents: true,
integrations: [
Sentry.piniaIntegration(usePinia(), {
actionTransformer: action => `Transformed: ${action}`,
stateTransformer: state => ({
transformed: true,
...state,
}),
}),
],
});
43 changes: 43 additions & 0 deletions dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Original file line number Diff line number Diff line change
@@ -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'],
});
});
1 change: 1 addition & 0 deletions packages/nuxt/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from '@sentry/vue';

export { init } from './sdk';
export { piniaIntegration } from './piniaIntegration';
39 changes: 39 additions & 0 deletions packages/nuxt/src/client/piniaIntegration.ts
Original file line number Diff line number Diff line change
@@ -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 = 'Pinia';

type Pinia = { use: (plugin: ReturnType<typeof createSentryPiniaPlugin>) => void };

const _piniaIntegration = ((
// `unknown` here as well because usePinia declares this type: `export declare const usePinia: () => unknown;`
pinia: unknown | Pinia,
options: Parameters<typeof createSentryPiniaPlugin>[0] = {},
) => {
return {
name: INTEGRATION_NAME,
setup() {
if (!pinia || (typeof pinia === 'object' && !('use' in pinia))) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
'[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`:',
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);

0 comments on commit 738870d

Please sign in to comment.