useTrace.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import {useMemo} from 'react';
  2. import type {Location} from 'history';
  3. import * as qs from 'query-string';
  4. import type {Client} from 'sentry/api';
  5. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  6. import type {PageFilters} from 'sentry/types/core';
  7. import type {EventTransaction} from 'sentry/types/event';
  8. import type {
  9. TraceFullDetailed,
  10. TraceSplitResults,
  11. } from 'sentry/utils/performance/quickTrace/types';
  12. import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import usePageFilters from 'sentry/utils/usePageFilters';
  16. import type {TraceTree} from '../traceModels/traceTree';
  17. export function fetchTrace(
  18. api: Client,
  19. params: {
  20. orgSlug: string;
  21. query: string;
  22. traceId: string;
  23. }
  24. ): Promise<TraceSplitResults<TraceFullDetailed>> {
  25. return api.requestPromise(
  26. `/organizations/${params.orgSlug}/events-trace/${params.traceId}/?${params.query}`
  27. );
  28. }
  29. const DEFAULT_TIMESTAMP_LIMIT = 10_000;
  30. const DEFAULT_LIMIT = 1_000;
  31. export function getTraceQueryParams(
  32. query: Location['query'],
  33. filters?: Partial<PageFilters>,
  34. options: {limit?: number; timestamp?: number} = {}
  35. ): {
  36. eventId: string | undefined;
  37. limit: number;
  38. timestamp: string | undefined;
  39. useSpans: number;
  40. demo?: string | undefined;
  41. pageEnd?: string | undefined;
  42. pageStart?: string | undefined;
  43. statsPeriod?: string | undefined;
  44. } {
  45. const normalizedParams = normalizeDateTimeParams(query, {
  46. allowAbsolutePageDatetime: true,
  47. });
  48. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  49. const demo = decodeScalar(normalizedParams.demo);
  50. const timestamp = options.timestamp ?? decodeScalar(normalizedParams.timestamp);
  51. let limit = options.limit ?? decodeScalar(normalizedParams.limit);
  52. if (typeof limit === 'string') {
  53. limit = parseInt(limit, 10);
  54. }
  55. const eventId = decodeScalar(normalizedParams.eventId);
  56. if (timestamp) {
  57. limit = limit ?? DEFAULT_TIMESTAMP_LIMIT;
  58. } else {
  59. limit = limit ?? DEFAULT_LIMIT;
  60. }
  61. const otherParams: Record<string, string | string[] | undefined | null> = {
  62. end: normalizedParams.pageEnd,
  63. start: normalizedParams.pageStart,
  64. statsPeriod: statsPeriod || filters?.datetime?.period,
  65. };
  66. // We prioritize timestamp over statsPeriod as it makes the query more specific, faster
  67. // and not prone to time drift issues.
  68. if (timestamp) {
  69. delete otherParams.statsPeriod;
  70. }
  71. const queryParams = {
  72. ...otherParams,
  73. demo,
  74. limit,
  75. timestamp: timestamp?.toString(),
  76. eventId,
  77. useSpans: 1,
  78. };
  79. for (const key in queryParams) {
  80. if (
  81. queryParams[key] === '' ||
  82. queryParams[key] === null ||
  83. queryParams[key] === undefined
  84. ) {
  85. delete queryParams[key];
  86. }
  87. }
  88. return queryParams;
  89. }
  90. function parseDemoEventSlug(
  91. demoEventSlug: string | undefined
  92. ): {event_id: string; project_slug: string} | null {
  93. if (!demoEventSlug) {
  94. return null;
  95. }
  96. const [project_slug, event_id] = demoEventSlug.split(':');
  97. return {project_slug, event_id};
  98. }
  99. function makeTraceFromTransaction(
  100. event: EventTransaction | undefined
  101. ): TraceSplitResults<TraceFullDetailed> | undefined {
  102. if (!event) {
  103. return undefined;
  104. }
  105. const traceContext = event.contexts?.trace;
  106. const transaction = {
  107. event_id: event.eventID,
  108. generation: 0,
  109. parent_event_id: '',
  110. parent_span_id: traceContext?.parent_span_id ?? '',
  111. performance_issues: [],
  112. project_id: Number(event.projectID),
  113. project_slug: event.projectSlug ?? '',
  114. span_id: traceContext?.span_id ?? '',
  115. timestamp: event.endTimestamp,
  116. transaction: event.title,
  117. 'transaction.duration': (event.endTimestamp - event.startTimestamp) * 1000,
  118. errors: [],
  119. sdk_name: event.sdk?.name ?? '',
  120. children: [],
  121. start_timestamp: event.startTimestamp,
  122. 'transaction.op': traceContext?.op ?? '',
  123. 'transaction.status': traceContext?.status ?? '',
  124. measurements: event.measurements ?? {},
  125. tags: [],
  126. };
  127. return {transactions: [transaction], orphan_errors: []};
  128. }
  129. function useDemoTrace(
  130. demo: string | undefined,
  131. organization: {slug: string}
  132. ): UseApiQueryResult<TraceSplitResults<TraceTree.Transaction> | undefined, any> {
  133. const demoEventSlug = parseDemoEventSlug(demo);
  134. // When projects don't have performance set up, we allow them to view a demo transaction.
  135. // The backend creates the demo transaction, however the trace is created async, so when the
  136. // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
  137. // When this happens, we assemble a fake trace response to only include the transaction that had already been
  138. // created and stored already so that the users can visualize in the context of a trace.
  139. const demoEventQuery = useApiQuery<EventTransaction>(
  140. [
  141. `/organizations/${organization.slug}/events/${demoEventSlug?.project_slug}:${demoEventSlug?.event_id}/`,
  142. {
  143. query: {
  144. referrer: 'trace-view',
  145. },
  146. },
  147. ],
  148. {
  149. staleTime: Infinity,
  150. enabled: !!demoEventSlug,
  151. }
  152. );
  153. // Without the useMemo, the trace from the transformed response will be re-created on every render,
  154. // causing the trace view to re-render as we interact with it.
  155. const data = useMemo(() => {
  156. return makeTraceFromTransaction(demoEventQuery.data);
  157. }, [demoEventQuery.data]);
  158. // Casting here since the 'select' option is not available in the useApiQuery hook to transform the data
  159. // from EventTransaction to TraceSplitResults<TraceFullDetailed>
  160. return {...demoEventQuery, data} as UseApiQueryResult<
  161. TraceSplitResults<TraceTree.Transaction> | undefined,
  162. any
  163. >;
  164. }
  165. type UseTraceParams = {
  166. limit?: number;
  167. timestamp?: number;
  168. traceSlug?: string;
  169. };
  170. export function useTrace(
  171. options: UseTraceParams
  172. ): UseApiQueryResult<TraceSplitResults<TraceTree.Transaction> | undefined, any> {
  173. const filters = usePageFilters();
  174. const organization = useOrganization();
  175. const queryParams = useMemo(() => {
  176. const query = qs.parse(location.search);
  177. return getTraceQueryParams(query, filters.selection, {
  178. limit: options.limit,
  179. timestamp: options.timestamp,
  180. });
  181. // eslint-disable-next-line react-hooks/exhaustive-deps
  182. }, [options.limit, options.timestamp]);
  183. const mode = queryParams.demo ? 'demo' : undefined;
  184. const demoTrace = useDemoTrace(queryParams.demo, organization);
  185. const traceQuery = useApiQuery<TraceSplitResults<TraceTree.Transaction>>(
  186. [
  187. `/organizations/${organization.slug}/events-trace/${options.traceSlug ?? ''}/`,
  188. {query: queryParams},
  189. ],
  190. {
  191. staleTime: Infinity,
  192. enabled: !!options.traceSlug && !!organization.slug && mode !== 'demo',
  193. }
  194. );
  195. return mode === 'demo' ? demoTrace : traceQuery;
  196. }