From 58eaa20cc26c6946434365918d8f2e282f7bc7ad Mon Sep 17 00:00:00 2001 From: sfreeman422 Date: Mon, 30 Mar 2026 17:10:26 -0400 Subject: [PATCH 1/3] Sequential DB queries to minimize CPU load + optimized caching for leaderboards --- .../dashboard.persistence.service.spec.ts | 52 +++++++- .../dashboard.persistence.service.ts | 119 +++++++++++------- 2 files changed, 125 insertions(+), 46 deletions(-) diff --git a/packages/backend/src/dashboard/dashboard.persistence.service.spec.ts b/packages/backend/src/dashboard/dashboard.persistence.service.spec.ts index 71398adb..75c67df0 100644 --- a/packages/backend/src/dashboard/dashboard.persistence.service.spec.ts +++ b/packages/backend/src/dashboard/dashboard.persistence.service.spec.ts @@ -242,7 +242,13 @@ describe('DashboardPersistenceService', () => { await service.getDashboardData('U1', 'T1', 'weekly'); expect(redis.setValueWithExpire).toHaveBeenCalledWith( - 'dashboard:T1:U1:weekly', + 'dashboard:user:T1:U1:weekly', + expect.any(String), + 'PX', + expect.any(Number), + ); + expect(redis.setValueWithExpire).toHaveBeenCalledWith( + 'dashboard:leaderboards:T1:weekly', expect.any(String), 'PX', expect.any(Number), @@ -251,17 +257,55 @@ describe('DashboardPersistenceService', () => { expect(stored).toMatchObject({ myStats: { totalMessages: 5 } }); }); - it('uses a cache key scoped to teamId, userId, and period', async () => { + it('uses user-scoped and team-scoped cache keys', async () => { redis.getValue.mockResolvedValue(null); await service.getDashboardData('U7', 'T3', 'yearly'); - expect(redis.getValue).toHaveBeenCalledWith('dashboard:T3:U7:yearly'); + expect(redis.getValue).toHaveBeenCalledWith('dashboard:user:T3:U7:yearly'); + expect(redis.getValue).toHaveBeenCalledWith('dashboard:leaderboards:T3:yearly'); expect(redis.setValueWithExpire).toHaveBeenCalledWith( - 'dashboard:T3:U7:yearly', + 'dashboard:user:T3:U7:yearly', expect.any(String), 'PX', expect.any(Number), ); + expect(redis.setValueWithExpire).toHaveBeenCalledWith( + 'dashboard:leaderboards:T3:yearly', + expect.any(String), + 'PX', + expect.any(Number), + ); + }); + + it('returns merged payload from split cache entries without querying DB', async () => { + redis.getValue.mockImplementation((key: string) => { + if (key === 'dashboard:user:T1:U1:weekly') { + return Promise.resolve( + JSON.stringify({ + myStats: { totalMessages: 10, rep: 20, avgSentiment: 0.8 }, + myActivity: [], + myTopChannels: [], + mySentimentTrend: [], + }), + ); + } + if (key === 'dashboard:leaderboards:T1:weekly') { + return Promise.resolve(JSON.stringify({ leaderboard: [{ name: 'alice', count: 1 }], repLeaderboard: [] })); + } + return Promise.resolve(null); + }); + + const result = await service.getDashboardData('U1', 'T1', 'weekly'); + + expect(query).not.toHaveBeenCalled(); + expect(result).toEqual({ + myStats: { totalMessages: 10, rep: 20, avgSentiment: 0.8 }, + myActivity: [], + myTopChannels: [], + mySentimentTrend: [], + leaderboard: [{ name: 'alice', count: 1 }], + repLeaderboard: [], + }); }); }); diff --git a/packages/backend/src/dashboard/dashboard.persistence.service.ts b/packages/backend/src/dashboard/dashboard.persistence.service.ts index 7faa4810..8c63a3dc 100644 --- a/packages/backend/src/dashboard/dashboard.persistence.service.ts +++ b/packages/backend/src/dashboard/dashboard.persistence.service.ts @@ -20,36 +20,72 @@ export class DashboardPersistenceService { private redisService: RedisPersistenceService = RedisPersistenceService.getInstance(); async getDashboardData(userId: string, teamId: string, period: TimePeriod): Promise { - const cacheKey = `dashboard:${teamId}:${userId}:${period}`; + const userCacheKey = `dashboard:user:${teamId}:${userId}:${period}`; + const leaderboardCacheKey = `dashboard:leaderboards:${teamId}:${period}`; + + let cachedUserData: Pick< + DashboardResponse, + 'myStats' | 'myActivity' | 'myTopChannels' | 'mySentimentTrend' + > | null = null; + let cachedLeaderboards: Pick | null = null; + try { - const cached = await this.redisService.getValue(cacheKey); - if (cached) { - this.logger.info('dashboard cache hit', { userId, teamId, period }); - const data: DashboardResponse = JSON.parse(cached); - return data; + const cachedUser = await this.redisService.getValue(userCacheKey); + if (cachedUser) { + this.logger.info('dashboard user cache hit', { userId, teamId, period }); + const parsedUserData: Pick = + JSON.parse(cachedUser); + cachedUserData = parsedUserData; + } + + const cachedTeamLeaderboards = await this.redisService.getValue(leaderboardCacheKey); + if (cachedTeamLeaderboards) { + this.logger.info('dashboard leaderboard cache hit', { teamId, period }); + const parsedLeaderboardData: Pick = + JSON.parse(cachedTeamLeaderboards); + cachedLeaderboards = parsedLeaderboardData; } } catch (e: unknown) { logError(this.logger, 'Failed to read or parse dashboard cache', e, { userId, teamId, period }); // Treat cache failures as a cache miss and continue to load data from the database. } + if (cachedUserData && cachedLeaderboards) { + return { ...cachedUserData, ...cachedLeaderboards }; + } + const intervalDays = PERIOD_DAYS[period]; const repo = getRepository(Message); - const [myStats, myActivity, myTopChannels, mySentimentTrend, leaderboards] = await Promise.all([ - this.getMyStats(repo, userId, teamId, intervalDays), - this.getMyActivity(repo, userId, teamId, intervalDays), - this.getMyTopChannels(repo, userId, teamId, intervalDays), - this.getMySentimentTrend(repo, userId, teamId, intervalDays), - this.getLeaderboards(repo, teamId, intervalDays), - ]).catch((e: unknown) => { + let userData = cachedUserData; + let leaderboards = cachedLeaderboards; + + try { + if (!userData) { + const myStats = await this.getMyStats(repo, userId, teamId, intervalDays); + const myActivity = await this.getMyActivity(repo, userId, teamId, intervalDays); + const myTopChannels = await this.getMyTopChannels(repo, userId, teamId, intervalDays); + const mySentimentTrend = await this.getMySentimentTrend(repo, userId, teamId, intervalDays); + userData = { myStats, myActivity, myTopChannels, mySentimentTrend }; + } + + if (!leaderboards) { + leaderboards = await this.getLeaderboards(repo, teamId, intervalDays); + } + } catch (e: unknown) { logError(this.logger, 'Failed to load dashboard data', e, { userId, teamId }); throw e; - }); + } - const data: DashboardResponse = { myStats, myActivity, myTopChannels, mySentimentTrend, ...leaderboards }; + const data: DashboardResponse = { ...userData, ...leaderboards }; try { - await this.redisService.setValueWithExpire(cacheKey, JSON.stringify(data), 'PX', CACHE_TTL_MS[period]); + await this.redisService.setValueWithExpire(userCacheKey, JSON.stringify(userData), 'PX', CACHE_TTL_MS[period]); + await this.redisService.setValueWithExpire( + leaderboardCacheKey, + JSON.stringify(leaderboards), + 'PX', + CACHE_TTL_MS[period], + ); } catch (e: unknown) { logError(this.logger, 'Failed to write dashboard data to cache', e, { userId, teamId, period }); } @@ -199,34 +235,33 @@ export class DashboardPersistenceService { if (intervalDays !== null) repParams.push(intervalDays); repParams.push(LEADERBOARD_LIMIT); - const [activityRows, repRows] = await Promise.all([ - this.timeQuery('getLeaderboards:activity', () => - repo.query<{ name: string; value: string }[]>( - `SELECT u.name AS name, CAST(COUNT(*) AS SIGNED) AS value - FROM message m - INNER JOIN slack_user u ON u.id = m.userIdId - WHERE m.teamId = ? AND u.isBot = 0 AND m.channel LIKE 'C%' - ${activityInterval} - GROUP BY u.slackId, u.name - ORDER BY value DESC - LIMIT ?`, - activityParams, - ), + const activityRows = await this.timeQuery('getLeaderboards:activity', () => + repo.query<{ name: string; value: string }[]>( + `SELECT u.name AS name, CAST(COUNT(*) AS SIGNED) AS value + FROM message m + INNER JOIN slack_user u ON u.id = m.userIdId + WHERE m.teamId = ? AND u.isBot = 0 AND m.channel LIKE 'C%' + ${activityInterval} + GROUP BY u.slackId, u.name + ORDER BY value DESC + LIMIT ?`, + activityParams, ), - this.timeQuery('getLeaderboards:rep', () => - repo.query<{ name: string; value: string }[]>( - `SELECT u.name AS name, CAST(SUM(r.value) AS SIGNED) AS value - FROM reaction r - INNER JOIN slack_user u ON u.slackId = r.affectedUser AND u.teamId = r.teamId - WHERE r.teamId = ? - ${repInterval} - GROUP BY r.affectedUser, u.name - ORDER BY value DESC - LIMIT ?`, - repParams, - ), + ); + + const repRows = await this.timeQuery('getLeaderboards:rep', () => + repo.query<{ name: string; value: string }[]>( + `SELECT u.name AS name, CAST(SUM(r.value) AS SIGNED) AS value + FROM reaction r + INNER JOIN slack_user u ON u.slackId = r.affectedUser AND u.teamId = r.teamId + WHERE r.teamId = ? + ${repInterval} + GROUP BY r.affectedUser, u.name + ORDER BY value DESC + LIMIT ?`, + repParams, ), - ]); + ); return { leaderboard: activityRows.map((r) => ({ name: r.name, count: Number(r.value) })), From 8f7afdef0289db58c880b7993e6a0a03c47c48a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:18:04 +0000 Subject: [PATCH 2/3] Add specific null guards before spreading userData and leaderboards Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/7a7d0ef5-dad1-434c-adaa-528e7a52fa0c Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> --- .../backend/src/dashboard/dashboard.persistence.service.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/dashboard/dashboard.persistence.service.ts b/packages/backend/src/dashboard/dashboard.persistence.service.ts index 8c63a3dc..1821344d 100644 --- a/packages/backend/src/dashboard/dashboard.persistence.service.ts +++ b/packages/backend/src/dashboard/dashboard.persistence.service.ts @@ -77,6 +77,12 @@ export class DashboardPersistenceService { throw e; } + if (!userData) { + throw new Error('Failed to load dashboard data: missing user data'); + } + if (!leaderboards) { + throw new Error('Failed to load dashboard data: missing leaderboards'); + } const data: DashboardResponse = { ...userData, ...leaderboards }; try { await this.redisService.setValueWithExpire(userCacheKey, JSON.stringify(userData), 'PX', CACHE_TTL_MS[period]); From 2939b4910b4ba3cdb40ee035844628f6e2a3e966 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:28:50 +0000 Subject: [PATCH 3/3] Remove redundant null guards flagged by @typescript-eslint/no-unnecessary-condition Agent-Logs-Url: https://github.com/dev-chat/mocker/sessions/cdabef6c-a50c-4a38-96d9-c81990063ec0 Co-authored-by: sfreeman422 <16405652+sfreeman422@users.noreply.github.com> --- .../backend/src/dashboard/dashboard.persistence.service.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/backend/src/dashboard/dashboard.persistence.service.ts b/packages/backend/src/dashboard/dashboard.persistence.service.ts index 1821344d..8c63a3dc 100644 --- a/packages/backend/src/dashboard/dashboard.persistence.service.ts +++ b/packages/backend/src/dashboard/dashboard.persistence.service.ts @@ -77,12 +77,6 @@ export class DashboardPersistenceService { throw e; } - if (!userData) { - throw new Error('Failed to load dashboard data: missing user data'); - } - if (!leaderboards) { - throw new Error('Failed to load dashboard data: missing leaderboards'); - } const data: DashboardResponse = { ...userData, ...leaderboards }; try { await this.redisService.setValueWithExpire(userCacheKey, JSON.stringify(userData), 'PX', CACHE_TTL_MS[period]);