traceTimelineOrRelatedIssue.spec.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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/traceTimeline';
  9. import type {TraceEventResponse} from './traceTimeline/useTraceTimelineEvents';
  10. import {TraceTimeLineOrRelatedIssue} from './traceTimelineOrRelatedIssue';
  11. jest.mock('sentry/utils/routeAnalytics/useRouteAnalyticsParams');
  12. jest.mock('sentry/utils/analytics');
  13. describe('TraceTimeline & TraceRelated Issue', () => {
  14. // Paid plans have global-views enabled
  15. // Include project: -1 in all matchQuery calls to ensure we are looking at all projects
  16. const organization = OrganizationFixture({
  17. features: ['global-views'],
  18. });
  19. const firstEventTimestamp = '2024-01-24T09:09:01+00:00';
  20. // This creates the ApiException event
  21. const event = EventFixture({
  22. // This is used to determine the presence of seconds
  23. dateCreated: firstEventTimestamp,
  24. contexts: {
  25. trace: {
  26. // This is used to determine if we should attempt
  27. // to render the trace timeline
  28. trace_id: '123',
  29. },
  30. },
  31. });
  32. const project = ProjectFixture();
  33. const emptyBody: TraceEventResponse = {data: [], meta: {fields: {}, units: {}}};
  34. const issuePlatformBody: TraceEventResponse = {
  35. data: [
  36. {
  37. // In issuePlatform, the message contains the title and the transaction
  38. message: '/api/slow/ Slow DB Query SELECT "sentry_monitorcheckin"."monitor_id"',
  39. timestamp: '2024-01-24T09:09:03+00:00',
  40. 'issue.id': 1000,
  41. project: project.slug,
  42. 'project.name': project.name,
  43. title: 'Slow DB Query',
  44. id: 'abc',
  45. transaction: '/api/slow/',
  46. },
  47. ],
  48. meta: {fields: {}, units: {}},
  49. };
  50. const mainError = {
  51. message: 'This is the message for the issue',
  52. timestamp: firstEventTimestamp,
  53. 'issue.id': event['issue.id'],
  54. project: project.slug,
  55. 'project.name': project.name,
  56. title: event.title,
  57. id: event.id,
  58. transaction: 'important.task',
  59. 'event.type': event.type,
  60. 'stack.function': ['important.task', 'task.run'],
  61. };
  62. const secondError = {
  63. message: 'Message of the second issue',
  64. timestamp: '2024-01-24T09:09:04+00:00',
  65. 'issue.id': 9999,
  66. project: project.slug,
  67. 'project.name': project.name,
  68. title: 'someTitle',
  69. id: '12345',
  70. transaction: 'foo',
  71. 'event.type': event.type,
  72. };
  73. const discoverBody: TraceEventResponse = {
  74. data: [mainError],
  75. meta: {fields: {}, units: {}},
  76. };
  77. const twoIssuesBody: TraceEventResponse = {
  78. data: [mainError, secondError],
  79. meta: {fields: {}, units: {}},
  80. };
  81. beforeEach(() => {
  82. ProjectsStore.loadInitialData([project]);
  83. jest.clearAllMocks();
  84. });
  85. it('renders items and highlights the current event', async () => {
  86. MockApiClient.addMockResponse({
  87. url: `/organizations/${organization.slug}/events/`,
  88. body: issuePlatformBody,
  89. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  90. });
  91. MockApiClient.addMockResponse({
  92. url: `/organizations/${organization.slug}/events/`,
  93. body: twoIssuesBody,
  94. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  95. });
  96. render(<TraceTimeLineOrRelatedIssue event={event} />, {organization});
  97. expect(await screen.findByLabelText('Current Event')).toBeInTheDocument();
  98. await userEvent.hover(screen.getByTestId('trace-timeline-tooltip-1'));
  99. expect(await screen.findByText('You are here')).toBeInTheDocument();
  100. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  101. trace_timeline_status: 'shown',
  102. });
  103. });
  104. it('displays nothing if the only event is the current event', async () => {
  105. MockApiClient.addMockResponse({
  106. url: `/organizations/${organization.slug}/events/`,
  107. body: emptyBody,
  108. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  109. });
  110. MockApiClient.addMockResponse({
  111. url: `/organizations/${organization.slug}/events/`,
  112. body: discoverBody,
  113. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  114. });
  115. const {container} = render(<TraceTimeline event={event} />, {organization});
  116. await waitFor(() =>
  117. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  118. trace_timeline_status: 'empty',
  119. })
  120. );
  121. expect(container).toBeEmptyDOMElement();
  122. });
  123. it('displays nothing if there are no events', async () => {
  124. MockApiClient.addMockResponse({
  125. url: `/organizations/${organization.slug}/events/`,
  126. body: emptyBody,
  127. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  128. });
  129. MockApiClient.addMockResponse({
  130. url: `/organizations/${organization.slug}/events/`,
  131. body: emptyBody,
  132. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  133. });
  134. const {container} = render(<TraceTimeline event={event} />, {organization});
  135. await waitFor(() =>
  136. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  137. trace_timeline_status: 'empty',
  138. })
  139. );
  140. expect(container).toBeEmptyDOMElement();
  141. });
  142. it('shows seconds for very short timelines', async () => {
  143. MockApiClient.addMockResponse({
  144. url: `/organizations/${organization.slug}/events/`,
  145. body: issuePlatformBody,
  146. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  147. });
  148. MockApiClient.addMockResponse({
  149. url: `/organizations/${organization.slug}/events/`,
  150. body: twoIssuesBody,
  151. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  152. });
  153. render(<TraceTimeLineOrRelatedIssue event={event} />, {organization});
  154. // Checking for the presence of seconds
  155. expect(await screen.findAllByText(/\d{1,2}:\d{2}:\d{2} (AM|PM)/)).toHaveLength(5);
  156. });
  157. // useTraceTimelineEvents() adds the current event if missing
  158. it('adds the current event if not in the api response', async () => {
  159. MockApiClient.addMockResponse({
  160. url: `/organizations/${organization.slug}/events/`,
  161. body: issuePlatformBody,
  162. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  163. });
  164. MockApiClient.addMockResponse({
  165. url: `/organizations/${organization.slug}/events/`,
  166. body: {
  167. // The event for the mainError is missing, thus, it will get added
  168. data: [secondError],
  169. },
  170. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  171. });
  172. render(<TraceTimeLineOrRelatedIssue event={event} />, {organization});
  173. expect(await screen.findByLabelText('Current Event')).toBeInTheDocument();
  174. });
  175. it('skips the timeline and shows related issues (2 issues)', async () => {
  176. MockApiClient.addMockResponse({
  177. url: `/organizations/${organization.slug}/events/`,
  178. body: issuePlatformBody,
  179. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  180. });
  181. MockApiClient.addMockResponse({
  182. url: `/organizations/${organization.slug}/events/`,
  183. body: discoverBody,
  184. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  185. });
  186. // Used to determine the project badge
  187. MockApiClient.addMockResponse({
  188. url: `/organizations/${organization.slug}/projects/`,
  189. body: [],
  190. });
  191. render(<TraceTimeLineOrRelatedIssue event={event} />, {organization});
  192. // Instead of a timeline, we should see the other related issue
  193. expect(await screen.findByText('Slow DB Query')).toBeInTheDocument(); // The title
  194. expect(await screen.findByText('/api/slow/')).toBeInTheDocument(); // The subtitle/transaction
  195. expect(
  196. await screen.findByText('One other issue appears in the same trace.')
  197. ).toBeInTheDocument();
  198. expect(
  199. await screen.findByText('SELECT "sentry_monitorcheckin"."monitor_id"') // The message
  200. ).toBeInTheDocument();
  201. expect(screen.queryByLabelText('Current Event')).not.toBeInTheDocument();
  202. // Test analytics
  203. await userEvent.click(await screen.findByText('Slow DB Query'));
  204. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({
  205. has_related_trace_issue: true,
  206. });
  207. expect(trackAnalytics).toHaveBeenCalledTimes(1);
  208. expect(trackAnalytics).toHaveBeenCalledWith(
  209. 'issue_details.related_trace_issue.trace_issue_clicked',
  210. {
  211. group_id: issuePlatformBody.data[0]['issue.id'],
  212. organization: organization,
  213. }
  214. );
  215. });
  216. it('skips the timeline and shows NO related issues (only 1 issue)', async () => {
  217. MockApiClient.addMockResponse({
  218. url: `/organizations/${organization.slug}/events/`,
  219. body: emptyBody,
  220. match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})],
  221. });
  222. MockApiClient.addMockResponse({
  223. url: `/organizations/${organization.slug}/events/`,
  224. // Only 1 issue
  225. body: discoverBody,
  226. match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})],
  227. });
  228. // Used to determine the project badge
  229. MockApiClient.addMockResponse({
  230. url: `/organizations/${organization.slug}/projects/`,
  231. body: [],
  232. });
  233. render(<TraceTimeLineOrRelatedIssue event={event} />, {organization});
  234. // We do not display any related issues because we only have 1 issue
  235. expect(await screen.queryByText('Slow DB Query')).not.toBeInTheDocument();
  236. expect(
  237. await screen.queryByText('AttributeError: Something Failed')
  238. ).not.toBeInTheDocument();
  239. // We do not display the timeline because we only have 1 event
  240. expect(await screen.queryByLabelText('Current Event')).not.toBeInTheDocument();
  241. expect(useRouteAnalyticsParams).toHaveBeenCalledWith({});
  242. });
  243. it('trace timeline works for plans with no global-views feature', async () => {
  244. // This test will call the endpoint without the global-views feature, thus,
  245. // we will only look at the current project (project: event.projectID) instead of passing -1
  246. MockApiClient.addMockResponse({
  247. url: `/organizations/${organization.slug}/events/`,
  248. body: issuePlatformBody,
  249. match: [
  250. MockApiClient.matchQuery({
  251. dataset: 'issuePlatform',
  252. project: event.projectID,
  253. }),
  254. ],
  255. });
  256. MockApiClient.addMockResponse({
  257. url: `/organizations/${organization.slug}/events/`,
  258. body: twoIssuesBody,
  259. match: [
  260. MockApiClient.matchQuery({
  261. dataset: 'discover',
  262. project: event.projectID,
  263. }),
  264. ],
  265. });
  266. render(<TraceTimeLineOrRelatedIssue event={event} />, {
  267. organization: OrganizationFixture({
  268. features: [],
  269. }),
  270. });
  271. expect(await screen.findByLabelText('Current Event')).toBeInTheDocument();
  272. });
  273. it('trace-related issue works for plans with no global-views feature', async () => {
  274. // This test will call the endpoint without the global-views feature, thus,
  275. // we will only look at the current project (project: event.projectID) instead of passing -1
  276. MockApiClient.addMockResponse({
  277. url: `/organizations/${organization.slug}/events/`,
  278. body: issuePlatformBody,
  279. match: [
  280. MockApiClient.matchQuery({
  281. dataset: 'issuePlatform',
  282. project: event.projectID,
  283. }),
  284. ],
  285. });
  286. MockApiClient.addMockResponse({
  287. url: `/organizations/${organization.slug}/events/`,
  288. body: discoverBody,
  289. match: [
  290. MockApiClient.matchQuery({
  291. dataset: 'discover',
  292. project: event.projectID,
  293. }),
  294. ],
  295. });
  296. // Used to determine the project badge
  297. MockApiClient.addMockResponse({
  298. url: `/organizations/${organization.slug}/projects/`,
  299. body: [],
  300. });
  301. render(<TraceTimeLineOrRelatedIssue event={event} />, {
  302. organization: OrganizationFixture({
  303. features: [],
  304. }),
  305. });
  306. expect(await screen.findByText('Slow DB Query')).toBeInTheDocument();
  307. });
  308. });