Last active
February 11, 2025 15:53
-
-
Save harryi3t/a1819441294b56b557a58b6b925a6733 to your computer and use it in GitHub Desktop.
Tamper monkey: Sentry copy on-call info
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 Sentry Copy On-Call Info | |
// @namespace http://tampermonkey.net/ | |
// @version 0.1 | |
// @description Add a button to copy on-call info from Sentry issue pages | |
// @match https://people-center-inc.sentry.io/issues/* | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
const enableDebugMode = true; | |
const colors = { | |
yellow: "#e39d02", | |
plum: "#512f3e", | |
}; | |
function debug(msg, ...params) { | |
enableDebugMode && console.log(`tamperMonkey: ${msg}`, ...params); | |
} | |
const Toast = { | |
show(message, type, delay = 3000) { | |
const toast = document.createElement("div"); | |
toast.textContent = message; | |
toast.style.position = "fixed"; | |
toast.style.bottom = "20px"; | |
toast.style.right = "20px"; | |
toast.style.padding = "10px 20px"; | |
toast.style.borderRadius = "5px"; | |
toast.style.color = "#fff"; | |
toast.style.zIndex = "10000"; | |
toast.style.transition = "opacity 0.5s ease-in-out"; | |
switch (type) { | |
case "success": | |
toast.style.backgroundColor = "#4CAF50"; | |
break; | |
case "error": | |
toast.style.backgroundColor = "#F44336"; | |
break; | |
default: | |
toast.style.backgroundColor = "#2196F3"; | |
} | |
document.body.appendChild(toast); | |
setTimeout(() => { | |
toast.style.opacity = "0"; | |
setTimeout(() => { | |
document.body.removeChild(toast); | |
}, 500); | |
}, delay); | |
}, | |
info(message, delay = 3000) { | |
this.show(message, "info"); | |
}, | |
success(message, delay = 3000) { | |
this.show(message, "success"); | |
}, | |
error(message, delay = 3000) { | |
this.show(message, "error"); | |
}, | |
}; | |
function createOrUpdateGenericButton({ label, id, color, onClick, styles }) { | |
let button = document.getElementById(id); | |
debug("existing button", button); | |
if (!button) { | |
button = document.createElement("button"); | |
button.id = id; | |
debug("creating new button", button); | |
} | |
button.textContent = label; | |
Object.assign(button.style, { | |
backgroundColor: color ?? colors.yellow, | |
padding: "8px 12px", | |
minHeight: "32px", | |
transition: "background 0.1s, border 0.1s, box-shadow 0.1s", | |
borderRadius: "6px", | |
...styles, | |
}); | |
button.addEventListener("click", onClick); | |
return button; | |
} | |
function addSingleCopyButton() { | |
debug("calling addSingleCopyButton"); | |
const copyButtonId = "singleCopyButton"; | |
const header = document.querySelector( | |
'header[data-sentry-element="Header"]' | |
); | |
if (!header) { | |
debug("header not found, bailing out"); | |
return; | |
} | |
const resolveButton = document.querySelector( | |
'button[aria-label="Resolve"]' | |
); | |
if (!resolveButton) { | |
debug("resolveButton not found, bailing out"); | |
return; | |
} | |
let copyButton = createOrUpdateGenericButton({ | |
label: "Copy on-call info", | |
id: copyButtonId, | |
onClick: copySingleOnCallInfo, | |
}); | |
copyButton.style.marginRight = "5px"; | |
copyButton.className = resolveButton.className; | |
resolveButton.parentNode.insertBefore(copyButton, resolveButton); | |
} | |
function addBulkCopyButton() { | |
debug("calling addBulkCopyButton"); | |
const copyButtonId = "bulkCopyButton"; | |
const realTimeButton = document.querySelector('[data-test-id="real-time"]'); | |
if (!realTimeButton) { | |
debug("realTimeButton not found, bailing out"); | |
return; | |
} | |
let copyButton = createOrUpdateGenericButton({ | |
label: "Copy on-call info", | |
id: copyButtonId, | |
styles: { padding: "0px 8px" }, | |
onClick: copyOnCallInfoFromList, | |
}); | |
copyButton.style.marginRight = "5px"; | |
realTimeButton.parentNode.insertBefore(copyButton, realTimeButton); | |
debug("added bulk button", copyButton); | |
} | |
function copySingleOnCallInfo() { | |
const headerGrid = document.querySelector( | |
'[data-sentry-element="HeaderGrid"]' | |
); | |
const titleElement = headerGrid.querySelector("div>span"); | |
const eventsElement = headerGrid.children[headerGrid.children.length - 2]; | |
const usersElement = headerGrid.children[headerGrid.children.length - 1]; | |
const url = window.location.href.split("?")[0]; // Remove query parameters | |
if (!titleElement || !eventsElement || !usersElement) { | |
alert("Unable to find required information"); | |
return; | |
} | |
const title = titleElement.textContent | |
.replace(/^\[spend_management\]\s*/, "") | |
.trim(); | |
const events = eventsElement.textContent.trim(); | |
const users = usersElement.textContent.trim(); | |
const info = `Title: ${title} | |
Events: ${events} | |
Users: ${users} | |
Relevant links: | |
${url}`; | |
navigator.clipboard | |
.writeText(info) | |
.then(() => { | |
Toast.success("On-call info copied to clipboard!"); | |
}) | |
.catch((err) => { | |
Toast.error("Failed to copy text: ", err); | |
}); | |
} | |
function copyOnCallInfoFromList() { | |
const objects = []; | |
const baseUrl = window.location.origin; | |
const rows = document.querySelectorAll('[data-test-id="group"]'); | |
rows.forEach((row) => { | |
const titleElement = row.querySelector( | |
'[data-testid="stacktrace-preview"]' | |
); | |
let title = titleElement ? titleElement.innerText.trim() : ""; | |
const prefix = "[spend_management] "; | |
if (title.startsWith(prefix)) { | |
title = title.replace(prefix, ""); | |
} | |
const innerDiv = row.children[3]; | |
const spans = innerDiv ? innerDiv.querySelectorAll("span") : []; | |
const events = spans[1] ? spans[1].innerText.trim() : ""; | |
const users = spans[2] ? spans[2].innerText.trim() : ""; | |
const linkElement = row.querySelector( | |
'[data-sentry-element="TitleWithLink"]' | |
); | |
let link = ""; | |
if (linkElement) { | |
const href = linkElement.getAttribute("href"); | |
const urlParts = href.split("?")[0]; | |
link = `${baseUrl}${urlParts}`; | |
} | |
const obj = { | |
title: title, | |
events: events, | |
users: users, | |
link: link, | |
}; | |
objects.push(obj); | |
}); | |
const formattedStrings = objects.map((obj) => { | |
return `Title: ${obj.title}\nEvents: ${obj.events}\nUsers: ${obj.users}\nRelevant links:\n ${obj.link}`; | |
}); | |
const withQuotes = '"' + formattedStrings.join('"\n"') + '"'; | |
console.log(withQuotes); | |
navigator.clipboard | |
.writeText(withQuotes) | |
.then(() => { | |
Toast.success("On-call info copied to clipboard!"); | |
}) | |
.catch((err) => { | |
Toast.error("Failed to copy text: ", err); | |
}); | |
} | |
function initialize() { | |
const currentUrl = window.location.href; | |
const isListingPage = currentUrl.match(/issues\/\?/); | |
const isDetailsPage = currentUrl.match(/issues\/\d+/); | |
if (isDetailsPage) { | |
addSingleCopyButton(); | |
} else if (isListingPage) { | |
addBulkCopyButton(); | |
} | |
} | |
function waitForHeaderAndInitialize() { | |
debug("Observing the header"); | |
// Create a MutationObserver to watch for changes in the DOM | |
const observer = new MutationObserver((mutations) => { | |
for (const mutation of mutations) { | |
if (mutation.type === "childList") { | |
const header = document.querySelector("header"); | |
if (header) { | |
debug("Header found, initializing"); | |
initialize(); | |
observer.disconnect(); // Stop observing once the button is added | |
break; | |
} | |
} | |
} | |
}); | |
// Start observing the document body for changes | |
observer.observe(document.body, { childList: true, subtree: true }); | |
} | |
const titleObserver = new MutationObserver((mutations) => { | |
for (const mutation of mutations) { | |
if ( | |
mutation.type === "childList" && | |
mutation.target.nodeName === "TITLE" | |
) { | |
debug("TITLE changed, initializing", mutation.target); | |
waitForHeaderAndInitialize(); | |
break; | |
} | |
} | |
}); | |
debug("attaching mutation observer on title"); | |
titleObserver.observe(document.querySelector("head > title"), { | |
childList: true, | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment