traceTimeline.spec.tsx 8.8 KB

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