Skip to content

Commit 0d82d42

Browse files
authored
fix: data anomalies in chart (#2012)
1 parent 68ddbfc commit 0d82d42

File tree

2 files changed

+173
-50
lines changed

2 files changed

+173
-50
lines changed

app/utils/download-anomalies.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,21 @@ function isDateAffected(
4646
const weekEndDate = new Date(weekStartDate)
4747
weekEndDate.setUTCDate(weekEndDate.getUTCDate() + 6)
4848
const endWeek = weekEndDate.toISOString().slice(0, 10)
49-
return startWeek <= anomaly.end.date && endWeek >= anomaly.start.date
49+
return startWeek < anomaly.end.date && endWeek > anomaly.start.date
5050
}
5151
case 'monthly': {
52-
const startMonth = anomaly.start.date.slice(0, 7) + '-01'
53-
const endMonth = anomaly.end.date.slice(0, 7) + '-01'
54-
return date >= startMonth && date <= endMonth
52+
const monthStart = date
53+
const monthStartDate = new Date(`${date}T00:00:00Z`)
54+
const monthEndDate = new Date(monthStartDate)
55+
monthEndDate.setUTCMonth(monthEndDate.getUTCMonth() + 1)
56+
monthEndDate.setUTCDate(monthEndDate.getUTCDate() - 1)
57+
const monthEnd = monthEndDate.toISOString().slice(0, 10)
58+
return monthStart < anomaly.end.date && monthEnd > anomaly.start.date
5559
}
5660
case 'yearly': {
57-
const startYear = anomaly.start.date.slice(0, 4) + '-01-01'
58-
const endYear = anomaly.end.date.slice(0, 4) + '-01-01'
59-
return date >= startYear && date <= endYear
61+
const yearStart = date
62+
const yearEnd = `${date.slice(0, 4)}-12-31`
63+
return yearStart < anomaly.end.date && yearEnd > anomaly.start.date
6064
}
6165
}
6266
}
Lines changed: 162 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,52 @@
11
import { describe, expect, it } from 'vitest'
22
import { applyBlocklistCorrection } from '../../../../app/utils/download-anomalies'
3-
import type { WeeklyDataPoint } from '../../../../app/types/chart'
3+
import type {
4+
MonthlyDataPoint,
5+
WeeklyDataPoint,
6+
YearlyDataPoint,
7+
} from '../../../../app/types/chart'
8+
9+
/** Helper to build a WeeklyDataPoint from a start date and value. */
10+
function week(weekStart: string, value: number): WeeklyDataPoint {
11+
const start = new Date(`${weekStart}T00:00:00Z`)
12+
const end = new Date(start)
13+
end.setUTCDate(end.getUTCDate() + 6)
14+
const weekEnd = end.toISOString().slice(0, 10)
15+
return {
16+
value,
17+
weekKey: `${weekStart}_${weekEnd}`,
18+
weekStart,
19+
weekEnd,
20+
timestampStart: start.getTime(),
21+
timestampEnd: end.getTime(),
22+
}
23+
}
24+
25+
function month(monthStr: string, value: number): MonthlyDataPoint {
26+
return {
27+
value,
28+
month: monthStr,
29+
timestamp: new Date(`${monthStr}-01T00:00:00Z`).getTime(),
30+
}
31+
}
32+
33+
function year(yearStr: string, value: number): YearlyDataPoint {
34+
return {
35+
value,
36+
year: yearStr,
37+
timestamp: new Date(`${yearStr}-01-01T00:00:00Z`).getTime(),
38+
}
39+
}
440

541
describe('applyBlocklistCorrection', () => {
6-
it('treats overlapping weekly buckets as affected anomalies', () => {
7-
const data: WeeklyDataPoint[] = [
8-
{
9-
value: 100,
10-
weekKey: '2022-11-07_2022-11-13',
11-
weekStart: '2022-11-07',
12-
weekEnd: '2022-11-13',
13-
timestampStart: 0,
14-
timestampEnd: 0,
15-
},
16-
{
17-
value: 999,
18-
weekKey: '2022-11-14_2022-11-20',
19-
weekStart: '2022-11-14',
20-
weekEnd: '2022-11-20',
21-
timestampStart: 0,
22-
timestampEnd: 0,
23-
},
24-
{
25-
value: 999,
26-
weekKey: '2022-11-21_2022-11-27',
27-
weekStart: '2022-11-21',
28-
weekEnd: '2022-11-27',
29-
timestampStart: 0,
30-
timestampEnd: 0,
31-
},
32-
{
33-
value: 999,
34-
weekKey: '2022-11-28_2022-12-04',
35-
weekStart: '2022-11-28',
36-
weekEnd: '2022-12-04',
37-
timestampStart: 0,
38-
timestampEnd: 0,
39-
},
40-
{
41-
value: 200,
42-
weekKey: '2022-12-05_2022-12-11',
43-
weekStart: '2022-12-05',
44-
weekEnd: '2022-12-11',
45-
timestampStart: 0,
46-
timestampEnd: 0,
47-
},
42+
// Anomaly Nov 2022: start=2022-11-15, end=2022-11-30
43+
it('corrects weeks overlapping the anomaly', () => {
44+
const data = [
45+
week('2022-11-07', 100),
46+
week('2022-11-14', 999),
47+
week('2022-11-21', 999),
48+
week('2022-11-28', 999),
49+
week('2022-12-05', 200),
4850
]
4951

5052
expect(
@@ -61,4 +63,121 @@ describe('applyBlocklistCorrection', () => {
6163
data[4],
6264
])
6365
})
66+
67+
// Anomaly Jun 2023: start=2023-06-19, end=2023-06-22
68+
it('does not over-correct a week starting on the anomaly end boundary', () => {
69+
const data = [
70+
week('2023-06-01', 500_000),
71+
week('2023-06-08', 500_000),
72+
week('2023-06-15', 10_000_000), // contains spike
73+
week('2023-06-22', 500_000), // starts on anomaly end boundary — normal!
74+
week('2023-06-29', 500_000),
75+
]
76+
77+
const result = applyBlocklistCorrection({
78+
data,
79+
packageName: 'svelte',
80+
granularity: 'weekly',
81+
}) as WeeklyDataPoint[]
82+
83+
// The spike week must be corrected
84+
expect(result[2]!.hasAnomaly).toBe(true)
85+
expect(result[2]!.value).toBeLessThan(1_000_000)
86+
87+
// The boundary week must NOT be modified
88+
expect(result[3]!.value).toBe(500_000)
89+
expect(result[3]!.hasAnomaly).toBeUndefined()
90+
})
91+
92+
it('does not over-correct a week ending on the anomaly start boundary', () => {
93+
const data = [
94+
week('2023-06-13', 500_000), // ends on anomaly start boundary — normal!
95+
week('2023-06-20', 10_000_000), // contains spike
96+
week('2023-06-27', 500_000),
97+
]
98+
99+
const result = applyBlocklistCorrection({
100+
data,
101+
packageName: 'svelte',
102+
granularity: 'weekly',
103+
}) as WeeklyDataPoint[]
104+
105+
// The boundary week must NOT be modified
106+
expect(result[0]!.value).toBe(500_000)
107+
expect(result[0]!.hasAnomaly).toBeUndefined()
108+
109+
// The spike week must be corrected
110+
expect(result[1]!.hasAnomaly).toBe(true)
111+
expect(result[1]!.value).toBeLessThan(1_000_000)
112+
})
113+
114+
// Vite anomaly: start=2025-08-04, end=2025-09-08 (spans Aug-Sep)
115+
it('does not over-correct a month that only touches the anomaly end boundary', () => {
116+
const data = [
117+
month('2025-07', 30_000_000),
118+
month('2025-08', 100_000_000), // contains spike
119+
month('2025-09', 100_000_000), // contains spike (Sep 1-7)
120+
month('2025-10', 30_000_000), // after anomaly end — normal!
121+
]
122+
123+
const result = applyBlocklistCorrection({
124+
data,
125+
packageName: 'vite',
126+
granularity: 'monthly',
127+
}) as MonthlyDataPoint[]
128+
129+
expect(result[1]!.hasAnomaly).toBe(true)
130+
expect(result[2]!.hasAnomaly).toBe(true)
131+
132+
// October must NOT be modified
133+
expect(result[3]!.value).toBe(30_000_000)
134+
expect(result[3]!.hasAnomaly).toBeUndefined()
135+
})
136+
137+
it('does not over-correct a month that only touches the anomaly start boundary', () => {
138+
const data = [
139+
month('2025-07', 30_000_000), // before anomaly start — normal!
140+
month('2025-08', 100_000_000), // contains spike
141+
month('2025-09', 100_000_000), // contains spike
142+
month('2025-10', 30_000_000),
143+
]
144+
145+
const result = applyBlocklistCorrection({
146+
data,
147+
packageName: 'vite',
148+
granularity: 'monthly',
149+
}) as MonthlyDataPoint[]
150+
151+
// July must NOT be modified
152+
expect(result[0]!.value).toBe(30_000_000)
153+
expect(result[0]!.hasAnomaly).toBeUndefined()
154+
155+
expect(result[1]!.hasAnomaly).toBe(true)
156+
expect(result[2]!.hasAnomaly).toBe(true)
157+
})
158+
159+
it('does not over-correct a year that only touches the anomaly boundary', () => {
160+
const data = [
161+
year('2024', 500_000_000),
162+
year('2025', 2_000_000_000), // contains spike
163+
year('2026', 500_000_000),
164+
]
165+
166+
const result = applyBlocklistCorrection({
167+
data,
168+
packageName: 'vite',
169+
granularity: 'yearly',
170+
}) as YearlyDataPoint[]
171+
172+
// 2024 must NOT be modified
173+
expect(result[0]!.value).toBe(500_000_000)
174+
expect(result[0]!.hasAnomaly).toBeUndefined()
175+
176+
// 2025 must be corrected
177+
expect(result[1]!.hasAnomaly).toBe(true)
178+
179+
// 2026 must NOT be modified
180+
expect(result[2]!.value).toBe(500_000_000)
181+
expect(result[2]!.hasAnomaly).toBeUndefined()
182+
})
64183
})

0 commit comments

Comments
 (0)