import type { Media, MediaType } from '~/types'
-defineProps<{
+const props = defineProps<{
item: Media
type: MediaType
}>()
const tab = ref<'overview' | 'videos' | 'photos'>('overview')
+const { $posthog } = useNuxtApp()
+
+function selectTab(selectedTab: typeof tab.value) {
+ tab.value = selectedTab
+ $posthog?.capture('media_tab_selected', {
+ media_id: props.item.id,
+ media_type: props.type,
+ tab: selectedTab,
+ })
+}
-
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/components/media/Hero.vue b/apps/basic-integration/nuxt/movies-nuxt-4/components/media/Hero.vue
index 46dc73bae..f56c44731 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/components/media/Hero.vue
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/components/media/Hero.vue
@@ -11,9 +11,16 @@ const props = withDefaults(defineProps<{
const trailer = computed(() => getTrailer(props.item))
const showModal = useIframeModal()
+const { $posthog } = useNuxtApp()
+
function playTrailer() {
- if (trailer.value)
- showModal(trailer.value)
+ if (!trailer.value)
+ return
+
+ $posthog?.capture('trailer_played', {
+ media_id: props.item.id,
+ })
+ showModal(trailer.value)
}
const mounted = useMounted()
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/components/video/Card.vue b/apps/basic-integration/nuxt/movies-nuxt-4/components/video/Card.vue
index d99a57c34..98ab17f5a 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/components/video/Card.vue
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/components/video/Card.vue
@@ -6,7 +6,12 @@ const props = defineProps<{
}>()
const showModal = useIframeModal()
+const { $posthog } = useNuxtApp()
+
function play() {
+ $posthog?.capture('video_played', {
+ video_type: props.item.type,
+ })
return showModal(getVideoLink(props.item)!)
}
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/nuxt.config.ts b/apps/basic-integration/nuxt/movies-nuxt-4/nuxt.config.ts
index d7846c64c..31d8af984 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/nuxt.config.ts
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/nuxt.config.ts
@@ -21,6 +21,11 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBaseUrl,
+ posthog: {
+ publicKey: process.env.NUXT_PUBLIC_POSTHOG_PROJECT_TOKEN,
+ host: process.env.NUXT_PUBLIC_POSTHOG_HOST,
+ posthogDefaults: '2026-01-30',
+ },
},
},
From b9f61a0d4229f78358ad184afc3cfc2a6d22c808 Mon Sep 17 00:00:00 2001
From: "wizard-ci-bot[bot]"
<254716194+wizard-ci-bot[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 16:54:33 +0000
Subject: [PATCH 2/3] wizard-ci: nuxt/movies-nuxt-4 (2/3)
---
.../nuxt/movies-nuxt-4/package-lock.json | 298 +++++++++++++++++-
1 file changed, 297 insertions(+), 1 deletion(-)
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/package-lock.json b/apps/basic-integration/nuxt/movies-nuxt-4/package-lock.json
index 73446c245..8dc2e8710 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/package-lock.json
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/package-lock.json
@@ -4,9 +4,13 @@
"requires": true,
"packages": {
"": {
- "name": "movies-nuxt-4",
"dependencies": {
"@nuxtjs/html-validator": "^1.8.2",
+ "@opentelemetry/api-logs": "^0.223.0",
+ "@opentelemetry/exporter-logs-otlp-http": "^0.300.0",
+ "@opentelemetry/resources": "^3.0.0",
+ "@opentelemetry/sdk-logs": "^3.0.0",
+ "posthog-js": "^1.441.2",
"vue-router-better-scroller": "^0.0.0"
},
"devDependencies": {
@@ -3207,6 +3211,169 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.10.0.tgz",
+ "integrity": "sha512-lMkyes8iHrG+mk6WH7X70MdCLI2RS0FwNBY1PyQ8UemaijE2m5mv5A0Niy3HtbYTFrgkUEQjFMXmSAk2hQDuPQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.223.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.223.0.tgz",
+ "integrity": "sha512-Oepm6etVPFCn0VWB1t833iOTgz1eYBDUPxcrOR/XvwzD1+Ag9VzSPSrrKAFNJ/haatj+8CXbG47pZXvdNQKDJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-whOLlww0R/RLtCWGBXhK1fdvL9t2/TL0fqdjZvcaoUUuNilRPjXecqQdzyVwV68s6XEDA5oYbm7/bZT62GXF4A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.10.0 <1.11.0"
+ }
+ },
+ "node_modules/@opentelemetry/exporter-logs-otlp-http": {
+ "version": "0.300.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.300.0.tgz",
+ "integrity": "sha512-S8yP9Fg33CU9+0fr1pnh/afPfgEpSyxQO4JRUZvmDMhAzgR/RLzvYg93MjWjbC1c2VrXffXROMO68RZ1P/ebgQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/otlp-exporter-base": "0.300.0",
+ "@opentelemetry/otlp-transformer": "0.300.0",
+ "@opentelemetry/sdk-logs": "3.0.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-exporter-base": {
+ "version": "0.300.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.300.0.tgz",
+ "integrity": "sha512-D+PhzYURtv6p7Kk1rUrilLtV+ZvS218tI0jiKTV5z1agOeELw78C+cFd3o9UUshUx1j+EIpFM62yGk8BuydQvg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "3.0.0",
+ "@opentelemetry/otlp-transformer": "0.300.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/otlp-transformer": {
+ "version": "0.300.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.300.0.tgz",
+ "integrity": "sha512-T55lRmjRwvXWyTFdy/BbJ3T5RRsq3bAwQgxxA7qHFxgRd4+pZzRk02ZwOFsYK5072OXGYjYFNJcp68eQV4i0Ig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "3.0.0",
+ "@opentelemetry/resources": "3.0.0",
+ "@opentelemetry/sdk-logs": "3.0.0",
+ "@opentelemetry/sdk-metrics": "3.0.0",
+ "@opentelemetry/sdk-trace": "3.0.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.10.0 <1.11.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-3.0.0.tgz",
+ "integrity": "sha512-4762KVmlDroRIt+4Ra8AnwcQqdXlkAR55jDrhzna8bzhJx1mUzvHW/Lh/OemFuY9+ouM4xaDc6CbUmoJmM6BFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "3.0.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.10.0 <1.11.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-3.0.0.tgz",
+ "integrity": "sha512-IysNZWrBk2PqYw4qU9PZcC7pjrZLa+KLk7vcLmPPNBUr97MgeamWhX0DUHsmxwJv3dnjYM6mLRmP8mw69ld+dA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "3.0.0",
+ "@opentelemetry/resources": "3.0.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.10.0 <1.11.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-3.0.0.tgz",
+ "integrity": "sha512-YozyS2rIlizvUNLZwcWxH0x2FhX02/mcZwzM8hsl8yvQk+xeKmAc7vcSieWiO4oSwIB20B8eS/zwdopAd/J5zQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "3.0.0",
+ "@opentelemetry/resources": "3.0.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.10.0 <1.11.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-3.0.0.tgz",
+ "integrity": "sha512-4T0DbDGdtsjNmYK5/lNalHro4fsMZ0UZ3JE+C8fcqYP25xaJC22FHg8kdJNJ21srjJY5XoT7WNFCtj88NNTl4Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "3.0.0",
+ "@opentelemetry/resources": "3.0.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": ">=22.15.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.10.0 <1.11.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@oxc-minify/binding-android-arm-eabi": {
"version": "0.112.0",
"resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.112.0.tgz",
@@ -4672,6 +4839,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@posthog/browser-common": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.10.1.tgz",
+ "integrity": "sha512-gKtBGEsjxNExO/19trnkiXTHFX0075vXn69zZ+lPlXy6P/DkED4644WqjDN/GIWr1Y5en2dcVJPOqUmVO+drkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@posthog/core": "^1.61.1",
+ "@posthog/types": "^1.418.0",
+ "preact": "^10.29.3"
+ }
+ },
+ "node_modules/@posthog/core": {
+ "version": "1.61.1",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.61.1.tgz",
+ "integrity": "sha512-qyMTxPfjeJkHIhpcukOgBN3BWXbCyFRPz8/Voy5qZtVh9NZqj3EhC6LLeYZuJhL3WpqhQTgn2FMIwAo/ag6fmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@posthog/types": "^1.417.0"
+ }
+ },
+ "node_modules/@posthog/types": {
+ "version": "1.418.0",
+ "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.418.0.tgz",
+ "integrity": "sha512-WqDXUWH1R8OilBp2PfDpQIRZSCs3QyOnw7H3YK7lvZLCMIsOqSZAKJrrqYWt4f7FvnVCACRuNGCC0HKh3J/HWw==",
+ "license": "MIT"
+ },
"node_modules/@quansync/fs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz",
@@ -5488,6 +5681,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -9025,6 +9225,20 @@
"iconv-lite": "^0.4.8"
}
},
+ "node_modules/core-js": {
+ "version": "3.51.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.51.0.tgz",
+ "integrity": "sha512-7bDKx/5jm5MxzsXxc93g6Z2+4ZsuT/MddrfRmTDJNAkWsYWHU8EVNEsBiIEWnMAMCHMmRfUEjkuo3jeNBOIHRw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
@@ -9630,6 +9844,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
+ "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -15719,6 +15942,60 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/posthog-js": {
+ "version": "1.441.2",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.441.2.tgz",
+ "integrity": "sha512-PsELSUSVCx80BfbtMtir8fW6scxVj+2CvsG+Vj2j9eTQYQL1AzsaZr73pqJk1XWEcSt+B1hEfirz3iFVQGVQkQ==",
+ "license": "(Apache-2.0 AND MIT)",
+ "dependencies": {
+ "@posthog/browser-common": "^0.10.1",
+ "@posthog/core": "^1.61.1",
+ "@posthog/types": "^1.418.0",
+ "core-js": "^3.49.0",
+ "dompurify": "^3.4.13",
+ "fflate": "^0.4.8",
+ "preact": "^10.29.3",
+ "query-selector-shadow-dom": "^1.0.1",
+ "web-vitals": "^6.2.1",
+ "web-vitals-soft-navs": "npm:web-vitals@6.2.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/posthog-js/node_modules/fflate": {
+ "version": "0.4.9",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.9.tgz",
+ "integrity": "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==",
+ "license": "MIT"
+ },
+ "node_modules/preact": {
+ "version": "10.29.8",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz",
+ "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ },
+ "peerDependencies": {
+ "preact-render-to-string": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "preact-render-to-string": {
+ "optional": true
+ }
+ }
+ },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -15916,6 +16193,12 @@
],
"license": "MIT"
},
+ "node_modules/query-selector-shadow-dom": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
+ "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -19233,6 +19516,19 @@
"node": ">=10.13.0"
}
},
+ "node_modules/web-vitals": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.3.tgz",
+ "integrity": "sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/web-vitals-soft-navs": {
+ "name": "web-vitals",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.1.tgz",
+ "integrity": "sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==",
+ "license": "Apache-2.0"
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
From 57d818b228984335a8c531b494552ecb5bab4fe5 Mon Sep 17 00:00:00 2001
From: "wizard-ci-bot[bot]"
<254716194+wizard-ci-bot[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 16:54:36 +0000
Subject: [PATCH 3/3] wizard-ci: nuxt/movies-nuxt-4 (3/3)
---
.../nuxt/movies-nuxt-4/package.json | 5 ++
.../nuxt/movies-nuxt-4/pages/login.vue | 11 ++++
.../nuxt/movies-nuxt-4/pages/search.vue | 2 +
.../movies-nuxt-4/plugins/posthog.client.ts | 51 +++++++++++++++++
.../server/api/auth/login.post.ts | 17 ++++++
.../server/api/auth/logout.post.ts | 6 ++
.../server/utils/posthog-logs.ts | 55 +++++++++++++++++++
7 files changed, 147 insertions(+)
create mode 100644 apps/basic-integration/nuxt/movies-nuxt-4/plugins/posthog.client.ts
create mode 100644 apps/basic-integration/nuxt/movies-nuxt-4/server/utils/posthog-logs.ts
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/package.json b/apps/basic-integration/nuxt/movies-nuxt-4/package.json
index 7cb919f44..cfd8f10ef 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/package.json
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/package.json
@@ -19,6 +19,11 @@
},
"dependencies": {
"@nuxtjs/html-validator": "^1.8.2",
+ "@opentelemetry/api-logs": "^0.223.0",
+ "@opentelemetry/exporter-logs-otlp-http": "^0.300.0",
+ "@opentelemetry/resources": "^3.0.0",
+ "@opentelemetry/sdk-logs": "^3.0.0",
+ "posthog-js": "^1.441.2",
"vue-router-better-scroller": "^0.0.0"
},
"devDependencies": {
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/pages/login.vue b/apps/basic-integration/nuxt/movies-nuxt-4/pages/login.vue
index 67072592b..f15bb020b 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/pages/login.vue
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/pages/login.vue
@@ -4,6 +4,7 @@ const password = ref('')
const error = ref('')
const loading = ref(false)
const { login } = useAuth()
+const { $posthog } = useNuxtApp()
const handleLogin = async () => {
error.value = ''
@@ -11,7 +12,17 @@ const handleLogin = async () => {
try {
await login(username.value, password.value)
+ $posthog?.capture('login_succeeded')
+ $posthog?.logger.info('login completed', {
+ operation: 'login',
+ outcome: 'success',
+ })
} catch (e: any) {
+ $posthog?.capture('login_failed')
+ $posthog?.logger.warn('login completed', {
+ operation: 'login',
+ outcome: 'failed',
+ })
error.value = e.message || 'Login failed'
} finally {
loading.value = false
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/pages/search.vue b/apps/basic-integration/nuxt/movies-nuxt-4/pages/search.vue
index 19c0ec609..6b746391e 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/pages/search.vue
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/pages/search.vue
@@ -13,12 +13,14 @@ const count = ref()
const items = ref([])
const currentSearch = ref(input.value)
+const { $posthog } = useNuxtApp()
function search() {
if (currentSearch.value === input.value)
return
currentSearch.value = input.value.toString()
+ $posthog?.capture('search_performed')
count.value = undefined
items.value = []
router.replace({ query: { s: input.value } })
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/plugins/posthog.client.ts b/apps/basic-integration/nuxt/movies-nuxt-4/plugins/posthog.client.ts
new file mode 100644
index 000000000..4577db9e5
--- /dev/null
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/plugins/posthog.client.ts
@@ -0,0 +1,51 @@
+import type { PostHog, PostHogInterface } from 'posthog-js'
+import posthog from 'posthog-js'
+
+export default defineNuxtPlugin((nuxtApp) => {
+ const runtimeConfig = useRuntimeConfig()
+ const publicKey = runtimeConfig.public.posthog.publicKey
+ const host = runtimeConfig.public.posthog.host
+
+ if (!publicKey) {
+ if (import.meta.dev) {
+ throw new Error('NUXT_PUBLIC_POSTHOG_PROJECT_TOKEN variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once NUXT_PUBLIC_POSTHOG_PROJECT_TOKEN is configured')
+ }
+ return
+ }
+
+ if (!host) {
+ if (import.meta.dev) {
+ throw new Error('NUXT_PUBLIC_POSTHOG_HOST variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once NUXT_PUBLIC_POSTHOG_HOST is configured')
+ }
+ return
+ }
+
+ const posthogClient = posthog.init(publicKey, {
+ api_host: host,
+ defaults: runtimeConfig.public.posthog.posthogDefaults as any,
+ logs: {
+ serviceName: 'nuxt-movies-web',
+ environment: import.meta.dev ? 'development' : 'production',
+ },
+ capture_exceptions: {
+ capture_unhandled_errors: true,
+ capture_unhandled_rejections: true,
+ capture_console_errors: false,
+ },
+ tracing_headers: [window.location.hostname],
+ loaded: (client: PostHogInterface) => {
+ if (import.meta.dev)
+ client.debug()
+ },
+ })
+
+ nuxtApp.hook('vue:error', (error) => {
+ posthogClient.captureException(error)
+ })
+
+ return {
+ provide: {
+ posthog: posthogClient as PostHog,
+ },
+ }
+})
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/login.post.ts b/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/login.post.ts
index b1312960e..965d0eb4d 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/login.post.ts
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/login.post.ts
@@ -1,3 +1,5 @@
+import { logPosthogServer } from '~/server/utils/posthog-logs'
+
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event)
@@ -21,14 +23,29 @@ export default defineEventHandler(async (event) => {
maxAge: 60 * 60 * 24 * 7, // 7 days
})
+ await logPosthogServer('INFO', 'login completed', {
+ operation: 'login',
+ outcome: 'success',
+ })
+
return {
success: true,
user: sanitizedUsername,
}
} catch (error: any) {
if (error.statusCode) {
+ await logPosthogServer('WARN', 'login completed', {
+ operation: 'login',
+ outcome: 'rejected',
+ status_code: error.statusCode,
+ })
throw error
}
+ await logPosthogServer('ERROR', 'login completed', {
+ operation: 'login',
+ outcome: 'failed',
+ status_code: 500,
+ })
throw createError({
statusCode: 500,
message: 'An error occurred during login',
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/logout.post.ts b/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/logout.post.ts
index a6f935002..3543f071a 100644
--- a/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/logout.post.ts
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/server/api/auth/logout.post.ts
@@ -1,4 +1,10 @@
+import { logPosthogServer } from '~/server/utils/posthog-logs'
+
export default defineEventHandler(async (event) => {
deleteCookie(event, 'auth-user')
+ await logPosthogServer('INFO', 'logout completed', {
+ operation: 'logout',
+ outcome: 'success',
+ })
return { success: true }
})
diff --git a/apps/basic-integration/nuxt/movies-nuxt-4/server/utils/posthog-logs.ts b/apps/basic-integration/nuxt/movies-nuxt-4/server/utils/posthog-logs.ts
new file mode 100644
index 000000000..7f984a930
--- /dev/null
+++ b/apps/basic-integration/nuxt/movies-nuxt-4/server/utils/posthog-logs.ts
@@ -0,0 +1,55 @@
+import process from 'node:process'
+import { logs } from '@opentelemetry/api-logs'
+import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'
+import { resourceFromAttributes } from '@opentelemetry/resources'
+import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'
+
+const projectToken = process.env.NUXT_PUBLIC_POSTHOG_PROJECT_TOKEN
+const host = process.env.NUXT_PUBLIC_POSTHOG_HOST
+const isDevelopment = process.env.NODE_ENV === 'development'
+
+function requiredEnvironmentVariable(name: string) {
+ if (isDevelopment) {
+ throw new Error(`${name} variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once ${name} is configured`)
+ }
+}
+
+if (!projectToken)
+ requiredEnvironmentVariable('NUXT_PUBLIC_POSTHOG_PROJECT_TOKEN')
+
+if (!host)
+ requiredEnvironmentVariable('NUXT_PUBLIC_POSTHOG_HOST')
+
+const loggerProvider = projectToken && host
+ ? new LoggerProvider({
+ resource: resourceFromAttributes({
+ 'service.name': 'nuxt-movies-server',
+ 'deployment.environment': isDevelopment ? 'development' : 'production',
+ }),
+ processors: [
+ new BatchLogRecordProcessor({
+ exporter: new OTLPLogExporter({
+ url: new URL('/i/v1/logs', host).toString(),
+ headers: { Authorization: `Bearer ${projectToken}` },
+ }),
+ }),
+ ],
+ })
+ : undefined
+
+if (loggerProvider)
+ logs.setGlobalLoggerProvider(loggerProvider)
+
+const posthogLogger = loggerProvider ? logs.getLogger('posthog-nuxt-server') : undefined
+
+export async function logPosthogServer(
+ severityText: 'INFO' | 'WARN' | 'ERROR',
+ body: string,
+ attributes: Record,
+) {
+ try {
+ posthogLogger?.emit({ severityText, body, attributes })
+ await loggerProvider?.forceFlush()
+ }
+ catch {}
+}