Skip to content

Commit 636c288

Browse files
committed
Merge branch 'feat/redis-rate-limit-series' into stage
2 parents 01e7e5a + 37e1711 commit 636c288

7 files changed

Lines changed: 97 additions & 22 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.23",
3+
"version": "1.2.24",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

src/models/eventsFactory.js

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -443,25 +443,50 @@ class EventsFactory extends Factory {
443443
const days = Math.ceil((end - start) / (24 * 60 * 60 * 1000));
444444

445445
try {
446-
const redisData = await this.chartDataService.getProjectChartData(
447-
projectId,
448-
startDate,
449-
endDate,
450-
groupBy,
451-
timezoneOffset
452-
);
453-
454-
if (redisData && redisData.length > 0) {
455-
return redisData;
456-
}
457-
458-
// Fallback to Mongo (empty groupHash for project-level data)
459-
return this.findChartData(days, timezoneOffset, '');
446+
const [acceptedSeries, rateLimitedSeries] = await Promise.all([
447+
this.chartDataService.getProjectChartData(
448+
projectId,
449+
startDate,
450+
endDate,
451+
groupBy,
452+
timezoneOffset,
453+
'events-accepted'
454+
),
455+
this.chartDataService.getProjectChartData(
456+
projectId,
457+
startDate,
458+
endDate,
459+
groupBy,
460+
timezoneOffset,
461+
'events-rate-limited'
462+
),
463+
]);
464+
465+
return [
466+
{
467+
label: 'accepted',
468+
data: acceptedSeries,
469+
},
470+
{
471+
label: 'rate-limited',
472+
data: rateLimitedSeries,
473+
},
474+
];
460475
} catch (err) {
461476
console.error('[EventsFactory] getProjectChartData error:', err);
462477

463-
// Fallback to Mongo on error (empty groupHash for project-level data)
464-
return this.findChartData(days, timezoneOffset, '');
478+
const fallbackAccepted = await this.findChartData(days, timezoneOffset, '');
479+
480+
return [
481+
{
482+
label: 'accepted',
483+
data: fallbackAccepted,
484+
},
485+
{
486+
label: 'rate-limited',
487+
data: this._composeZeroSeries(fallbackAccepted),
488+
},
489+
];
465490
}
466491
}
467492

@@ -474,7 +499,14 @@ class EventsFactory extends Factory {
474499
* @returns {Promise<Array>}
475500
*/
476501
async getEventDailyChart(groupHash, days, timezoneOffset = 0) {
477-
return this.findChartData(days, timezoneOffset, groupHash);
502+
const data = await this.findChartData(days, timezoneOffset, groupHash);
503+
504+
return [
505+
{
506+
label: 'accepted',
507+
data,
508+
},
509+
];
478510
}
479511

480512
/**
@@ -568,6 +600,23 @@ class EventsFactory extends Factory {
568600
return result;
569601
}
570602

603+
/**
604+
* Compose zero-filled chart series using timestamps from the provided template
605+
*
606+
* @param {Array<{timestamp: number, count: number}>} template - reference series for timestamps
607+
* @returns {Array<{timestamp: number, count: number}>}
608+
*/
609+
_composeZeroSeries(template = []) {
610+
if (!Array.isArray(template) || template.length === 0) {
611+
return [];
612+
}
613+
614+
return template.map((point) => ({
615+
timestamp: point.timestamp,
616+
count: 0,
617+
}));
618+
}
619+
571620
/**
572621
* Returns number of documents that occurred after the last visit time
573622
*

src/redisHelper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export default class RedisHelper {
3636
constructor() {
3737
if (!process.env.REDIS_URL) {
3838
console.warn('[Redis] REDIS_URL not set, Redis features will be disabled');
39+
3940
return;
4041
}
4142

@@ -49,7 +50,9 @@ export default class RedisHelper {
4950
* Max wait time: 30 seconds
5051
*/
5152
const delay = Math.min(retries * 1000, 30000);
53+
5254
console.log(`[Redis] Reconnecting... attempt ${retries}, waiting ${delay}ms`);
55+
5356
return delay;
5457
},
5558
},
@@ -93,6 +96,7 @@ export default class RedisHelper {
9396
if (!RedisHelper.instance) {
9497
RedisHelper.instance = new RedisHelper();
9598
}
99+
96100
return RedisHelper.instance;
97101
}
98102

@@ -102,6 +106,7 @@ export default class RedisHelper {
102106
public async initialize(): Promise<void> {
103107
if (!this.redisClient) {
104108
console.warn('[Redis] Client not initialized, skipping connection');
109+
105110
return;
106111
}
107112

src/services/chartDataService.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default class ChartDataService {
1919
* @param endDate - end date as ISO string (e.g., '2025-01-31T23:59:59Z')
2020
* @param groupBy - grouping interval in minutes (1=minute, 60=hour, 1440=day)
2121
* @param timezoneOffset - user's local timezone offset in minutes (default: 0)
22+
* @param metricType - Redis metric type suffix (e.g., 'events-accepted', 'events-rate-limited')
2223
* @returns Array of data points with timestamp and count
2324
* @throws Error if Redis is not connected (caller should fallback to MongoDB)
2425
*/
@@ -27,7 +28,8 @@ export default class ChartDataService {
2728
startDate: string,
2829
endDate: string,
2930
groupBy: number,
30-
timezoneOffset = 0
31+
timezoneOffset = 0,
32+
metricType = 'events-accepted'
3133
): Promise<{ timestamp: number; count: number }[]> {
3234
// Check if Redis is connected
3335
if (!this.redisHelper.isConnected()) {
@@ -37,7 +39,7 @@ export default class ChartDataService {
3739

3840
// Determine granularity and compose key
3941
const granularity = getTimeSeriesSuffix(groupBy);
40-
const key = composeProjectMetricsKey(granularity, projectId);
42+
const key = composeProjectMetricsKey(granularity, projectId, metricType);
4143

4244
// Parse ISO date strings to milliseconds
4345
const start = new Date(startDate).getTime();
@@ -46,6 +48,7 @@ export default class ChartDataService {
4648

4749
// Fetch data from Redis
4850
let result: TsRangeResult[] = [];
51+
4952
try {
5053
result = await this.redisHelper.tsRange(
5154
key,
@@ -65,8 +68,10 @@ export default class ChartDataService {
6568

6669
// Transform data from Redis
6770
const dataPoints: { [ts: number]: number } = {};
71+
6872
for (const [tsStr, valStr] of result) {
6973
const tsMs = Number(tsStr);
74+
7075
dataPoints[tsMs] = Number(valStr) || 0;
7176
}
7277

@@ -79,6 +84,7 @@ export default class ChartDataService {
7984

8085
while (current <= end) {
8186
const count = dataPoints[current] || 0;
87+
8288
filled.push({
8389
timestamp: Math.floor((current + timezoneOffset * 60 * 1000) / 1000),
8490
count,

src/typeDefs/chart.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,19 @@ export default gql`
1212
"""
1313
count: Int
1414
}
15+
16+
"""
17+
Chart line definition
18+
"""
19+
type ChartLine {
20+
"""
21+
Series label (e.g., events-accepted)
22+
"""
23+
label: String!
24+
25+
"""
26+
Data points for the series
27+
"""
28+
data: [ChartDataItem!]!
29+
}
1530
`;

src/typeDefs/event.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ type Event {
295295
User's local timezone offset in minutes
296296
"""
297297
timezoneOffset: Int! = 0
298-
): [ChartDataItem!]!
298+
): [ChartLine!]!
299299
}
300300
301301
"""

src/typeDefs/project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ type Project {
372372
User's local timezone offset in minutes
373373
"""
374374
timezoneOffset: Int! = 0
375-
): [ChartDataItem]
375+
): [ChartLine!]!
376376
"""
377377
Returns number of unread events
378378
"""

0 commit comments

Comments
 (0)