Created
March 15, 2025 13:59
-
-
Save tluyben/cadbf8fe92782cc712a2a972fdcd4904 to your computer and use it in GitHub Desktop.
This file contains 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
function monitorAllNetworkTraffic(callback = null) { | |
// Store original methods | |
const originalFetch = window.fetch; | |
const originalXHROpen = XMLHttpRequest.prototype.open; | |
const originalXHRSend = XMLHttpRequest.prototype.send; | |
const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; | |
// Collection of all messages in order | |
const messageLog = []; | |
// Map to store XHR request data by XHR instance | |
const xhrMap = new WeakMap(); | |
// Function to add messages to the log | |
function logMessage(message) { | |
// Add timestamp if not present | |
if (!message.timestamp) { | |
message.timestamp = Date.now(); | |
} | |
// Add to log | |
messageLog.push(message); | |
// Print to console | |
console.log(JSON.stringify(message, null, 2)); | |
// Forward to callback if provided | |
if (callback) { | |
callback(message); | |
} | |
} | |
// Cleanup function to remove our interceptors | |
function stopMonitoring() { | |
window.fetch = originalFetch; | |
XMLHttpRequest.prototype.open = originalXHROpen; | |
XMLHttpRequest.prototype.send = originalXHRSend; | |
XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader; | |
return messageLog; | |
} | |
// Intercept XMLHttpRequest | |
XMLHttpRequest.prototype.open = function(method, url) { | |
const requestId = Date.now() + Math.random().toString(36).substr(2, 9); | |
// Store request info | |
xhrMap.set(this, { | |
method, | |
url, | |
requestId, | |
startTime: Date.now(), | |
headers: {} | |
}); | |
// Log the request start | |
logMessage({ | |
type: 'xhr-request-start', | |
requestId, | |
url, | |
method, | |
timestamp: Date.now() | |
}); | |
// Add event listener for response | |
this.addEventListener('load', function() { | |
const requestInfo = xhrMap.get(this); | |
if (!requestInfo) return; | |
let responseData; | |
try { | |
// Try to parse as JSON first | |
responseData = JSON.parse(this.responseText); | |
} catch (e) { | |
// If not JSON, use the raw text | |
responseData = this.responseText; | |
} | |
logMessage({ | |
type: 'xhr-response-complete', | |
requestId: requestInfo.requestId, | |
status: this.status, | |
statusText: this.statusText, | |
headers: this.getAllResponseHeaders().split('\r\n').reduce((acc, line) => { | |
const parts = line.split(': '); | |
if (parts[0] && parts[1]) { | |
acc[parts[0]] = parts[1]; | |
} | |
return acc; | |
}, {}), | |
body: responseData, | |
duration: Date.now() - requestInfo.startTime, | |
timestamp: Date.now() | |
}); | |
}); | |
// Add event listener for errors | |
this.addEventListener('error', function() { | |
const requestInfo = xhrMap.get(this); | |
if (!requestInfo) return; | |
logMessage({ | |
type: 'xhr-error', | |
requestId: requestInfo.requestId, | |
error: 'Network error', | |
timestamp: Date.now() | |
}); | |
}); | |
// Call the original open | |
return originalXHROpen.apply(this, arguments); | |
}; | |
// Intercept setRequestHeader | |
XMLHttpRequest.prototype.setRequestHeader = function(header, value) { | |
const requestInfo = xhrMap.get(this); | |
if (requestInfo) { | |
requestInfo.headers[header] = value; | |
} | |
return originalXHRSetRequestHeader.apply(this, arguments); | |
}; | |
// Intercept send | |
XMLHttpRequest.prototype.send = function(body) { | |
const requestInfo = xhrMap.get(this); | |
if (requestInfo && body) { | |
let requestBody; | |
try { | |
requestBody = typeof body === 'string' ? JSON.parse(body) : body; | |
} catch (e) { | |
requestBody = body; | |
} | |
logMessage({ | |
type: 'xhr-request-body', | |
requestId: requestInfo.requestId, | |
headers: requestInfo.headers, | |
body: requestBody, | |
timestamp: Date.now() | |
}); | |
} | |
return originalXHRSend.apply(this, arguments); | |
}; | |
// Custom implementation to intercept fetch | |
window.fetch = async function(resource, options = {}) { | |
const url = resource instanceof Request ? resource.url : resource; | |
const fetchMethod = (options.method || (resource instanceof Request ? resource.method : 'GET')).toUpperCase(); | |
// Track the original call timing | |
const startTime = Date.now(); | |
const requestId = Date.now() + Math.random().toString(36).substr(2, 9); | |
// Log that we found a request | |
logMessage({ | |
type: 'fetch-request-start', | |
requestId, | |
url, | |
method: fetchMethod, | |
timestamp: startTime | |
}); | |
// Track request body if available | |
if (options.body) { | |
try { | |
const requestBody = typeof options.body === 'string' | |
? JSON.parse(options.body) | |
: options.body; | |
logMessage({ | |
type: 'fetch-request-body', | |
requestId, | |
body: requestBody, | |
headers: options.headers || {}, | |
timestamp: Date.now() | |
}); | |
} catch (e) { | |
logMessage({ | |
type: 'fetch-request-body', | |
requestId, | |
body: options.body, | |
headers: options.headers || {}, | |
timestamp: Date.now() | |
}); | |
} | |
} | |
try { | |
// Call the original fetch | |
const response = await originalFetch.apply(this, arguments); | |
// Create a clone to read the body (because response body can only be read once) | |
const clonedResponse = response.clone(); | |
// Handle streaming or regular response | |
try { | |
// For streaming responses | |
const reader = clonedResponse.body.getReader(); | |
let chunks = []; | |
// Read the stream | |
const processStream = async () => { | |
try { | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) { | |
// Stream is complete | |
break; | |
} | |
// Convert the chunk to text | |
const chunk = new TextDecoder().decode(value); | |
chunks.push(chunk); | |
// Try to parse JSON chunks (for streaming APIs) | |
try { | |
const jsonData = JSON.parse(chunk); | |
logMessage({ | |
type: 'response-chunk', | |
chunk: jsonData, | |
timestamp: Date.now() | |
}); | |
} catch (e) { | |
// Not a valid JSON, just send the raw chunk | |
logMessage({ | |
type: 'response-chunk', | |
chunk: chunk, | |
timestamp: Date.now() | |
}); | |
} | |
} | |
// Concatenate all chunks | |
const fullBody = chunks.join(''); | |
// Try to parse the complete response | |
try { | |
const jsonResponse = JSON.parse(fullBody); | |
const completeMsg = { | |
type: 'fetch-response-complete', | |
requestId, | |
status: response.status, | |
headers: Object.fromEntries([...response.headers.entries()]), | |
body: jsonResponse, | |
duration: Date.now() - startTime, | |
timestamp: Date.now() | |
}; | |
logMessage(completeMsg); | |
} catch (e) { | |
// Not JSON, return as text | |
const completeMsg = { | |
type: 'fetch-response-complete', | |
requestId, | |
status: response.status, | |
headers: Object.fromEntries([...response.headers.entries()]), | |
body: fullBody, | |
duration: Date.now() - startTime, | |
timestamp: Date.now() | |
}; | |
logMessage(completeMsg); | |
} | |
} catch (streamError) { | |
logMessage({ | |
type: 'fetch-stream-error', | |
requestId, | |
error: streamError.message, | |
timestamp: Date.now() | |
}); | |
} | |
}; | |
// Start processing the stream | |
processStream(); | |
} catch (streamSetupError) { | |
// Fallback to regular response handling if streaming fails | |
handleRegularResponse(clonedResponse, startTime); | |
} | |
// Return the original response so the application works normally | |
return response; | |
} catch (error) { | |
logMessage({ | |
type: 'fetch-error', | |
requestId, | |
error: error.message, | |
timestamp: Date.now() | |
}); | |
throw error; // Re-throw to not interfere with app error handling | |
} | |
}; | |
// Helper function to handle regular (non-streaming) responses | |
async function handleRegularResponse(response, startTime) { | |
try { | |
let responseData; | |
const contentType = response.headers.get('content-type') || ''; | |
if (contentType.includes('application/json')) { | |
responseData = await response.json(); | |
} else { | |
responseData = await response.text(); | |
} | |
const completeMsg = { | |
type: 'fetch-response-complete', | |
requestId, | |
status: response.status, | |
headers: Object.fromEntries([...response.headers.entries()]), | |
body: responseData, | |
duration: Date.now() - startTime, | |
timestamp: Date.now() | |
}; | |
logMessage(completeMsg); | |
} catch (error) { | |
logMessage({ | |
type: 'fetch-error', | |
requestId, | |
error: error.message, | |
timestamp: Date.now() | |
}); | |
} | |
} | |
// Check if we can intercept Beacon API | |
if (navigator.sendBeacon) { | |
const originalSendBeacon = navigator.sendBeacon; | |
navigator.sendBeacon = function(url, data) { | |
const beaconId = Date.now() + Math.random().toString(36).substr(2, 9); | |
// Log the beacon request | |
logMessage({ | |
type: 'beacon-request', | |
beaconId, | |
url, | |
timestamp: Date.now() | |
}); | |
// Try to log the data | |
if (data) { | |
let beaconData; | |
try { | |
if (data instanceof Blob) { | |
logMessage({ | |
type: 'beacon-data', | |
beaconId, | |
dataType: 'Blob', | |
size: data.size, | |
timestamp: Date.now() | |
}); | |
} else if (data instanceof FormData) { | |
logMessage({ | |
type: 'beacon-data', | |
beaconId, | |
dataType: 'FormData', | |
timestamp: Date.now() | |
}); | |
} else if (typeof data === 'string') { | |
try { | |
beaconData = JSON.parse(data); | |
logMessage({ | |
type: 'beacon-data', | |
beaconId, | |
dataType: 'JSON', | |
data: beaconData, | |
timestamp: Date.now() | |
}); | |
} catch (e) { | |
logMessage({ | |
type: 'beacon-data', | |
beaconId, | |
dataType: 'string', | |
data: data, | |
timestamp: Date.now() | |
}); | |
} | |
} else { | |
logMessage({ | |
type: 'beacon-data', | |
beaconId, | |
dataType: typeof data, | |
data: data, | |
timestamp: Date.now() | |
}); | |
} | |
} catch (e) { | |
logMessage({ | |
type: 'beacon-data-error', | |
beaconId, | |
error: e.message, | |
timestamp: Date.now() | |
}); | |
} | |
} | |
return originalSendBeacon.apply(navigator, arguments); | |
}; | |
// Update the stop monitoring function | |
const originalStopMonitoring = stopMonitoring; | |
stopMonitoring = function() { | |
navigator.sendBeacon = originalSendBeacon; | |
return originalStopMonitoring(); | |
}; | |
} | |
// Log that we've started monitoring | |
console.log('Monitoring all network traffic (fetch, XHR, and Beacon)...'); | |
// Return the function to stop monitoring | |
return stopMonitoring; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment