Files
supabase/apps/studio/components/interfaces/ProjectHome/ChartDataTransform.utils.test.ts
Pamela Chia 01c178e159 chore(studio): graduate homeNew experiment (#43437)
## Summary

The `homeNew` PostHog experiment has concluded. This PR graduates it by
making the new homepage (`ProjectHome`, formerly `HomeV2`) the permanent
default for all users, and removes all dead code from the old
experiment.

## Changes

- Remove `homeNew` PostHog feature flag checks and `home_new` experiment
exposure tracking from 3 files
- Rename `HomeNew/` → `ProjectHome/` directory and `HomeV2` →
`ProjectHome` export
- Delete old `Home/Home.tsx` component (shared components like
`ProjectList/` are kept — still used by org pages)
- Delete `pages/project/[ref]/building.tsx` and add a server-side
redirect from `/project/:ref/building` → `/project/:ref` to prevent 404s
during rollout (old cached JS bundles may still route to `/building`)
- Simplify `ContentWrapper` building-state logic in `ProjectLayout` —
always redirect building projects to home, always suppress building
interstitial on home page
- Always route to `/project/{ref}` after project creation (remove
`/building` path)
- Update all Observability imports from `HomeNew` → `ProjectHome`

## Self-hosted behavior change

Self-hosted Studio previously showed the old `Home` component (client
libraries + example projects) since PostHog flags don't load. This PR
changes self-hosted to show `ProjectHome` (TopSection with service
status + instance diagram, advisor, custom reports). All sections query
backend APIs that exist on self-hosted. E2E tests pass against the
self-hosted build.

## Testing

- [x] `pnpm turbo run build --filter=studio` passes
- [x] No remaining references to `homeNew`, `home_new`, or `HomeNew` in
codebase
- [x] No broken imports to deleted files
- [x] Self-hosted E2E tests pass (145 passed, 1 flaky, 4 skipped)
- [x] `/building` redirect added to both platform and self-hosted config
blocks

**Quick test:**
1. Navigate to any project homepage — should render the ProjectHome
component
2. Create a new project — should redirect to `/project/{ref}` (not
`/building`)
3. Visit a project in `COMING_UP` state on a non-home route — should
redirect to home
4. Visit `/project/{ref}/building` directly — should 302 redirect to
`/project/{ref}`

## Linear

- fixes GROWTH-671
2026-03-10 17:03:58 +09:00

222 lines
7.0 KiB
TypeScript

import dayjs from 'dayjs'
import { describe, expect, it } from 'vitest'
import { normalizeChartBuckets } from './ChartDataTransform.utils'
import type { LogsBarChartDatum } from './ProjectUsage.metrics'
describe('normalizeChartBuckets', () => {
const now = dayjs('2024-01-28T12:00:00.000Z')
describe('1hr interval', () => {
it('should create exactly 30 buckets with 2-minute intervals', () => {
const result = normalizeChartBuckets([], '1hr', now.toDate())
expect(result).toHaveLength(30)
// Check first bucket
expect(result[0].timestamp).toBe(now.subtract(60, 'minute').toISOString())
// Check last bucket
expect(result[29].timestamp).toBe(now.subtract(2, 'minute').toISOString())
// Check all buckets are 2 minutes apart
for (let i = 0; i < result.length - 1; i++) {
const diff = dayjs(result[i + 1].timestamp).diff(dayjs(result[i].timestamp), 'minute')
expect(diff).toBe(2)
}
})
it('should aggregate data points into correct 2-minute buckets', () => {
const data: LogsBarChartDatum[] = [
{
// First bucket starts at -60 minutes, so -60 to -59 minutes is in bucket 0
timestamp: now.subtract(60, 'minute').toISOString(),
ok_count: 10,
warning_count: 1,
error_count: 2,
},
{
timestamp: now.subtract(59, 'minute').add(30, 'second').toISOString(),
ok_count: 5,
warning_count: 0,
error_count: 1,
},
{
timestamp: now.subtract(30, 'minute').toISOString(),
ok_count: 20,
warning_count: 2,
error_count: 0,
},
]
const result = normalizeChartBuckets(data, '1hr', now.toDate())
// First bucket (60-58 minutes ago) should contain aggregated data
const firstBucket = result[0]
expect(firstBucket.ok_count).toBe(15) // 10 + 5
expect(firstBucket.warning_count).toBe(1) // 1 + 0
expect(firstBucket.error_count).toBe(3) // 2 + 1
// Bucket at 30 minutes ago
const bucket15 = result[15] // 30 minutes / 2 minutes per bucket = bucket 15
expect(bucket15.ok_count).toBe(20)
expect(bucket15.warning_count).toBe(2)
expect(bucket15.error_count).toBe(0)
})
it('should return empty buckets when no data provided', () => {
const result = normalizeChartBuckets([], '1hr', now.toDate())
expect(result).toHaveLength(30)
result.forEach((bucket) => {
expect(bucket.ok_count).toBe(0)
expect(bucket.warning_count).toBe(0)
expect(bucket.error_count).toBe(0)
})
})
})
describe('1day interval', () => {
it('should create exactly 24 buckets with 1-hour intervals', () => {
const result = normalizeChartBuckets([], '1day', now.toDate())
expect(result).toHaveLength(24)
// Check first bucket
expect(result[0].timestamp).toBe(now.subtract(24, 'hour').toISOString())
// Check last bucket
expect(result[23].timestamp).toBe(now.subtract(1, 'hour').toISOString())
// Check all buckets are 1 hour apart
for (let i = 0; i < result.length - 1; i++) {
const diff = dayjs(result[i + 1].timestamp).diff(dayjs(result[i].timestamp), 'hour')
expect(diff).toBe(1)
}
})
it('should aggregate multiple data points into hourly buckets', () => {
const data: LogsBarChartDatum[] = [
{
timestamp: now.subtract(23, 'hour').subtract(30, 'minute').toISOString(),
ok_count: 100,
warning_count: 5,
error_count: 3,
},
{
timestamp: now.subtract(23, 'hour').subtract(15, 'minute').toISOString(),
ok_count: 50,
warning_count: 2,
error_count: 1,
},
]
const result = normalizeChartBuckets(data, '1day', now.toDate())
// First bucket should contain aggregated data
expect(result[0].ok_count).toBe(150)
expect(result[0].warning_count).toBe(7)
expect(result[0].error_count).toBe(4)
})
})
describe('7day interval', () => {
it('should create exactly 28 buckets with 6-hour intervals', () => {
const result = normalizeChartBuckets([], '7day', now.toDate())
expect(result).toHaveLength(28)
// Check first bucket (7 days = 168 hours ago)
expect(result[0].timestamp).toBe(now.subtract(168, 'hour').toISOString())
// Check last bucket
expect(result[27].timestamp).toBe(now.subtract(6, 'hour').toISOString())
// Check all buckets are 6 hours apart
for (let i = 0; i < result.length - 1; i++) {
const diff = dayjs(result[i + 1].timestamp).diff(dayjs(result[i].timestamp), 'hour')
expect(diff).toBe(6)
}
})
it('should aggregate data points into 6-hour buckets', () => {
const data: LogsBarChartDatum[] = [
{
timestamp: now.subtract(167, 'hour').toISOString(),
ok_count: 1000,
warning_count: 10,
error_count: 5,
},
{
timestamp: now.subtract(165, 'hour').toISOString(),
ok_count: 500,
warning_count: 5,
error_count: 2,
},
]
const result = normalizeChartBuckets(data, '7day', now.toDate())
// First bucket should contain aggregated data
expect(result[0].ok_count).toBe(1500)
expect(result[0].warning_count).toBe(15)
expect(result[0].error_count).toBe(7)
})
})
describe('edge cases', () => {
it('should handle data points outside the time range', () => {
const data: LogsBarChartDatum[] = [
{
timestamp: now.subtract(120, 'minute').toISOString(), // Outside 1hr range
ok_count: 100,
warning_count: 10,
error_count: 5,
},
{
timestamp: now.add(10, 'minute').toISOString(), // Future data
ok_count: 50,
warning_count: 5,
error_count: 2,
},
{
timestamp: now.subtract(30, 'minute').toISOString(), // Within range
ok_count: 25,
warning_count: 2,
error_count: 1,
},
]
const result = normalizeChartBuckets(data, '1hr', now.toDate())
// Should only include the data within range
const validBucket = result[15] // 30 minutes ago
expect(validBucket.ok_count).toBe(25)
expect(validBucket.warning_count).toBe(2)
expect(validBucket.error_count).toBe(1)
// Other buckets should be empty
expect(result[0].ok_count).toBe(0)
expect(result[29].ok_count).toBe(0)
})
it('should handle undefined/null values in data', () => {
const data: LogsBarChartDatum[] = [
{
timestamp: now.subtract(30, 'minute').toISOString(),
ok_count: undefined as any,
warning_count: null as any,
error_count: 5,
},
]
const result = normalizeChartBuckets(data, '1hr', now.toDate())
const bucket = result[15]
expect(bucket.ok_count).toBe(0)
expect(bucket.warning_count).toBe(0)
expect(bucket.error_count).toBe(5)
})
})
})