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,