traceTimeline.spec.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {EventFixture} from 'sentry-fixture/event';
  2. import {OrganizationFixture} from 'sentry-fixture/organization';
  3. import {ProjectFixture} from 'sentry-fixture/project';
  4. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import ProjectsStore from 'sentry/stores/projectsStore';
  6. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  7. import {TraceTimeline} from './traceTimeline';
  8. import type {TraceEventResponse} from './useTraceTimelineEvents';
  9. jest.mock('sentry/utils/routeAnalytics/useRouteAnalyticsParams');
  10. describe('TraceTimeline', () => {
  11. const organization = OrganizationFixture();
  12. // This creates the ApiException event
  13. const event = EventFixture({
  14. dateCreated: '2024-01-24T09:09:03+00:00',
  15. contexts: {
  16. trace: {
  17. trace_id: '123',
  18. },
  19. },
  20. });
  21. const project = ProjectFixture();
  22. const emptyBody: TraceEventResponse = {data: [], meta: {fields: {}, units: {}}};
  23. const issuePlatformBody: TraceEventResponse = {
  24. data: [
  25. {
  26. timestamp: '2024-01-24T09:09:03+00:00',
  27. 'issue.id': 1000,
  28. project: project.slug,
  29. 'project.name': project.name,
  30. title: 'Slow DB Query',
  31. id: 'abc',
  32. transaction: '/api/slow/',
  33. },
  34. ],
  35. meta: {fields: {}, units: {}},
  36. };
  37. const discoverBody: TraceEventResponse = {
  38. data: [
  39. {
  40. timestamp: '2024-01-23T22:11:42+00:00',
  41. 'issue.id': 4909507143,
  42. project: project.slug,
  43. 'project.name': project.name,
  44. title: 'AttributeError: Something Failed',
  45. id: event.id,
  46. transaction: 'important.task',
  47. 'event.type': 'error',
  48. 'stack.function': ['important.task', 'task.run'],
  49. },
  50. ],
  51. meta: {fields: {}, units: {}},
  52. };
  53. beforeEach(() => {
  54. ProjectsStore.loadInitialData([project]);
  55. jest.clearAllMocks();
  56. });
  57. it('renders items and highlights the current event', async () => {
  58. MockApiClient.addMockResponse({
  59. url: `/organizations/${organization.slug}/events/`,
  60. body: issuePlatformBody,
  61. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  62. });
  63. MockApiClient.addMockResponse({
  64. url: `/organizations/${organization.slug}/events/`,
  65. body: discoverBody,
  66. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  67. });
  68. render(<TraceTimeline event={event} />, {organization});
  69. expect(await screen.findByLabelText('Current Event')).toBeInTheDocument();
  70. await userEvent.hover(screen.getByTestId('trace-timeline-tooltip-1'));
  71. expect(await screen.findByText('You are here')).toBeInTheDocument();
  72. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  73. trace_timeline_status: 'shown',
  74. });
  75. });
  76. it('displays nothing if the only event is the current event', async () => {
  77. MockApiClient.addMockResponse({
  78. url: `/organizations/${organization.slug}/events/`,
  79. body: emptyBody,
  80. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  81. });
  82. MockApiClient.addMockResponse({
  83. url: `/organizations/${organization.slug}/events/`,
  84. body: discoverBody,
  85. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  86. });
  87. const {container} = render(<TraceTimeline event={event} />, {organization});
  88. await waitFor(() =>
  89. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  90. trace_timeline_status: 'empty',
  91. })
  92. );
  93. expect(container).toBeEmptyDOMElement();
  94. });
  95. it('displays nothing if there are no events', async () => {
  96. MockApiClient.addMockResponse({
  97. url: `/organizations/${organization.slug}/events/`,
  98. body: emptyBody,
  99. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  100. });
  101. MockApiClient.addMockResponse({
  102. url: `/organizations/${organization.slug}/events/`,
  103. body: emptyBody,
  104. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  105. });
  106. const {container} = render(<TraceTimeline event={event} />, {organization});
  107. await waitFor(() =>
  108. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  109. trace_timeline_status: 'empty',
  110. })
  111. );
  112. expect(container).toBeEmptyDOMElement();
  113. });
  114. it('shows seconds for very short timelines', async () => {
  115. MockApiClient.addMockResponse({
  116. url: `/organizations/${organization.slug}/events/`,
  117. body: issuePlatformBody,
  118. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  119. });
  120. MockApiClient.addMockResponse({
  121. url: `/organizations/${organization.slug}/events/`,
  122. body: emptyBody,
  123. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  124. });
  125. render(<TraceTimeline event={event} />, {organization});
  126. // Checking for the presence of seconds
  127. expect(await screen.findAllByText(/\d{1,2}:\d{2}:\d{2} (AM|PM)/)).toHaveLength(5);
  128. });
  129. it('adds the current event if not in the api response', async () => {
  130. MockApiClient.addMockResponse({
  131. url: `/organizations/${organization.slug}/events/`,
  132. body: issuePlatformBody,
  133. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  134. });
  135. MockApiClient.addMockResponse({
  136. url: `/organizations/${organization.slug}/events/`,
  137. body: emptyBody,
  138. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  139. });
  140. render(<TraceTimeline event={event} />, {organization});
  141. expect(await screen.findByLabelText('Current Event')).toBeInTheDocument();
  142. });
  143. it('skips the timeline and shows related issues (2 issues)', async () => {
  144. MockApiClient.addMockResponse({
  145. url: `/organizations/${organization.slug}/events/`,
  146. body: issuePlatformBody,
  147. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  148. });
  149. MockApiClient.addMockResponse({
  150. url: `/organizations/${organization.slug}/events/`,
  151. body: emptyBody,
  152. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  153. });
  154. // I believe the call to projects is to determine what projects a user belongs to
  155. MockApiClient.addMockResponse({
  156. url: `/organizations/${organization.slug}/projects/`,
  157. body: [],
  158. });
  159. // We need the metadata from the issue in order to render the related issue
  160. MockApiClient.addMockResponse({
  161. url: `/organizations/${organization.slug}/issues/1000/`,
  162. body: {
  163. metadata: {
  164. title: 'Slow DB Query',
  165. value: 'bar',
  166. },
  167. },
  168. });
  169. render(<TraceTimeline event={event} />, {
  170. organization: OrganizationFixture({
  171. features: ['related-issues-issue-details-page'],
  172. }),
  173. });
  174. // Instead of a timeline, we should see the other related issue
  175. expect(await screen.findByText('Slow DB Query')).toBeInTheDocument();
  176. expect(screen.queryByLabelText('Current Event')).not.toBeInTheDocument();
  177. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  178. trace_timeline_status: 'empty',
  179. });
  180. });
  181. it('skips the timeline and shows NO related issues (only 1 issue)', async () => {
  182. MockApiClient.addMockResponse({
  183. url: `/organizations/${organization.slug}/events/`,
  184. body: emptyBody,
  185. match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})],
  186. });
  187. MockApiClient.addMockResponse({
  188. url: `/organizations/${organization.slug}/events/`,
  189. // Only 1 issue
  190. body: discoverBody,
  191. match: [MockApiClient.matchQuery({dataset: 'discover'})],
  192. });
  193. // I believe the call to projects is to determine what projects a user belongs to
  194. MockApiClient.addMockResponse({
  195. url: `/organizations/${organization.slug}/projects/`,
  196. body: [],
  197. });
  198. render(<TraceTimeline event={event} />, {
  199. organization: OrganizationFixture({
  200. features: ['related-issues-issue-details-page'],
  201. }),
  202. });
  203. // We do not display any related issues because we only have 1 issue
  204. expect(await screen.queryByText('Slow DB Query')).not.toBeInTheDocument();
  205. expect(
  206. await screen.queryByText('AttributeError: Something Failed')
  207. ).not.toBeInTheDocument();
  208. // We do not display the timeline because we only have 1 event
  209. expect(await screen.queryByLabelText('Current Event')).not.toBeInTheDocument();
  210. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  211. trace_timeline_status: 'empty',
  212. });
  213. });
  214. });