useTrace.tsx 6.8 KB

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