Skip to content

Instantly share code, notes, and snippets.

@martinamps
Created August 22, 2024 00:35
Show Gist options
  • Save martinamps/42ccbc930e44f963ff25d960ce5138ee to your computer and use it in GitHub Desktop.
Save martinamps/42ccbc930e44f963ff25d960ce5138ee to your computer and use it in GitHub Desktop.
tampermonkey script to make cloudflare ai gateway ux better
// ==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 = '&times;';
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