getFrameDetails.tsx 8.7 KB

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