Skip to content

Commit 4b6dff7

Browse files
authored
feat(pinia)!: Include state of all stores in breadcrumb (#15312)
Adds all stores to the state object. closes #15232 Before, it could only hold the state of the currently changed store like: ```javascript // before { count: 0, name: 'Counter Store' } ``` Now, when having a counter and a cart store, it looks like this (using the store IDs as the object keys): ```javascript // now { cart: { rawItems: ['item'], }, counter: { count: 0, name: 'Counter Store', }, } ```
1 parent 215e1dc commit 4b6dff7

File tree

9 files changed

+142
-53
lines changed

9 files changed

+142
-53
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Sentry.init({
88
tracesSampleRate: 1.0,
99
integrations: [
1010
Sentry.piniaIntegration(usePinia(), {
11-
actionTransformer: action => `Transformed: ${action}`,
11+
actionTransformer: action => `${action}.transformed`,
1212
stateTransformer: state => ({
1313
transformed: true,
1414
...state,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ test('sends pinia action breadcrumbs and state context', async ({ page }) => {
1818
expect(error).toBeTruthy();
1919
expect(error.breadcrumbs?.length).toBeGreaterThan(0);
2020

21-
const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'action');
21+
const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'pinia.action');
2222

2323
expect(actionBreadcrumb).toBeDefined();
24-
expect(actionBreadcrumb?.message).toBe('Transformed: addItem');
24+
expect(actionBreadcrumb?.message).toBe('Store: cart | Action: addItem.transformed');
2525
expect(actionBreadcrumb?.level).toBe('info');
2626

2727
const stateContext = error.contexts?.state?.state;
@@ -30,6 +30,6 @@ test('sends pinia action breadcrumbs and state context', async ({ page }) => {
3030
expect(stateContext?.type).toBe('pinia');
3131
expect(stateContext?.value).toEqual({
3232
transformed: true,
33-
rawItems: ['item'],
33+
cart: { rawItems: ['item'] },
3434
});
3535
});

dev-packages/e2e-tests/test-applications/vue-3/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Sentry.init({
3131

3232
pinia.use(
3333
Sentry.createSentryPiniaPlugin({
34-
actionTransformer: action => `Transformed: ${action}`,
34+
actionTransformer: action => `${action}.transformed`,
3535
stateTransformer: state => ({
3636
transformed: true,
3737
...state,

dev-packages/e2e-tests/test-applications/vue-3/src/stores/cart.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { acceptHMRUpdate, defineStore } from 'pinia';
22

3-
export const useCartStore = defineStore({
4-
id: 'cart',
3+
export const useCartStore = defineStore('cart', {
54
state: () => ({
65
rawItems: [] as string[],
76
}),
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineStore } from 'pinia';
2+
3+
export const useCounterStore = defineStore('counter', {
4+
state: () => ({ name: 'Counter Store', count: 0 }),
5+
actions: {
6+
increment() {
7+
this.count++;
8+
},
9+
},
10+
});

dev-packages/e2e-tests/test-applications/vue-3/src/views/CartView.vue

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,50 +29,45 @@
2929
data-testid="clear"
3030
>Clear the cart</button>
3131
</form>
32+
33+
<br/>
34+
35+
<div>
36+
<h3>Counter: {{ $counter.count }}</h3>
37+
<button @click="$counter.increment">+</button>
38+
</div>
3239
</div>
3340
</Layout>
3441
</template>
3542

36-
<script lang="ts">
37-
import { defineComponent, ref } from 'vue'
43+
<script setup lang="ts">
44+
import { ref } from 'vue'
3845
import { useCartStore } from '../stores/cart'
46+
import { useCounterStore } from '@/stores/counter';
3947
48+
const cart = useCartStore()
49+
const $counter = useCounterStore()
4050
41-
export default defineComponent({
42-
setup() {
43-
const cart = useCartStore()
44-
45-
const itemName = ref('')
46-
47-
function addItemToCart() {
48-
if (!itemName.value) return
49-
cart.addItem(itemName.value)
50-
itemName.value = ''
51-
}
51+
const itemName = ref('')
5252
53-
function throwError() {
54-
throw new Error('This is an error')
55-
}
56-
57-
function clearCart() {
58-
if (window.confirm('Are you sure you want to clear the cart?')) {
59-
cart.rawItems = []
60-
}
61-
}
53+
function addItemToCart() {
54+
if (!itemName.value) return
55+
cart.addItem(itemName.value)
56+
itemName.value = ''
57+
}
6258
63-
// @ts-ignore
64-
window.stores = { cart }
59+
function throwError() {
60+
throw new Error('This is an error')
61+
}
6562
66-
return {
67-
itemName,
68-
addItemToCart,
69-
cart,
63+
function clearCart() {
64+
if (window.confirm('Are you sure you want to clear the cart?')) {
65+
cart.rawItems = []
66+
}
67+
}
7068
71-
throwError,
72-
clearCart,
73-
}
74-
},
75-
})
69+
// @ts-ignore
70+
window.stores = { cart }
7671
</script>
7772

7873
<style scoped>

dev-packages/e2e-tests/test-applications/vue-3/tests/pinia.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ test('sends pinia action breadcrumbs and state context', async ({ page }) => {
1818
expect(error).toBeTruthy();
1919
expect(error.breadcrumbs?.length).toBeGreaterThan(0);
2020

21-
const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'action');
21+
const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'pinia.action');
2222

2323
expect(actionBreadcrumb).toBeDefined();
24-
expect(actionBreadcrumb?.message).toBe('Transformed: addItem');
24+
expect(actionBreadcrumb?.message).toBe('Store: cart | Action: addItem.transformed');
2525
expect(actionBreadcrumb?.level).toBe('info');
2626

2727
const stateContext = error.contexts?.state?.state;
@@ -30,6 +30,75 @@ test('sends pinia action breadcrumbs and state context', async ({ page }) => {
3030
expect(stateContext?.type).toBe('pinia');
3131
expect(stateContext?.value).toEqual({
3232
transformed: true,
33-
rawItems: ['item'],
33+
cart: {
34+
rawItems: ['item'],
35+
},
36+
counter: {
37+
count: 0,
38+
name: 'Counter Store',
39+
},
3440
});
3541
});
42+
43+
test('state transformer receives full state object and is stored in state context', async ({ page }) => {
44+
await page.goto('/cart');
45+
46+
await page.locator('#item-input').fill('multiple store test');
47+
await page.locator('#item-add').click();
48+
49+
await page.locator('button:text("+")').click();
50+
await page.locator('button:text("+")').click();
51+
await page.locator('button:text("+")').click();
52+
53+
await page.locator('#item-input').fill('multiple store pinia');
54+
await page.locator('#item-add').click();
55+
56+
const errorPromise = waitForError('vue-3', async errorEvent => {
57+
return errorEvent?.exception?.values?.[0].value === 'This is an error';
58+
});
59+
60+
await page.locator('#throw-error').click();
61+
62+
const error = await errorPromise;
63+
64+
// Verify stateTransformer was called with full state and modified state
65+
const stateContext = error.contexts?.state?.state;
66+
67+
expect(stateContext?.value).toEqual({
68+
transformed: true,
69+
cart: {
70+
rawItems: ['multiple store test', 'multiple store pinia'],
71+
},
72+
counter: {
73+
name: 'Counter Store',
74+
count: 3,
75+
},
76+
});
77+
});
78+
79+
test('different store interaction order maintains full state tracking', async ({ page }) => {
80+
await page.goto('/cart');
81+
82+
await page.locator('button:text("+")').click();
83+
84+
await page.locator('#item-input').fill('order test item');
85+
await page.locator('#item-add').click();
86+
87+
await page.locator('button:text("+")').click();
88+
89+
const errorPromise = waitForError('vue-3', async errorEvent => {
90+
return errorEvent?.exception?.values?.[0].value === 'This is an error';
91+
});
92+
93+
await page.locator('#throw-error').click();
94+
95+
const error = await errorPromise;
96+
97+
const stateContext = error.contexts?.state?.state;
98+
99+
expect(stateContext).toBeDefined();
100+
101+
const stateValue = stateContext?.value;
102+
expect(stateValue.cart.rawItems).toEqual(['order test item']);
103+
expect(stateValue.counter.count).toBe(2);
104+
});

docs/migration/v8-to-v9.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,12 @@ All exports and APIs of `@sentry/utils` and `@sentry/types` (except for the ones
357357
```
358358

359359
- The option `logErrors` in the `vueIntegration` has been removed. The Sentry Vue error handler will always propagate the error to a user-defined error handler or re-throw the error (which will log the error without modifying).
360+
- The option `stateTransformer` in `createSentryPiniaPlugin()` now receives the full state from all stores as its parameter. The top-level keys of the state object are the store IDs.
360361

361362
### `@sentry/nuxt`
362363

363364
- The `tracingOptions` option in `Sentry.init()` was removed in favor of passing the `vueIntegration()` to `Sentry.init({ integrations: [...] })` and setting `tracingOptions` there.
365+
- The option `stateTransformer` in the `piniaIntegration` now receives the full state from all stores as its parameter. The top-level keys of the state object are the store IDs.
364366

365367
### `@sentry/vue` and `@sentry/nuxt`
366368

packages/vue/src/pinia.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { addBreadcrumb, addNonEnumerableProperty, getClient, getCurrentScope, getGlobalScope } from '@sentry/core';
2+
import type { Ref } from 'vue';
23

3-
// Inline PiniaPlugin type
4+
// Inline Pinia types
5+
type StateTree = Record<string | number | symbol, any>;
46
type PiniaPlugin = (context: {
57
store: {
68
$id: string;
79
$state: unknown;
810
$onAction: (callback: (context: { name: string; after: (callback: () => void) => void }) => void) => void;
911
};
12+
pinia: { state: Ref<Record<string, StateTree>> };
1013
}) => void;
1114

1215
type SentryPiniaPluginOptions = {
1316
attachPiniaState?: boolean;
1417
addBreadcrumbs?: boolean;
15-
actionTransformer?: (action: any) => any;
16-
stateTransformer?: (state: any) => any;
18+
actionTransformer?: (action: string) => any;
19+
stateTransformer?: (state: Record<string, unknown>) => any;
1720
};
1821

1922
export const createSentryPiniaPlugin: (options?: SentryPiniaPluginOptions) => PiniaPlugin = (
@@ -24,19 +27,29 @@ export const createSentryPiniaPlugin: (options?: SentryPiniaPluginOptions) => Pi
2427
stateTransformer: state => state,
2528
},
2629
) => {
27-
const plugin: PiniaPlugin = ({ store }) => {
30+
const plugin: PiniaPlugin = ({ store, pinia }) => {
31+
const getAllStoreStates = (): Record<string, unknown> => {
32+
const states: Record<string, unknown> = {};
33+
34+
Object.keys(pinia.state.value).forEach(storeId => {
35+
states[storeId] = pinia.state.value[storeId];
36+
});
37+
38+
return states;
39+
};
40+
2841
options.attachPiniaState !== false &&
2942
getGlobalScope().addEventProcessor((event, hint) => {
3043
try {
3144
// Get current timestamp in hh:mm:ss
3245
const timestamp = new Date().toTimeString().split(' ')[0];
33-
const filename = `pinia_state_${store.$id}_${timestamp}.json`;
46+
const filename = `pinia_state_all_stores_${timestamp}.json`;
3447

3548
hint.attachments = [
3649
...(hint.attachments || []),
3750
{
3851
filename,
39-
data: JSON.stringify(store.$state),
52+
data: JSON.stringify(getAllStoreStates()),
4053
},
4154
];
4255
} catch (_) {
@@ -58,14 +71,15 @@ export const createSentryPiniaPlugin: (options?: SentryPiniaPluginOptions) => Pi
5871
options.addBreadcrumbs !== false
5972
) {
6073
addBreadcrumb({
61-
category: 'action',
62-
message: transformedActionName,
74+
category: 'pinia.action',
75+
message: `Store: ${store.$id} | Action: ${transformedActionName}`,
6376
level: 'info',
6477
});
6578
}
6679

67-
/* Set latest state to scope */
68-
const transformedState = options.stateTransformer ? options.stateTransformer(store.$state) : store.$state;
80+
/* Set latest state of all stores to scope */
81+
const allStates = getAllStoreStates();
82+
const transformedState = options.stateTransformer ? options.stateTransformer(allStates) : allStates;
6983
const scope = getCurrentScope();
7084
const currentState = scope.getScopeData().contexts.state;
7185

0 commit comments

Comments
 (0)