replayDataUtils.tsx 7.6 KB

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