From ff7dfe62c8aa6badcdff149c409a72cc3b10e15f Mon Sep 17 00:00:00 2001 From: Armen Zambrano G <44410+armenzg@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:06:24 -0400 Subject: [PATCH 1/3] feat(related_issues): Free tier support for trace timeline and related issues If we fetch the events endpoint without specifying a project it queries across all projects. Organizations without the `global-views` feature (e.g. free plan) would fail to return any events, thus, the trace timeline would not be displayed. This changes the `useTraceTimelineEvents` to always include the `project` query parameter by either setting it to `-1` (which queries all projects) or to the current project (e.g. the organization is on the free plan). This will enable the trace timeline and related issues for free plans for events/issues within the same project. If a trace contains events for other projects, we will only show the link to the trace but not the trace timeline or related issues. --- .../traceTimeline/traceTimeline.spec.tsx | 72 ++++++++++++++----- .../traceTimeline/useTraceTimelineEvents.tsx | 6 ++ 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx b/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx index 05102db90ffe69..b34c2211e4a4fa 100644 --- a/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx @@ -15,7 +15,11 @@ jest.mock('sentry/utils/routeAnalytics/useRouteAnalyticsParams'); jest.mock('sentry/utils/analytics'); describe('TraceTimeline', () => { - const organization = OrganizationFixture(); + // Paid plans have global-views enabled + // Include project: -1 in all matchQuery calls to ensure we are looking at all projects + const organization = OrganizationFixture({ + features: ['global-views'], + }); // This creates the ApiException event const event = EventFixture({ dateCreated: '2024-01-24T09:09:03+00:00', @@ -72,12 +76,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: discoverBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); render(, {organization}); expect(await screen.findByLabelText('Current Event')).toBeInTheDocument(); @@ -94,12 +98,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: discoverBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); const {container} = render(, {organization}); await waitFor(() => @@ -115,12 +119,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); const {container} = render(, {organization}); await waitFor(() => @@ -136,12 +140,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); render(, {organization}); // Checking for the presence of seconds @@ -152,12 +156,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); render(, {organization}); expect(await screen.findByLabelText('Current Event')).toBeInTheDocument(); @@ -167,12 +171,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); // I believe the call to projects is to determine what projects a user belongs to MockApiClient.addMockResponse({ @@ -182,7 +186,7 @@ describe('TraceTimeline', () => { render(, { organization: OrganizationFixture({ - features: ['related-issues-issue-details-page'], + features: ['related-issues-issue-details-page', 'global-views'], }), }); @@ -201,7 +205,7 @@ describe('TraceTimeline', () => { { group_id: issuePlatformBody.data[0]['issue.id'], organization: OrganizationFixture({ - features: ['related-issues-issue-details-page'], + features: ['related-issues-issue-details-page', 'global-views'], }), } ); @@ -211,13 +215,13 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, // Only 1 issue body: discoverBody, - match: [MockApiClient.matchQuery({dataset: 'discover'})], + match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], }); // I believe the call to projects is to determine what projects a user belongs to MockApiClient.addMockResponse({ @@ -227,7 +231,7 @@ describe('TraceTimeline', () => { render(, { organization: OrganizationFixture({ - features: ['related-issues-issue-details-page'], + features: ['related-issues-issue-details-page', 'global-views'], }), }); @@ -244,4 +248,36 @@ describe('TraceTimeline', () => { trace_timeline_status: 'empty', }); }); + + it('works for free plans (no global-views feature)', async () => { + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: issuePlatformBody, + match: [ + MockApiClient.matchQuery({ + dataset: 'issuePlatform', + // Since we don't have global-views, we only look at the current project + project: event.projectID, + }), + ], + }); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/`, + body: emptyBody, + match: [ + MockApiClient.matchQuery({ + dataset: 'discover', + // Since we don't have global-views, we only look at the current project + project: event.projectID, + }), + ], + }); + + render(, { + organization: OrganizationFixture({ + features: [], // No global-views feature + }), + }); + expect(await screen.findByLabelText('Current Event')).toBeInTheDocument(); + }); }); diff --git a/static/app/views/issueDetails/traceTimeline/useTraceTimelineEvents.tsx b/static/app/views/issueDetails/traceTimeline/useTraceTimelineEvents.tsx index 6ccc734bde8e65..fe572bf0af236a 100644 --- a/static/app/views/issueDetails/traceTimeline/useTraceTimelineEvents.tsx +++ b/static/app/views/issueDetails/traceTimeline/useTraceTimelineEvents.tsx @@ -43,6 +43,10 @@ export function useTraceTimelineEvents({event}: UseTraceTimelineEventsOptions): traceEvents: TimelineEvent[]; } { const organization = useOrganization(); + // If the org has global views, we want to look across all projects, + // otherwise, just look at the current project. + const hasGlobalViews = organization.features.includes('global-views'); + const project = hasGlobalViews ? -1 : event.projectID; const {start, end} = getTraceTimeRangeFromEvent(event); const traceId = event.contexts?.trace?.trace_id ?? ''; @@ -65,6 +69,7 @@ export function useTraceTimelineEvents({event}: UseTraceTimelineEventsOptions): sort: '-timestamp', start, end, + project: project, }, }, ], @@ -100,6 +105,7 @@ export function useTraceTimelineEvents({event}: UseTraceTimelineEventsOptions): sort: '-timestamp', start, end, + project: project, }, }, ], From 52c42cf6f8f2f630cf0a23b063b7c3dacfeea8a5 Mon Sep 17 00:00:00 2001 From: Armen Zambrano G <44410+armenzg@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:06:18 -0400 Subject: [PATCH 2/3] Fix bug --- .../traceTimeline/traceTimeline.spec.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx b/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx index b34c2211e4a4fa..ba65228e3093dc 100644 --- a/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx @@ -76,12 +76,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: discoverBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); render(, {organization}); expect(await screen.findByLabelText('Current Event')).toBeInTheDocument(); @@ -98,12 +98,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: discoverBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); const {container} = render(, {organization}); await waitFor(() => @@ -119,12 +119,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); const {container} = render(, {organization}); await waitFor(() => @@ -140,12 +140,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); render(, {organization}); // Checking for the presence of seconds @@ -156,12 +156,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); render(, {organization}); expect(await screen.findByLabelText('Current Event')).toBeInTheDocument(); @@ -171,12 +171,12 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); // I believe the call to projects is to determine what projects a user belongs to MockApiClient.addMockResponse({ @@ -215,13 +215,13 @@ describe('TraceTimeline', () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: emptyBody, - match: [MockApiClient.matchQuery({dataset: 'issuePlatform, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'issuePlatform', project: -1})], }); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, // Only 1 issue body: discoverBody, - match: [MockApiClient.matchQuery({dataset: 'discover, project: -1'})], + match: [MockApiClient.matchQuery({dataset: 'discover', project: -1})], }); // I believe the call to projects is to determine what projects a user belongs to MockApiClient.addMockResponse({ From 904064c19467e49c47865d4869caf44629970842 Mon Sep 17 00:00:00 2001 From: Armen Zambrano G <44410+armenzg@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:38:30 -0400 Subject: [PATCH 3/3] Update static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx --- .../app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx b/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx index ba65228e3093dc..ce657e2a29d17e 100644 --- a/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceTimeline.spec.tsx @@ -249,7 +249,7 @@ describe('TraceTimeline', () => { }); }); - it('works for free plans (no global-views feature)', async () => { + it('works for plans with no global-views feature', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, body: issuePlatformBody,