Skip to content

Commit 7a22134

Browse files
authored
fix(cache): include both from and to dates in contribution cache key (JhaSourav07#1668)
## Description The `cacheKey` function for contributions only used the year portion of `options.from`, completely ignoring `options.to`. This meant requests with different date ranges but the same start year produced identical cache keys, causing data poisoning across queries. Fixes JhaSourav07#1367: - Cache collision: `?from=2024-01-01&to=2024-06-30` and `?from=2024-01-01&to=2024-12-31` shared key `contributions:user:2024` - Stale data: the first fetch's response was served to all subsequent queries regardless of their date range - Hard to detect: no error surfaced, just silently wrong data for half-year projections ## Pillar - [ ] Pillar 1 - New Theme Design - [ ] Pillar 2 - Geometric SVG Improvement - [ ] Pillar 3 - Timezone Logic Optimization - [x] Other (Bug fix, refactoring, docs) ## Checklist before requesting a review: - [x] I have read the CONTRIBUTING.md file. - [x] I have tested these changes locally. - [x] I have run npm run format and npm run lint locally and resolved all errors. - [x] My commits follow the Conventional Commits format.
2 parents be44fcb + be6e1a4 commit 7a22134

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

lib/github.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,18 @@ describe('cacheKey', () => {
11061106
it('creates key with year', () => {
11071107
expect(cacheKey('contributions', 'DeepSikha', '2025')).toBe('contributions:deepsikha:2025');
11081108
});
1109+
1110+
it('creates key with from and to date range', () => {
1111+
expect(cacheKey('contributions', 'octocat', '2024-01-01', '2024-06-30')).toBe(
1112+
'contributions:octocat:2024-01-01:2024-06-30'
1113+
);
1114+
});
1115+
1116+
it('collision test: different to dates produce different keys', () => {
1117+
const keyA = cacheKey('contributions', 'octocat', '2024-01-01', '2024-06-30');
1118+
const keyB = cacheKey('contributions', 'octocat', '2024-01-01', '2024-12-31');
1119+
expect(keyA).not.toBe(keyB);
1120+
});
11091121
});
11101122
describe('buildInsights', () => {
11111123
it('uses active streak message when current streak > 3', () => {

lib/github.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,25 @@ export function cacheKey(
212212
kind: 'contributions' | 'profile' | 'repos',
213213
username: string,
214214
year?: string
215+
): string;
216+
export function cacheKey(
217+
kind: 'contributions' | 'profile' | 'repos',
218+
username: string,
219+
from?: string,
220+
to?: string
221+
): string;
222+
export function cacheKey(
223+
kind: 'contributions' | 'profile' | 'repos',
224+
username: string,
225+
yearOrFrom?: string,
226+
to?: string
215227
): string {
216-
return year ? `${kind}:${username.toLowerCase()}:${year}` : `${kind}:${username.toLowerCase()}`;
228+
if (yearOrFrom && to) {
229+
return `${kind}:${username.toLowerCase()}:${yearOrFrom.substring(0, 10)}:${to.substring(0, 10)}`;
230+
}
231+
return yearOrFrom
232+
? `${kind}:${username.toLowerCase()}:${yearOrFrom.substring(0, 4)}`
233+
: `${kind}:${username.toLowerCase()}`;
217234
}
218235

219236
export function clearGitHubApiCacheForTests(): void {
@@ -272,7 +289,7 @@ export async function fetchGitHubContributions(
272289
username: string,
273290
options: FetchOptions = {}
274291
): Promise<ContributionCalendar> {
275-
const key = cacheKey('contributions', username, options.from?.substring(0, 4));
292+
const key = cacheKey('contributions', username, options.from, options.to);
276293
if (!options.bypassCache) {
277294
const cached = await contributionsCache.get(key);
278295
if (cached) return cached;

0 commit comments

Comments
 (0)