Skip to content

Commit 105ba5d

Browse files
committed
Refactor CDN detection into shared constants with test framework
Moved all CDN detection logic and constants into a shared constants.js file that is used by both background.js and popup.js. This provides: - Declarative CDN_RULES: Each CDN has clear detection conditions (e.g., Akamai ← x-akamai-request-id OR server contains "akamai") - Single source of truth for detection logic, header lists, colors, etc. - Test framework with 27 test cases covering all supported CDNs The rules are self-documenting - looking at CDN_RULES makes it obvious why a response is detected as a particular CDN. Also added `just test` command to run the test suite.
1 parent 9cacabc commit 105ba5d

7 files changed

Lines changed: 703 additions & 211 deletions

File tree

CF Cache Status/CF Cache Status Extension/Resources/background.js

Lines changed: 9 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
*
44
* Intercepts HTTP responses to extract CDN cache headers and updates
55
* the toolbar badge with the cache status (HIT/MISS/etc).
6-
*
7-
* Supports: Cloudflare, CloudFront, Fastly, Akamai, Bunny CDN, Varnish
86
*/
97

8+
// Import shared constants (loaded via manifest.json)
9+
// Uses: TRACKED_HEADERS, STATUS_COLORS, detectCDN, parseCacheStatus
10+
1011
// =============================================================================
1112
// State Management
1213
// =============================================================================
@@ -17,122 +18,12 @@ const tabData = new Map();
1718
/** Tracks tabs with pending navigations to capture only the first main request */
1819
const pendingNavigations = new Set();
1920

20-
// =============================================================================
21-
// Configuration
22-
// =============================================================================
23-
24-
/** HTTP headers to capture from responses */
25-
const TRACKED_HEADERS = [
26-
// Cloudflare
27-
'cf-cache-status', 'cf-ray', 'cf-pop',
28-
// CloudFront
29-
'x-amz-cf-id', 'x-amz-cf-pop',
30-
// Fastly
31-
'x-served-by', 'x-cache-hits', 'x-timer',
32-
// Akamai
33-
'x-akamai-request-id',
34-
// Bunny CDN
35-
'cdn-cache', 'cdn-pullzone', 'cdn-requestid',
36-
// Generic (CloudFront, Fastly, Akamai, Varnish, KeyCDN, etc.)
37-
'x-cache', 'x-cache-status', 'x-varnish', 'x-edge-location', 'via',
38-
// Standard cache headers
39-
'age', 'cache-control', 'expires', 'etag', 'last-modified', 'vary', 'pragma',
40-
// Response metadata
41-
'server', 'content-type'
42-
];
43-
44-
/** Badge colors for each cache status */
45-
const STATUS_COLORS = {
46-
'HIT': { badge: '#22c55e', text: '#fff' }, // Green
47-
'MISS': { badge: '#ef4444', text: '#fff' }, // Red
48-
'EXPIRED': { badge: '#eab308', text: '#000' }, // Yellow
49-
'STALE': { badge: '#eab308', text: '#000' }, // Yellow
50-
'REVALIDATED': { badge: '#eab308', text: '#000' }, // Yellow
51-
'REFRESH': { badge: '#eab308', text: '#000' }, // Yellow
52-
'BYPASS': { badge: '#6b7280', text: '#fff' }, // Gray
53-
'DYNAMIC': { badge: '#6b7280', text: '#fff' }, // Gray
54-
'ERROR': { badge: '#ef4444', text: '#fff' }, // Red
55-
'NONE': { badge: '#6b7280', text: '#fff' } // Gray (no CDN)
56-
};
57-
58-
// =============================================================================
59-
// CDN Detection
60-
// =============================================================================
61-
62-
/**
63-
* Detects the CDN provider based on response headers.
64-
* @param {Object} headers - Lowercase header name to value mapping
65-
* @returns {string|null} CDN identifier or null if not detected
66-
*/
67-
function detectCDN(headers) {
68-
if (headers['cf-cache-status'] || headers['cf-ray']) return 'cloudflare';
69-
if (headers['x-amz-cf-id'] || headers['x-amz-cf-pop']) return 'cloudfront';
70-
if (headers['x-served-by'] || headers['x-timer']) return 'fastly';
71-
if (headers['x-akamai-request-id']) return 'akamai';
72-
if (headers['cdn-cache'] || headers['cdn-pullzone']) return 'bunny';
73-
if (headers['x-varnish']) return 'varnish';
74-
75-
// Check server header for CDN hints
76-
const server = (headers['server'] || '').toLowerCase();
77-
if (server.includes('akamai')) return 'akamai';
78-
79-
// Check x-cache with via header for additional hints
80-
if (headers['x-cache']) {
81-
const via = (headers['via'] || '').toLowerCase();
82-
if (via.includes('cloudfront')) return 'cloudfront';
83-
if (via.includes('varnish')) return 'varnish';
84-
if (via.includes('akamai')) return 'akamai';
85-
return 'cdn'; // Generic CDN
86-
}
87-
88-
return null;
89-
}
90-
91-
/**
92-
* Parses cache status from headers based on the detected CDN.
93-
* @param {Object} headers - Lowercase header name to value mapping
94-
* @param {string} cdn - CDN identifier from detectCDN()
95-
* @returns {string|null} Normalized cache status (HIT, MISS, etc.) or null
96-
*/
97-
function parseCacheStatus(headers, cdn) {
98-
// Cloudflare uses its own header
99-
if (cdn === 'cloudflare') {
100-
return headers['cf-cache-status']?.toUpperCase() || null;
101-
}
102-
103-
// Bunny CDN uses cdn-cache header
104-
if (cdn === 'bunny') {
105-
const status = headers['cdn-cache'];
106-
if (status) {
107-
const lower = status.toLowerCase();
108-
if (lower.includes('hit')) return 'HIT';
109-
if (lower.includes('miss')) return 'MISS';
110-
}
111-
return null;
112-
}
113-
114-
// Most CDNs use x-cache header (CloudFront, Fastly, Akamai, Varnish, etc.)
115-
const xCache = headers['x-cache'] || headers['x-cache-status'];
116-
if (xCache) {
117-
const lower = xCache.toLowerCase();
118-
if (lower.includes('hit')) return 'HIT';
119-
if (lower.includes('miss')) return 'MISS';
120-
if (lower.includes('refresh')) return 'REFRESH';
121-
if (lower.includes('error')) return 'ERROR';
122-
if (lower.includes('pass')) return 'BYPASS';
123-
if (lower.includes('expired')) return 'EXPIRED';
124-
}
125-
126-
return null;
127-
}
128-
12921
// =============================================================================
13022
// Badge Management
13123
// =============================================================================
13224

13325
/**
13426
* Updates the toolbar badge text and color for a tab.
135-
* Sets both globally and per-tab (Safari has inconsistent per-tab support).
13627
* @param {number} tabId - Browser tab ID
13728
* @param {string|null} status - Cache status (HIT, MISS, etc.)
13829
* @param {string|null} cdn - CDN identifier
@@ -141,19 +32,16 @@ function updateBadge(tabId, status, cdn) {
14132
const displayStatus = status || 'NONE';
14233
const colors = STATUS_COLORS[displayStatus] || STATUS_COLORS['NONE'];
14334

144-
// Map status to shortened badge text
14535
const badgeTextMap = {
14636
'HIT': 'HIT', 'MISS': 'MISS', 'EXPIRED': 'EXP', 'STALE': 'STL',
14737
'REVALIDATED': 'REV', 'BYPASS': 'BYP', 'DYNAMIC': 'DYN',
14838
'REFRESH': 'REF', 'ERROR': 'ERR'
14939
};
15040
const badgeText = status ? (badgeTextMap[status] || status.substring(0, 3)) : '';
15141

152-
// Set badge globally (Safari doesn't always support per-tab badges)
15342
browser.action.setBadgeText({ text: badgeText });
15443
browser.action.setBadgeBackgroundColor({ color: colors.badge });
15544

156-
// Also try per-tab for browsers that support it
15745
try {
15846
browser.action.setBadgeText({ text: badgeText, tabId });
15947
browser.action.setBadgeBackgroundColor({ color: colors.badge, tabId });
@@ -163,7 +51,7 @@ function updateBadge(tabId, status, cdn) {
16351
}
16452

16553
/**
166-
* Clears the badge, setting it globally and per-tab.
54+
* Clears the badge for a tab.
16755
* @param {number} tabId - Browser tab ID
16856
*/
16957
function clearBadge(tabId) {
@@ -181,26 +69,25 @@ function clearBadge(tabId) {
18169

18270
// Track navigation start to capture only the first main document request
18371
browser.webNavigation.onBeforeNavigate.addListener((details) => {
184-
if (details.frameId === 0) { // Main frame only
72+
if (details.frameId === 0) {
18573
pendingNavigations.add(details.tabId);
186-
tabData.delete(details.tabId); // Clear stale data
74+
tabData.delete(details.tabId);
18775
}
18876
});
18977

19078
// Update URL after navigation completes (handles redirects)
19179
browser.webNavigation.onCompleted.addListener((details) => {
192-
if (details.frameId === 0) { // Main frame only
80+
if (details.frameId === 0) {
19381
const data = tabData.get(details.tabId);
19482
if (data && data.url !== details.url) {
195-
data.url = details.url; // Update to final URL after redirects
83+
data.url = details.url;
19684
}
19785
}
19886
});
19987

20088
// Capture response headers for main document requests
20189
browser.webRequest.onHeadersReceived.addListener(
20290
(details) => {
203-
// Only process main frame requests we're expecting
20491
if (details.type !== 'main_frame' || details.frameId !== 0) return;
20592
if (!pendingNavigations.has(details.tabId)) return;
20693

@@ -215,11 +102,10 @@ browser.webRequest.onHeadersReceived.addListener(
215102
}
216103
}
217104

218-
// Detect CDN and parse status
105+
// Detect CDN and parse status using shared functions
219106
const cdn = detectCDN(headers);
220107
const status = parseCacheStatus(headers, cdn);
221108

222-
// Store data for popup
223109
tabData.set(details.tabId, {
224110
url: details.url,
225111
headers,
@@ -275,13 +161,11 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
275161
sendResponse(tabData.get(message.tabId) || null);
276162
}
277163

278-
// Handle performance data from content script
279164
if (message.type === 'performanceData' && sender.tab) {
280165
const existing = tabData.get(sender.tab.id);
281166
if (existing) {
282167
existing.performance = message.metrics;
283168
} else {
284-
// Store performance data even if no cache headers yet
285169
tabData.set(sender.tab.id, {
286170
url: sender.tab.url,
287171
headers: {},

0 commit comments

Comments
 (0)