Chapter 199 · Omnibus Instrument Product Analytics
Subchapter 199.36
references/EXAMPLE-vue-3.mdMarkdown18 KBView on GitHub
Repository: https://github.com/PostHog/context-mill Path: basics/vue-3
This is a Vue 3 (opens in a new tab) + Vite (opens in a new tab) example demonstrating PostHog integration with product analytics, session replay, and error tracking.
It uses the posthog-js browser SDK directly and shows how to:
errorHandlerposthog-js configurationnpm install
# or
pnpm installCreate a .env file in the project root:
VITE_POSTHOG_PROJECT_TOKEN=your_posthog_project_token
VITE_POSTHOG_HOST=https://us.i.posthog.comGet your PostHog project token from your project settings in PostHog.
npm run dev
# or
pnpm devOpen http://localhost:5173 (or whatever Vite prints) in your browser.
src/
main.ts # Vue app entrypoint, PostHog init + global errorHandler
router/
index.ts # Routes + simple auth guard
stores/
auth.ts # Pinia auth store (login, logout, user state)
components/
Header.vue # Navigation + logout, calls posthog.reset()
views/
Home.vue # Login form, identifies user + captures 'user_logged_in'
Burrito.vue # Burrito consideration demo, captures 'burrito_considered'
Profile.vue # Profile + error tracking demo (if implemented)
App.vue # Root layoutposthog-js is initialized once when the app boots:
import posthog from 'posthog-js'
posthog.init(import.meta.env.VITE_POSTHOG_PROJECT_TOKEN || '', {
api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com',
})
app.config.errorHandler = (err) => {
posthog.captureException(err)
}This ensures:
After a successful “login”, the app identifies the user and captures a login event:
const success = await authStore.login(username.value, password.value)
if (success) {
posthog.identify(username.value)
posthog.capture('user_logged_in')
}Identification happens only on login, all further requests will automatically use the same distinct ID.
The burrito page tracks a custom event when a user “considers” the burrito:
posthog.capture('burrito_considered', {
total_considerations: updatedUser.burritoConsiderations,
username: updatedUser.username,
})This shows how to attach useful properties to events (e.g. counts, usernames).
On logout, both the local auth state and PostHog state are cleared:
authStore.logout()
posthog.reset()
router.push({ name: 'home' })posthog.reset() clears the current distinct ID and session so the next login starts a fresh identity.
# Run dev server
npm run dev
# Type-check, compile, and minify for production
npm run build
# Lint
npm run lint[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100
VITE_POSTHOG_PROJECT_TOKEN=your_posthog_project_token
VITE_POSTHOG_HOST=https://us.i.posthog.com
/// <reference types="vite/client" />
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<script setup lang="ts">
import Header from '@/components/Header.vue'
</script>
<template>
<Header />
<main>
<RouterView />
</main>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
max-width: 1200px;
width: 100%;
margin: 2rem auto;
padding: 0 1rem;
}
h1 {
margin-bottom: 1rem;
}
p {
margin-bottom: 1rem;
}
</style>
<template>
<header class="header">
<div class="header-container">
<nav>
<RouterLink to="/">Home</RouterLink>
<template v-if="authStore.user">
<RouterLink to="/burrito">Burrito Consideration</RouterLink>
<RouterLink to="/profile">Profile</RouterLink>
</template>
</nav>
<div class="user-section">
<template v-if="authStore.user && authStore.user.username">
<span>Welcome, {{ authStore.user.username }}!</span>
<button @click="handleLogout" class="btn-logout">
Logout
</button>
</template>
<template v-else>
<span>Not logged in</span>
<button v-if="authStore.user" @click="handleLogout" class="btn-logout">
Clear Session
</button>
</template>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import posthog from 'posthog-js'
const authStore = useAuthStore()
const router = useRouter()
const handleLogout = () => {
authStore.logout()
// IMPORTANT: Reset the PostHog instance to clear the user session
posthog.reset()
router.push({ name: 'home' })
}
</script>
<style scoped>
.header {
background-color: #333;
color: white;
padding: 1rem;
}
.header-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.header nav {
display: flex;
gap: 1rem;
}
.header a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s;
}
.header a:hover {
background-color: #555;
text-decoration: none;
}
.user-section {
display: flex;
align-items: center;
gap: 1rem;
}
.btn-logout {
background-color: #dc3545;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-logout:hover {
background-color: #c82333;
}
</style>
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import posthog from "posthog-js";
const app = createApp(App);
posthog.init(import.meta.env.VITE_POSTHOG_PROJECT_TOKEN || '', {
api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com',
defaults: '2026-01-30',
});
app.use(createPinia())
app.use(router)
app.config.errorHandler = (err, instance, info) => {
// report error to tracking services
posthog.captureException(err)
}
app.mount('#app')
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import Home from '@/views/Home.vue'
import Burrito from '@/views/Burrito.vue'
import Profile from '@/views/Profile.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/burrito',
name: 'burrito',
component: Burrito,
meta: { requiresAuth: true }
},
{
path: '/profile',
name: 'profile',
component: Profile,
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
// Check if user exists and has a valid username
const isValidUser = authStore.user && authStore.user.username
if (to.meta.requiresAuth && !isValidUser) {
// Clear invalid state
if (authStore.user && !authStore.user.username) {
authStore.logout()
}
next({ name: 'home' })
} else {
next()
}
})
export default router
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
username: string
burritoConsiderations: number
}
const users = new Map<string, User>()
export const useAuthStore = defineStore('auth', () => {
const getInitialUser = (): User | null => {
if (typeof window === 'undefined') return null
const storedUsername = localStorage.getItem('currentUser')
if (storedUsername) {
const existingUser = users.get(storedUsername)
if (existingUser && existingUser.username) {
return existingUser
} else {
// Clean up invalid state
localStorage.removeItem('currentUser')
}
}
return null
}
const user = ref<User | null>(getInitialUser())
const isAuthenticated = computed(() => user.value !== null)
const login = async (username: string, password: string): Promise<boolean> => {
// Client-side only fake auth - no server calls
if (!username || !password) {
return false
}
let localUser = users.get(username)
if (!localUser) {
localUser = {
username,
burritoConsiderations: 0
}
users.set(username, localUser)
}
user.value = localUser
localStorage.setItem('currentUser', username)
return true
}
const logout = () => {
user.value = null
localStorage.removeItem('currentUser')
}
const setUser = (newUser: User) => {
user.value = newUser
users.set(newUser.username, newUser)
}
return {
user,
isAuthenticated,
login,
logout,
setUser
}
})
<template>
<div class="container">
<h1>Burrito consideration zone</h1>
<p>Take a moment to truly consider the potential of burritos.</p>
<div style="text-align: center">
<button @click="handleConsideration" class="btn-burrito">
I have considered the burrito potential
</button>
<p v-if="hasConsidered" class="success">
Thank you for your consideration! Count: {{ authStore.user?.burritoConsiderations }}
</p>
</div>
<div class="stats">
<h3>Consideration stats</h3>
<p>Total considerations: {{ authStore.user?.burritoConsiderations }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import posthog from 'posthog-js'
const authStore = useAuthStore()
const hasConsidered = ref(false)
const handleConsideration = () => {
if (!authStore.user) return
// Client-side only - no server calls
const updatedUser = {
...authStore.user,
burritoConsiderations: authStore.user.burritoConsiderations + 1
}
authStore.setUser(updatedUser)
hasConsidered.value = true
setTimeout(() => {
hasConsidered.value = false
}, 2000)
// Capture burrito consideration event
posthog.capture('burrito_considered', {
total_considerations: updatedUser.burritoConsiderations,
username: updatedUser.username
})
}
</script>
<style scoped>
.container {
padding: 2rem;
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-burrito {
background-color: #28a745;
color: white;
border: none;
padding: 1rem 2rem;
border-radius: 4px;
font-size: 18px;
cursor: pointer;
margin: 2rem 0;
}
.btn-burrito:hover {
background-color: #218838;
}
.success {
color: #28a745;
margin-top: 0.5rem;
}
.stats {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
}
h3 {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
</style>
<template>
<div class="container">
<template v-if="authStore.user && authStore.user.username">
<h1>Welcome back, {{ authStore.user.username }}!</h1>
<p>You are now logged in. Feel free to explore:</p>
<ul>
<li>Consider the potential of burritos</li>
<li
<template>
<div class="container">
<h1>User Profile</h1>
<div class="stats">
<h2>Your Information</h2>
<p><strong>Username:</strong> {{ authStore.user?.username }}</p>
<p><strong>Burrito Considerations:</strong> {{ authStore.user?.burritoConsiderations }}</p>
</div>
<div style="margin-top: 2rem">
<h3>Your Burrito Journey</h3>
<template v-if="authStore.user">
<p v-if="authStore.user.burritoConsiderations === 0">
You haven't considered any burritos yet. Visit the Burrito Consideration page to start!
</p>
<p v-else-if="authStore.user.burritoConsiderations === 1">
You've considered the burrito potential once. Keep going!
</p>
<p v-else-if="authStore.user.burritoConsiderations < 5">
You're getting the hang of burrito consideration!
</p>
<p v-else-if="authStore.user.burritoConsiderations < 10">
You're becoming a burrito consideration expert!
</p>
<p v-else>
You are a true burrito consideration master! 🌯
</p>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
</script>
<style scoped>
.container {
padding: 2rem;
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stats {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
}
h2 {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
h3 {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
</style>
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})