Created
August 22, 2024 00:35
-
-
Save martinamps/42ccbc930e44f963ff25d960ce5138ee to your computer and use it in GitHub Desktop.
tampermonkey script to make cloudflare ai gateway ux better
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
// ==UserScript== | |
// @name Cloudflare AI Gateway Improver | |
// @namespace https://dash.cloudflare.com/ | |
// @version 1.5 | |
// @description Adds some missing columns / a modal for viewing request/response in browser | |
// @author @martinamps | |
// @match https://dash.cloudflare.com/* | |
// @grant none | |
// ==/UserScript== | |
(() => { | |
'use strict'; | |
console.log('Running Tampermonkey CF script'); | |
if (window.location.hostname !== "dash.cloudflare.com") return; | |
const originalFetch = window.fetch; | |
let responseStore = []; | |
console.log('Patching fetch from CF script'); | |
const updateTable = () => { | |
const table = document.querySelector('.ai-gateway-logs-table'); | |
if (!table) { | |
console.log('Table not found, retrying...'); | |
setTimeout(updateTable, 200); | |
return; | |
} | |
const headerRow = table.querySelector('thead tr'); | |
if (headerRow && !headerRow.classList.contains('custom-columns-added')) { | |
const modelCol = headerRow.querySelector('td:nth-child(4)'); | |
modelCol.style = 'width:200px !important; min-width: 200px !important;'; | |
const statusCol = headerRow.querySelector('td:nth-child(3)'); | |
const durationHeader = document.createElement('th'); | |
durationHeader.innerText = 'Duration'; | |
durationHeader.className = statusCol.className; | |
durationHeader.style = 'width: 100px'; | |
headerRow.appendChild(durationHeader); | |
const costHeader = document.createElement('th'); | |
costHeader.innerText = 'Cost'; | |
costHeader.className = statusCol.className; | |
costHeader.style = 'width: 100px'; | |
headerRow.appendChild(costHeader); | |
headerRow.classList.add('custom-columns-added'); | |
} | |
const rows = table.querySelectorAll('div > tr:not(thead tr)'); | |
rows.forEach((row, index) => { | |
if (row.querySelectorAll('td').length <= 6 && !row.classList.contains('custom-columns-added')) { | |
if (responseStore[index]) { | |
try { | |
const response = responseStore[index]; | |
const modelRow = row.querySelector('td:nth-child(4)'); | |
modelRow.style = 'width: 200px; min-width:200px;'; | |
const durationCell = document.createElement('td'); | |
durationCell.innerText = `${response.duration} ms`; | |
row.appendChild(durationCell); | |
const costCell = document.createElement('td'); | |
costCell.innerText = `$${response.cost.toFixed(8)}`; | |
row.appendChild(costCell); | |
row.classList.add('custom-columns-added'); | |
} catch (error) { | |
console.error('Error processing response:', error); | |
} | |
} | |
} | |
}); | |
responseStore = []; | |
}; | |
window.fetch = async (...args) => { | |
const [url, config] = args; | |
const urlPattern = /\/api\/v4\/accounts\/[^\/]+\/ai-gateway\/gateways\/[^\/]+\/logs\?.*/; | |
if (urlPattern.test(url)) { | |
console.log("Matched URL:", url); | |
try { | |
const response = await originalFetch(...args); | |
const clonedResponse = response.clone(); | |
const responseBody = await clonedResponse.json(); | |
if (responseBody?.result && Array.isArray(responseBody.result)) { | |
responseStore.push(...responseBody.result); | |
} | |
setTimeout(updateTable, 200); | |
return response; | |
} catch (error) { | |
console.error("Fetch error:", error); | |
throw error; | |
} | |
} else { | |
return originalFetch(...args); | |
} | |
}; | |
const showModal = (jsonContent) => { | |
let modal = document.getElementById('jsonModal'); | |
let overlay = document.getElementById('modalOverlay'); | |
if (!overlay) { | |
overlay = document.createElement('div'); | |
overlay.id = 'modalOverlay'; | |
overlay.style.cssText = ` | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100vw; | |
height: 100vh; | |
background-color: rgba(0, 0, 0, 0.7); | |
z-index: 999; | |
display: none; | |
`; | |
document.body.appendChild(overlay); | |
} | |
if (!modal) { | |
modal = document.createElement('div'); | |
modal.id = 'jsonModal'; | |
modal.style.cssText = ` | |
position: fixed; | |
top: 80px; | |
left: 300px; | |
background-color: #fefefe; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); | |
z-index: 10000; | |
width: 75vw; | |
max-height: 80vh; | |
overflow-y: auto; | |
display: none; | |
`; | |
const closeButton = document.createElement('span'); | |
closeButton.innerHTML = '×'; | |
closeButton.style.cssText = ` | |
position: absolute; | |
top: 10px; | |
right: 20px; | |
font-size: 24px; | |
cursor: pointer; | |
`; | |
closeButton.onclick = () => { | |
modal.style.display = 'none'; | |
overlay.style.display = 'none'; | |
}; | |
modal.appendChild(closeButton); | |
const content = document.createElement('pre'); | |
content.id = 'jsonContent'; | |
content.style.whiteSpace = 'pre-wrap'; | |
modal.appendChild(content); | |
document.body.appendChild(modal); | |
} | |
modal.querySelector('#jsonContent').innerText = JSON.stringify(jsonContent, null, 2); | |
overlay.style.display = 'block'; | |
modal.style.display = 'block'; | |
modal.style.opacity = '0'; | |
setTimeout(() => { | |
modal.style.opacity = '1'; | |
modal.style.transition = 'opacity 0.3s ease'; | |
}, 10); | |
}; | |
const handleLinkClick = (event) => { | |
event.preventDefault(); | |
const href = event.currentTarget.href; | |
fetch(href) | |
.then(response => response.json()) | |
.then(data => showModal(data)) | |
.catch(error => console.error('Error fetching JSON:', error)); | |
}; | |
const hookLinks = () => { | |
const requestPattern = /https:\/\/dash\.cloudflare\.com\/api\/v4\/accounts\/[a-zA-Z0-9]+\/ai-gateway\/gateways\/[a-zA-Z0-9-]+\/logs\/[a-zA-Z0-9]+\/request/; | |
const responsePattern = /https:\/\/dash\.cloudflare\.com\/api\/v4\/accounts\/[a-zA-Z0-9]+\/ai-gateway\/gateways\/[a-zA-Z0-9-]+\/logs\/[a-zA-Z0-9]+\/response/; | |
document.querySelectorAll('a').forEach(link => { | |
const href = link.href; | |
if (requestPattern.test(href) || responsePattern.test(href)) { | |
link.addEventListener('click', handleLinkClick); | |
} | |
}); | |
}; | |
hookLinks(); | |
const observer = new MutationObserver(hookLinks); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
document.addEventListener('click', hookLinks); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment