-
Notifications
You must be signed in to change notification settings - Fork 267
Fix: Cache key includes query parameters to prevent stale data after 422 errors #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
68605d6
Initial plan
Copilot 8456f1e
Fix: Update cache key to include query parameters and clear cache on …
Copilot 23ac471
fix for caching
karpikpl 91b5fd3
error message
karpikpl d686913
Update shared/utils/metrics-util.ts
karpikpl bca89fa
code review comments
karpikpl bfabca6
fix test setup
karpikpl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// @vitest-environment nuxt | ||
import { describe, it, expect } from 'vitest' | ||
import { buildMetricsCacheKey } from '../shared/utils/metrics-util' | ||
|
||
/** | ||
* These tests validate that the new cache key generation logic binds cache entries | ||
* to the caller's Authorization header fingerprint and that different auth headers | ||
* produce distinct keys even when path + query are identical. | ||
*/ | ||
|
||
describe('Metrics Auth-Bound Cache Key', () => { | ||
const path = '/api/metrics' | ||
const query = { since: '2024-01-01', until: '2024-01-31', scope: 'organization', githubOrg: 'test-org' } | ||
|
||
it('produces different keys for different auth headers', () => { | ||
const keyA1 = buildMetricsCacheKey(path, query, 'token userA-1') | ||
const keyA2 = buildMetricsCacheKey(path, query, 'token userA-2') | ||
const keyB = buildMetricsCacheKey(path, query, 'token userB-1') | ||
|
||
expect(keyA1).not.toBe(keyA2) | ||
expect(keyA1.split(':')[0]).not.toBe(keyA2.split(':')[0]) | ||
expect(keyA1.split(':')[0]).not.toBe(keyB.split(':')[0]) | ||
}) | ||
|
||
it('is stable for same auth header + query', () => { | ||
const key1 = buildMetricsCacheKey(path, query, 'token stable-user') | ||
const key2 = buildMetricsCacheKey(path, query, 'token stable-user') | ||
expect(key1).toBe(key2) | ||
}) | ||
|
||
it('filters out undefined/empty query params', () => { | ||
const key = buildMetricsCacheKey(path, { since: '2024-01-01', empty: '', undef: undefined }, 'token x') | ||
expect(key).toContain('since=2024-01-01') | ||
expect(key).not.toContain('empty=') | ||
expect(key).not.toContain('undef=') | ||
}) | ||
|
||
it('joins array query params deterministically', () => { | ||
const key = buildMetricsCacheKey(path, { since: ['2024-01-01'], tag: ['a','b'] }, 'token y') | ||
expect(key).toContain('since=2024-01-01') | ||
expect(key).toMatch(/tag=a%2Cb|tag=b%2Ca/) // order preserved from input array join | ||
}) | ||
|
||
it('merges existing path query params with provided query object without duplication', () => { | ||
const pathWithQuery = '/api/metrics?since=2024-01-01&scope=organization' | ||
const key = buildMetricsCacheKey(pathWithQuery, { githubOrg: 'test-org', since: '2024-01-01' }, 'token z') | ||
// should not duplicate since param and should include githubOrg | ||
const pieces = key.split(':') | ||
expect(pieces.length).toBeGreaterThan(1) | ||
const qs = pieces.slice(1).join(':').split('?')[1] | ||
expect(qs?.match(/since=/g)?.length).toBe(1) | ||
expect(qs).toContain('githubOrg=test-org') | ||
}) | ||
|
||
it('sorts final query params for stable ordering', () => { | ||
const key1 = buildMetricsCacheKey('/api/metrics?b=2&a=1', { c: '3' }, 'token sort') | ||
const key2 = buildMetricsCacheKey('/api/metrics?a=1&b=2', { c: '3' }, 'token sort') | ||
expect(key1).toBe(key2) | ||
const qs = key1.split('?')[1] | ||
expect(qs).toBeDefined() | ||
expect(qs!.startsWith('a=1&b=2&c=3')).toBe(true) | ||
}) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// @vitest-environment nuxt | ||
import { describe, it, expect, beforeEach } from 'vitest' | ||
|
||
describe('Metrics Cache Key Generation', () => { | ||
it('should create unique cache keys for different query parameters', () => { | ||
// Test the cache key logic that was implemented | ||
const createCacheKey = (path: string, query: Record<string, string>) => { | ||
const queryString = new URLSearchParams(query).toString() | ||
return queryString ? `${path}?${queryString}` : path | ||
} | ||
|
||
const path = '/api/metrics' | ||
|
||
// Different date ranges should create different cache keys | ||
const query1 = { since: '2024-01-01', until: '2024-01-31', scope: 'organization', githubOrg: 'test-org' } | ||
const query2 = { since: '2024-02-01', until: '2024-02-28', scope: 'organization', githubOrg: 'test-org' } | ||
const query3 = { since: '2024-01-01', until: '2024-01-31', scope: 'organization', githubOrg: 'test-org' } | ||
|
||
const key1 = createCacheKey(path, query1) | ||
const key2 = createCacheKey(path, query2) | ||
const key3 = createCacheKey(path, query3) | ||
|
||
// Different date ranges should have different keys | ||
expect(key1).not.toBe(key2) | ||
|
||
// Same parameters should have same key | ||
expect(key1).toBe(key3) | ||
|
||
// Keys should include query parameters | ||
expect(key1).toContain('since=2024-01-01') | ||
expect(key1).toContain('until=2024-01-31') | ||
expect(key2).toContain('since=2024-02-01') | ||
expect(key2).toContain('until=2024-02-28') | ||
}) | ||
|
||
it('should handle empty query parameters', () => { | ||
const createCacheKey = (path: string, query: Record<string, string>) => { | ||
const queryString = new URLSearchParams(query).toString() | ||
return queryString ? `${path}?${queryString}` : path | ||
} | ||
|
||
const path = '/api/metrics' | ||
const emptyQuery = {} | ||
|
||
const key = createCacheKey(path, emptyQuery) | ||
expect(key).toBe(path) | ||
}) | ||
|
||
it('should handle undefined query values', () => { | ||
const createCacheKey = (path: string, query: Record<string, any>) => { | ||
// Filter out undefined values before creating query string | ||
const filteredQuery = Object.fromEntries( | ||
Object.entries(query).filter(([_, value]) => value !== undefined) | ||
) | ||
const queryString = new URLSearchParams(filteredQuery).toString() | ||
return queryString ? `${path}?${queryString}` : path | ||
} | ||
|
||
const path = '/api/metrics' | ||
const queryWithUndefined = { since: '2024-01-01', until: undefined, scope: 'organization' } | ||
|
||
const key = createCacheKey(path, queryWithUndefined) | ||
expect(key).toContain('since=2024-01-01') | ||
expect(key).toContain('scope=organization') | ||
expect(key).not.toContain('until=') | ||
}) | ||
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Global test setup | ||
// Force mock mode so server-side handlers use local mock data instead of hitting GitHub APIs. | ||
process.env.NUXT_PUBLIC_IS_DATA_MOCKED = 'true'; | ||
|
||
// Block accidental real GitHub API calls if mock mode logic fails. | ||
const originalFetch: typeof globalThis.fetch | undefined = globalThis.fetch; | ||
if (originalFetch) { | ||
globalThis.fetch = (async (...args: Parameters<typeof fetch>): Promise<Response> => { | ||
const url = String(args[0]); | ||
if (url.startsWith('https://api.github.com')) { | ||
|
||
throw new Error(`Blocked external GitHub API call during tests: ${url}`); | ||
} | ||
return originalFetch(...args); | ||
}) as typeof fetch; | ||
} | ||
|
||
// Stub $fetch (ofetch) similarly if present at runtime. | ||
if (typeof globalThis.$fetch === 'function') { | ||
const original$fetch = globalThis.$fetch; | ||
const wrapped: typeof globalThis.$fetch = ((url: unknown, opts: unknown) => { | ||
const str = String(url); | ||
if (str.startsWith('https://api.github.com')) { | ||
|
||
return Promise.reject(new Error(`Blocked external GitHub API call during tests via $fetch: ${str}`)); | ||
} | ||
return original$fetch(url as never, opts as never); | ||
}) as typeof globalThis.$fetch; | ||
// Preserve special properties if present | ||
(wrapped as unknown as { raw?: unknown }).raw = (original$fetch as unknown as { raw?: () => unknown }).raw?.bind(original$fetch); | ||
(wrapped as unknown as { create?: unknown }).create = (original$fetch as unknown as { create?: () => unknown }).create?.bind(original$fetch); | ||
globalThis.$fetch = wrapped; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.