diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87ac1c8b9f16..bfcb48fb1a83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+- **feat(vue): Add TanStack Router integration ([#18359](https://github.com/getsentry/sentry-javascript/pull/18359))**
+
+The `@sentry/vue` package now includes support for TanStack Router. Use `tanstackRouterBrowserTracingIntegration` to automatically instrument pageload and navigation transactions with parameterized routes:
+
+```javascript
+import { createApp } from 'vue';
+import { createRouter } from '@tanstack/vue-router';
+import * as Sentry from '@sentry/vue';
+import { tanstackRouterBrowserTracingIntegration } from '@sentry/vue/tanstackrouter';
+
+const router = createRouter({
+ // your router config
+});
+
+Sentry.init({
+ app,
+ dsn: '__PUBLIC_DSN__',
+ integrations: [tanstackRouterBrowserTracingIntegration(router)],
+ tracesSampleRate: 1.0,
+});
+```
+
- **feat(nextjs): Add tree-shaking configuration to `webpack` build config ([#18359](https://github.com/getsentry/sentry-javascript/pull/18359))**
- **feat(replay): Add Request body with `attachRawBodyFromRequest` option ([#18501](https://github.com/getsentry/sentry-javascript/pull/18501))**
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.gitignore b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.gitignore
new file mode 100644
index 000000000000..04471798a413
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.npmrc b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/README.md b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/README.md
new file mode 100644
index 000000000000..f9e5908ae9b6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/README.md
@@ -0,0 +1,61 @@
+# Vue TanStack Router E2E Test App
+
+E2E test application for `@sentry/vue` with TanStack Router integration.
+
+## Getting Started
+
+To run this application:
+
+```bash
+pnpm install
+pnpm dev
+```
+
+## Building For Production
+
+To build this application for production:
+
+```bash
+pnpm build
+```
+
+## Running Tests
+
+To run E2E tests:
+
+```bash
+pnpm test:build # Install deps and build
+pnpm test:assert # Run Playwright tests
+```
+
+## Routing
+
+This project uses [TanStack Router](https://tanstack.com/router) for Vue.js. The router is set up with code-based routing in the `./src/main.ts` file.
+
+### Routes
+
+- `/` - Home page with navigation links
+- `/posts/$postId` - Post detail page with parameterized route
+
+### Sentry Integration
+
+The app demonstrates:
+
+- TanStack Router browser tracing integration
+- Pageload transaction tracking with parameterized routes
+- Navigation transaction tracking
+- Route parameter extraction and span attribution
+
+## Testing
+
+The E2E tests verify:
+
+1. Pageload transactions are created with correct route parameters
+2. Navigation transactions are properly instrumented
+3. Route parameters are captured in transaction data
+4. Sentry origins are correctly set for Vue TanStack Router
+
+## Learn More
+
+- [TanStack Router Documentation](https://tanstack.com/router)
+- [Sentry Vue SDK](https://docs.sentry.io/platforms/javascript/guides/vue/)
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/env.d.ts b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/env.d.ts
new file mode 100644
index 000000000000..11f02fe2a006
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/index.html b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/index.html
new file mode 100644
index 000000000000..22db65a78f3c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ Vue TanStack Router - Sentry E2E Tests
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json
new file mode 100644
index 000000000000..fc99c9ea3c6c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "vue-tanstack-router",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml dist",
+ "dev": "vite",
+ "start": "vite preview",
+ "preview": "vite preview",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:prod"
+ },
+ "dependencies": {
+ "@sentry/vue": "latest || *",
+ "@tanstack/vue-router": "^1.64.0",
+ "vue": "^3.4.15"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@tsconfig/node20": "^20.1.2",
+ "@types/node": "^18.19.1",
+ "@vitejs/plugin-vue": "^5.0.3",
+ "@vue/tsconfig": "^0.5.1",
+ "typescript": "~5.3.0",
+ "vite": "^5.4.11"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/playwright.config.mjs
new file mode 100644
index 000000000000..94f38d88ad70
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: 'pnpm preview --port 4173',
+ port: 4173,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/public/favicon.ico b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/public/favicon.ico
new file mode 100644
index 000000000000..df36fcfb7258
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/public/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/App.vue b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/App.vue
new file mode 100644
index 000000000000..fa9ef37a3794
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/App.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/HomeView.vue b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/HomeView.vue
new file mode 100644
index 000000000000..b87ae701593b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/HomeView.vue
@@ -0,0 +1,16 @@
+
+
+
Vue + TanStack Router
+
Welcome to the Sentry E2E Test App!
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/PostView.vue b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/PostView.vue
new file mode 100644
index 000000000000..3c595e1a7cfb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/PostView.vue
@@ -0,0 +1,18 @@
+
+
+
Post View
+
Post ID: {{ params.postId }}
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/main.ts
new file mode 100644
index 000000000000..cdeec524fb50
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/src/main.ts
@@ -0,0 +1,62 @@
+import { createApp } from 'vue';
+import { RouterProvider, createRoute, createRootRoute, createRouter } from '@tanstack/vue-router';
+import * as Sentry from '@sentry/vue';
+import { tanstackRouterBrowserTracingIntegration } from '@sentry/vue/tanstackrouter';
+
+import App from './App.vue';
+import HomeView from './HomeView.vue';
+import PostView from './PostView.vue';
+
+const rootRoute = createRootRoute({
+ component: App,
+});
+
+const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: HomeView,
+});
+
+const postsRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: 'posts/',
+});
+
+const postIdRoute = createRoute({
+ getParentRoute: () => postsRoute,
+ path: '$postId',
+ loader: async ({ params }) => {
+ return Sentry.startSpan({ name: `loading-post-${params.postId}` }, async () => {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ });
+ },
+ component: PostView,
+});
+
+const routeTree = rootRoute.addChildren([indexRoute, postsRoute.addChildren([postIdRoute])]);
+
+const router = createRouter({
+ routeTree,
+ defaultPreload: 'intent',
+});
+
+declare module '@tanstack/vue-router' {
+ interface Register {
+ router: typeof router;
+ }
+}
+
+const app = createApp(RouterProvider, { router });
+
+Sentry.init({
+ app,
+ dsn: import.meta.env.PUBLIC_E2E_TEST_DSN,
+ debug: true,
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ integrations: [tanstackRouterBrowserTracingIntegration(router)],
+ release: 'e2e-test',
+ tunnel: 'http://localhost:3031/', // proxy server
+ tracesSampleRate: 1.0,
+});
+
+app.mount('#app');
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/start-event-proxy.mjs
new file mode 100644
index 000000000000..00c6a511b251
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'vue-tanstack-router',
+});
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tests/routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tests/routing-instrumentation.test.ts
new file mode 100644
index 000000000000..d6d9d4d9b0ba
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tests/routing-instrumentation.test.ts
@@ -0,0 +1,114 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('sends pageload transaction with web vitals measurements', async ({ page }) => {
+ const transactionPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.vue.tanstack_router',
+ },
+ },
+ transaction: '/',
+ transaction_info: {
+ source: 'route',
+ },
+ measurements: expect.objectContaining({
+ ttfb: expect.objectContaining({
+ value: expect.any(Number),
+ unit: 'millisecond',
+ }),
+ lcp: expect.objectContaining({
+ value: expect.any(Number),
+ unit: 'millisecond',
+ }),
+ fp: expect.objectContaining({
+ value: expect.any(Number),
+ unit: 'millisecond',
+ }),
+ fcp: expect.objectContaining({
+ value: expect.any(Number),
+ unit: 'millisecond',
+ }),
+ }),
+ });
+});
+
+test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
+ const transactionPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/posts/456`);
+
+ const rootSpan = await transactionPromise;
+
+ expect(rootSpan).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.source': 'route',
+ 'sentry.origin': 'auto.pageload.vue.tanstack_router',
+ 'sentry.op': 'pageload',
+ 'url.path.parameter.postId': '456',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.vue.tanstack_router',
+ },
+ },
+ transaction: '/posts/$postId',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
+test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
+ const pageloadTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTxnPromise = waitForTransaction('vue-tanstack-router', async transactionEvent => {
+ return (
+ !!transactionEvent?.transaction &&
+ transactionEvent.transaction === '/posts/$postId' &&
+ transactionEvent.contexts?.trace?.op === 'navigation'
+ );
+ });
+
+ await page.goto(`/`);
+ await pageloadTxnPromise;
+
+ await page.waitForTimeout(5000);
+
+ await page.locator('#nav-link').click();
+
+ const navigationTxn = await navigationTxnPromise;
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.source': 'route',
+ 'sentry.origin': 'auto.navigation.vue.tanstack_router',
+ 'sentry.op': 'navigation',
+ 'url.path.parameter.postId': '2',
+ },
+ op: 'navigation',
+ origin: 'auto.navigation.vue.tanstack_router',
+ },
+ },
+ transaction: '/posts/$postId',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.app.json
new file mode 100644
index 000000000000..e14c754d3ae5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.app.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+ "exclude": ["src/**/__tests__/*"],
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.json
new file mode 100644
index 000000000000..66b5e5703e83
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.node.json"
+ },
+ {
+ "path": "./tsconfig.app.json"
+ }
+ ]
+}
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.node.json
new file mode 100644
index 000000000000..7463c5c1b1f5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/tsconfig.node.json
@@ -0,0 +1,13 @@
+{
+ "extends": "@tsconfig/node20/tsconfig.json",
+ "include": ["vite.config.*", "vitest.config.*", "playwright.config.*"],
+ "compilerOptions": {
+ "composite": true,
+ "noEmit": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/vue-tanstack-router/vite.config.ts b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/vite.config.ts
new file mode 100644
index 000000000000..9a935c24bba8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-tanstack-router/vite.config.ts
@@ -0,0 +1,15 @@
+import { fileURLToPath, URL } from 'node:url';
+
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ envPrefix: 'PUBLIC_',
+});
diff --git a/packages/vue/.eslintignore b/packages/vue/.eslintignore
new file mode 100644
index 000000000000..3e89751310a5
--- /dev/null
+++ b/packages/vue/.eslintignore
@@ -0,0 +1,2 @@
+/*.d.ts
+/*.d.ts.map
diff --git a/packages/vue/.gitignore b/packages/vue/.gitignore
new file mode 100644
index 000000000000..3e89751310a5
--- /dev/null
+++ b/packages/vue/.gitignore
@@ -0,0 +1,2 @@
+/*.d.ts
+/*.d.ts.map
diff --git a/packages/vue/README.md b/packages/vue/README.md
index b2b0b349b15f..7573e61e115c 100644
--- a/packages/vue/README.md
+++ b/packages/vue/README.md
@@ -58,3 +58,35 @@ new Vue({
template: '',
});
```
+
+### TanStack Router
+
+If you use TanStack Router for Vue instead of Vue Router, you can use the TanStack Router instrumentation to create
+navigation spans and collect meaningful performance data about the health of your page loads and associated requests.
+
+Add `tanstackRouterBrowserTracingIntegration` from `@sentry/vue/tanstackrouter` instead of the regular
+`Sentry.browserTracingIntegration`.
+
+Make sure `tanstackRouterBrowserTracingIntegration` is initialized by your `Sentry.init` call. Otherwise, the routing
+instrumentation will not work properly.
+
+Pass your router instance from `createRouter` to the integration.
+
+```javascript
+import { createApp } from 'vue';
+import { createRouter } from '@tanstack/vue-router';
+import * as Sentry from '@sentry/vue';
+import { tanstackRouterBrowserTracingIntegration } from '@sentry/vue/tanstackrouter';
+
+const router = createRouter({
+ // your router config
+ // ...
+});
+
+Sentry.init({
+ app,
+ dsn: '__PUBLIC_DSN__',
+ integrations: [tanstackRouterBrowserTracingIntegration(router)],
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+});
+```
diff --git a/packages/vue/package.json b/packages/vue/package.json
index 13b3c4cd29d1..43959fc71901 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -10,7 +10,9 @@
"node": ">=18"
},
"files": [
- "/build"
+ "/build",
+ "/*.d.ts",
+ "/*.d.ts.map"
],
"main": "build/cjs/index.js",
"module": "build/esm/index.js",
@@ -26,6 +28,16 @@
"types": "./build/types/index.d.ts",
"default": "./build/cjs/index.js"
}
+ },
+ "./tanstackrouter": {
+ "import": {
+ "types": "./tanstackrouter.d.ts",
+ "default": "./build/esm/tanstackrouter.js"
+ },
+ "require": {
+ "types": "./tanstackrouter.d.ts",
+ "default": "./build/cjs/tanstackrouter.js"
+ }
}
},
"typesVersions": {
@@ -43,30 +55,36 @@
"@sentry/core": "10.31.0"
},
"peerDependencies": {
+ "@tanstack/vue-router": "^1.64.0",
"pinia": "2.x || 3.x",
"vue": "2.x || 3.x"
},
"peerDependenciesMeta": {
+ "@tanstack/vue-router": {
+ "optional": true
+ },
"pinia": {
"optional": true
}
},
"devDependencies": {
+ "@tanstack/vue-router": "^1.64.0",
"vue": "~3.2.41"
},
"scripts": {
"build": "run-p build:transpile build:types",
"build:dev": "run-p build:transpile build:types",
"build:transpile": "rollup -c rollup.npm.config.mjs",
- "build:types": "run-s build:types:core build:types:downlevel",
+ "build:types": "run-s build:types:core build:types:router build:types:downlevel",
"build:types:core": "tsc -p tsconfig.types.json",
+ "build:types:router": "tsc -p tsconfig.router-types.json",
"build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8",
"build:watch": "run-p build:transpile:watch build:types:watch",
"build:dev:watch": "run-p build:transpile:watch build:types:watch",
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
"build:types:watch": "tsc -p tsconfig.types.json --watch",
"build:tarball": "npm pack",
- "circularDepCheck": "madge --circular src/index.ts",
+ "circularDepCheck": "madge --circular src/index.ts && madge --circular src/tanstackrouter.ts",
"clean": "rimraf build coverage sentry-vue-*.tgz",
"fix": "eslint . --format stylish --fix",
"lint": "eslint . --format stylish",
diff --git a/packages/vue/rollup.npm.config.mjs b/packages/vue/rollup.npm.config.mjs
index 84a06f2fb64a..f7f7d8786fcc 100644
--- a/packages/vue/rollup.npm.config.mjs
+++ b/packages/vue/rollup.npm.config.mjs
@@ -1,3 +1,7 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
-export default makeNPMConfigVariants(makeBaseNPMConfig());
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ entrypoints: ['src/index.ts', 'src/tanstackrouter.ts'],
+ }),
+);
diff --git a/packages/vue/src/pinia.ts b/packages/vue/src/pinia.ts
index f55be4d6602c..596efb6ef182 100644
--- a/packages/vue/src/pinia.ts
+++ b/packages/vue/src/pinia.ts
@@ -2,7 +2,7 @@ import { addBreadcrumb, addNonEnumerableProperty, getClient, getCurrentScope, ge
import type { Ref } from 'vue';
// Inline Pinia types
-type StateTree = Record;
+type StateTree = Record;
type PiniaPlugin = (context: {
store: {
$id: string;
@@ -15,7 +15,9 @@ type PiniaPlugin = (context: {
type SentryPiniaPluginOptions = {
attachPiniaState: boolean;
addBreadcrumbs: boolean;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
actionTransformer: (action: string) => any;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
stateTransformer: (state: Record) => any;
};
@@ -29,7 +31,8 @@ const DEFAULT_PINIA_PLUGIN_OPTIONS: SentryPiniaPluginOptions = {
const getAllStoreStates = (
pinia: { state: Ref> },
stateTransformer?: SentryPiniaPluginOptions['stateTransformer'],
-): Record => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): any => {
const states: Record = {};
try {
diff --git a/packages/vue/src/tanstackrouter.ts b/packages/vue/src/tanstackrouter.ts
new file mode 100644
index 000000000000..a0ae76814c0c
--- /dev/null
+++ b/packages/vue/src/tanstackrouter.ts
@@ -0,0 +1,145 @@
+import {
+ browserTracingIntegration as originalBrowserTracingIntegration,
+ startBrowserTracingNavigationSpan,
+ startBrowserTracingPageLoadSpan,
+ WINDOW,
+} from '@sentry/browser';
+import type { Integration } from '@sentry/core';
+import {
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
+ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
+} from '@sentry/core';
+import type { AnyRouter } from '@tanstack/vue-router';
+
+type RouteMatch = ReturnType[number];
+
+/**
+ * A custom browser tracing integration for TanStack Router.
+ *
+ * The minimum compatible version of `@tanstack/vue-router` is `1.64.0`.
+ *
+ * @param router A TanStack Router `Router` instance that should be used for routing instrumentation.
+ * @param options Sentry browser tracing configuration.
+ */
+export function tanstackRouterBrowserTracingIntegration(
+ router: R,
+ options: Parameters[0] = {},
+): Integration {
+ const browserTracingIntegrationInstance = originalBrowserTracingIntegration({
+ ...options,
+ instrumentNavigation: false,
+ instrumentPageLoad: false,
+ });
+
+ const { instrumentPageLoad = true, instrumentNavigation = true } = options;
+
+ return {
+ ...browserTracingIntegrationInstance,
+ afterAllSetup(client) {
+ browserTracingIntegrationInstance.afterAllSetup(client);
+
+ const initialWindowLocation = WINDOW.location;
+ if (instrumentPageLoad && initialWindowLocation) {
+ const matchedRoutes = router.matchRoutes(
+ initialWindowLocation.pathname,
+
+ router.options.parseSearch(initialWindowLocation.search),
+ { preload: false, throwOnError: false },
+ );
+
+ const lastMatch = matchedRoutes[matchedRoutes.length - 1];
+
+ startBrowserTracingPageLoadSpan(client, {
+ name: lastMatch ? lastMatch.routeId : initialWindowLocation.pathname,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: lastMatch ? 'route' : 'url',
+ ...routeMatchToParamSpanAttributes(lastMatch),
+ },
+ });
+ }
+
+ if (instrumentNavigation) {
+ // The onBeforeNavigate hook is called at the very beginning of a navigation and is only called once per navigation, even when the user is redirected
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ router.subscribe('onBeforeNavigate', (onBeforeNavigateArgs: any) => {
+ // onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by:
+ // 1. Checking if there's no fromLocation (initial pageload)
+ // 2. Comparing the states of the to and from arguments
+
+ if (
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ !onBeforeNavigateArgs.fromLocation ||
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation.state
+ ) {
+ return;
+ }
+
+ const onResolvedMatchedRoutes = router.matchRoutes(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ onBeforeNavigateArgs.toLocation.pathname,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ onBeforeNavigateArgs.toLocation.search,
+ { preload: false, throwOnError: false },
+ );
+
+ const onBeforeNavigateLastMatch = onResolvedMatchedRoutes[onResolvedMatchedRoutes.length - 1];
+
+ const navigationLocation = WINDOW.location;
+ const navigationSpan = startBrowserTracingNavigationSpan(client, {
+ name: onBeforeNavigateLastMatch
+ ? onBeforeNavigateLastMatch.routeId
+ : // In SSR/non-browser contexts, WINDOW.location may be undefined, so fall back to the router's location
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ navigationLocation?.pathname || onBeforeNavigateArgs.toLocation.pathname,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: onBeforeNavigateLastMatch ? 'route' : 'url',
+ },
+ });
+
+ // In case the user is redirected during navigation we want to update the span with the right value.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const unsubscribeOnResolved = router.subscribe('onResolved', (onResolvedArgs: any) => {
+ unsubscribeOnResolved();
+ if (navigationSpan) {
+ const onResolvedMatchedRoutes = router.matchRoutes(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ onResolvedArgs.toLocation.pathname,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ onResolvedArgs.toLocation.search,
+ { preload: false, throwOnError: false },
+ );
+
+ const onResolvedLastMatch = onResolvedMatchedRoutes[onResolvedMatchedRoutes.length - 1];
+
+ if (onResolvedLastMatch) {
+ navigationSpan.updateName(onResolvedLastMatch.routeId);
+ navigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ navigationSpan.setAttributes(routeMatchToParamSpanAttributes(onResolvedLastMatch));
+ }
+ }
+ });
+ });
+ }
+ },
+ };
+}
+
+function routeMatchToParamSpanAttributes(match: RouteMatch | undefined): Record {
+ if (!match) {
+ return {};
+ }
+
+ const paramAttributes: Record = {};
+ Object.entries(match.params as Record).forEach(([key, value]) => {
+ paramAttributes[`url.path.parameter.${key}`] = value;
+ paramAttributes[`params.${key}`] = value; // params.[key] is an alias
+ });
+
+ return paramAttributes;
+}
diff --git a/packages/vue/test/tanstackrouter.test.ts b/packages/vue/test/tanstackrouter.test.ts
new file mode 100644
index 000000000000..7b23a4241799
--- /dev/null
+++ b/packages/vue/test/tanstackrouter.test.ts
@@ -0,0 +1,269 @@
+import * as SentryBrowser from '@sentry/browser';
+import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
+import type { AnyRouter } from '@tanstack/vue-router';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { tanstackRouterBrowserTracingIntegration } from '../src/tanstackrouter';
+
+vi.mock('@sentry/browser', async () => {
+ const actual = await vi.importActual('@sentry/browser');
+ return {
+ ...actual,
+ WINDOW: {
+ location: {
+ pathname: '/test/123',
+ search: '?foo=bar',
+ },
+ },
+ };
+});
+
+const startBrowserTracingPageLoadSpanSpy = vi.spyOn(SentryBrowser, 'startBrowserTracingPageLoadSpan');
+const startBrowserTracingNavigationSpanSpy = vi.spyOn(SentryBrowser, 'startBrowserTracingNavigationSpan');
+
+const mockNavigationSpan = {
+ updateName: vi.fn(),
+ setAttribute: vi.fn(),
+ setAttributes: vi.fn(),
+};
+
+describe('tanstackRouterBrowserTracingIntegration', () => {
+ const mockMatchedRoutes = [
+ {
+ routeId: '/test/:id',
+ pathname: '/test/123',
+ params: { id: '123' },
+ },
+ ];
+
+ const mockRouter: Partial = {
+ options: {
+ parseSearch: vi.fn((search: string) => {
+ const params = new URLSearchParams(search);
+ const result: Record = {};
+ params.forEach((value, key) => {
+ result[key] = value;
+ });
+ return result;
+ }),
+ } as AnyRouter['options'],
+ matchRoutes: vi.fn(() => mockMatchedRoutes),
+ subscribe: vi.fn(() => vi.fn()), // Return an unsubscribe function
+ };
+
+ const mockClient = {
+ on: vi.fn(),
+ emit: vi.fn(),
+ getOptions: vi.fn(() => ({})),
+ addEventProcessor: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ startBrowserTracingNavigationSpanSpy.mockReturnValue(mockNavigationSpan as any);
+
+ // Mock window.location
+ vi.stubGlobal('window', {
+ location: {
+ pathname: '/test/123',
+ search: '?foo=bar',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ vi.unstubAllGlobals();
+ });
+
+ it('creates an integration with the correct name', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter);
+ expect(integration.name).toBe('BrowserTracing');
+ });
+
+ it('instruments pageload on setup', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentPageLoad: true,
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ expect(mockRouter.matchRoutes).toHaveBeenCalledWith(
+ '/test/123',
+ { foo: 'bar' },
+ {
+ preload: false,
+ throwOnError: false,
+ },
+ );
+
+ expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledTimes(1);
+ expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(mockClient, {
+ name: '/test/:id',
+ attributes: expect.objectContaining({
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue.tanstack_router',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ 'url.path.parameter.id': '123',
+ 'params.id': '123',
+ }),
+ });
+ });
+
+ it('does not instrument pageload when instrumentPageLoad is false', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentPageLoad: false,
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ expect(startBrowserTracingPageLoadSpanSpy).not.toHaveBeenCalled();
+ });
+
+ it('subscribes to router navigation events when instrumentNavigation is true', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentNavigation: true,
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ expect(mockRouter.subscribe).toHaveBeenCalledWith('onBeforeNavigate', expect.any(Function));
+ });
+
+ it('does not subscribe to navigation events when instrumentNavigation is false', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentNavigation: false,
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ // Only pageload should have been called
+ expect(mockRouter.subscribe).not.toHaveBeenCalled();
+ });
+
+ it('creates navigation spans with correct attributes', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentNavigation: true,
+ instrumentPageLoad: false, // Disable pageload to isolate navigation test
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ // Get the onBeforeNavigate callback
+ const onBeforeNavigateCallback = (mockRouter.subscribe as any).mock.calls.find(
+ (call: [string, (...args: any[]) => void]) => call[0] === 'onBeforeNavigate',
+ )?.[1];
+
+ expect(onBeforeNavigateCallback).toBeDefined();
+
+ // Simulate navigation
+ onBeforeNavigateCallback({
+ toLocation: {
+ pathname: '/test/456',
+ search: {},
+ state: 'state-1',
+ },
+ fromLocation: {
+ pathname: '/test/123',
+ search: {},
+ state: 'state-0',
+ },
+ });
+
+ expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledTimes(1);
+ expect(startBrowserTracingNavigationSpanSpy).toHaveBeenCalledWith(mockClient, {
+ name: '/test/:id',
+ attributes: expect.objectContaining({
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue.tanstack_router',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ }),
+ });
+ });
+
+ it('skips navigation span creation when state is the same', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentNavigation: true,
+ instrumentPageLoad: false,
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ const onBeforeNavigateCallback = (mockRouter.subscribe as any).mock.calls.find(
+ (call: [string, (...args: any[]) => void]) => call[0] === 'onBeforeNavigate',
+ )?.[1];
+
+ // Simulate navigation with same state (e.g., during pageload)
+ onBeforeNavigateCallback({
+ toLocation: {
+ pathname: '/test/456',
+ search: {},
+ state: 'same-state',
+ },
+ fromLocation: {
+ pathname: '/test/123',
+ search: {},
+ state: 'same-state',
+ },
+ });
+
+ expect(startBrowserTracingNavigationSpanSpy).not.toHaveBeenCalled();
+ });
+
+ it('updates navigation span on redirect using onResolved', () => {
+ const integration = tanstackRouterBrowserTracingIntegration(mockRouter, {
+ instrumentNavigation: true,
+ instrumentPageLoad: false,
+ });
+
+ integration.afterAllSetup(mockClient as any);
+
+ const onBeforeNavigateCallback = (mockRouter.subscribe as any).mock.calls.find(
+ (call: [string, (...args: any[]) => void]) => call[0] === 'onBeforeNavigate',
+ )?.[1];
+
+ // Simulate navigation
+ onBeforeNavigateCallback({
+ toLocation: {
+ pathname: '/test/456',
+ search: {},
+ state: 'state-1',
+ },
+ fromLocation: {
+ pathname: '/test/123',
+ search: {},
+ state: 'state-0',
+ },
+ });
+
+ // Get the onResolved callback that was registered
+ const onResolvedCallback = (mockRouter.subscribe as any).mock.calls.find(
+ (call: [string, (...args: any[]) => void]) => call[0] === 'onResolved',
+ )?.[1];
+
+ expect(onResolvedCallback).toBeDefined();
+
+ // Mock different matched routes for the redirect
+ const redirectedMatchedRoutes = [
+ {
+ routeId: '/redirected/:id',
+ pathname: '/redirected/789',
+ params: { id: '789' },
+ },
+ ];
+
+ (mockRouter.matchRoutes as any).mockReturnValueOnce(redirectedMatchedRoutes);
+
+ // Simulate redirect resolution
+ onResolvedCallback({
+ toLocation: {
+ pathname: '/redirected/789',
+ search: {},
+ },
+ });
+
+ expect(mockNavigationSpan.updateName).toHaveBeenCalledWith('/redirected/:id');
+ expect(mockNavigationSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
+ expect(mockNavigationSpan.setAttributes).toHaveBeenCalledWith({
+ 'url.path.parameter.id': '789',
+ 'params.id': '789',
+ });
+ });
+});
diff --git a/packages/vue/tsconfig.router-types.json b/packages/vue/tsconfig.router-types.json
new file mode 100644
index 000000000000..ef17c20dd7c1
--- /dev/null
+++ b/packages/vue/tsconfig.router-types.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "./"
+ },
+
+ "//": "This type is built separately because it is for a subpath export, which has problems if it is not in the root",
+ "include": ["src/tanstackrouter.ts"],
+ "//": "Without this, we cannot output into the root dir",
+ "exclude": []
+}
diff --git a/yarn.lock b/yarn.lock
index b26b3f069801..db80dbff16ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8008,6 +8008,11 @@
resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.132.21.tgz#09ae649b0c0c2d1093f0b1e34b9ab0cd3b2b1d2f"
integrity sha512-5ziPz3YarKU5cBJoEJ4muV8cy+5W4oWdJMqW7qosMrK5fb9Qfm+QWX+kO3emKJMu4YOUofVu3toEuuD3x1zXKw==
+"@tanstack/history@1.141.0":
+ version "1.141.0"
+ resolved "https://registry.yarnpkg.com/@tanstack/history/-/history-1.141.0.tgz#6fa5119dd870f1617943903d30e799222a21f6c2"
+ integrity sha512-LS54XNyxyTs5m/pl1lkwlg7uZM3lvsv2FIIV1rsJgnfwVCnI+n4ZGZ2CcjNT13BPu/3hPP+iHmliBSscJxW5FQ==
+
"@tanstack/router-core@1.132.27":
version "1.132.27"
resolved "https://registry.yarnpkg.com/@tanstack/router-core/-/router-core-1.132.27.tgz#8869e98d10ea42338cb115af45bdcbc10eaf2b7f"
@@ -8021,6 +8026,19 @@
tiny-invariant "^1.3.3"
tiny-warning "^1.0.3"
+"@tanstack/router-core@1.141.4":
+ version "1.141.4"
+ resolved "https://registry.yarnpkg.com/@tanstack/router-core/-/router-core-1.141.4.tgz#035caa9c8df8ed3e5cfb0c5249d080461782ba93"
+ integrity sha512-/7x0Ilo/thg1Hiev6wLL9SA4kk9PICga96qfLuLHVYMEYMfm1+F8BBxgNYeSZfehxTI0Fdo03LjMLYka9JCk4g==
+ dependencies:
+ "@tanstack/history" "1.141.0"
+ "@tanstack/store" "^0.8.0"
+ cookie-es "^2.0.0"
+ seroval "^1.4.0"
+ seroval-plugins "^1.4.0"
+ tiny-invariant "^1.3.3"
+ tiny-warning "^1.0.3"
+
"@tanstack/solid-router@^1.132.27":
version "1.132.27"
resolved "https://registry.yarnpkg.com/@tanstack/solid-router/-/solid-router-1.132.27.tgz#cafa331a8190fb6775f3cd3b88f31adce82e8cc8"
@@ -8048,11 +8066,37 @@
resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.7.0.tgz#afef29b06c6b592e93181cee9baa62fe77454459"
integrity sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==
+"@tanstack/store@0.8.0", "@tanstack/store@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.8.0.tgz#f1c533b9cff000fc792ed77edda178000abc9442"
+ integrity sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==
+
"@tanstack/store@^0.7.0":
version "0.7.7"
resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.7.7.tgz#2c8b1d8c094f3614ae4e0483253239abd0e14488"
integrity sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==
+"@tanstack/vue-router@^1.64.0":
+ version "1.141.4"
+ resolved "https://registry.yarnpkg.com/@tanstack/vue-router/-/vue-router-1.141.4.tgz#1a66bc2b4db182b732c6ca2b11cb58b49ecaffe3"
+ integrity sha512-GGTNBaWcrDmT80sptyWDxz+f25tmgTsBiZCSw94bzIoMk3M4qWgi5uFmnyVkDQ3rYIu5BVO6OgiO/1o1+UK5hQ==
+ dependencies:
+ "@tanstack/history" "1.141.0"
+ "@tanstack/router-core" "1.141.4"
+ "@tanstack/vue-store" "^0.8.0"
+ isbot "^5.1.22"
+ jsesc "^3.0.2"
+ tiny-invariant "^1.3.3"
+ tiny-warning "^1.0.3"
+
+"@tanstack/vue-store@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@tanstack/vue-store/-/vue-store-0.8.0.tgz#beba4021b52513a00a9924416bde0032de6ff5c4"
+ integrity sha512-YLsinYboBLIjNkxDpAn1ydaMS35dKq3M3a788JRCJi4/stWcN7Swp0pxxJ+p0IwKSY4tBXx7vMz22OYWQ1QsUQ==
+ dependencies:
+ "@tanstack/store" "0.8.0"
+ vue-demi "^0.14.10"
+
"@testing-library/dom@^7.21.4":
version "7.31.2"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a"
@@ -27687,12 +27731,22 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^
dependencies:
randombytes "^2.1.0"
-seroval-plugins@^1.0.2, seroval-plugins@^1.3.2, seroval-plugins@~1.3.0:
+seroval-plugins@^1.0.2, seroval-plugins@^1.3.2, seroval-plugins@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.4.0.tgz#0bd453983a0a26039ca161a341c00eca7d42b19a"
+ integrity sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==
+
+seroval-plugins@~1.3.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/seroval-plugins/-/seroval-plugins-1.3.3.tgz#51bcacf09e5384080d7ea4002b08fd9f6166daf5"
integrity sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==
-seroval@^1.0.2, seroval@^1.3.2, seroval@~1.3.0:
+seroval@^1.0.2, seroval@^1.3.2, seroval@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.4.0.tgz#b62f1d0c332d2a085f1059866b9c7dac57486682"
+ integrity sha512-BdrNXdzlofomLTiRnwJTSEAaGKyHHZkbMXIywOh7zlzp4uZnXErEwl9XZ+N1hJSNpeTtNxWvVwN0wUzAIQ4Hpg==
+
+seroval@~1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.3.2.tgz#7e5be0dc1a3de020800ef013ceae3a313f20eca7"
integrity sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==
@@ -31248,6 +31302,11 @@ vue-bundle-renderer@^2.1.0:
dependencies:
ufo "^1.5.3"
+vue-demi@^0.14.10:
+ version "0.14.10"
+ resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
+ integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
+
vue-devtools-stub@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz#a65b9485edecd4273cedcb8102c739b83add2c81"