frame.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {ReactNode} from 'react';
  2. import {Tooltip} from 'sentry/components/tooltip';
  3. import {IconWarning} from 'sentry/icons';
  4. import {t, tct} from 'sentry/locale';
  5. import {BreadcrumbType} from 'sentry/types/breadcrumbs';
  6. import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
  7. import {
  8. BreadcrumbFrame,
  9. LargestContentfulPaintFrame,
  10. MultiClickFrame,
  11. MutationFrame,
  12. NavFrame,
  13. ReplayFrame,
  14. SlowClickFrame,
  15. SpanFrame,
  16. } from 'sentry/utils/replays/types';
  17. import type {Color} from 'sentry/utils/theme';
  18. import stripOrigin from 'sentry/utils/url/stripOrigin';
  19. export function getColor(frame: ReplayFrame): Color {
  20. if ('category' in frame) {
  21. switch (frame.category) {
  22. case 'replay.init':
  23. return 'gray300';
  24. case 'navigation':
  25. return 'green300';
  26. case 'issue':
  27. return 'red300';
  28. case 'ui.slowClickDetected':
  29. return (frame as SlowClickFrame).data.endReason === 'timeout'
  30. ? 'red300'
  31. : 'yellow300';
  32. case 'ui.multiClick':
  33. return 'red300';
  34. case 'replay.mutations':
  35. return 'yellow300';
  36. case 'ui.click':
  37. case 'ui.input':
  38. case 'ui.keyDown':
  39. case 'ui.blur':
  40. case 'ui.focus':
  41. return 'purple300';
  42. case 'console':
  43. default: // Custom breadcrumbs will fall through here
  44. return 'gray300';
  45. }
  46. }
  47. switch (frame.op) {
  48. case 'navigation.navigate':
  49. case 'navigation.reload':
  50. case 'navigation.back_forward':
  51. case 'navigation.push':
  52. return 'green300';
  53. case 'largest-contentful-paint':
  54. case 'memory':
  55. case 'paint':
  56. case 'resource.fetch':
  57. case 'resource.xhr':
  58. default:
  59. return 'gray300';
  60. }
  61. }
  62. /**
  63. * The breadcrumbType is used as a value for <BreadcrumbIcon/>
  64. * We could remove the indirection by associating frames with icons directly.
  65. *
  66. * @deprecated
  67. */
  68. export function getBreadcrumbType(frame: ReplayFrame): BreadcrumbType {
  69. if ('category' in frame) {
  70. switch (frame.category) {
  71. case 'replay.init':
  72. return BreadcrumbType.DEFAULT;
  73. case 'navigation':
  74. return BreadcrumbType.NAVIGATION;
  75. case 'issue':
  76. return BreadcrumbType.ERROR;
  77. case 'ui.slowClickDetected':
  78. return (frame as SlowClickFrame).data.endReason === 'timeout'
  79. ? BreadcrumbType.ERROR
  80. : BreadcrumbType.WARNING;
  81. case 'ui.multiClick':
  82. return BreadcrumbType.ERROR;
  83. case 'replay.mutations':
  84. return BreadcrumbType.WARNING;
  85. case 'ui.click':
  86. case 'ui.input':
  87. case 'ui.keyDown':
  88. case 'ui.blur':
  89. case 'ui.focus':
  90. return BreadcrumbType.UI;
  91. case 'console':
  92. return BreadcrumbType.DEBUG;
  93. default: // Custom breadcrumbs will fall through here
  94. return BreadcrumbType.DEFAULT;
  95. }
  96. }
  97. switch (frame.op) {
  98. case 'navigation.navigate':
  99. case 'navigation.reload':
  100. case 'navigation.back_forward':
  101. case 'navigation.push':
  102. return BreadcrumbType.NAVIGATION;
  103. case 'largest-contentful-paint':
  104. case 'memory':
  105. case 'paint':
  106. return BreadcrumbType.INFO;
  107. case 'resource.fetch':
  108. case 'resource.xhr':
  109. return BreadcrumbType.HTTP;
  110. default:
  111. return BreadcrumbType.DEFAULT;
  112. }
  113. }
  114. export function getTitle(frame: ReplayFrame): ReactNode {
  115. if (
  116. typeof frame.data === 'object' &&
  117. frame.data !== null &&
  118. 'label' in frame.data &&
  119. frame.data.label
  120. ) {
  121. return frame.data.label; // TODO(replay): Included for backwards compat
  122. }
  123. if ('category' in frame) {
  124. const [type, action] = frame.category.split('.');
  125. switch (frame.category) {
  126. case 'replay.init':
  127. return 'Replay Init';
  128. case 'navigation':
  129. return 'Navigation';
  130. case 'ui.slowClickDetected':
  131. return (frame as SlowClickFrame).data.endReason === 'timeout'
  132. ? 'Dead Click'
  133. : 'Slow Click';
  134. case 'ui.multiClick':
  135. return 'Rage Click';
  136. case 'replay.mutations':
  137. return 'Replay';
  138. case 'ui.click':
  139. case 'ui.input':
  140. case 'ui.keyDown':
  141. case 'ui.blur':
  142. case 'ui.focus':
  143. return `User ${action || ''}`;
  144. default: // Custom breadcrumbs will fall through here
  145. return `${type} ${action || ''}`.trim();
  146. }
  147. }
  148. if ('message' in frame) {
  149. return frame.message; // TODO(replay): Included for backwards compat
  150. }
  151. switch (frame.op) {
  152. case 'navigation.navigate':
  153. return 'Page Load';
  154. case 'navigation.reload':
  155. return 'Reload';
  156. case 'navigation.back_forward':
  157. return 'Navigate Back';
  158. case 'navigation.push':
  159. return 'Navigation';
  160. default:
  161. return frame.description;
  162. }
  163. }
  164. function stringifyNodeAttributes(node: SlowClickFrame['data']['node']) {
  165. const {tagName, attributes} = node ?? {};
  166. const attributesEntries = Object.entries(attributes ?? {});
  167. return `${tagName}${
  168. attributesEntries.length
  169. ? attributesEntries.map(([attr, val]) => `[${attr}="${val}"]`).join('')
  170. : ''
  171. }`;
  172. }
  173. export function getDescription(frame: ReplayFrame): ReactNode {
  174. if ('category' in frame) {
  175. switch (frame.category) {
  176. case 'replay.init':
  177. return stripOrigin(frame.message ?? '');
  178. case 'navigation':
  179. const navFrame = frame as NavFrame;
  180. return stripOrigin(navFrame.data.to);
  181. case 'issue':
  182. case 'ui.slowClickDetected': {
  183. const slowClickFrame = frame as SlowClickFrame;
  184. const node = slowClickFrame.data.node;
  185. return slowClickFrame.data.endReason === 'timeout'
  186. ? tct(
  187. 'Click on [selector] did not cause a visible effect within [timeout] ms',
  188. {
  189. selector: stringifyNodeAttributes(node),
  190. timeout: slowClickFrame.data.timeAfterClickMs,
  191. }
  192. )
  193. : tct('Click on [selector] took [duration] ms to have a visible effect', {
  194. selector: stringifyNodeAttributes(node),
  195. duration: slowClickFrame.data.timeAfterClickMs,
  196. });
  197. }
  198. case 'ui.multiClick':
  199. const multiClickFrame = frame as MultiClickFrame;
  200. return tct('Rage clicked [clickCount] times on [selector]', {
  201. clickCount: multiClickFrame.data.clickCount,
  202. selector: stringifyNodeAttributes(multiClickFrame.data.node),
  203. });
  204. case 'replay.mutations': {
  205. const mutationFrame = frame as MutationFrame;
  206. return mutationFrame.data.limit
  207. ? t(
  208. 'A large number of mutations was detected (%s). Replay is now stopped to prevent poor performance for your customer.',
  209. mutationFrame.data.count
  210. )
  211. : t(
  212. 'A large number of mutations was detected (%s). This can slow down the Replay SDK and impact your customers.',
  213. mutationFrame.data.count
  214. );
  215. }
  216. case 'ui.click':
  217. return frame.message ?? ''; // This should be the selector
  218. case 'ui.input':
  219. case 'ui.keyDown':
  220. case 'ui.blur':
  221. case 'ui.focus':
  222. return t('User Action');
  223. case 'console':
  224. default: // Custom breadcrumbs will fall through here
  225. return frame.message ?? '';
  226. }
  227. }
  228. switch (frame.op) {
  229. case 'navigation.navigate':
  230. case 'navigation.reload':
  231. case 'navigation.back_forward':
  232. case 'navigation.push':
  233. return stripOrigin(frame.description);
  234. case 'largest-contentful-paint': {
  235. const lcpFrame = frame as LargestContentfulPaintFrame;
  236. if (typeof lcpFrame.data.value === 'number') {
  237. return `${Math.round((frame as LargestContentfulPaintFrame).data.value)}ms`;
  238. }
  239. // Included for backwards compat
  240. return (
  241. <Tooltip
  242. title={t(
  243. 'This replay uses a SDK version that is subject to inaccurate LCP values. Please upgrade to the latest version for best results if you have not already done so.'
  244. )}
  245. >
  246. <IconWarning />
  247. </Tooltip>
  248. );
  249. }
  250. default:
  251. return undefined;
  252. }
  253. }
  254. export function getTabKeyForFrame(frame: BreadcrumbFrame | SpanFrame): TabKey {
  255. if ('category' in frame) {
  256. switch (frame.category) {
  257. case 'replay.init':
  258. return TabKey.CONSOLE;
  259. case 'navigation':
  260. return TabKey.NETWORK;
  261. case 'issue':
  262. return TabKey.ERRORS;
  263. case 'replay.mutations':
  264. case 'ui.click':
  265. case 'ui.input':
  266. case 'ui.keyDown':
  267. case 'ui.multiClick':
  268. case 'ui.slowClickDetected':
  269. return TabKey.DOM;
  270. case 'console':
  271. default: // Custom breadcrumbs will fall through here
  272. return TabKey.CONSOLE;
  273. }
  274. }
  275. switch (frame.op) {
  276. case 'memory':
  277. return TabKey.MEMORY;
  278. case 'navigation.navigate':
  279. case 'navigation.reload':
  280. case 'navigation.back_forward':
  281. case 'navigation.push':
  282. case 'largest-contentful-paint':
  283. case 'paint':
  284. case 'resource.fetch':
  285. case 'resource.xhr':
  286. default:
  287. return TabKey.NETWORK;
  288. }
  289. }