Track how many users are connected to your application and which versions they're running. This is useful for admin dashboards, deployment monitoring, and understanding rollout progress.
sse or ws update strategies. It does not support polling or external adapters (Pusher/Ably).Enable connection tracking in your Nuxt config using connectionTracking:
export default defineNuxtConfig({
skewProtection: {
connectionTracking: true,
routeTracking: true, // optional: track which routes users are viewing
ipTracking: true, // optional: track user IP addresses
}
})
polling strategy or external adapters (Pusher/Ably).@nuxtjs/robots is installed, bot/crawler traffic is automatically excluded from connection counts. See Bot Traffic Filtering.IP tracking requires explicit opt-in via ipTracking: true due to privacy considerations. When enabled, IP addresses are extracted from request headers in this order:
cf-connecting-ip (Cloudflare)x-forwarded-for (proxies/load balancers)x-real-ip (nginx)Stats are only sent to connections that pass authorization. Implement the skew:authorize-stats hook in a server plugin.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
// Works with nuxt-auth-utils
const session = await getUserSession(event)
if (session?.user?.role === 'admin') {
authorize()
}
})
})
For WebSocket connections (e.g., with cloudflare-durable preset), the event object is not a full H3 event—it only contains { headers: Headers }. This means getUserSession() won't work directly. You need to manually unseal the session cookie:
import { unsealSession } from 'h3'
const ADMIN_EMAILS = ['admin@example.com']
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
// Try getUserSession first (works for SSE/POST)
const session = await getUserSession(event).catch(() => null)
if (session?.user?.email && ADMIN_EMAILS.includes(session.user.email)) {
authorize()
return
}
// For WebSocket, manually unseal the session cookie
if (event?.headers instanceof Headers) {
const cookieHeader = event.headers.get('cookie') || ''
const match = cookieHeader.match(/nuxt-session=([^;]+)/)
if (match?.[1]) {
const config = useRuntimeConfig()
const password = config.session?.password || process.env.NUXT_SESSION_PASSWORD || ''
if (password) {
const unsealed = await unsealSession(null as any, { password }, decodeURIComponent(match[1])).catch(() => null)
// Note: unsealSession returns { data: { user } }, not { user } directly
const email = unsealed?.data?.user?.email
if (email && ADMIN_EMAILS.includes(email)) {
authorize()
}
}
}
}
})
})
getUserSession() returns { user: { email } } while unsealSession() returns { data: { user: { email } } }.The useActiveConnections() composable provides reactive access to connection statistics:
<script setup lang="ts">
import { useActiveConnections } from '#imports'
const { total, versions, authorized } = useActiveConnections()
</script>
<template>
<div v-if="authorized">
<p>Active users: {{ total }}</p>
<ul>
<li v-for="(count, version) in versions" :key="version">
{{ version.slice(0, 8) }}: {{ count }} users
</li>
</ul>
</div>
<div v-else>
Not authorized for stats
</div>
</template>
| Property | Type | Description |
|---|---|---|
total | ComputedRef<number> | Total active connections |
versions | ComputedRef<Record<string, number>> | Map of buildId → connection count |
routes | ComputedRef<Record<string, number>> | Map of route → connection count (requires routeTracking) |
connections | ComputedRef<ConnectionInfo[]> | Individual connections with id, version, route, and ip (requires ipTracking for IP) |
yourId | ComputedRef<string | undefined> | Your connection ID to identify yourself in the list |
authorized | ComputedRef<boolean | null> | Whether the connection is authorized for stats (null while pending) |
<script setup>
const { total, versions, authorized } = useActiveConnections()
const currentBuildId = useRuntimeConfig().app.buildId
const stats = computed(() => {
const current = versions.value[currentBuildId] || 0
const outdated = total.value - current
return {
total: total.value,
current,
outdated,
percentUpToDate: total.value ? Math.round((current / total.value) * 100) : 100
}
})
</script>
<template>
<div v-if="authorized" class="grid grid-cols-4 gap-4">
<div class="stat">
<span class="label">Total Users</span>
<span class="value">{{ stats.total }}</span>
</div>
<div class="stat">
<span class="label">On Latest</span>
<span class="value text-green">{{ stats.current }}</span>
</div>
<div class="stat">
<span class="label">On Old Version</span>
<span class="value text-orange">{{ stats.outdated }}</span>
</div>
<div class="stat">
<span class="label">Rollout Progress</span>
<span class="value">{{ stats.percentUpToDate }}%</span>
</div>
</div>
<div v-else>
Not authorized for live stats
</div>
</template>
For advanced use cases, you can use hooks directly instead of the composable.
skew:messageListen to all messages from the SSE/WebSocket connection:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hooks.hook('skew:message', (message) => {
if (message.type === 'stats') {
// Custom handling
console.log('Connections:', message.total)
console.log('Versions:', message.versions)
}
})
})
Build custom tracking logic with Nitro hooks:
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('skew:connection:open', ({ id, version, route, ip, send }) => {
console.log(`Client ${id} connected on ${version} from ${ip}`)
// Send custom welcome message
send({ type: 'welcome', serverTime: Date.now() })
})
nitroApp.hooks.hook('skew:connection:close', ({ id }) => {
console.log(`Client ${id} disconnected`)
})
})
See Nuxt Hooks and Nitro Hooks for full documentation.