Skip to content

Instantly share code, notes, and snippets.

@harryi3t
Last active February 11, 2025 15:53
Show Gist options
  • Save harryi3t/a1819441294b56b557a58b6b925a6733 to your computer and use it in GitHub Desktop.
Save harryi3t/a1819441294b56b557a58b6b925a6733 to your computer and use it in GitHub Desktop.
Tamper monkey: Sentry copy on-call info
// ==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