utils.tsx 9.6 KB

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