Skip to content

Commit 5db739f

Browse files
committed
refactor(metrics): extract bucketTimestampMs to utils and add unit tests
1 parent 08b5729 commit 5db739f

File tree

3 files changed

+86
-17
lines changed

3 files changed

+86
-17
lines changed

workers/grouper/src/index.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import TimeMs from '../../../lib/utils/time';
2323
import DataFilter from './data-filter';
2424
import RedisHelper from './redisHelper';
2525
import { computeDelta } from './utils/repetitionDiff';
26+
import { bucketTimestampMs } from './utils/bucketTimestamp';
2627
import { rightTrim } from '../../../lib/utils/string';
2728
import { hasValue } from '../../../lib/utils/hasValue';
2829

@@ -325,20 +326,6 @@ export default class GrouperWorker extends Worker {
325326
return `ts:project-${metricType}:${projectId}:${granularity}`;
326327
}
327328

328-
/**
329-
* Returns the current time truncated to the start of the given granularity
330-
* bucket in milliseconds. All events within the same bucket share one
331-
* timestamp so ON_DUPLICATE SUM accumulates them into a single sample.
332-
*/
333-
private bucketTimestampMs(granularity: 'minutely' | 'hourly' | 'daily'): number {
334-
const now = Date.now();
335-
switch (granularity) {
336-
case 'hourly': return now - (now % TimeMs.HOUR);
337-
case 'daily': return now - (now % TimeMs.DAY);
338-
default: return now - (now % TimeMs.MINUTE); // minutely
339-
}
340-
}
341-
342329
/**
343330
* Record project metrics to Redis TimeSeries.
344331
*
@@ -357,9 +344,9 @@ export default class GrouperWorker extends Worker {
357344
};
358345

359346
const series = [
360-
{ key: minutelyKey, label: 'minutely', retentionMs: TimeMs.DAY, timestampMs: this.bucketTimestampMs('minutely') },
361-
{ key: hourlyKey, label: 'hourly', retentionMs: TimeMs.WEEK, timestampMs: this.bucketTimestampMs('hourly') },
362-
{ key: dailyKey, label: 'daily', retentionMs: 90 * TimeMs.DAY, timestampMs: this.bucketTimestampMs('daily') },
347+
{ key: minutelyKey, label: 'minutely', retentionMs: TimeMs.DAY, timestampMs: bucketTimestampMs('minutely') },
348+
{ key: hourlyKey, label: 'hourly', retentionMs: TimeMs.WEEK, timestampMs: bucketTimestampMs('hourly') },
349+
{ key: dailyKey, label: 'daily', retentionMs: 90 * TimeMs.DAY, timestampMs: bucketTimestampMs('daily') },
363350
];
364351

365352
for (const { key, label, retentionMs, timestampMs } of series) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import TimeMs from '../../../../lib/utils/time';
2+
3+
/**
4+
* Returns the current time truncated to the start of the given granularity
5+
* bucket in milliseconds (UTC). All events within the same bucket share one
6+
* timestamp so ON_DUPLICATE SUM accumulates them into a single sample.
7+
*
8+
* @param granularity - time granularity level
9+
* @param now - current timestamp in ms, defaults to Date.now()
10+
*/
11+
export function bucketTimestampMs(granularity: 'minutely' | 'hourly' | 'daily', now = Date.now()): number {
12+
switch (granularity) {
13+
case 'hourly': return now - (now % TimeMs.HOUR);
14+
case 'daily': return now - (now % TimeMs.DAY);
15+
default: return now - (now % TimeMs.MINUTE); // minutely
16+
}
17+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import '../../../env-test';
2+
import { bucketTimestampMs } from '../src/utils/bucketTimestamp';
3+
4+
describe('bucketTimestampMs', () => {
5+
/**
6+
* 2026-04-14T15:37:42.500Z
7+
* minute start: 2026-04-14T15:37:00.000Z
8+
* hour start: 2026-04-14T15:00:00.000Z
9+
* day start: 2026-04-14T00:00:00.000Z
10+
*/
11+
const now = new Date('2026-04-14T15:37:42.500Z').getTime();
12+
13+
it('truncates to the start of the current minute', () => {
14+
const expected = new Date('2026-04-14T15:37:00.000Z').getTime();
15+
16+
expect(bucketTimestampMs('minutely', now)).toBe(expected);
17+
});
18+
19+
it('truncates to the start of the current hour', () => {
20+
const expected = new Date('2026-04-14T15:00:00.000Z').getTime();
21+
22+
expect(bucketTimestampMs('hourly', now)).toBe(expected);
23+
});
24+
25+
it('truncates to the start of the current day (UTC midnight)', () => {
26+
const expected = new Date('2026-04-14T00:00:00.000Z').getTime();
27+
28+
expect(bucketTimestampMs('daily', now)).toBe(expected);
29+
});
30+
31+
it('returns the same value for two calls within the same minute', () => {
32+
const t1 = new Date('2026-04-14T15:37:00.000Z').getTime();
33+
const t2 = new Date('2026-04-14T15:37:59.999Z').getTime();
34+
35+
expect(bucketTimestampMs('minutely', t1)).toBe(bucketTimestampMs('minutely', t2));
36+
});
37+
38+
it('returns different values for two calls in different minutes', () => {
39+
const t1 = new Date('2026-04-14T15:37:59.999Z').getTime();
40+
const t2 = new Date('2026-04-14T15:38:00.000Z').getTime();
41+
42+
expect(bucketTimestampMs('minutely', t1)).not.toBe(bucketTimestampMs('minutely', t2));
43+
});
44+
45+
it('returns the same value for two calls within the same hour', () => {
46+
const t1 = new Date('2026-04-14T15:00:00.000Z').getTime();
47+
const t2 = new Date('2026-04-14T15:59:59.999Z').getTime();
48+
49+
expect(bucketTimestampMs('hourly', t1)).toBe(bucketTimestampMs('hourly', t2));
50+
});
51+
52+
it('returns the same value for two calls within the same day', () => {
53+
const t1 = new Date('2026-04-14T00:00:00.000Z').getTime();
54+
const t2 = new Date('2026-04-14T23:59:59.999Z').getTime();
55+
56+
expect(bucketTimestampMs('daily', t1)).toBe(bucketTimestampMs('daily', t2));
57+
});
58+
59+
it('returns different values for two calls on different days', () => {
60+
const t1 = new Date('2026-04-14T23:59:59.999Z').getTime();
61+
const t2 = new Date('2026-04-15T00:00:00.000Z').getTime();
62+
63+
expect(bucketTimestampMs('daily', t1)).not.toBe(bucketTimestampMs('daily', t2));
64+
});
65+
});

0 commit comments

Comments
 (0)