replayDataUtils.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import first from 'lodash/first';
  2. import {duration} from 'moment';
  3. import {transformCrumbs} from 'sentry/components/events/interfaces/breadcrumbs/utils';
  4. import {t} from 'sentry/locale';
  5. import type {
  6. BreadcrumbTypeDefault,
  7. BreadcrumbTypeNavigation,
  8. Crumb,
  9. RawCrumb,
  10. } from 'sentry/types/breadcrumbs';
  11. import {
  12. BreadcrumbLevelType,
  13. BreadcrumbType,
  14. isBreadcrumbTypeDefault,
  15. } from 'sentry/types/breadcrumbs';
  16. import type {
  17. MemorySpanType,
  18. RecordingEvent,
  19. ReplayCrumb,
  20. ReplayError,
  21. ReplayRecord,
  22. ReplaySpan,
  23. } from 'sentry/views/replays/types';
  24. export const isMemorySpan = (span: ReplaySpan): span is MemorySpanType => {
  25. return span.op === 'memory';
  26. };
  27. export const isNetworkSpan = (span: ReplaySpan) => {
  28. return span.op.startsWith('navigation.') || span.op.startsWith('resource.');
  29. };
  30. export const getBreadcrumbsByCategory = (breadcrumbs: Crumb[], categories: string[]) => {
  31. return breadcrumbs
  32. .filter(isBreadcrumbTypeDefault)
  33. .filter(breadcrumb => categories.includes(breadcrumb.category || ''));
  34. };
  35. export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord {
  36. // Add missing tags to the response
  37. const unorderedTags: ReplayRecord['tags'] = {
  38. ...apiResponse.tags,
  39. ...(apiResponse.os?.name ? {'os.name': [apiResponse.os.name]} : {}),
  40. ...(apiResponse.os?.version ? {'os.version': [apiResponse.os.version]} : {}),
  41. ...(apiResponse.browser?.name ? {'browser.name': [apiResponse.browser.name]} : {}),
  42. ...(apiResponse.browser?.version
  43. ? {'browser.version': [apiResponse.browser.version]}
  44. : {}),
  45. ...(apiResponse.device?.name ? {'device.name': [apiResponse.device.name]} : {}),
  46. ...(apiResponse.device?.family ? {'device.family': [apiResponse.device.family]} : {}),
  47. ...(apiResponse.device?.brand ? {'device.brand': [apiResponse.device.brand]} : {}),
  48. ...(apiResponse.device?.model ? {'device.model': [apiResponse.device.model]} : {}),
  49. ...(apiResponse.sdk?.name ? {'sdk.name': [apiResponse.sdk.name]} : {}),
  50. ...(apiResponse.sdk?.version ? {'sdk.version': [apiResponse.sdk.version]} : {}),
  51. ...(apiResponse.user?.ip_address
  52. ? {'user.ip_address': [apiResponse.user.ip_address]}
  53. : {}),
  54. };
  55. // Sort the tags by key
  56. const tags = Object.keys(unorderedTags)
  57. .sort()
  58. .reduce((acc, key) => {
  59. acc[key] = unorderedTags[key];
  60. return acc;
  61. }, {});
  62. return {
  63. ...apiResponse,
  64. ...(apiResponse.startedAt ? {startedAt: new Date(apiResponse.startedAt)} : {}),
  65. ...(apiResponse.finishedAt ? {finishedAt: new Date(apiResponse.finishedAt)} : {}),
  66. ...(apiResponse.duration ? {duration: duration(apiResponse.duration * 1000)} : {}),
  67. tags,
  68. };
  69. }
  70. export function rrwebEventListFactory(
  71. replayRecord: ReplayRecord,
  72. rrwebEvents: RecordingEvent[]
  73. ) {
  74. const events = ([] as RecordingEvent[]).concat(rrwebEvents).concat({
  75. type: 5, // EventType.Custom,
  76. timestamp: replayRecord.finishedAt.getTime(),
  77. data: {
  78. tag: 'replay-end',
  79. },
  80. });
  81. events.sort((a, b) => a.timestamp - b.timestamp);
  82. const firstRRWebEvent = first(events) as RecordingEvent;
  83. firstRRWebEvent.timestamp = replayRecord.startedAt.getTime();
  84. return events;
  85. }
  86. export function breadcrumbFactory(
  87. replayRecord: ReplayRecord,
  88. errors: ReplayError[],
  89. rawCrumbs: ReplayCrumb[],
  90. spans: ReplaySpan[]
  91. ): Crumb[] {
  92. const UNWANTED_CRUMB_CATEGORIES = ['ui.focus', 'ui.blur'];
  93. const initialUrl = replayRecord.tags.url?.join(', ');
  94. const initBreadcrumb = {
  95. type: BreadcrumbType.INIT,
  96. timestamp: replayRecord.startedAt.toISOString(),
  97. level: BreadcrumbLevelType.INFO,
  98. message: initialUrl,
  99. data: {
  100. action: 'replay-init',
  101. label: t('Start recording'),
  102. url: initialUrl,
  103. },
  104. } as BreadcrumbTypeDefault;
  105. const errorCrumbs: RawCrumb[] = errors.map(error => ({
  106. type: BreadcrumbType.ERROR,
  107. level: BreadcrumbLevelType.ERROR,
  108. category: 'issue',
  109. message: error.title,
  110. data: {
  111. label: error['error.type'].join(''),
  112. eventId: error.id,
  113. groupId: error['issue.id'] || 1,
  114. groupShortId: error.issue || 'POKEDEX-4NN',
  115. project: error['project.name'],
  116. },
  117. timestamp: error.timestamp,
  118. }));
  119. const spanCrumbs: (BreadcrumbTypeDefault | BreadcrumbTypeNavigation)[] = spans
  120. .filter(span =>
  121. ['navigation.navigate', 'navigation.reload', 'largest-contentful-paint'].includes(
  122. span.op
  123. )
  124. )
  125. .map(span => {
  126. if (span.op.startsWith('navigation')) {
  127. const [, action] = span.op.split('.');
  128. return {
  129. category: 'default',
  130. type: BreadcrumbType.NAVIGATION,
  131. timestamp: new Date(span.startTimestamp * 1000).toISOString(),
  132. level: BreadcrumbLevelType.INFO,
  133. message: span.description,
  134. action,
  135. data: {
  136. to: span.description,
  137. label:
  138. action === 'reload'
  139. ? t('Reload')
  140. : action === 'navigate'
  141. ? t('Page load')
  142. : t('Navigation'),
  143. ...span.data,
  144. },
  145. };
  146. }
  147. return {
  148. type: BreadcrumbType.DEBUG,
  149. timestamp: new Date(span.startTimestamp * 1000).toISOString(),
  150. level: BreadcrumbLevelType.INFO,
  151. category: 'default',
  152. data: {
  153. action: span.op,
  154. ...span.data,
  155. label: span.op === 'largest-contentful-paint' ? t('LCP') : span.op,
  156. },
  157. };
  158. });
  159. const hasPageLoad = spans.find(span => span.op === 'navigation.navigate');
  160. const rawCrumbsWithTimestamp: RawCrumb[] = rawCrumbs
  161. .filter(crumb => {
  162. return !UNWANTED_CRUMB_CATEGORIES.includes(crumb.category || '');
  163. })
  164. .map(crumb => {
  165. return {
  166. ...crumb,
  167. type: BreadcrumbType.DEFAULT,
  168. timestamp: new Date(crumb.timestamp * 1000).toISOString(),
  169. };
  170. });
  171. const result = transformCrumbs([
  172. ...(!hasPageLoad ? [initBreadcrumb] : []),
  173. ...rawCrumbsWithTimestamp,
  174. ...errorCrumbs,
  175. ...spanCrumbs,
  176. ]);
  177. return result.sort((a, b) => +new Date(a.timestamp || 0) - +new Date(b.timestamp || 0));
  178. }
  179. export function spansFactory(spans: ReplaySpan[]) {
  180. return spans
  181. .sort((a, b) => a.startTimestamp - b.startTimestamp)
  182. .map(span => ({
  183. ...span,
  184. id: `${span.description ?? span.op}-${span.startTimestamp}-${span.endTimestamp}`,
  185. timestamp: span.startTimestamp * 1000,
  186. }));
  187. }
  188. /**
  189. * We need to figure out the real start and end timestamps based on when
  190. * first and last bits of data were collected. In milliseconds.
  191. *
  192. * @deprecated Once the backend returns the corrected timestamps, this is not needed.
  193. */
  194. export function replayTimestamps(
  195. replayRecord: ReplayRecord,
  196. rrwebEvents: RecordingEvent[],
  197. rawCrumbs: ReplayCrumb[],
  198. rawSpanData: ReplaySpan[]
  199. ) {
  200. const rrwebTimestamps = rrwebEvents.map(event => event.timestamp).filter(Boolean);
  201. const breadcrumbTimestamps = (
  202. rawCrumbs.map(rawCrumb => rawCrumb.timestamp).filter(Boolean) as number[]
  203. )
  204. .map(timestamp => +new Date(timestamp * 1000))
  205. .filter(Boolean);
  206. const spanStartTimestamps = rawSpanData.map(span => span.startTimestamp * 1000);
  207. const spanEndTimestamps = rawSpanData.map(span => span.endTimestamp * 1000);
  208. return {
  209. startTimestampMs: Math.min(
  210. replayRecord.startedAt.getTime(),
  211. ...[...rrwebTimestamps, ...breadcrumbTimestamps, ...spanStartTimestamps]
  212. ),
  213. endTimestampMs: Math.max(
  214. replayRecord.finishedAt.getTime(),
  215. ...[...rrwebTimestamps, ...breadcrumbTimestamps, ...spanEndTimestamps]
  216. ),
  217. };
  218. }