utils.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import omit from 'lodash/omit';
  2. import moment from 'moment-timezone';
  3. import {getTraceDateTimeRange} from 'sentry/components/events/interfaces/spans/utils';
  4. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  5. import {OrganizationSummary} from 'sentry/types';
  6. import {Event, EventTransaction} from 'sentry/types/event';
  7. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
  10. import {
  11. QuickTrace,
  12. QuickTraceEvent,
  13. TraceFull,
  14. TraceFullDetailed,
  15. TraceLite,
  16. } from 'sentry/utils/performance/quickTrace/types';
  17. export function isTransaction(event: Event): event is EventTransaction {
  18. return event.type === 'transaction';
  19. }
  20. /**
  21. * An event can be an error or a transaction. We need to check whether the current
  22. * event id is in the list of errors as well
  23. */
  24. export function isCurrentEvent(
  25. event: TraceFull | QuickTraceEvent,
  26. currentEvent: Event
  27. ): boolean {
  28. if (isTransaction(currentEvent)) {
  29. return event.event_id === currentEvent.id;
  30. }
  31. return (
  32. event.errors !== undefined && event.errors.some(e => e.event_id === currentEvent.id)
  33. );
  34. }
  35. type PathNode = {
  36. event: TraceFull;
  37. path: TraceLite;
  38. };
  39. /**
  40. * The `events-full` endpoint returns the full trace containing the specified event.
  41. * This means any sibling paths in the trace will also be returned.
  42. *
  43. * This method strips away these sibling paths leaving only the path from the root to
  44. * the specified event and all of its children/descendants.
  45. *
  46. * This method additionally flattens the trace into an array of the transactions in
  47. * the trace.
  48. */
  49. export function flattenRelevantPaths(
  50. currentEvent: Event,
  51. traceFull: TraceFull
  52. ): TraceLite {
  53. const relevantPath: TraceLite = [];
  54. const events: TraceFull[] = [];
  55. /**
  56. * First find a path from the root transaction to the current transaction via
  57. * a breadth first search. This adds all transactions from the root to the
  58. * current transaction (excluding the current transaction itself), to the
  59. * relevant path.
  60. */
  61. const paths: PathNode[] = [{event: traceFull, path: []}];
  62. while (paths.length) {
  63. const current = paths.shift()!;
  64. if (isCurrentEvent(current.event, currentEvent)) {
  65. for (const node of current.path) {
  66. relevantPath.push(node);
  67. }
  68. events.push(current.event);
  69. } else {
  70. const path = [...current.path, simplifyEvent(current.event)];
  71. for (const child of current.event.children) {
  72. paths.push({event: child, path});
  73. }
  74. }
  75. }
  76. if (!events.length) {
  77. throw new Error('No relevant path exists!');
  78. }
  79. /**
  80. * Traverse all transactions from current transaction onwards and add
  81. * them all to the relevant path.
  82. */
  83. while (events.length) {
  84. const current = events.shift()!;
  85. for (const child of current.children) {
  86. events.push(child);
  87. }
  88. relevantPath.push(simplifyEvent(current));
  89. }
  90. return relevantPath;
  91. }
  92. function simplifyEvent(event: TraceFull): QuickTraceEvent {
  93. return omit(event, ['children']);
  94. }
  95. type ParsedQuickTrace = {
  96. /**
  97. * `[]` represents the lack of ancestors in a full trace navigator
  98. * `null` represents the uncertainty of ancestors in a lite trace navigator
  99. */
  100. ancestors: QuickTraceEvent[] | null;
  101. /**
  102. * `[]` represents the lack of children in a full/lite trace navigator
  103. */
  104. children: QuickTraceEvent[];
  105. current: QuickTraceEvent;
  106. /**
  107. * `[]` represents the lack of descendants in a full trace navigator
  108. * `null` represents the uncertainty of descendants in a lite trace navigator
  109. */
  110. descendants: QuickTraceEvent[] | null;
  111. /**
  112. * `null` represents either the lack of a direct parent or the uncertainty
  113. * of what the parent is
  114. */
  115. parent: QuickTraceEvent | null;
  116. /**
  117. * `null` represents the lack of a root. It may still have a parent
  118. */
  119. root: QuickTraceEvent | null;
  120. };
  121. export function parseQuickTrace(
  122. quickTrace: QuickTrace,
  123. event: Event,
  124. organization: OrganizationSummary
  125. ): ParsedQuickTrace | null {
  126. const {type, trace} = quickTrace;
  127. if (type === 'empty' || trace === null) {
  128. throw new Error('Current event not in trace navigator!');
  129. }
  130. const isFullTrace = type === 'full';
  131. const current = trace.find(e => isCurrentEvent(e, event)) ?? null;
  132. if (current === null) {
  133. throw new Error('Current event not in trace navigator!');
  134. }
  135. /**
  136. * The parent event is the direct ancestor of the current event.
  137. * This takes priority over the root, meaning if the parent is
  138. * the root of the trace, this favours showing it as the parent.
  139. */
  140. const parent = current.parent_event_id
  141. ? trace.find(e => e.event_id === current.parent_event_id) ?? null
  142. : null;
  143. /**
  144. * The root event is the first event in the trace. This has lower priority
  145. * than the parent event, meaning if the root event is the parent event of
  146. * the current event, this favours showing it as the parent event.
  147. */
  148. const root =
  149. trace.find(
  150. e =>
  151. // a root can't be the current event
  152. e.event_id !== current.event_id &&
  153. // a root can't be the direct parent
  154. e.event_id !== parent?.event_id &&
  155. // a root has to to be the first generation
  156. e.generation === 0
  157. ) ?? null;
  158. const isChildren = e => e.parent_event_id === current.event_id;
  159. const isDescendant = e =>
  160. // the current generation needs to be known to determine a descendant
  161. current.generation !== null &&
  162. // the event's generation needs to be known to determine a descendant
  163. e.generation !== null &&
  164. // a descendant is the generation after the direct children
  165. current.generation + 1 < e.generation;
  166. const isAncestor = e =>
  167. // the current generation needs to be known to determine an ancestor
  168. current.generation !== null &&
  169. // the event's generation needs to be known to determine an ancestor
  170. e.generation !== null &&
  171. // an ancestor can't be the root
  172. e.generation > 0 &&
  173. // an ancestor is the generation before the direct parent
  174. current.generation - 1 > e.generation;
  175. const ancestors: TraceLite | null = isFullTrace ? [] : null;
  176. const children: TraceLite = [];
  177. const descendants: TraceLite | null = isFullTrace ? [] : null;
  178. const projects = new Set();
  179. trace.forEach(e => {
  180. projects.add(e.project_id);
  181. if (isChildren(e)) {
  182. children.push(e);
  183. } else if (isFullTrace) {
  184. if (isAncestor(e)) {
  185. ancestors?.push(e);
  186. } else if (isDescendant(e)) {
  187. descendants?.push(e);
  188. }
  189. }
  190. });
  191. if (isFullTrace && projects.size > 1) {
  192. handleProjectMeta(organization, projects.size);
  193. }
  194. return {
  195. root,
  196. ancestors: ancestors === null ? null : sortTraceLite(ancestors),
  197. parent,
  198. current,
  199. children: sortTraceLite(children),
  200. descendants: descendants === null ? null : sortTraceLite(descendants),
  201. };
  202. }
  203. function sortTraceLite(trace: TraceLite): TraceLite {
  204. return trace.sort((a, b) => b['transaction.duration'] - a['transaction.duration']);
  205. }
  206. export function getTraceRequestPayload({
  207. eventView,
  208. location,
  209. }: Pick<DiscoverQueryProps, 'eventView' | 'location'>) {
  210. return omit(eventView.getEventsAPIPayload(location), ['field', 'sort', 'per_page']);
  211. }
  212. export function makeEventView({
  213. start,
  214. end,
  215. statsPeriod,
  216. }: {
  217. end?: string;
  218. start?: string;
  219. statsPeriod?: string | null;
  220. }) {
  221. return EventView.fromSavedQuery({
  222. id: undefined,
  223. version: 2,
  224. name: '',
  225. // This field doesn't actually do anything,
  226. // just here to satisfy a constraint in EventView.
  227. fields: ['transaction.duration'],
  228. projects: [ALL_ACCESS_PROJECTS],
  229. query: '',
  230. environment: [],
  231. start,
  232. end,
  233. range: statsPeriod ?? undefined,
  234. });
  235. }
  236. export function getTraceTimeRangeFromEvent(event: Event): {end: string; start: string} {
  237. const start = isTransaction(event)
  238. ? event.startTimestamp
  239. : moment(event.dateReceived ? event.dateReceived : event.dateCreated).valueOf() /
  240. 1000;
  241. const end = isTransaction(event) ? event.endTimestamp : start;
  242. return getTraceDateTimeRange({start, end});
  243. }
  244. export function reduceTrace<T>(
  245. trace: TraceFullDetailed,
  246. visitor: (acc: T, e: TraceFullDetailed) => T,
  247. initialValue: T
  248. ): T {
  249. let result = initialValue;
  250. const events = [trace];
  251. while (events.length) {
  252. const current = events.pop()!;
  253. for (const child of current.children) {
  254. events.push(child);
  255. }
  256. result = visitor(result, current);
  257. }
  258. return result;
  259. }
  260. export function filterTrace(
  261. trace: TraceFullDetailed,
  262. predicate: (transaction: TraceFullDetailed) => boolean
  263. ): TraceFullDetailed[] {
  264. return reduceTrace<TraceFullDetailed[]>(
  265. trace,
  266. (transactions, transaction) => {
  267. if (predicate(transaction)) {
  268. transactions.push(transaction);
  269. }
  270. return transactions;
  271. },
  272. []
  273. );
  274. }
  275. export function isTraceFull(transaction): transaction is TraceFull {
  276. return Boolean((transaction as TraceFull).event_id);
  277. }
  278. export function isTraceFullDetailed(transaction): transaction is TraceFullDetailed {
  279. return Boolean((transaction as TraceFullDetailed).event_id);
  280. }
  281. function handleProjectMeta(organization: OrganizationSummary, projects: number) {
  282. trackAnalyticsEvent({
  283. eventKey: 'quick_trace.connected_services',
  284. eventName: 'Quick Trace: Connected Services',
  285. organization_id: parseInt(organization.id, 10),
  286. projects,
  287. });
  288. }