perf(api): remove legacy rolling user metric (#2233)
This commit is contained in:
@@ -36,37 +36,28 @@ describe('registerUserMetricsSnapshotGauges', () => {
|
||||
const totalUsers = createGaugeProbe()
|
||||
const activeSessions = createGaugeProbe()
|
||||
const distinctActiveUsers = createGaugeProbe()
|
||||
const rollingActiveUsers = createGaugeProbe()
|
||||
|
||||
const recorder = registerUserMetricsSnapshotGauges({
|
||||
totalUsers: totalUsers.gauge,
|
||||
activeSessions: activeSessions.gauge,
|
||||
distinctActiveUsers: distinctActiveUsers.gauge,
|
||||
rollingActiveUsers: rollingActiveUsers.gauge,
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
totalUsers.collect(),
|
||||
activeSessions.collect(),
|
||||
distinctActiveUsers.collect(),
|
||||
rollingActiveUsers.collect(),
|
||||
])
|
||||
|
||||
expect(totalUsers.observe).not.toHaveBeenCalled()
|
||||
expect(activeSessions.observe).not.toHaveBeenCalled()
|
||||
expect(distinctActiveUsers.observe).not.toHaveBeenCalled()
|
||||
expect(rollingActiveUsers.observe).not.toHaveBeenCalled()
|
||||
|
||||
recorder.record(
|
||||
{
|
||||
totalUsers: 42,
|
||||
activeSessions: 7,
|
||||
distinctActiveUsers: 5,
|
||||
rollingActiveUsers: {
|
||||
'24h': 9,
|
||||
'7d': 18,
|
||||
'30d': 30,
|
||||
},
|
||||
},
|
||||
refreshedAt,
|
||||
)
|
||||
@@ -75,15 +66,11 @@ describe('registerUserMetricsSnapshotGauges', () => {
|
||||
totalUsers.collect(),
|
||||
activeSessions.collect(),
|
||||
distinctActiveUsers.collect(),
|
||||
rollingActiveUsers.collect(),
|
||||
])
|
||||
|
||||
expect(totalUsers.observe).toHaveBeenLastCalledWith(42)
|
||||
expect(activeSessions.observe).toHaveBeenLastCalledWith(7)
|
||||
expect(distinctActiveUsers.observe).toHaveBeenLastCalledWith(5)
|
||||
expect(rollingActiveUsers.observe).toHaveBeenNthCalledWith(1, 9, { window: '24h' })
|
||||
expect(rollingActiveUsers.observe).toHaveBeenNthCalledWith(2, 18, { window: '7d' })
|
||||
expect(rollingActiveUsers.observe).toHaveBeenNthCalledWith(3, 30, { window: '30d' })
|
||||
})
|
||||
|
||||
it('stops observing a snapshot after its bounded freshness interval', async () => {
|
||||
@@ -92,13 +79,11 @@ describe('registerUserMetricsSnapshotGauges', () => {
|
||||
const totalUsers = createGaugeProbe()
|
||||
const activeSessions = createGaugeProbe()
|
||||
const distinctActiveUsers = createGaugeProbe()
|
||||
const rollingActiveUsers = createGaugeProbe()
|
||||
|
||||
const recorder = registerUserMetricsSnapshotGauges({
|
||||
totalUsers: totalUsers.gauge,
|
||||
activeSessions: activeSessions.gauge,
|
||||
distinctActiveUsers: distinctActiveUsers.gauge,
|
||||
rollingActiveUsers: rollingActiveUsers.gauge,
|
||||
})
|
||||
|
||||
recorder.record(
|
||||
@@ -106,7 +91,6 @@ describe('registerUserMetricsSnapshotGauges', () => {
|
||||
totalUsers: 42,
|
||||
activeSessions: 7,
|
||||
distinctActiveUsers: 5,
|
||||
rollingActiveUsers: { '24h': 9, '7d': 18, '30d': 30 },
|
||||
},
|
||||
refreshedAt,
|
||||
)
|
||||
@@ -116,12 +100,10 @@ describe('registerUserMetricsSnapshotGauges', () => {
|
||||
totalUsers.collect(),
|
||||
activeSessions.collect(),
|
||||
distinctActiveUsers.collect(),
|
||||
rollingActiveUsers.collect(),
|
||||
])
|
||||
|
||||
expect(totalUsers.observe).not.toHaveBeenCalled()
|
||||
expect(activeSessions.observe).not.toHaveBeenCalled()
|
||||
expect(distinctActiveUsers.observe).not.toHaveBeenCalled()
|
||||
expect(rollingActiveUsers.observe).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import type { ObservableGauge } from '@opentelemetry/api'
|
||||
|
||||
const ROLLING_WINDOWS = ['24h', '7d', '30d'] as const
|
||||
|
||||
// The admin endpoint caches DB aggregates for 60 seconds. Two minutes keeps a
|
||||
// continuously refreshed dashboard stable across cache rollover and several
|
||||
// 15-second OTel collections, while still bounding stale replica dominance.
|
||||
export const USER_METRICS_SNAPSHOT_MAX_AGE_MS = 2 * 60_000
|
||||
|
||||
type RollingWindow = (typeof ROLLING_WINDOWS)[number]
|
||||
|
||||
export interface UserMetricsSnapshot {
|
||||
totalUsers: number
|
||||
activeSessions: number
|
||||
distinctActiveUsers: number
|
||||
rollingActiveUsers: Record<RollingWindow, number>
|
||||
}
|
||||
|
||||
export interface UserMetricsSnapshotRecorder {
|
||||
@@ -26,7 +21,6 @@ export interface UserMetricsSnapshotGauges {
|
||||
totalUsers: ObservableGaugeRegistration
|
||||
activeSessions: ObservableGaugeRegistration
|
||||
distinctActiveUsers: ObservableGaugeRegistration
|
||||
rollingActiveUsers: ObservableGaugeRegistration
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,15 +64,6 @@ export function registerUserMetricsSnapshotGauges(
|
||||
result.observe(snapshot.distinctActiveUsers)
|
||||
})
|
||||
|
||||
gauges.rollingActiveUsers.addCallback((result) => {
|
||||
const snapshot = readFreshSnapshot()
|
||||
if (!snapshot)
|
||||
return
|
||||
|
||||
for (const window of ROLLING_WINDOWS)
|
||||
result.observe(snapshot.rollingActiveUsers[window], { window })
|
||||
})
|
||||
|
||||
return {
|
||||
record(snapshot, refreshedAt) {
|
||||
latest = { snapshot, refreshedAt }
|
||||
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
METRIC_STRIPE_EVENTS,
|
||||
METRIC_STRIPE_PAYMENT_FAILED,
|
||||
METRIC_STRIPE_SUBSCRIPTION_EVENT,
|
||||
METRIC_USER_ACTIVE_ROLLING,
|
||||
METRIC_USER_ACTIVE_SESSIONS,
|
||||
METRIC_USER_DISTINCT_ACTIVE,
|
||||
METRIC_USER_LOGIN,
|
||||
@@ -125,26 +124,6 @@ export interface AuthMetrics {
|
||||
* `avg()`/`max()`, not `sum()`.
|
||||
*/
|
||||
distinctActiveUsers: ObservableGauge
|
||||
/**
|
||||
* Gauge for the latest requested rolling-window active users (DAU / WAU /
|
||||
* MAU).
|
||||
*
|
||||
* Use when:
|
||||
* - Reporting "how many users were active in the last 24h / 7d / 30d" —
|
||||
* the standard product-engagement funnel, distinct from
|
||||
* {@link AuthMetrics.distinctActiveUsers} which only counts users with a
|
||||
* currently-live session.
|
||||
*
|
||||
* Expects:
|
||||
* - Refreshed by `/api/admin/metrics` using `COUNT(*) FILTER` over the
|
||||
* `user` table. `last_seen_at` is touched on sign-in and on every
|
||||
* OIDC access-token refresh (~hourly), so it is a per-user last-activity
|
||||
* timestamp (see the `user.lastSeenAt` schema note).
|
||||
* - Observed once per window with a `window` attribute (`24h` / `7d` /
|
||||
* `30d`). Periodic collection reads memory only; dashboards MUST
|
||||
* aggregate with `max()`/`avg()`, not `sum()`.
|
||||
*/
|
||||
rollingActiveUsers: ObservableGauge
|
||||
}
|
||||
|
||||
export interface EngagementMetrics {
|
||||
@@ -413,9 +392,6 @@ export function initOtel(env: Env): OtelInstance | null {
|
||||
distinctActiveUsers: meter.createObservableGauge(METRIC_USER_DISTINCT_ACTIVE, {
|
||||
description: 'Fresh admin-requested distinct active-user snapshot, immune to per-row session inflation and omitted when stale (dashboard must use max(), not sum())',
|
||||
}),
|
||||
rollingActiveUsers: meter.createObservableGauge(METRIC_USER_ACTIVE_ROLLING, {
|
||||
description: 'Fresh admin-requested DAU/WAU/MAU snapshot from user.last_seen_at, omitted when stale and labelled by window=24h|7d|30d (dashboard must use max(), not sum())',
|
||||
}),
|
||||
}
|
||||
|
||||
// Engagement metrics
|
||||
|
||||
@@ -54,14 +54,15 @@ describe('admin metrics', () => {
|
||||
|
||||
const firstResponse = await app.request('/api/admin/metrics')
|
||||
expect(firstResponse.status).toBe(200)
|
||||
expect(await firstResponse.json()).toMatchObject({
|
||||
const firstMetrics = await firstResponse.json()
|
||||
expect(firstMetrics).toMatchObject({
|
||||
totalUsers: 1,
|
||||
verifiedUsers: 1,
|
||||
adminSeats: 1,
|
||||
activeSessions: 0,
|
||||
distinctActiveUsers: 0,
|
||||
rollingActiveUsers: { '24h': 0, '7d': 0, '30d': 0 },
|
||||
})
|
||||
expect(firstMetrics).not.toHaveProperty('rollingActiveUsers')
|
||||
expect(recordUserMetrics).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
totalUsers: 1,
|
||||
activeSessions: 0,
|
||||
|
||||
@@ -93,9 +93,6 @@ function createAdminMetricsReader(db: Database) {
|
||||
inFlight = (async () => {
|
||||
const currentTime = Date.now()
|
||||
const yesterday = new Date(currentTime - 24 * 60 * 60 * 1000)
|
||||
const weekAgo = new Date(currentTime - 7 * 24 * 60 * 60 * 1000)
|
||||
const monthAgo = new Date(currentTime - 30 * 24 * 60 * 60 * 1000)
|
||||
|
||||
const [
|
||||
users,
|
||||
sessions,
|
||||
@@ -107,9 +104,6 @@ function createAdminMetricsReader(db: Database) {
|
||||
totalUsers: count(),
|
||||
verifiedUsers: sql<number>`count(*) filter (where ${userTable.emailVerified} = true)`,
|
||||
adminSeats: sql<number>`count(*) filter (where 'admin' = any(regexp_split_to_array(coalesce(${userTable.role}, ''), '\\s*,\\s*')))`,
|
||||
active24h: sql<number>`count(*) filter (where ${userTable.lastSeenAt} > ${yesterday})`,
|
||||
active7d: sql<number>`count(*) filter (where ${userTable.lastSeenAt} > ${weekAgo})`,
|
||||
active30d: sql<number>`count(*) filter (where ${userTable.lastSeenAt} > ${monthAgo})`,
|
||||
}).from(userTable),
|
||||
db
|
||||
.select({
|
||||
@@ -134,11 +128,6 @@ function createAdminMetricsReader(db: Database) {
|
||||
verifiedUsers: Number(users[0]?.verifiedUsers ?? 0),
|
||||
activeSessions: Number(sessions[0]?.activeSessions ?? 0),
|
||||
distinctActiveUsers: Number(sessions[0]?.distinctActiveUsers ?? 0),
|
||||
rollingActiveUsers: {
|
||||
'24h': Number(users[0]?.active24h ?? 0),
|
||||
'7d': Number(users[0]?.active7d ?? 0),
|
||||
'30d': Number(users[0]?.active30d ?? 0),
|
||||
},
|
||||
currentFlux: Number(currentFlux[0]?.total ?? 0),
|
||||
issuedFlux: Number(issuedFlux[0]?.total ?? 0),
|
||||
llmRequests24h: Number(llmUsage24h[0]?.count ?? 0),
|
||||
|
||||
@@ -54,12 +54,6 @@ export const METRIC_USER_ACTIVE_SESSIONS = 'user.active_sessions'
|
||||
// creates a new row per sign-in / per OIDC token refresh and never GCs)
|
||||
// vs real user growth.
|
||||
export const METRIC_USER_DISTINCT_ACTIVE = 'user.distinct_active'
|
||||
// Rolling-window distinct active users (DAU / WAU / MAU), sourced from
|
||||
// `user.last_seen_at` (touched on sign-in and every OIDC token refresh).
|
||||
// Single gauge, observed once per window with a `window="24h"|"7d"|"30d"`
|
||||
// attribute. Unlike USER_DISTINCT_ACTIVE (live-session count) this measures
|
||||
// activity over a trailing time window, not "currently signed in".
|
||||
export const METRIC_USER_ACTIVE_ROLLING = 'user.active_rolling'
|
||||
|
||||
// Engagement (AIRI custom)
|
||||
export const METRIC_CHAT_MESSAGES = 'chat.messages'
|
||||
|
||||
Reference in New Issue
Block a user