getFrameDetails.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import {ReactNode} from 'react';
  2. import {Tooltip} from 'sentry/components/tooltip';
  3. import {
  4. IconCursorArrow,
  5. IconFire,
  6. IconFix,
  7. IconInfo,
  8. IconInput,
  9. IconKeyDown,
  10. IconLocation,
  11. IconSort,
  12. IconTerminal,
  13. IconUser,
  14. IconWarning,
  15. } from 'sentry/icons';
  16. import {t, tct} from 'sentry/locale';
  17. import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
  18. import type {
  19. BreadcrumbFrame,
  20. ErrorFrame,
  21. LargestContentfulPaintFrame,
  22. MultiClickFrame,
  23. MutationFrame,
  24. NavFrame,
  25. ReplayFrame,
  26. SlowClickFrame,
  27. } from 'sentry/utils/replays/types';
  28. import {
  29. getFrameOpOrCategory,
  30. isDeadClick,
  31. isDeadRageClick,
  32. isRageClick,
  33. } from 'sentry/utils/replays/types';
  34. import type {Color} from 'sentry/utils/theme';
  35. import stripOrigin from 'sentry/utils/url/stripOrigin';
  36. interface Details {
  37. color: Color;
  38. description: ReactNode;
  39. icon: ReactNode;
  40. tabKey: TabKey;
  41. title: ReactNode;
  42. }
  43. const MAPPER_FOR_FRAME: Record<string, (frame) => Details> = {
  44. 'replay.init': (frame: BreadcrumbFrame) => ({
  45. color: 'gray300',
  46. description: stripOrigin(frame.message ?? ''),
  47. tabKey: TabKey.CONSOLE,
  48. title: 'Replay Start',
  49. icon: <IconTerminal size="xs" />,
  50. }),
  51. navigation: (frame: NavFrame) => ({
  52. color: 'green300',
  53. description: stripOrigin((frame as NavFrame).data.to),
  54. tabKey: TabKey.NETWORK,
  55. title: 'Navigation',
  56. icon: <IconLocation size="xs" />,
  57. }),
  58. issue: (frame: ErrorFrame) => ({
  59. color: 'red300',
  60. description: frame.message,
  61. tabKey: TabKey.ERRORS,
  62. title: defaultTitle(frame),
  63. icon: <IconFire size="xs" />,
  64. }),
  65. 'ui.slowClickDetected': (frame: SlowClickFrame) => {
  66. const node = frame.data.node;
  67. if (isDeadClick(frame)) {
  68. return {
  69. color: isDeadRageClick(frame) ? 'red300' : 'yellow300',
  70. description: tct(
  71. 'Click on [selector] did not cause a visible effect within [timeout] ms',
  72. {
  73. selector: stringifyNodeAttributes(node),
  74. timeout: Math.round(frame.data.timeAfterClickMs),
  75. }
  76. ),
  77. icon: <IconCursorArrow size="xs" />,
  78. title: isDeadRageClick(frame) ? 'Rage Click' : 'Dead Click',
  79. tabKey: TabKey.DOM,
  80. };
  81. }
  82. return {
  83. color: 'yellow300',
  84. description: tct(
  85. 'Click on [selector] took [duration] ms to have a visible effect',
  86. {
  87. selector: stringifyNodeAttributes(node),
  88. duration: Math.round(frame.data.timeAfterClickMs),
  89. }
  90. ),
  91. icon: <IconWarning size="xs" />,
  92. title: 'Slow Click',
  93. tabKey: TabKey.DOM,
  94. };
  95. },
  96. 'ui.multiClick': (frame: MultiClickFrame) => {
  97. if (isRageClick(frame)) {
  98. return {
  99. color: 'red300',
  100. description: tct('Rage clicked [clickCount] times on [selector]', {
  101. clickCount: frame.data.clickCount,
  102. selector: stringifyNodeAttributes(frame.data.node),
  103. }),
  104. tabKey: TabKey.DOM,
  105. title: 'Rage Click',
  106. icon: <IconFire size="xs" />,
  107. };
  108. }
  109. return {
  110. color: 'yellow300',
  111. description: tct('[clickCount] clicks on [selector]', {
  112. clickCount: frame.data.clickCount,
  113. selector: stringifyNodeAttributes(frame.data.node),
  114. }),
  115. tabKey: TabKey.DOM,
  116. title: 'Multi Click',
  117. icon: <IconWarning size="xs" />,
  118. };
  119. },
  120. 'replay.mutations': (frame: MutationFrame) => ({
  121. color: 'yellow300',
  122. description: frame.data.limit
  123. ? t(
  124. 'A large number of mutations was detected (%s). Replay is now stopped to prevent poor performance for your customer.',
  125. frame.data.count
  126. )
  127. : t(
  128. 'A large number of mutations was detected (%s). This can slow down the Replay SDK and impact your customers.',
  129. frame.data.count
  130. ),
  131. tabKey: TabKey.DOM,
  132. title: 'Replay',
  133. icon: <IconWarning size="xs" />,
  134. }),
  135. 'ui.click': frame => ({
  136. color: 'purple300',
  137. description: frame.message ?? '',
  138. tabKey: TabKey.DOM,
  139. title: 'User Click',
  140. icon: <IconCursorArrow size="xs" />,
  141. }),
  142. 'ui.input': () => ({
  143. color: 'purple300',
  144. description: 'User Action',
  145. tabKey: TabKey.DOM,
  146. title: 'User Input',
  147. icon: <IconInput size="xs" />,
  148. }),
  149. 'ui.keyDown': () => ({
  150. color: 'purple300',
  151. description: 'User Action',
  152. tabKey: TabKey.DOM,
  153. title: 'User KeyDown',
  154. icon: <IconKeyDown size="xs" />,
  155. }),
  156. 'ui.blur': () => ({
  157. color: 'purple300',
  158. description: 'User Action',
  159. tabKey: TabKey.DOM,
  160. title: 'User Blur',
  161. icon: <IconUser size="xs" />,
  162. }),
  163. 'ui.focus': () => ({
  164. color: 'purple300',
  165. description: 'User Action',
  166. tabKey: TabKey.DOM,
  167. title: 'User Focus',
  168. icon: <IconUser size="xs" />,
  169. }),
  170. console: frame => ({
  171. color: 'gray300',
  172. description: frame.message ?? '',
  173. tabKey: TabKey.CONSOLE,
  174. title: 'Console',
  175. icon: <IconFix size="xs" />,
  176. }),
  177. 'navigation.navigate': frame => ({
  178. color: 'green300',
  179. description: stripOrigin(frame.description),
  180. tabKey: TabKey.NETWORK,
  181. title: 'Page Load',
  182. icon: <IconLocation size="xs" />,
  183. }),
  184. 'navigation.reload': frame => ({
  185. color: 'green300',
  186. description: stripOrigin(frame.description),
  187. tabKey: TabKey.NETWORK,
  188. title: 'Reload',
  189. icon: <IconLocation size="xs" />,
  190. }),
  191. 'navigation.back_forward': frame => ({
  192. color: 'green300',
  193. description: stripOrigin(frame.description),
  194. tabKey: TabKey.NETWORK,
  195. title: 'Navigate Back/Forward',
  196. icon: <IconLocation size="xs" />,
  197. }),
  198. 'navigation.push': frame => ({
  199. color: 'green300',
  200. description: stripOrigin(frame.description),
  201. tabKey: TabKey.NETWORK,
  202. title: 'Navigation',
  203. icon: <IconLocation size="xs" />,
  204. }),
  205. 'largest-contentful-paint': (frame: LargestContentfulPaintFrame) => ({
  206. color: 'gray300',
  207. description:
  208. typeof frame.data.value === 'number' ? (
  209. `${Math.round(frame.data.value)}ms`
  210. ) : (
  211. <Tooltip
  212. title={t(
  213. '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.'
  214. )}
  215. >
  216. <IconWarning />
  217. </Tooltip>
  218. ),
  219. tabKey: TabKey.NETWORK,
  220. title: 'LCP',
  221. icon: <IconInfo size="xs" />,
  222. }),
  223. memory: () => ({
  224. color: 'gray300',
  225. description: undefined,
  226. tabKey: TabKey.MEMORY,
  227. title: 'Memory',
  228. icon: <IconInfo size="xs" />,
  229. }),
  230. paint: () => ({
  231. color: 'gray300',
  232. description: undefined,
  233. tabKey: TabKey.NETWORK,
  234. title: 'Paint',
  235. icon: <IconInfo size="xs" />,
  236. }),
  237. 'resource.css': frame => ({
  238. color: 'gray300',
  239. description: undefined,
  240. tabKey: TabKey.NETWORK,
  241. title: frame.description,
  242. icon: <IconSort size="xs" rotated />,
  243. }),
  244. 'resource.fetch': frame => ({
  245. color: 'gray300',
  246. description: undefined,
  247. tabKey: TabKey.NETWORK,
  248. title: frame.description,
  249. icon: <IconSort size="xs" rotated />,
  250. }),
  251. 'resource.iframe': frame => ({
  252. color: 'gray300',
  253. description: undefined,
  254. tabKey: TabKey.NETWORK,
  255. title: frame.description,
  256. icon: <IconSort size="xs" rotated />,
  257. }),
  258. 'resource.img': frame => ({
  259. color: 'gray300',
  260. description: undefined,
  261. tabKey: TabKey.NETWORK,
  262. title: frame.description,
  263. icon: <IconSort size="xs" rotated />,
  264. }),
  265. 'resource.link': frame => ({
  266. color: 'gray300',
  267. description: undefined,
  268. tabKey: TabKey.NETWORK,
  269. title: frame.description,
  270. icon: <IconSort size="xs" rotated />,
  271. }),
  272. 'resource.other': frame => ({
  273. color: 'gray300',
  274. description: undefined,
  275. tabKey: TabKey.NETWORK,
  276. title: frame.description,
  277. icon: <IconSort size="xs" rotated />,
  278. }),
  279. 'resource.script': frame => ({
  280. color: 'gray300',
  281. description: undefined,
  282. tabKey: TabKey.NETWORK,
  283. title: frame.description,
  284. icon: <IconSort size="xs" rotated />,
  285. }),
  286. 'resource.xhr': frame => ({
  287. color: 'gray300',
  288. description: undefined,
  289. tabKey: TabKey.NETWORK,
  290. title: frame.description,
  291. icon: <IconSort size="xs" rotated />,
  292. }),
  293. };
  294. const MAPPER_DEFAULT = (frame): Details => ({
  295. color: 'gray300',
  296. description: frame.message ?? '',
  297. tabKey: TabKey.CONSOLE,
  298. title: defaultTitle(frame),
  299. icon: <IconTerminal size="xs" />,
  300. });
  301. export default function getFrameDetails(frame: ReplayFrame): Details {
  302. const key = getFrameOpOrCategory(frame);
  303. const fn = MAPPER_FOR_FRAME[key] ?? MAPPER_DEFAULT;
  304. try {
  305. return fn(frame);
  306. } catch (error) {
  307. return MAPPER_DEFAULT(frame);
  308. }
  309. }
  310. function defaultTitle(frame: ReplayFrame) {
  311. if ('category' in frame) {
  312. const [type, action] = frame.category.split('.');
  313. return `${type} ${action || ''}`.trim();
  314. }
  315. if ('message' in frame) {
  316. return frame.message as string; // TODO(replay): Included for backwards compat
  317. }
  318. return frame.description ?? '';
  319. }
  320. function stringifyNodeAttributes(node: SlowClickFrame['data']['node']) {
  321. const {tagName, attributes} = node ?? {};
  322. const attributesEntries = Object.entries(attributes ?? {});
  323. return `${tagName}${
  324. attributesEntries.length
  325. ? attributesEntries.map(([attr, val]) => `[${attr}="${val}"]`).join('')
  326. : ''
  327. }`;
  328. }