From c5defdf51135da12d24893747e10cdedfb66f885 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Mon, 7 Sep 2026 12:00:14 +0200 Subject: [PATCH] test(e2e): Keep a static-lifecycle copy of the Vue 3 E2E app The Vue 3 app is about to move to span streaming. This copy stays on transaction envelopes so the static trace lifecycle remains covered. Version-matrix variants stay on vue-3; this app only needs default static coverage. Co-Authored-By: Cursor Grok 4.6 --- .../test-applications/vue-3-static/.gitignore | 30 ++ .../test-applications/vue-3-static/README.md | 3 + .../test-applications/vue-3-static/env.d.ts | 1 + .../test-applications/vue-3-static/index.html | 13 + .../vue-3-static/package.json | 46 +++ .../vue-3-static/playwright.config.mjs | 8 + .../vue-3-static/public/favicon.ico | Bin 0 -> 4286 bytes .../vue-3-static/src/App.vue | 83 +++++ .../vue-3-static/src/assets/base.css | 86 +++++ .../vue-3-static/src/assets/logo.svg | 1 + .../vue-3-static/src/assets/main.css | 35 +++ .../vue-3-static/src/main.ts | 45 +++ .../vue-3-static/src/router/index.ts | 44 +++ .../vue-3-static/src/stores/cart.ts | 42 +++ .../vue-3-static/src/stores/counter.ts | 10 + .../vue-3-static/src/views/AboutView.vue | 3 + .../vue-3-static/src/views/CartView.vue | 77 +++++ .../vue-3-static/src/views/CategoryIdView.vue | 3 + .../src/views/ComponentMainView.vue | 10 + .../src/views/ComponentOneView.vue | 10 + .../src/views/ComponentTwoView.vue | 10 + .../vue-3-static/src/views/HomeView.vue | 13 + .../src/views/UserIdErrorView.vue | 10 + .../vue-3-static/src/views/UserIdView.vue | 3 + .../vue-3-static/start-event-proxy.mjs | 6 + .../vue-3-static/tests/errors.test.ts | 58 ++++ .../vue-3-static/tests/performance.test.ts | 293 ++++++++++++++++++ .../vue-3-static/tests/pinia.test.ts | 104 +++++++ .../vue-3-static/tsconfig.app.json | 14 + .../vue-3-static/tsconfig.json | 11 + .../vue-3-static/tsconfig.node.json | 13 + .../vue-3-static/vite.config.ts | 22 ++ 32 files changed, 1107 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/README.md create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/index.html create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/package.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/App.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/base.css create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/logo.svg create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/main.css create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/router/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/cart.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/counter.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/AboutView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CartView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CategoryIdView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentMainView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentOneView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentTwoView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/HomeView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdErrorView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdView.vue create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/tests/performance.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/tests/pinia.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.app.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.node.json create mode 100644 dev-packages/e2e-tests/test-applications/vue-3-static/vite.config.ts diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/.gitignore b/dev-packages/e2e-tests/test-applications/vue-3-static/.gitignore new file mode 100644 index 000000000000..8ee54e8d343e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/.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 +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/README.md b/dev-packages/e2e-tests/test-applications/vue-3-static/README.md new file mode 100644 index 000000000000..6af7bb60b866 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/README.md @@ -0,0 +1,3 @@ +# Vue 3 E2E Test App + +E2E test app for Vue 3 and `@sentry/vue`. diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/env.d.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/index.html b/dev-packages/e2e-tests/test-applications/vue-3-static/index.html new file mode 100644 index 000000000000..dcd696757f46 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/package.json b/dev-packages/e2e-tests/test-applications/vue-3-static/package.json new file mode 100644 index 000000000000..ec52fbcbeeaf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/package.json @@ -0,0 +1,46 @@ +{ + "name": "vue-3-static", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "clean": "npx rimraf node_modules pnpm-lock.yaml dist", + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:print-version && playwright test", + "test:build-canary": "pnpm install && pnpm test:install-canary && pnpm build", + "test:build-latest": "pnpm install && pnpm add vue@latest && pnpm build", + "test:build-no-options-api": "pnpm install && VUE_OPTIONS_API=false pnpm build", + "test:assert-no-options-api": "VUE_OPTIONS_API=false pnpm test:assert", + "test:install-canary": "pnpm add vue@$(git ls-remote --tags --sort='v:refname' https://github.com/vuejs/core.git | tail -n1 | awk -F'/' '{print $NF}')", + "test:print-version": "node -p \"'Vue version: ' + require('vue/package.json').version\"" + }, + "dependencies": { + "@sentry/vue": "file:../../packed/sentry-vue-packed.tgz", + "pinia": "^4.0.0", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "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", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vue/tsconfig": "^0.5.1", + "http-server": "^14.1.1", + "npm-run-all2": "^6.2.0", + "typescript": "~5.3.0", + "vite": "^5.4.11", + "vue-tsc": "^1.8.27" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/vue-3-static/playwright.config.mjs new file mode 100644 index 000000000000..94f38d88ad70 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/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-3-static/public/favicon.ico b/dev-packages/e2e-tests/test-applications/vue-3-static/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/App.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/App.vue new file mode 100644 index 000000000000..2024c25f6d1e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/App.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/base.css b/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/base.css new file mode 100644 index 000000000000..8816868a41b6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/logo.svg b/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/logo.svg new file mode 100644 index 000000000000..7565660356e5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/main.css b/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/main.css new file mode 100644 index 000000000000..36fb845b5232 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/src/main.ts new file mode 100644 index 000000000000..bcdc97fdb8df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/main.ts @@ -0,0 +1,45 @@ +import './assets/main.css'; + +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router'; + +import { createPinia } from 'pinia'; + +import * as Sentry from '@sentry/vue'; +import { browserTracingIntegration, vueIntegration } from '@sentry/vue'; + +const app = createApp(App); +const pinia = createPinia(); + +Sentry.init({ + traceLifecycle: 'static', + app, + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + tracesSampleRate: 1.0, + integrations: [ + vueIntegration({ + tracingOptions: { + trackComponents: ['ComponentMainView', ''], + }, + }), + browserTracingIntegration({ + router, + }), + ], + tunnel: `http://localhost:3031/`, // proxy server +}); + +pinia.use( + Sentry.createSentryPiniaPlugin({ + actionTransformer: action => `${action}.transformed`, + stateTransformer: state => ({ + transformed: true, + ...state, + }), + }), +); + +app.use(pinia); +app.use(router); +app.mount('#app'); diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/src/router/index.ts new file mode 100644 index 000000000000..c81a662c61e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/router/index.ts @@ -0,0 +1,44 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import HomeView from '../views/HomeView.vue'; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + component: HomeView, + }, + { + path: '/about', + name: 'AboutView', + component: () => import('../views/AboutView.vue'), + }, + { + path: '/users/:id', + component: () => import('../views/UserIdView.vue'), + }, + { + path: '/users-error/:id', + component: () => import('../views/UserIdErrorView.vue'), + }, + { + path: '/categories', + children: [ + { + path: ':id', + component: () => import('../views/CategoryIdView.vue'), + }, + ], + }, + { + path: '/components', + component: () => import('../views/ComponentMainView.vue'), + }, + { + path: '/cart', + component: () => import('../views/CartView.vue'), + }, + ], +}); + +export default router; diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/cart.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/cart.ts new file mode 100644 index 000000000000..2f406b19b5a6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/cart.ts @@ -0,0 +1,42 @@ +import { acceptHMRUpdate, defineStore } from 'pinia'; + +export const useCartStore = defineStore('cart', { + state: () => ({ + rawItems: [] as string[], + }), + getters: { + items: (state): Array<{ name: string; amount: number }> => + state.rawItems.reduce( + (items, item) => { + const existingItem = items.find(it => 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/vue-3-static/src/stores/counter.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/counter.ts new file mode 100644 index 000000000000..ae315c62ba8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/stores/counter.ts @@ -0,0 +1,10 @@ +import { defineStore } from 'pinia'; + +export const useCounterStore = defineStore('counter', { + state: () => ({ name: 'Counter Store', count: 0 }), + actions: { + increment() { + this.count++; + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/AboutView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/AboutView.vue new file mode 100644 index 000000000000..8c706352120a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/AboutView.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CartView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CartView.vue new file mode 100644 index 000000000000..eae40adb9f66 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CartView.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CategoryIdView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CategoryIdView.vue new file mode 100644 index 000000000000..c3b59c9fb7f5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/CategoryIdView.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentMainView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentMainView.vue new file mode 100644 index 000000000000..69f60c769f54 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentMainView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentOneView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentOneView.vue new file mode 100644 index 000000000000..02c5d133ffb3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentOneView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentTwoView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentTwoView.vue new file mode 100644 index 000000000000..f5a1c3c5f46d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/ComponentTwoView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/HomeView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/HomeView.vue new file mode 100644 index 000000000000..d78932ccd5fd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/HomeView.vue @@ -0,0 +1,13 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdErrorView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdErrorView.vue new file mode 100644 index 000000000000..1bda1aa68f4c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdErrorView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdView.vue b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdView.vue new file mode 100644 index 000000000000..a6c973ef6e35 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/src/views/UserIdView.vue @@ -0,0 +1,3 @@ + diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/vue-3-static/start-event-proxy.mjs new file mode 100644 index 000000000000..33ee9add31f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'vue-3-static', +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/tests/errors.test.ts new file mode 100644 index 000000000000..3ad8c45eef8c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/tests/errors.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('vue-3-static', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'This is a Vue test error', + mechanism: { + type: 'auto.function.vue.error_handler', + handled: false, + }, + }, + ], + }, + transaction: '/', + }); +}); + +test('sends an error with a parameterized transaction name', async ({ page }) => { + const errorPromise = waitForError('vue-3-static', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/users-error/456`); + + await page.locator('#userErrorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'This is a Vue test error', + mechanism: { + type: 'auto.function.vue.error_handler', + handled: false, + }, + }, + ], + }, + transaction: '/users-error/:id', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/tests/performance.test.ts new file mode 100644 index 000000000000..c79795fc9ff6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/tests/performance.test.ts @@ -0,0 +1,293 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +// Set when the Options API is compiled out of the Vue build. +const OPTIONS_API_DISABLED = process.env.VUE_OPTIONS_API === 'false'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/users/456`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.segment.name.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.id': '456', + 'url.template': '/users/:id', + 'url.path': '/users/456', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/), + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/users/:id', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.segment.name.source': 'route', + 'sentry.origin': 'auto.navigation.vue', + 'sentry.op': 'navigation', + 'params.id': '123', + 'url.template': '/users/:id', + 'url.path': '/users/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/), + }, + op: 'navigation', + origin: 'auto.navigation.vue', + }, + }, + transaction: '/users/:id', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a pageload transaction with a nested route URL', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/categories/123`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.segment.name.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.id': '123', + 'url.template': '/categories/:id', + 'url.path': '/categories/123', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/categories\/123$/), + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/categories/:id', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a pageload transaction with a route name as transaction name if available', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/about`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.segment.name.source': 'custom', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'navigation.route.id': 'AboutView', + 'url.path': '/about', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/about$/), + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: 'AboutView', + transaction_info: { + source: 'custom', + }, + }); +}); + +// The root component is always tracked, even when the route's view is missing from `trackComponents`. +// The root itself mounts synchronously on both routes (`app.mount()` does not wait for the router). +// What differs on `/components` is that its view arrives through a dynamic `import()`, so the +// async-loaded components must join the same pageload while `Application Render` is still open. +[ + { + route: '/', + routeDescription: 'a route with a synchronously mounted component', + // `HomeView` is missing from `trackComponents`, so the root spans are the only UI spans. + expectedUiSpanDescriptions: ['Application Render', 'Vue '], + }, + { + route: '/components', + routeDescription: 'a route with an async component', + expectedUiSpanDescriptions: [ + 'Application Render', + 'Vue ', + 'Vue ', + 'Vue ', + ], + }, +].forEach(({ route, routeDescription, expectedUiSpanDescriptions }) => { + test(`sends an application render span and a root component span on ${routeDescription}`, async ({ page }) => { + // Vue compiles `app.mixin()` down to a no-op when the Options API is disabled, so the SDK creates no UI spans at all. + test.fail(OPTIONS_API_DISABLED, 'Vue tracing is registered through app.mixin(), which needs the Options API'); + + const transactionPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'pageload' && + transactionEvent.contexts?.trace?.data?.['url.path'] === route + ); + }); + + await page.goto(route); + + const rootSpan = await transactionPromise; + const uiSpans = (rootSpan.spans || []).filter(span => span.origin === 'auto.ui.vue'); + + expect(uiSpans.map(span => span.description).sort()).toEqual(expectedUiSpanDescriptions); + + const applicationRenderSpan = uiSpans.find(span => span.description === 'Application Render'); + expect(applicationRenderSpan).toMatchObject({ + data: { + 'sentry.op': 'ui.render', + 'sentry.origin': 'auto.ui.vue', + }, + op: 'ui.render', + origin: 'auto.ui.vue', + }); + + const rootComponentSpan = uiSpans.find(span => span.description === 'Vue '); + expect(rootComponentSpan).toMatchObject({ + data: { + 'sentry.op': 'ui.mount', + 'sentry.origin': 'auto.ui.vue', + }, + op: 'ui.mount', + origin: 'auto.ui.vue', + }); + }); +}); + +test('sends a lifecycle span for the root and for each tracked component only', async ({ page }) => { + // Vue compiles `app.mixin()` down to a no-op when the Options API is disabled, so the SDK creates no UI spans at all. + test.fail(OPTIONS_API_DISABLED, 'Vue tracing is registered through app.mixin(), which needs the Options API'); + + const transactionPromise = waitForTransaction('vue-3-static', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/components`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.segment.name.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'url.template': '/components', + 'url.path': '/components', + 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/components$/), + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/components', + transaction_info: { + source: 'route', + }, + }); + + const uiSpans = (rootSpan.spans || []).filter(span => span.origin === 'auto.ui.vue'); + const uiSpanDescriptions = uiSpans.map(span => span.description).sort(); + + expect(uiSpanDescriptions).toEqual([ + 'Application Render', + 'Vue ', + 'Vue ', + 'Vue ', + ]); + + // enabled by default + const applicationRenderSpan = uiSpans.find(span => span.description === 'Application Render'); + expect(applicationRenderSpan).toMatchObject({ + data: { + 'sentry.op': 'ui.render', + 'sentry.origin': 'auto.ui.vue', + }, + op: 'ui.render', + origin: 'auto.ui.vue', + }); + + // enabled by default + const rootComponentSpan = uiSpans.find(span => span.description === 'Vue '); + expect(rootComponentSpan).toMatchObject({ + data: { + 'sentry.op': 'ui.mount', + 'sentry.origin': 'auto.ui.vue', + }, + op: 'ui.mount', + origin: 'auto.ui.vue', + }); + + // without `<>` + const componentMainViewSpan = uiSpans.find(span => span.description === 'Vue '); + expect(componentMainViewSpan).toMatchObject({ + data: { + 'sentry.op': 'ui.mount', + 'sentry.origin': 'auto.ui.vue', + }, + op: 'ui.mount', + origin: 'auto.ui.vue', + }); + + // with `<>` + const componentOneViewSpan = uiSpans.find(span => span.description === 'Vue '); + expect(componentOneViewSpan).toMatchObject({ + data: { + 'sentry.op': 'ui.mount', + 'sentry.origin': 'auto.ui.vue', + }, + op: 'ui.mount', + origin: 'auto.ui.vue', + }); + + // `ComponentTwoView` renders on this route but is absent from `trackComponents` + // not tracked + expect(uiSpanDescriptions).not.toContain('Vue '); +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/tests/pinia.test.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/tests/pinia.test.ts new file mode 100644 index 000000000000..64395a2a2ed1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/tests/pinia.test.ts @@ -0,0 +1,104 @@ +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('/cart'); + + await page.locator('#item-input').fill('item'); + await page.locator('#item-add').click(); + + const errorPromise = waitForError('vue-3-static', 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 === 'pinia.action'); + + expect(actionBreadcrumb).toBeDefined(); + expect(actionBreadcrumb?.message).toBe('Store: cart | Action: addItem.transformed'); + 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, + cart: { + rawItems: ['item'], + }, + counter: { + count: 0, + name: 'Counter Store', + }, + }); +}); + +test('state transformer receives full state object and is stored in state context', async ({ page }) => { + await page.goto('/cart'); + + await page.locator('#item-input').fill('multiple store test'); + await page.locator('#item-add').click(); + + await page.locator('button:text("+")').click(); + await page.locator('button:text("+")').click(); + await page.locator('button:text("+")').click(); + + await page.locator('#item-input').fill('multiple store pinia'); + await page.locator('#item-add').click(); + + const errorPromise = waitForError('vue-3-static', async errorEvent => { + return errorEvent?.exception?.values?.[0].value === 'This is an error'; + }); + + await page.locator('#throw-error').click(); + + const error = await errorPromise; + + // Verify stateTransformer was called with full state and modified state + const stateContext = error.contexts?.state?.state; + + expect(stateContext?.value).toEqual({ + transformed: true, + cart: { + rawItems: ['multiple store test', 'multiple store pinia'], + }, + counter: { + name: 'Counter Store', + count: 3, + }, + }); +}); + +test('different store interaction order maintains full state tracking', async ({ page }) => { + await page.goto('/cart'); + + await page.locator('button:text("+")').click(); + + await page.locator('#item-input').fill('order test item'); + await page.locator('#item-add').click(); + + await page.locator('button:text("+")').click(); + + const errorPromise = waitForError('vue-3-static', async errorEvent => { + return errorEvent?.exception?.values?.[0].value === 'This is an error'; + }); + + await page.locator('#throw-error').click(); + + const error = await errorPromise; + + const stateContext = error.contexts?.state?.state; + + expect(stateContext).toBeDefined(); + + const stateValue = stateContext?.value; + expect(stateValue.cart.rawItems).toEqual(['order test item']); + expect(stateValue.counter.count).toBe(2); +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.app.json new file mode 100644 index 000000000000..e14c754d3ae5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/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-3-static/tsconfig.json b/dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.json new file mode 100644 index 000000000000..66b5e5703e83 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/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-3-static/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.node.json new file mode 100644 index 000000000000..280d8ca857df --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*"], + "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-3-static/vite.config.ts b/dev-packages/e2e-tests/test-applications/vue-3-static/vite.config.ts new file mode 100644 index 000000000000..e385ddeaadee --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3-static/vite.config.ts @@ -0,0 +1,22 @@ +import { URL, fileURLToPath } from 'node:url'; + +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import { defineConfig } from 'vite'; + +// Nuxt 5 disables the Options API by default (users can disable it too for smaller bundle size) +const optionsApi = process.env.VUE_OPTIONS_API === 'false' ? 'false' : 'true'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), vueJsx()], + define: { + __VUE_OPTIONS_API__: optionsApi, + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + envPrefix: 'PUBLIC_', +});