diff --git a/CHANGELOG.md b/CHANGELOG.md
index d584d65cff65..b160a8e17293 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,20 +14,6 @@
### 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.
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
deleted file mode 100644
index 3d210cf459de..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
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 da988a9ee003..c00ba0d5d9ed 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: ['@pinia/nuxt', '@sentry/nuxt/module'],
+ modules: ['@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 178804768e87..db56273a7493 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -14,7 +14,6 @@
"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 b32effbff3b8..7547bafa6618 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,11 +7,4 @@ 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
deleted file mode 100644
index cad52916ac25..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-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
deleted file mode 100644
index 44b057a29f15..000000000000
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-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/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts
index e052782d50eb..37df64d182dd 100644
--- a/packages/aws-serverless/src/sdk.ts
+++ b/packages/aws-serverless/src/sdk.ts
@@ -168,6 +168,12 @@ export function tryPatchHandler(taskRoot: string, handlerPath: string): void {
return;
}
+ // Check for prototype pollution
+ if (functionName === '__proto__' || functionName === 'constructor' || functionName === 'prototype') {
+ DEBUG_BUILD && logger.error(`Invalid handler name: ${functionName}`);
+ return;
+ }
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(mod as HandlerModule)[functionName!] = wrapHandler(obj);
}
diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts
index 5b714968d3ca..6ba29752a308 100644
--- a/packages/nuxt/src/common/types.ts
+++ b/packages/nuxt/src/common/types.ts
@@ -1,21 +1,10 @@
import type { init as initNode } from '@sentry/node';
import type { SentryRollupPluginOptions } from '@sentry/rollup-plugin';
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
-import type { createSentryPiniaPlugin, init as initVue } from '@sentry/vue';
+import type { 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'> & {
- /**
- * 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 SentryNuxtClientOptions = Omit[0] & object, 'app'>;
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 a8b15b937d53..b89a2fa87a8d 100644
--- a/packages/nuxt/src/runtime/plugins/sentry.client.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts
@@ -1,6 +1,5 @@
import { getClient } from '@sentry/core';
-import { consoleSandbox } from '@sentry/utils';
-import { browserTracingIntegration, createSentryPiniaPlugin, vueIntegration } from '@sentry/vue';
+import { browserTracingIntegration, vueIntegration } from '@sentry/vue';
import { defineNuxtPlugin } from 'nuxt/app';
import { reportNuxtError } from '../utils';
@@ -35,12 +34,11 @@ 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' }),
@@ -48,23 +46,6 @@ 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();
diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts
index c3c448a3c6f2..1f690f9af88a 100644
--- a/packages/replay-internal/src/integration.ts
+++ b/packages/replay-internal/src/integration.ts
@@ -1,5 +1,11 @@
import { parseSampleRate } from '@sentry/core';
-import type { BrowserClientReplayOptions, Client, Integration, IntegrationFn } from '@sentry/types';
+import type {
+ BrowserClientReplayOptions,
+ Client,
+ Integration,
+ IntegrationFn,
+ ReplayRecordingMode,
+} from '@sentry/types';
import { consoleSandbox, dropUndefinedKeys, isBrowser } from '@sentry/utils';
import {
@@ -297,6 +303,22 @@ export class Replay implements Integration {
return this._replay.getSessionId();
}
+ /**
+ * Get the current recording mode. This can be either `session` or `buffer`.
+ *
+ * `session`: Recording the whole session, sending it continuously
+ * `buffer`: Always keeping the last 60s of recording, requires:
+ * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs
+ * - or calling `flush()` to send the replay
+ */
+ public getRecordingMode(): ReplayRecordingMode | undefined {
+ if (!this._replay || !this._replay.isEnabled()) {
+ return;
+ }
+
+ return this._replay.recordingMode;
+ }
+
/**
* Initializes replay.
*/
diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts
index cfd8a2a45c33..0976b7d57870 100644
--- a/packages/replay-internal/src/replay.ts
+++ b/packages/replay-internal/src/replay.ts
@@ -74,7 +74,7 @@ export class ReplayContainer implements ReplayContainerInterface {
public clickDetector: ClickDetector | undefined;
/**
- * Recording can happen in one of three modes:
+ * Recording can happen in one of two modes:
* - session: Record the whole session, sending it continuously
* - buffer: Always keep the last 60s of recording, requires:
* - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs
diff --git a/packages/replay-internal/test/integration/recordingMode.test.ts b/packages/replay-internal/test/integration/recordingMode.test.ts
new file mode 100644
index 000000000000..400a9ffd47b1
--- /dev/null
+++ b/packages/replay-internal/test/integration/recordingMode.test.ts
@@ -0,0 +1,83 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { describe, expect, test } from 'vitest';
+import { resetSdkMock } from '../mocks/resetSdkMock';
+import { useFakeTimers } from '../utils/use-fake-timers';
+
+useFakeTimers();
+
+describe('Integration | getRecordingMode()', () => {
+ test('returns "session" when session sampling is enabled', async () => {
+ const { integration } = await resetSdkMock({
+ replayOptions: {
+ stickySession: false,
+ },
+ sentryOptions: {
+ replaysSessionSampleRate: 1.0,
+ },
+ });
+ expect(integration.getRecordingMode()).toBe('session');
+ });
+
+ test('returns "buffer" when buffering is enabled', async () => {
+ const { integration, replay } = await resetSdkMock({
+ replayOptions: {
+ stickySession: false,
+ },
+ sentryOptions: {
+ replaysSessionSampleRate: 1.0,
+ },
+ });
+ replay.stop();
+ replay.startBuffering();
+ expect(integration.getRecordingMode()).toBe('buffer');
+ });
+
+ test('returns undefined when replay is stopped', async () => {
+ const { integration, replay } = await resetSdkMock({
+ replayOptions: {
+ stickySession: false,
+ },
+ sentryOptions: {
+ replaysSessionSampleRate: 1.0,
+ },
+ });
+ replay.stop();
+ expect(integration.getRecordingMode()).toBeUndefined();
+ });
+
+ test('returns undefined when session sampling is disabled', async () => {
+ const { integration } = await resetSdkMock({
+ replayOptions: { stickySession: false },
+ sentryOptions: {
+ replaysSessionSampleRate: 0.0,
+ replaysOnErrorSampleRate: 0.0,
+ },
+ });
+ expect(integration.getRecordingMode()).toBeUndefined();
+ });
+
+ test('returns "buffer" when session rate is 0 and onError rate is 1', async () => {
+ const { integration } = await resetSdkMock({
+ replayOptions: { stickySession: false },
+ sentryOptions: {
+ replaysSessionSampleRate: 0.0,
+ replaysOnErrorSampleRate: 1.0,
+ },
+ });
+ expect(integration.getRecordingMode()).toBe('buffer');
+ });
+
+ test('returns "session" when both sampling rates are 1', async () => {
+ const { integration } = await resetSdkMock({
+ replayOptions: { stickySession: false },
+ sentryOptions: {
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 1.0,
+ },
+ });
+ expect(integration.getRecordingMode()).toBe('session');
+ });
+});