Sentry to Datadog RUM and Log buttons

Read the README

  1. // ==UserScript==
  2. // @name Sentry to Datadog RUM and Log buttons
  3. // @version 3
  4. // @grant none
  5. // @match https://*.sentry.io/issues/*
  6. // @license MIT
  7. // @namespace happyviking
  8. // @description Read the README
  9. // ==/UserScript==
  10.  
  11. // This script requires getting some react props from these HTML elements.
  12. // Methodology for this is borrowed (and modified) from here:
  13. // https://stackoverflow.com/questions/70507318/how-to-get-react-element-props-from-html-element-with-javascript
  14. // But it requires some more DOM access than userscripts (and extensions) normally have
  15. // https://stackoverflow.com/questions/72726773/access-react-props-functions-via-native-javascript
  16. // so I've put everything in an actual script tag
  17. // Logic still behaves the same, comment out the first line below <COMMENT LINE IF EDITING> to get syntax highligting if you need to edit the script
  18.  
  19.  
  20. // <COMMENT LINE BELOW IF EDITING>
  21. const logic = `
  22.  
  23. //https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  24. function waitForElm(selector) {
  25. return new Promise(resolve => {
  26. if (document.querySelector(selector)) {
  27. return resolve(document.querySelector(selector));
  28. }
  29.  
  30. const observer = new MutationObserver(mutations => {
  31. if (document.querySelector(selector)) {
  32. observer.disconnect();
  33. resolve(document.querySelector(selector));
  34. }
  35. });
  36.  
  37. observer.observe(document.body, {
  38. childList: true,
  39. subtree: true
  40. });
  41. });
  42. }
  43.  
  44. const main = async () => {
  45.  
  46. const info = {}
  47.  
  48. //ID
  49. await waitForElm('div[data-test-id="event-tags"]') //Just waiting till it loads
  50. const ID = document.querySelector('td[data-test-id="user-context-id-value"]')?.querySelector(".val-string > span")?.textContent
  51. info.id = ID
  52.  
  53. //RUM
  54. const RUMTable = document.querySelector('div[data-test-id="event-section-context-datadog"]')?.nextElementSibling
  55. if (RUMTable){
  56. //https://stackoverflow.com/questions/37098405/javascript-queryselector-find-div-by-innertext
  57. const tableKey = document.evaluate('//td[text()="RUM"]', RUMTable, null, XPathResult.ANY_TYPE, null).iterateNext()
  58. const RUM = tableKey?.nextSibling?.querySelector(".val-string > span")?.textContent
  59. info.RUM = RUM
  60. }
  61.  
  62.  
  63. //Timestamp
  64. const timeElement = document.querySelector("time")
  65. let props = getReactProps(timeElement);
  66. const exactTimestamp = new Date(props.date)
  67. const imrovedTimeString = exactTimestamp.toLocaleString(undefined, {
  68. weekday: undefined,
  69. year: undefined,
  70. month: 'short',
  71. day: 'numeric',
  72. hour: 'numeric',
  73. minute: 'numeric',
  74. second: 'numeric',
  75. timeZone: "UTC"
  76. })
  77. timeElement.textContent = imrovedTimeString
  78. info.time = imrovedTimeString
  79.  
  80. const buttonHolder = document.createElement("div")
  81. buttonHolder.id = "thebuttonholder"
  82. const parent = document.querySelector("header")?.parentElement
  83. if (!parent) return
  84.  
  85. if (!document.getElementById("thebuttonholder")) parent.insertBefore(buttonHolder, document.querySelector('div[role=tabpanel]'))
  86.  
  87. if (!document.getElementById("rum-shortcut")){
  88. if (info.RUM){
  89. buttonHolder.appendChild(makeButton("Provided RUM","rum-shortcut", info.RUM))
  90. }else{
  91. const text = document.createElement("p")
  92. text.textContent = "NO PROVIDED RUM 🥲"
  93. text.style.color = "red"
  94. text.style.fontSize = "18px"
  95. text.id = "rum-shortcut"
  96. buttonHolder.appendChild(text)
  97. }
  98. }
  99.  
  100.  
  101. if (info.time && info.id && !document.getElementById("manual-rum-shortcut")){
  102. //Adding more info to the date so that the resultant date object is accurate
  103. info.time += " " + (new Date()).getFullYear() + " UTC"
  104. const eventTime = new Date(Date.parse(info.time)).getTime()
  105. const OFFSET = 300000 //5 minutes in milliseconfs
  106. const OFFSET_SHORTER = 60000 //1 minutes in milliseconfs
  107. const OFFSET_FOR_HIGHLIGHTING = 30000 //30 seconds in milliseconfs
  108.  
  109. //Adding inferred RUM button
  110. const manualRumURL = new URL("https://app.datadoghq.com/rum/sessions?query=%40type%3Aerror&cols=&tab=session&viz=stream&live=false")
  111. manualRumURL.searchParams.set("query", (manualRumURL.searchParams.get("query") || "") + " @usr.id:" + info.id)
  112. manualRumURL.searchParams.set("from_ts", eventTime - OFFSET )
  113. manualRumURL.searchParams.set("to_ts", eventTime + OFFSET)
  114. //For my "Datadog RUM log highlighting" script
  115. manualRumURL.searchParams.set("highlight_from", eventTime - OFFSET_FOR_HIGHLIGHTING )
  116. manualRumURL.searchParams.set("highlight_to", eventTime + OFFSET_FOR_HIGHLIGHTING)
  117. buttonHolder.appendChild(makeButton("Inferred RUM","manual-rum-shortcut", manualRumURL.toString()))
  118.  
  119. //Adding Logs button
  120. const logsUrl = new URL("https://app.datadoghq.com/logs?cols=host%2Cservice%2C%40accountName%2C%40args.url&index=&messageDisplay=inline&refresh_mode=sliding&stream_sort=time%2Cdesc&viz=stream&live=false")
  121. logsUrl.searchParams.set("query", "@usr.id:" + info.id)
  122. logsUrl.searchParams.set("from_ts", eventTime - OFFSET_SHORTER )
  123. logsUrl.searchParams.set("to_ts", eventTime + OFFSET_SHORTER)
  124. //For my "Datadog Log log highlighting" script
  125. logsUrl.searchParams.set("highlight_from", eventTime - OFFSET_FOR_HIGHLIGHTING )
  126. logsUrl.searchParams.set("highlight_to", eventTime + OFFSET_FOR_HIGHLIGHTING)
  127. buttonHolder.appendChild(makeButton("Relevant Logs","logs-shortcut", logsUrl.toString()))
  128. }
  129. }
  130.  
  131.  
  132. const makeButton = (text, id, href) => {
  133. const button = document.createElement("button")
  134. button.textContent = text
  135. const link = document.createElement("a")
  136. link.appendChild(button)
  137. link.href = href
  138. link.target="_blank"
  139. link.id = id
  140. button.className = document.querySelector('button[aria-label="Resolve"]')?.className ?? ""
  141. return link
  142. }
  143.  
  144.  
  145. const getReactProps = (target) => {
  146. let keyof_ReactProps = undefined
  147. let parent = target.parentElement
  148. while (parent) {
  149. keyof_ReactProps = Object.keys(parent).find(k => k.startsWith("__reactProps$"));
  150. if (!keyof_ReactProps){
  151. parent = parent.parentElement
  152. }else{
  153. break
  154. }
  155. }
  156.  
  157. const symof_ReactFragment = Symbol.for("react.fragment");
  158.  
  159. //Find the path from target to parent
  160. let path = [];
  161. let elem = target;
  162. while (elem !== parent) {
  163. let index = 0;
  164. for (let sibling = elem; sibling != null;) {
  165. if (sibling[keyof_ReactProps]) index++;
  166. sibling = sibling.previousElementSibling;
  167. }
  168. path.push({ child: elem, index });
  169. elem = elem.parentElement;
  170. }
  171. //Walk down the path to find the react state props
  172. let state = elem[keyof_ReactProps];
  173. for (let i = path.length - 1; i >= 0 && state != null; i--) {
  174. //Find the target child state index
  175. let childStateIndex = 0, childElemIndex = 0;
  176. while (childStateIndex < state.children.length) {
  177. let childState = state.children[childStateIndex];
  178. if (childState instanceof Object) {
  179. //Fragment children are inlined in the parent DOM element
  180. let isFragment = childState.type === symof_ReactFragment && childState.props.children.length;
  181. childElemIndex += isFragment ? childState.props.children.length : 1;
  182. if (childElemIndex === path[i].index) break;
  183. }
  184. childStateIndex++;
  185. }
  186. let childState = state.children[childStateIndex] ?? (childStateIndex === 0 ? state.children : null);
  187. state = childState?.props;
  188. elem = path[i].child;
  189. }
  190. return state;
  191. }
  192.  
  193. main()
  194.  
  195. // ` //DOn't remove this line, this terminales the script string when the line below the <COMMENT LINE IF EDITING> line is not commented out
  196.  
  197. const attachScript = () => {
  198. const id = "button-adder-script"
  199. if (document.getElementById(id)) return
  200. console.log("Adding script for Sentry to Datadog userscript")
  201. const script = document.createElement("script");
  202. script.textContent = logic;
  203. script.id = id
  204. document.body.appendChild(script);
  205. }
  206.  
  207. //There are probably cleaner ways to do this but I don't really care, this works and this
  208. //is supposed to be fast
  209. let currentPage = location.href;
  210. attachScript()
  211. setInterval(() =>
  212. {
  213. if (currentPage != location.href){
  214. currentPage = location.href;
  215. attachScript()
  216. }
  217. }, 500);
  218.  
  219.