Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions products/mcp_analytics/frontend/mcpDashboardOverviewLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,63 @@ describe('mcpDashboardOverviewLogic', () => {
expect(dayjs(usersTile?.filters.dateRange.date_from).isValid()).toBe(true)
})

// The server caches a query by its exact filters, so a window that moves with the clock makes
// every load recompute the two slowest tiles.
it.each(['-7d', '-24h', '-90d'])('keeps the comparison window stable between %s loads', async (dateFrom) => {
jest.useFakeTimers({ now: new Date('2026-11-06T14:23:41.517Z'), advanceTimers: true })
const logic = mcpDashboardOverviewLogic()
logic.mount()
const windowOf = (callIndex: number): string[] =>
reloadCallsSince(callIndex)
.filter((call) => call.query?.includes('AS bucket') || call.query?.includes('current_users'))
.map((call) => call.filters.dateRange.date_from)

await expectLogic(logic, () => logic.actions.setDateFilter(dateFrom, null)).toFinishAllListeners()
const first = windowOf(mockApi.query.mock.calls.length - 9)
jest.setSystemTime(new Date('2026-11-06T14:23:58.004Z'))
const callsBefore = mockApi.query.mock.calls.length
await expectLogic(logic, () => logic.actions.reloadAll()).toFinishAllListeners()
jest.useRealTimers()

expect(first).toHaveLength(2)
expect(windowOf(callsBefore)).toEqual(first)
})

// A previous period longer than the selected one makes flat traffic read as a drop. Sub-day ranges
// keep the bounded skew documented on the period totals loader, so only whole-day ranges are pinned.
it.each(['-7d', '-90d'])('compares %s against a previous period of the same length', async (dateFrom) => {
jest.useFakeTimers({ now: new Date('2026-11-06T00:59:59.900Z'), advanceTimers: true })
const logic = mcpDashboardOverviewLogic()
logic.mount()
const callsBefore = mockApi.query.mock.calls.length
await expectLogic(logic, () => logic.actions.setDateFilter(dateFrom, null)).toFinishAllListeners()
const now = dayjs()
jest.useRealTimers()

const reloads = reloadCallsSince(callsBefore)
const totals = reloads.find((call) => call.query?.includes('current_users'))
const selectedStart = dayjs.utc(totals.query.match(/toDateTime\('([^']+)'/)[1])
const previousMs = selectedStart.diff(dayjs(totals.filters.dateRange.date_from))
expect(Math.abs(previousMs - now.diff(selectedStart))).toBeLessThan(60_000)
})

// Rounding a window shorter than one bucket would reach into the selected period's first bucket and
// count events from before the selected start as current.
it('leaves a window shorter than one bucket where it starts', async () => {
const logic = mcpDashboardOverviewLogic()
logic.mount()
const callsBefore = mockApi.query.mock.calls.length

await expectLogic(logic, () =>
logic.actions.setDateFilter('2026-11-06T14:00:00Z', '2026-11-06T14:30:00Z')
).toFinishAllListeners()

const windows = reloadCallsSince(callsBefore)
.filter((call) => call.query?.includes('AS bucket') || call.query?.includes('current_users'))
.map((call) => dayjs(call.filters.dateRange.date_from).toISOString())
expect(windows).toEqual(['2026-11-06T13:30:00.000Z', '2026-11-06T13:30:00.000Z'])
})

it.each([[false], [true]])('passes filterTestAccounts=%s to every tile', async (enabled) => {
const logic = mcpDashboardOverviewLogic()
logic.mount()
Expand Down
23 changes: 11 additions & 12 deletions products/mcp_analytics/frontend/mcpDashboardOverviewLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { loaders } from 'kea-loaders'
import { actionToUrl, router, urlToAction } from 'kea-router'

import api from 'lib/api'
import { dayjs } from 'lib/dayjs'
import { getDefaultInterval } from 'lib/utils/dateFilters'
import {
type ComparisonWindow,
Expand Down Expand Up @@ -329,6 +330,14 @@ export function buildDailyActivity(rows: ActivityRow[], bucketKeys: string[]): D
}
}

// The window opens on a whole minute so the KPI and Users queries keep one cache key for that minute
// instead of a new one per millisecond. A minute is the most the previous period may grow: rounding to
// a whole bucket would make it up to a bucket longer than the selected period and skew every delta.
function kpiComparisonWindow(dateFilter: DateFilter, timezone: string, interval: IntervalType): ComparisonWindow {
const window = buildComparisonWindow(dateFilter.dateFrom, dateFilter.dateTo, timezone, interval)
return { ...window, dateFrom: dayjs(window.dateFrom).startOf('minute').toISOString() }
}

// Merge the dashboard's active filters with a doubled comparison window's date range.
// Shared by the KPI and Users loaders so both tiles are scoped to the exact same window —
// the tile-parity the reload test asserts. Keep the two loaders reading from here so the
Expand Down Expand Up @@ -803,12 +812,7 @@ export const mcpDashboardOverviewLogic = kea<mcpDashboardOverviewLogicType>([
{
loadKPIs: async (_: void, breakpoint) => {
const { interval } = values
const kpiWindow = buildComparisonWindow(
values.dateFilter.dateFrom,
values.dateFilter.dateTo,
values.timezone,
interval
)
const kpiWindow = kpiComparisonWindow(values.dateFilter, values.timezone, interval)
const response = await queryLatest<HogQLQueryResponse>(
{
kind: NodeKind.HogQLQuery,
Expand All @@ -827,12 +831,7 @@ export const mcpDashboardOverviewLogic = kea<mcpDashboardOverviewLogicType>([
{
loadPeriodTotals: async (_: void, breakpoint): Promise<PeriodTotals> => {
const { interval, timezone } = values
const kpiWindow = buildComparisonWindow(
values.dateFilter.dateFrom,
values.dateFilter.dateTo,
timezone,
interval
)
const kpiWindow = kpiComparisonWindow(values.dateFilter, timezone, interval)
// Split the doubled window at the selected period's start. currentStartBucket is
// interval-aligned (buildComparisonWindow truncates to the bucket start), so comparing the raw
// `timestamp` against toDateTime(bucket, tz) lands on the same instant as the KPI
Expand Down
Loading