(
*
* [1]: https://develop.sentry.dev/frontend/network-requests/
*/
-class DeprecatedAsyncComponent<
+export class DeprecatedAsyncComponent<
P extends AsyncComponentProps = AsyncComponentProps,
S extends AsyncComponentState = AsyncComponentState,
> extends Component {
@@ -389,5 +389,3 @@ class DeprecatedAsyncComponent<
return this.renderComponent();
}
}
-
-export default DeprecatedAsyncComponent;
diff --git a/static/app/components/deprecatedforms/booleanField.spec.tsx b/static/app/components/deprecatedforms/booleanField.spec.tsx
index 05478d7edef641..92f02921120071 100644
--- a/static/app/components/deprecatedforms/booleanField.spec.tsx
+++ b/static/app/components/deprecatedforms/booleanField.spec.tsx
@@ -1,7 +1,7 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import BooleanField from 'sentry/components/deprecatedforms/booleanField';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
describe('BooleanField', () => {
it('renders without form context', () => {
diff --git a/static/app/components/deprecatedforms/booleanField.tsx b/static/app/components/deprecatedforms/booleanField.tsx
index e9faf207257e13..05bda70075b576 100644
--- a/static/app/components/deprecatedforms/booleanField.tsx
+++ b/static/app/components/deprecatedforms/booleanField.tsx
@@ -1,7 +1,7 @@
import {Checkbox} from '@sentry/scraps/checkbox';
import {Tooltip} from '@sentry/scraps/tooltip';
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext';
import {IconQuestion} from 'sentry/icons';
import {defined} from 'sentry/utils';
diff --git a/static/app/components/deprecatedforms/dateTimeField.tsx b/static/app/components/deprecatedforms/dateTimeField.tsx
index 7745c3956fb82a..0a5357c5cff011 100644
--- a/static/app/components/deprecatedforms/dateTimeField.tsx
+++ b/static/app/components/deprecatedforms/dateTimeField.tsx
@@ -1,4 +1,4 @@
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
/**
* @deprecated Do not use this
diff --git a/static/app/components/deprecatedforms/emailField.spec.tsx b/static/app/components/deprecatedforms/emailField.spec.tsx
index ba74cda991fa52..3144fd69a9ad1a 100644
--- a/static/app/components/deprecatedforms/emailField.spec.tsx
+++ b/static/app/components/deprecatedforms/emailField.spec.tsx
@@ -1,7 +1,7 @@
import {render} from 'sentry-test/reactTestingLibrary';
import EmailField from 'sentry/components/deprecatedforms/emailField';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
describe('EmailField', () => {
describe('render()', () => {
diff --git a/static/app/components/deprecatedforms/emailField.tsx b/static/app/components/deprecatedforms/emailField.tsx
index 9f76766c16dc1f..6b9b0561298ddf 100644
--- a/static/app/components/deprecatedforms/emailField.tsx
+++ b/static/app/components/deprecatedforms/emailField.tsx
@@ -1,4 +1,4 @@
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext';
// XXX: This is ONLY used in GenericField. If we can delete that this can go.
diff --git a/static/app/components/deprecatedforms/form.spec.tsx b/static/app/components/deprecatedforms/form.spec.tsx
index 5f52d29f1639f2..be58ca853c7921 100644
--- a/static/app/components/deprecatedforms/form.spec.tsx
+++ b/static/app/components/deprecatedforms/form.spec.tsx
@@ -1,6 +1,6 @@
import {render} from 'sentry-test/reactTestingLibrary';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
describe('Form', () => {
describe('render()', () => {
diff --git a/static/app/components/deprecatedforms/form.tsx b/static/app/components/deprecatedforms/form.tsx
index 1bbbbe4603d895..81b02610bad6ed 100644
--- a/static/app/components/deprecatedforms/form.tsx
+++ b/static/app/components/deprecatedforms/form.tsx
@@ -6,7 +6,7 @@ import {Alert} from '@sentry/scraps/alert';
import {Button} from '@sentry/scraps/button';
import {FormContext} from 'sentry/components/deprecatedforms/formContext';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
import {t} from 'sentry/locale';
type FormProps = {
@@ -42,7 +42,7 @@ type FormClassState = {
state: FormState;
};
-class Form<
+export class Form<
Props extends FormProps = FormProps,
State extends FormClassState = FormClassState,
> extends Component {
@@ -191,5 +191,3 @@ class Form<
// Note: this is so we can use this as a selector for SelectField
// We need to keep `Form` as a React Component because ApiForm extends it :/
export const StyledForm = styled('form')``;
-
-export default Form;
diff --git a/static/app/components/deprecatedforms/genericField.spec.tsx b/static/app/components/deprecatedforms/genericField.spec.tsx
index e161e11ccfd604..88912dfd6ab505 100644
--- a/static/app/components/deprecatedforms/genericField.spec.tsx
+++ b/static/app/components/deprecatedforms/genericField.spec.tsx
@@ -1,7 +1,7 @@
import {render, screen} from 'sentry-test/reactTestingLibrary';
import {GenericField} from 'sentry/components/deprecatedforms/genericField';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
describe('GenericField', () => {
it('renders text as TextInput', () => {
diff --git a/static/app/components/deprecatedforms/genericField.tsx b/static/app/components/deprecatedforms/genericField.tsx
index fedab19c273197..1885669ee71d68 100644
--- a/static/app/components/deprecatedforms/genericField.tsx
+++ b/static/app/components/deprecatedforms/genericField.tsx
@@ -8,7 +8,7 @@ import SelectCreatableField from 'sentry/components/deprecatedforms/selectCreata
import SelectField from 'sentry/components/deprecatedforms/selectField';
import TextareaField from 'sentry/components/deprecatedforms/textareaField';
import TextField from 'sentry/components/deprecatedforms/textField';
-import type FormState from 'sentry/components/forms/state';
+import type {FormState} from 'sentry/components/forms/state';
import {defined} from 'sentry/utils';
type FieldType =
diff --git a/static/app/components/deprecatedforms/inputField.tsx b/static/app/components/deprecatedforms/inputField.tsx
index 06bb8b18876080..572c63580c235e 100644
--- a/static/app/components/deprecatedforms/inputField.tsx
+++ b/static/app/components/deprecatedforms/inputField.tsx
@@ -22,7 +22,7 @@ type InputFieldProps = FormFieldProps & {
/**
* @deprecated Do not use this
*/
-abstract class InputField<
+export abstract class InputField<
Props extends InputFieldProps = InputFieldProps,
State extends FormField['state'] = FormField['state'],
> extends FormField {
@@ -56,5 +56,3 @@ abstract class InputField<
abstract getType(): string;
}
-
-export default InputField;
diff --git a/static/app/components/deprecatedforms/numberField.spec.tsx b/static/app/components/deprecatedforms/numberField.spec.tsx
index fcbefbd243bb2a..5c35b089248d50 100644
--- a/static/app/components/deprecatedforms/numberField.spec.tsx
+++ b/static/app/components/deprecatedforms/numberField.spec.tsx
@@ -1,6 +1,6 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
import NumberField from 'sentry/components/deprecatedforms/numberField';
describe('NumberField', () => {
diff --git a/static/app/components/deprecatedforms/numberField.tsx b/static/app/components/deprecatedforms/numberField.tsx
index 7cd5a10b3f4519..4931fa8182b0ec 100644
--- a/static/app/components/deprecatedforms/numberField.tsx
+++ b/static/app/components/deprecatedforms/numberField.tsx
@@ -1,4 +1,4 @@
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext';
type Props = {
diff --git a/static/app/components/deprecatedforms/passwordField.spec.tsx b/static/app/components/deprecatedforms/passwordField.spec.tsx
index 45448c9030d5f3..c8b5f4a47f1de0 100644
--- a/static/app/components/deprecatedforms/passwordField.spec.tsx
+++ b/static/app/components/deprecatedforms/passwordField.spec.tsx
@@ -1,6 +1,6 @@
import {render} from 'sentry-test/reactTestingLibrary';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
import PasswordField from 'sentry/components/deprecatedforms/passwordField';
describe('PasswordField', () => {
diff --git a/static/app/components/deprecatedforms/passwordField.tsx b/static/app/components/deprecatedforms/passwordField.tsx
index addfe3b8cd3ae9..717e57df055945 100644
--- a/static/app/components/deprecatedforms/passwordField.tsx
+++ b/static/app/components/deprecatedforms/passwordField.tsx
@@ -1,6 +1,6 @@
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
type Props = InputField['props'] & {
formState?: (typeof FormState)[keyof typeof FormState];
diff --git a/static/app/components/deprecatedforms/selectAsyncField.spec.tsx b/static/app/components/deprecatedforms/selectAsyncField.spec.tsx
index 56dec9caaf768a..b3fe139d5c5682 100644
--- a/static/app/components/deprecatedforms/selectAsyncField.spec.tsx
+++ b/static/app/components/deprecatedforms/selectAsyncField.spec.tsx
@@ -1,7 +1,7 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {selectEvent} from 'sentry-test/selectEvent';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
import SelectAsyncField from 'sentry/components/deprecatedforms/selectAsyncField';
describe('SelectAsyncField', () => {
diff --git a/static/app/components/deprecatedforms/selectCreatableField.spec.tsx b/static/app/components/deprecatedforms/selectCreatableField.spec.tsx
index 5d32c5b44a13e0..385ffe0638f9cb 100644
--- a/static/app/components/deprecatedforms/selectCreatableField.spec.tsx
+++ b/static/app/components/deprecatedforms/selectCreatableField.spec.tsx
@@ -1,6 +1,6 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
import SelectCreatableField from 'sentry/components/deprecatedforms/selectCreatableField';
describe('SelectCreatableField', () => {
diff --git a/static/app/components/deprecatedforms/selectField.spec.tsx b/static/app/components/deprecatedforms/selectField.spec.tsx
index e78091ae497f20..5d678887e962f0 100644
--- a/static/app/components/deprecatedforms/selectField.spec.tsx
+++ b/static/app/components/deprecatedforms/selectField.spec.tsx
@@ -1,7 +1,7 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {selectEvent} from 'sentry-test/selectEvent';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
import SelectField from 'sentry/components/deprecatedforms/selectField';
describe('SelectField', () => {
diff --git a/static/app/components/deprecatedforms/textField.spec.tsx b/static/app/components/deprecatedforms/textField.spec.tsx
index b7e65e34c93b89..1444903a66acca 100644
--- a/static/app/components/deprecatedforms/textField.spec.tsx
+++ b/static/app/components/deprecatedforms/textField.spec.tsx
@@ -1,6 +1,6 @@
import {render} from 'sentry-test/reactTestingLibrary';
-import Form from 'sentry/components/deprecatedforms/form';
+import {Form} from 'sentry/components/deprecatedforms/form';
import TextField from 'sentry/components/deprecatedforms/textField';
describe('TextField', () => {
diff --git a/static/app/components/deprecatedforms/textField.tsx b/static/app/components/deprecatedforms/textField.tsx
index abf883dcad9d9b..d27685cfc5985b 100644
--- a/static/app/components/deprecatedforms/textField.tsx
+++ b/static/app/components/deprecatedforms/textField.tsx
@@ -1,4 +1,4 @@
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext';
type Props = InputField['props'] & {
diff --git a/static/app/components/deprecatedforms/textareaField.tsx b/static/app/components/deprecatedforms/textareaField.tsx
index 29c0d3e1b0bfcb..46476ce09df7c2 100644
--- a/static/app/components/deprecatedforms/textareaField.tsx
+++ b/static/app/components/deprecatedforms/textareaField.tsx
@@ -1,6 +1,6 @@
import {TextArea} from '@sentry/scraps/textarea';
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import {withFormContext} from 'sentry/components/deprecatedforms/withFormContext';
type State = InputField['state'] & {
diff --git a/static/app/components/discover/transactionsList.spec.tsx b/static/app/components/discover/transactionsList.spec.tsx
index 31c6879ecbef5f..ba6bbca3017061 100644
--- a/static/app/components/discover/transactionsList.spec.tsx
+++ b/static/app/components/discover/transactionsList.spec.tsx
@@ -2,7 +2,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {TransactionsList} from 'sentry/components/discover/transactionsList';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {OrganizationContext} from 'sentry/views/organizationContext';
diff --git a/static/app/components/discover/transactionsList.tsx b/static/app/components/discover/transactionsList.tsx
index 99984582644a05..fbd1b61a6acc5b 100644
--- a/static/app/components/discover/transactionsList.tsx
+++ b/static/app/components/discover/transactionsList.tsx
@@ -16,7 +16,7 @@ import {browserHistory} from 'sentry/utils/browserHistory';
import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {TrendsEventsDiscoverQuery} from 'sentry/utils/performance/trends/trendsDiscoverQuery';
diff --git a/static/app/components/discover/transactionsTable.tsx b/static/app/components/discover/transactionsTable.tsx
index d9fa81caf7bcdb..967bf22fb576ba 100644
--- a/static/app/components/discover/transactionsTable.tsx
+++ b/static/app/components/discover/transactionsTable.tsx
@@ -15,8 +15,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {MetaType} from 'sentry/utils/discover/eventView';
+import type {EventView, MetaType} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {fieldAlignment, getAggregateAlias} from 'sentry/utils/discover/fields';
import {ViewReplayLink} from 'sentry/utils/discover/viewReplayLink';
diff --git a/static/app/components/errorBoundary.spec.tsx b/static/app/components/errorBoundary.spec.tsx
index c0e64e06b046c7..56328a52119d96 100644
--- a/static/app/components/errorBoundary.spec.tsx
+++ b/static/app/components/errorBoundary.spec.tsx
@@ -1,6 +1,6 @@
import {render, screen} from 'sentry-test/reactTestingLibrary';
-import ErrorBoundary from './errorBoundary';
+import {ErrorBoundary} from './errorBoundary';
describe('ErrorBoundary', () => {
it('renders components', () => {
diff --git a/static/app/components/errorBoundary.tsx b/static/app/components/errorBoundary.tsx
index eea4086e40c48b..25712440cc4206 100644
--- a/static/app/components/errorBoundary.tsx
+++ b/static/app/components/errorBoundary.tsx
@@ -47,7 +47,7 @@ function getExclamation() {
return exclamation[Math.floor(Math.random() * exclamation.length)];
}
-class ErrorBoundary extends Component {
+export class ErrorBoundary extends Component {
static defaultProps: DefaultProps = {
mini: false,
};
@@ -167,5 +167,3 @@ const StackTrace = styled('pre')`
margin-left: 85px;
margin-right: 18px;
`;
-
-export default ErrorBoundary;
diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx
index 25ae11120feec9..7cf3e2e5c4bc8f 100644
--- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx
+++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx
@@ -198,7 +198,7 @@ describe('SolutionCard', () => {
/>
);
- expect(screen.getByText('Implementation Plan')).toBeInTheDocument();
+ expect(screen.getByText('Plan')).toBeInTheDocument();
expect(screen.getByText('Add null check before accessing user')).toBeInTheDocument();
});
@@ -235,10 +235,10 @@ describe('SolutionCard', () => {
/>
);
- expect(screen.getByText('Implementation Plan')).toBeInTheDocument();
+ expect(screen.getByText('Plan')).toBeInTheDocument();
expect(
screen.getByText(
- 'Seer failed to generate an implementation plan. This one is on us. Try running it again.'
+ 'Seer failed to generate a plan. This one is on us. Try running it again.'
)
).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Re-run'})).toBeInTheDocument();
@@ -351,8 +351,16 @@ describe('PullRequestsCard', () => {
autofix={mockAutofix}
section={makeSection('pull_request', 'completed', [
[
- makePR({repo_name: 'org/repo-a', pr_number: 10, pr_url: 'https://pr/10'}),
- makePR({repo_name: 'org/repo-b', pr_number: 20, pr_url: 'https://pr/20'}),
+ makePR({
+ repo_name: 'org/repo-a',
+ pr_number: 10,
+ pr_url: 'https://pr/10',
+ }),
+ makePR({
+ repo_name: 'org/repo-b',
+ pr_number: 20,
+ pr_url: 'https://pr/20',
+ }),
],
])}
/>
@@ -468,7 +476,11 @@ describe('CodingAgentCard', () => {
);
@@ -481,7 +493,11 @@ describe('CodingAgentCard', () => {
);
@@ -494,7 +510,11 @@ describe('CodingAgentCard', () => {
);
@@ -577,8 +597,16 @@ describe('CodingAgentCard', () => {
autofix={mockAutofix}
section={makeSection('coding_agents', 'completed', [
[
- makeCodingAgent({id: 'agent-1', name: 'Agent One', status: 'completed'}),
- makeCodingAgent({id: 'agent-2', name: 'Agent Two', status: 'running'}),
+ makeCodingAgent({
+ id: 'agent-1',
+ name: 'Agent One',
+ status: 'completed',
+ }),
+ makeCodingAgent({
+ id: 'agent-2',
+ name: 'Agent Two',
+ status: 'running',
+ }),
],
])}
/>
diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx
index 096b9555496a16..fa4b42347aa0bc 100644
--- a/static/app/components/events/autofix/v3/autofixCards.tsx
+++ b/static/app/components/events/autofix/v3/autofixCards.tsx
@@ -115,7 +115,7 @@ export function SolutionCard({autofix, section}: AutofixCardProps) {
const runId = runState?.run_id;
return (
- } title={t('Implementation Plan')}>
+ } title={t('Plan')}>
{section.status === 'processing' ? (
) : artifact?.data ? (
@@ -143,7 +143,7 @@ export function SolutionCard({autofix, section}: AutofixCardProps) {
{t(
- 'Seer failed to generate an implementation plan. This one is on us. Try running it again.'
+ 'Seer failed to generate a plan. This one is on us. Try running it again.'
)}
diff --git a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx
index b3e59b2b87e9ca..b7b1e87eab1a7c 100644
--- a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx
+++ b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx
@@ -74,7 +74,7 @@ describe('RootCausePreview', () => {
});
describe('SolutionPreview', () => {
- it('renders implementation plan title and summary', () => {
+ it('renders plan title and summary', () => {
const artifact: Artifact
= {
key: 'solution',
reason: 'Found solution',
@@ -86,7 +86,7 @@ describe('SolutionPreview', () => {
render();
- expect(screen.getByText('Implementation Plan')).toBeInTheDocument();
+ expect(screen.getByText('Plan')).toBeInTheDocument();
expect(screen.getByText('Add null check before accessing user')).toBeInTheDocument();
});
@@ -107,10 +107,10 @@ describe('SolutionPreview', () => {
render();
- expect(screen.getByText('Implementation Plan')).toBeInTheDocument();
+ expect(screen.getByText('Plan')).toBeInTheDocument();
expect(
screen.getByText(
- 'Seer failed to generate an implementation plan. This one is on us. Try running it again.'
+ 'Seer failed to generate a plan. This one is on us. Try running it again.'
)
).toBeInTheDocument();
});
@@ -227,8 +227,16 @@ describe('PullRequestsPreview', () => {
@@ -242,7 +250,13 @@ describe('PullRequestsPreview', () => {
render(
);
@@ -319,7 +333,11 @@ describe('CodingAgentPreview', () => {
render(
);
@@ -331,7 +349,11 @@ describe('CodingAgentPreview', () => {
render(
);
@@ -343,7 +365,11 @@ describe('CodingAgentPreview', () => {
render(
);
@@ -429,7 +455,11 @@ describe('CodingAgentPreview', () => {
section={makeSection('coding_agents', [
[
makeCodingAgent({id: 'a1', name: 'Agent One', status: 'running'}),
- makeCodingAgent({id: 'a2', name: 'Agent Two', status: 'completed'}),
+ makeCodingAgent({
+ id: 'a2',
+ name: 'Agent Two',
+ status: 'completed',
+ }),
],
])}
/>
diff --git a/static/app/components/events/autofix/v3/autofixPreviews.tsx b/static/app/components/events/autofix/v3/autofixPreviews.tsx
index ca73007c55c59a..1ab9f0499f1f9e 100644
--- a/static/app/components/events/autofix/v3/autofixPreviews.tsx
+++ b/static/app/components/events/autofix/v3/autofixPreviews.tsx
@@ -63,16 +63,14 @@ export function SolutionPreview({section}: ArtifactPreviewProps) {
}, [section]);
return (
- } title={t('Implementation Plan')}>
+ } title={t('Plan')}>
{section.status === 'processing' ? (
) : artifact?.data ? (
{artifact.data.one_line_summary}
) : (
- {t(
- 'Seer failed to generate an implementation plan. This one is on us. Try running it again.'
- )}
+ {t('Seer failed to generate a plan. This one is on us. Try running it again.')}
)}
diff --git a/static/app/components/events/autofix/v3/utils.spec.ts b/static/app/components/events/autofix/v3/utils.spec.ts
index 310a2d47250a4b..81edf1a57bbf32 100644
--- a/static/app/components/events/autofix/v3/utils.spec.ts
+++ b/static/app/components/events/autofix/v3/utils.spec.ts
@@ -67,7 +67,10 @@ function makeSolutionArtifact(
data: {
one_line_summary: 'Add null check before accessing property',
steps: [
- {title: 'Add guard clause', description: 'Check for null before accessing .name'},
+ {
+ title: 'Add guard clause',
+ description: 'Check for null before accessing .name',
+ },
{title: 'Add test', description: 'Cover the null input case'},
],
},
@@ -144,7 +147,7 @@ describe('artifactToMarkdown', () => {
it('renders full solution with steps', () => {
expect(artifactToMarkdown(makeSolutionArtifact())).toBe(
[
- '# Implementation Plan',
+ '# Plan',
'',
'Add null check before accessing property',
'',
@@ -169,9 +172,7 @@ describe('artifactToMarkdown', () => {
steps: [],
},
});
- expect(artifactToMarkdown(artifact)).toBe(
- ['# Implementation Plan', '', 'Quick fix'].join('\n')
- );
+ expect(artifactToMarkdown(artifact)).toBe(['# Plan', '', 'Quick fix'].join('\n'));
});
});
diff --git a/static/app/components/events/autofix/v3/utils.ts b/static/app/components/events/autofix/v3/utils.ts
index 93092f01796a1b..2a3d285537aca8 100644
--- a/static/app/components/events/autofix/v3/utils.ts
+++ b/static/app/components/events/autofix/v3/utils.ts
@@ -76,7 +76,7 @@ function solutionArtifactToMarkdown(artifact: Artifact): strin
return null;
}
- const parts: string[] = ['# Implementation Plan', '', solution.one_line_summary];
+ const parts: string[] = ['# Plan', '', solution.one_line_summary];
if (solution.steps.length) {
parts.push('');
diff --git a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx
index 7dddb5c486bde3..531a692d94c16c 100644
--- a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx
+++ b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx
@@ -5,7 +5,7 @@ import styled from '@emotion/styled';
import {Button} from '@sentry/scraps/button';
import {Grid} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
BreadcrumbControlOptions,
BreadcrumbsDrawer,
diff --git a/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx b/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx
index 9954cf175b04e0..4cc825b78f2572 100644
--- a/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx
+++ b/static/app/components/events/breadcrumbs/breadcrumbsTimeline.tsx
@@ -8,7 +8,7 @@ import {Tooltip} from '@sentry/scraps/tooltip';
import {DateTime} from 'sentry/components/dateTime';
import {Duration} from 'sentry/components/duration';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {BreadcrumbItemContent} from 'sentry/components/events/breadcrumbs/breadcrumbItemContent';
import type {EnhancedCrumb} from 'sentry/components/events/breadcrumbs/utils';
import {Timeline} from 'sentry/components/timeline';
diff --git a/static/app/components/events/contexts/contextBlock.tsx b/static/app/components/events/contexts/contextBlock.tsx
index fc649fda8d7c60..ea2ddc6cee7dd5 100644
--- a/static/app/components/events/contexts/contextBlock.tsx
+++ b/static/app/components/events/contexts/contextBlock.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList';
import type {KeyValueListData} from 'sentry/types/group';
diff --git a/static/app/components/events/contexts/contextCard.tsx b/static/app/components/events/contexts/contextCard.tsx
index 457f3971d47bc5..371f404a852fae 100644
--- a/static/app/components/events/contexts/contextCard.tsx
+++ b/static/app/components/events/contexts/contextCard.tsx
@@ -3,7 +3,7 @@ import startCase from 'lodash/startCase';
import {Flex} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import type {ContextValue} from 'sentry/components/events/contexts';
import {
getContextIcon,
diff --git a/static/app/components/events/contexts/contextDataSection.tsx b/static/app/components/events/contexts/contextDataSection.tsx
index 7df339d6b2f04a..70a8d04a0545f7 100644
--- a/static/app/components/events/contexts/contextDataSection.tsx
+++ b/static/app/components/events/contexts/contextDataSection.tsx
@@ -1,6 +1,6 @@
import {ExternalLink} from '@sentry/scraps/link';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {getOrderedContextItems} from 'sentry/components/events/contexts';
import {ContextCard} from 'sentry/components/events/contexts/contextCard';
import {CONTEXT_DOCS_LINK} from 'sentry/components/events/contexts/utils';
diff --git a/static/app/components/events/eventEntry.tsx b/static/app/components/events/eventEntry.tsx
index f508e2847f8325..02d12ca926a9af 100644
--- a/static/app/components/events/eventEntry.tsx
+++ b/static/app/components/events/eventEntry.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventBreadcrumbsSection} from 'sentry/components/events/eventBreadcrumbsSection';
import {t} from 'sentry/locale';
import type {Entry, Event, EventTransaction} from 'sentry/types/event';
diff --git a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx
index fc8e9a3ce7598c..7c1f125f64add6 100644
--- a/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx
+++ b/static/app/components/events/eventHydrationDiff/replayDiffContent.tsx
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {REPLAY_LOADING_HEIGHT} from 'sentry/components/events/eventReplay/constants';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {ArchivedReplayAlert} from 'sentry/components/replays/alerts/archivedReplayAlert';
diff --git a/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx b/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx
index 6cd8818c7e6c17..1c3a239abaf2b8 100644
--- a/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx
+++ b/static/app/components/events/eventHydrationDiff/replayDiffSection.tsx
@@ -3,7 +3,7 @@ import ReactLazyLoad from 'react-lazyload';
import styled from '@emotion/styled';
import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {REPLAY_LOADING_HEIGHT} from 'sentry/components/events/eventReplay/constants';
import {LazyLoad} from 'sentry/components/lazyLoad';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/components/events/eventReplay/index.tsx b/static/app/components/events/eventReplay/index.tsx
index 08c7c9c1d00a28..d356066fcb1b91 100644
--- a/static/app/components/events/eventReplay/index.tsx
+++ b/static/app/components/events/eventReplay/index.tsx
@@ -1,6 +1,6 @@
import {lazy} from 'react';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {ReplayClipSection} from 'sentry/components/events/eventReplay/replayClipSection';
import {LazyLoad} from 'sentry/components/lazyLoad';
import type {Event} from 'sentry/types/event';
diff --git a/static/app/components/events/eventReplay/replayClipSection.tsx b/static/app/components/events/eventReplay/replayClipSection.tsx
index 11db578c89311a..f2f2c87b35e648 100644
--- a/static/app/components/events/eventReplay/replayClipSection.tsx
+++ b/static/app/components/events/eventReplay/replayClipSection.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import {LinkButton} from '@sentry/scraps/button';
import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {REPLAY_LOADING_HEIGHT} from 'sentry/components/events/eventReplay/constants';
import {LazyLoad} from 'sentry/components/lazyLoad';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx
index b1bbdff68f7abe..ca903244f7f238 100644
--- a/static/app/components/events/eventReplay/replayPreviewPlayer.tsx
+++ b/static/app/components/events/eventReplay/replayPreviewPlayer.tsx
@@ -8,7 +8,7 @@ import {Button, LinkButton, type LinkButtonProps} from '@sentry/scraps/button';
import {Flex, Stack} from '@sentry/scraps/layout';
import {TooltipContext} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {useReplayContext} from 'sentry/components/replays/replayContext';
import {ReplayCurrentScreen} from 'sentry/components/replays/replayCurrentScreen';
import {ReplayCurrentUrl} from 'sentry/components/replays/replayCurrentUrl';
diff --git a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx
index ee89577994cb9c..ae5ea5e5d17399 100644
--- a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx
+++ b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx
@@ -1,14 +1,14 @@
import {LinkButton} from '@sentry/scraps/button';
import {ChartType} from 'sentry/chartcuterie/types';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {t} from 'sentry/locale';
import type {Event} from 'sentry/types/event';
import type {EventsStatsData} from 'sentry/types/organization';
import {toArray} from 'sentry/utils/array/toArray';
import type {MetaType} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
import {useGenericDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx
index 481191de5b8157..c52241a8b18cb6 100644
--- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx
+++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx
@@ -18,7 +18,7 @@ import {
MINIMAP_HEIGHT,
MinimapBackground,
} from 'sentry/components/events/interfaces/spans/minimap';
-import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
+import {WaterfallModel} from 'sentry/components/events/interfaces/spans/waterfallModel';
import {OpsBreakdown} from 'sentry/components/events/opsBreakdown';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {TextOverflow} from 'sentry/components/textOverflow';
@@ -29,7 +29,7 @@ import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
import {getShortEventId} from 'sentry/utils/events';
@@ -40,7 +40,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
const BUTTON_ICON_SIZE = 'sm';
const BUTTON_SIZE = 'sm';
-export function getSampleEventQuery({
+function getSampleEventQuery({
transaction,
durationBaseline,
addUpperBound = true,
diff --git a/static/app/components/events/eventTags/eventTagsTree.tsx b/static/app/components/events/eventTags/eventTagsTree.tsx
index 1dff687343b31b..f328b7c0b2558b 100644
--- a/static/app/components/events/eventTags/eventTagsTree.tsx
+++ b/static/app/components/events/eventTags/eventTagsTree.tsx
@@ -1,7 +1,7 @@
import {Fragment, useMemo, useRef} from 'react';
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
EventTagsTreeRow,
type EventTagsTreeRowProps,
diff --git a/static/app/components/events/eventViewHierarchy.tsx b/static/app/components/events/eventViewHierarchy.tsx
index 8204a915135795..f3ea25b585b5c3 100644
--- a/static/app/components/events/eventViewHierarchy.tsx
+++ b/static/app/components/events/eventViewHierarchy.tsx
@@ -2,7 +2,7 @@ import {useMemo} from 'react';
import * as Sentry from '@sentry/react';
import {useFetchEventAttachments} from 'sentry/actionCreators/events';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {getAttachmentUrl} from 'sentry/components/events/attachmentViewers/utils';
import {
getPlatform,
diff --git a/static/app/components/events/eventXrayDiff.tsx b/static/app/components/events/eventXrayDiff.tsx
index 87a9c29ea9e3cd..ef81b4707fa93a 100644
--- a/static/app/components/events/eventXrayDiff.tsx
+++ b/static/app/components/events/eventXrayDiff.tsx
@@ -1,5 +1,5 @@
import {EmptyStateWarning} from 'sentry/components/emptyStateWarning';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
diff --git a/static/app/components/events/highlights/highlightsDataSection.tsx b/static/app/components/events/highlights/highlightsDataSection.tsx
index c28a6dd5cfbd7b..04ff34a4898181 100644
--- a/static/app/components/events/highlights/highlightsDataSection.tsx
+++ b/static/app/components/events/highlights/highlightsDataSection.tsx
@@ -7,7 +7,7 @@ import {ExternalLink} from '@sentry/scraps/link';
import {openModal} from 'sentry/actionCreators/modal';
import {hasEveryAccess} from 'sentry/components/acl/access';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {ContextCardContent} from 'sentry/components/events/contexts/contextCard';
import {getContextMeta} from 'sentry/components/events/contexts/utils';
import {
diff --git a/static/app/components/events/interfaces/crashContent/exception/content.tsx b/static/app/components/events/interfaces/crashContent/exception/content.tsx
index 0b3193ed253aed..66b0a979536a00 100644
--- a/static/app/components/events/interfaces/crashContent/exception/content.tsx
+++ b/static/app/components/events/interfaces/crashContent/exception/content.tsx
@@ -5,7 +5,7 @@ import {Button} from '@sentry/scraps/button';
import {Container} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners';
import {useLineCoverageContext} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageContext';
import {
diff --git a/static/app/components/events/interfaces/crashContent/exception/index.tsx b/static/app/components/events/interfaces/crashContent/exception/index.tsx
index e55825c28e8805..08fbbc82a3b176 100644
--- a/static/app/components/events/interfaces/crashContent/exception/index.tsx
+++ b/static/app/components/events/interfaces/crashContent/exception/index.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {useStacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext';
import type {Event, ExceptionType} from 'sentry/types/event';
import type {Group} from 'sentry/types/group';
diff --git a/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx b/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx
index 129fca8516099e..73a6c278e7dc6b 100644
--- a/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx
+++ b/static/app/components/events/interfaces/crashContent/stackTrace/index.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import type {Event} from 'sentry/types/event';
import type {PlatformKey} from 'sentry/types/project';
import type {StacktraceType} from 'sentry/types/stacktrace';
diff --git a/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx b/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx
index ef7f4dd6629ffb..946de23e19bf16 100644
--- a/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx
+++ b/static/app/components/events/interfaces/debugMeta/debugImageDetails/candidates.tsx
@@ -49,7 +49,7 @@ type State = {
searchTerm: string;
};
-class Candidates extends Component {
+export class Candidates extends Component {
state: State = {
searchTerm: '',
filterOptions: [],
@@ -370,8 +370,6 @@ class Candidates extends Component {
}
}
-export default Candidates;
-
const Wrapper = styled('div')`
display: grid;
`;
diff --git a/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx b/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx
index 71f665fb567b70..20e013eda36216 100644
--- a/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx
+++ b/static/app/components/events/interfaces/debugMeta/debugImageDetails/index.tsx
@@ -26,7 +26,7 @@ import {useApi} from 'sentry/utils/useApi';
import {useOrganization} from 'sentry/utils/useOrganization';
import {getPrettyFileType} from 'sentry/views/settings/projectDebugFiles/utils';
-import Candidates from './candidates';
+import {Candidates} from './candidates';
import {GeneralInfo} from './generalInfo';
import {ReprocessAlert} from './reprocessAlert';
import {INTERNAL_SOURCE, INTERNAL_SOURCE_LOCATION} from './utils';
diff --git a/static/app/components/events/interfaces/exception.tsx b/static/app/components/events/interfaces/exception.tsx
index 81525a3b54b67c..28cdd1305b1fd5 100644
--- a/static/app/components/events/interfaces/exception.tsx
+++ b/static/app/components/events/interfaces/exception.tsx
@@ -1,7 +1,7 @@
import {Fragment} from 'react';
import {CommitRow} from 'sentry/components/commitRow';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {StacktraceContext} from 'sentry/components/events/interfaces/stackTraceContext';
import {SuspectCommits} from 'sentry/components/events/suspectCommits';
import {TraceEventDataSection} from 'sentry/components/events/traceEventDataSection';
diff --git a/static/app/components/events/interfaces/frame/deprecatedLine.tsx b/static/app/components/events/interfaces/frame/deprecatedLine.tsx
index 7cb66b96576f1f..8f666540d12f9b 100644
--- a/static/app/components/events/interfaces/frame/deprecatedLine.tsx
+++ b/static/app/components/events/interfaces/frame/deprecatedLine.tsx
@@ -8,7 +8,7 @@ import {Button} from '@sentry/scraps/button';
import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
import {openModal} from 'sentry/actionCreators/modal';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {analyzeFrameForRootCause} from 'sentry/components/events/interfaces/analyzeFrames';
import {LeadHint} from 'sentry/components/events/interfaces/frame/leadHint';
import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink';
diff --git a/static/app/components/events/interfaces/nativeFrame.tsx b/static/app/components/events/interfaces/nativeFrame.tsx
index 3d47927cbda43b..e356ebbfca0552 100644
--- a/static/app/components/events/interfaces/nativeFrame.tsx
+++ b/static/app/components/events/interfaces/nativeFrame.tsx
@@ -8,7 +8,7 @@ import InteractionStateLayer from '@sentry/scraps/interactionStateLayer';
import {Flex} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FRAME_TOOLTIP_MAX_WIDTH} from 'sentry/components/events/interfaces/frame/defaultTitle';
import {OpenInContextLine} from 'sentry/components/events/interfaces/frame/openInContextLine';
import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink';
diff --git a/static/app/components/events/interfaces/request/index.tsx b/static/app/components/events/interfaces/request/index.tsx
index 855760a97f1956..0803cab1dd2775 100644
--- a/static/app/components/events/interfaces/request/index.tsx
+++ b/static/app/components/events/interfaces/request/index.tsx
@@ -8,7 +8,7 @@ import {SegmentedControl} from '@sentry/scraps/segmentedControl';
import {Text} from '@sentry/scraps/text';
import {ClippedBox} from 'sentry/components/clippedBox';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventDataSection} from 'sentry/components/events/eventDataSection';
import {GraphQlRequestBody} from 'sentry/components/events/interfaces/request/graphQlRequestBody';
import {getCurlCommand, getFullUrl} from 'sentry/components/events/interfaces/utils';
diff --git a/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx b/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx
index 06f332e1096281..3f2f40293f1835 100644
--- a/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx
+++ b/static/app/components/events/interfaces/request/richHttpContentClippedBoxBodySection.tsx
@@ -1,5 +1,5 @@
import {ClippedBox} from 'sentry/components/clippedBox';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList';
import {StructuredEventData} from 'sentry/components/structuredEventData';
import {JsonEventData} from 'sentry/components/structuredEventData/jsonEventData';
diff --git a/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx b/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx
index 646c6d1af9b5bf..56cfc23a7449ef 100644
--- a/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx
+++ b/static/app/components/events/interfaces/request/richHttpContentClippedBoxKeyValueList.tsx
@@ -1,5 +1,5 @@
import {ClippedBox} from 'sentry/components/clippedBox';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList';
import type {EntryRequest} from 'sentry/types/event';
import type {Meta} from 'sentry/types/group';
diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx
index 2ee80d32006a81..1bf2c0d1ec2287 100644
--- a/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx
+++ b/static/app/components/events/interfaces/spans/spanTreeModel.spec.tsx
@@ -1,6 +1,6 @@
import {waitFor} from 'sentry-test/reactTestingLibrary';
-import SpanTreeModel from 'sentry/components/events/interfaces/spans/spanTreeModel';
+import {SpanTreeModel} from 'sentry/components/events/interfaces/spans/spanTreeModel';
import type {
EnhancedProcessedSpanType,
RawSpanType,
@@ -607,7 +607,7 @@ describe('SpanTreeModel', () => {
// If statement here is required to avoid TS linting issues
if (spans[1]!.type === 'span_group_siblings') {
- expect(spans[1]!.spanSiblingGrouping!).toHaveLength(5);
+ expect(spans[1]!.spanSiblingGrouping).toHaveLength(5);
}
});
diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.tsx
index 40e6f5981b4fe2..1894714678ff56 100644
--- a/static/app/components/events/interfaces/spans/spanTreeModel.tsx
+++ b/static/app/components/events/interfaces/spans/spanTreeModel.tsx
@@ -34,7 +34,7 @@ import {
const MIN_SIBLING_GROUP_SIZE = 5;
-class SpanTreeModel {
+export class SpanTreeModel {
api: Client;
// readonly state
@@ -847,5 +847,3 @@ class SpanTreeModel {
};
};
}
-
-export default SpanTreeModel;
diff --git a/static/app/components/events/interfaces/spans/types.tsx b/static/app/components/events/interfaces/spans/types.tsx
index 824cb7ba9e529d..1acc38d15d6685 100644
--- a/static/app/components/events/interfaces/spans/types.tsx
+++ b/static/app/components/events/interfaces/spans/types.tsx
@@ -1,6 +1,6 @@
import type {Fuse} from 'sentry/utils/fuzzySearch';
-import type SpanTreeModel from './spanTreeModel';
+import type {SpanTreeModel} from './spanTreeModel';
export type GapSpanType = {
isOrphan: boolean;
diff --git a/static/app/components/events/interfaces/spans/utils.tsx b/static/app/components/events/interfaces/spans/utils.tsx
index e37285992d726f..34625c4a442603 100644
--- a/static/app/components/events/interfaces/spans/utils.tsx
+++ b/static/app/components/events/interfaces/spans/utils.tsx
@@ -13,7 +13,7 @@ import type {
import {EntryType} from 'sentry/types/event';
import {assert} from 'sentry/types/utils';
-import type SpanTreeModel from './spanTreeModel';
+import type {SpanTreeModel} from './spanTreeModel';
import type {
AggregateSpanType,
GapSpanType,
diff --git a/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx b/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx
index 72bdb31a948427..06e213a28bc52a 100644
--- a/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx
+++ b/static/app/components/events/interfaces/spans/waterfallModel.spec.tsx
@@ -1,7 +1,7 @@
import type {ActiveFilter} from 'sentry/components/events/interfaces/spans/filter';
import {noFilter} from 'sentry/components/events/interfaces/spans/filter';
import type {EnhancedProcessedSpanType} from 'sentry/components/events/interfaces/spans/types';
-import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
+import {WaterfallModel} from 'sentry/components/events/interfaces/spans/waterfallModel';
import type {EventTransaction} from 'sentry/types/event';
import {EntryType} from 'sentry/types/event';
import {assert} from 'sentry/types/utils';
diff --git a/static/app/components/events/interfaces/spans/waterfallModel.tsx b/static/app/components/events/interfaces/spans/waterfallModel.tsx
index 309f8575865003..042ffb5ee5bbae 100644
--- a/static/app/components/events/interfaces/spans/waterfallModel.tsx
+++ b/static/app/components/events/interfaces/spans/waterfallModel.tsx
@@ -9,7 +9,7 @@ import {createFuzzySearch} from 'sentry/utils/fuzzySearch';
import type {ActiveOperationFilter} from './filter';
import {noFilter, toggleAllFilters, toggleFilter} from './filter';
-import SpanTreeModel from './spanTreeModel';
+import {SpanTreeModel} from './spanTreeModel';
import type {
EnhancedProcessedSpanType,
FilterSpans,
@@ -21,7 +21,7 @@ import type {
} from './types';
import {boundsGenerator, generateRootSpan, getSpanID, parseTrace} from './utils';
-class WaterfallModel {
+export class WaterfallModel {
api: Client = new Client();
// readonly state
@@ -356,5 +356,3 @@ class WaterfallModel {
});
};
}
-
-export default WaterfallModel;
diff --git a/static/app/components/events/interfaces/threads.tsx b/static/app/components/events/interfaces/threads.tsx
index 6ba512e40688c3..593aeb24b2a58d 100644
--- a/static/app/components/events/interfaces/threads.tsx
+++ b/static/app/components/events/interfaces/threads.tsx
@@ -5,7 +5,7 @@ import {Button, ButtonBar} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
import {CommitRow} from 'sentry/components/commitRow';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
StacktraceContext,
useStacktraceContext,
diff --git a/static/app/components/feedback/feedbackItem/feedbackActions.tsx b/static/app/components/feedback/feedbackItem/feedbackActions.tsx
index e64feca2f18304..6289a92c5e3fac 100644
--- a/static/app/components/feedback/feedbackItem/feedbackActions.tsx
+++ b/static/app/components/feedback/feedbackItem/feedbackActions.tsx
@@ -6,7 +6,7 @@ import {Flex} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackAssignedTo} from 'sentry/components/feedback/feedbackItem/feedbackAssignedTo';
import {useFeedbackActions} from 'sentry/components/feedback/feedbackItem/useFeedbackActions';
import {IconCopy, IconEllipsis} from 'sentry/icons';
diff --git a/static/app/components/feedback/feedbackItem/feedbackItem.tsx b/static/app/components/feedback/feedbackItem/feedbackItem.tsx
index b88ab29990baf3..8cb117b0886408 100644
--- a/static/app/components/feedback/feedbackItem/feedbackItem.tsx
+++ b/static/app/components/feedback/feedbackItem/feedbackItem.tsx
@@ -2,7 +2,7 @@ import {Fragment, useEffect, useMemo, useRef} from 'react';
import styled from '@emotion/styled';
import {AnalyticsArea} from 'sentry/components/analyticsArea';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {getOrderedContextItems} from 'sentry/components/events/contexts';
import {ContextCard} from 'sentry/components/events/contexts/contextCard';
import {EventTagsTree} from 'sentry/components/events/eventTags/eventTagsTree';
diff --git a/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx b/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx
index cb32a3b88c0a9b..ab87b8a395250c 100644
--- a/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx
+++ b/static/app/components/feedback/feedbackItem/feedbackItemHeader.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import {Button} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackActions} from 'sentry/components/feedback/feedbackItem/feedbackActions';
import {FeedbackShortId} from 'sentry/components/feedback/feedbackItem/feedbackShortId';
import {FeedbackViewers} from 'sentry/components/feedback/feedbackItem/feedbackViewers';
diff --git a/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx b/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx
index 51b51a907de38e..07ae5a5486978c 100644
--- a/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx
+++ b/static/app/components/feedback/feedbackItem/feedbackItemLoader.tsx
@@ -1,6 +1,6 @@
import {useEffect} from 'react';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackEmptyDetails} from 'sentry/components/feedback/details/feedbackEmptyDetails';
import {FeedbackErrorDetails} from 'sentry/components/feedback/details/feedbackErrorDetails';
import {FeedbackItem} from 'sentry/components/feedback/feedbackItem/feedbackItem';
diff --git a/static/app/components/feedback/feedbackItem/feedbackReplay.tsx b/static/app/components/feedback/feedbackItem/feedbackReplay.tsx
index 84aa2bb573fea7..632f57ebcef1f5 100644
--- a/static/app/components/feedback/feedbackItem/feedbackReplay.tsx
+++ b/static/app/components/feedback/feedbackItem/feedbackReplay.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackItemSection} from 'sentry/components/feedback/feedbackItem/feedbackItemSection';
import {ReplayInlineCTAPanel} from 'sentry/components/feedback/feedbackItem/replayInlineCTAPanel';
import {ReplaySection} from 'sentry/components/feedback/feedbackItem/replaySection';
diff --git a/static/app/components/feedback/list/feedbackList.tsx b/static/app/components/feedback/list/feedbackList.tsx
index f7eff63c352788..d7500de50dd00f 100644
--- a/static/app/components/feedback/list/feedbackList.tsx
+++ b/static/app/components/feedback/list/feedbackList.tsx
@@ -8,7 +8,7 @@ import {Stack} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
import type {ApiResult} from 'sentry/api';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackListHeader} from 'sentry/components/feedback/list/feedbackListHeader';
import {FeedbackListItem} from 'sentry/components/feedback/list/feedbackListItem';
import {useFeedbackQueryKeys} from 'sentry/components/feedback/useFeedbackQueryKeys';
diff --git a/static/app/components/feedback/list/feedbackListBulkSelection.tsx b/static/app/components/feedback/list/feedbackListBulkSelection.tsx
index 3c8e5b338dcb6a..219694163da955 100644
--- a/static/app/components/feedback/list/feedbackListBulkSelection.tsx
+++ b/static/app/components/feedback/list/feedbackListBulkSelection.tsx
@@ -2,7 +2,7 @@ import {Button} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {useBulkEditFeedbacks} from 'sentry/components/feedback/list/useBulkEditFeedbacks';
import type {Mailbox} from 'sentry/components/feedback/useMailbox';
import {IconEllipsis} from 'sentry/icons/iconEllipsis';
diff --git a/static/app/components/feedback/useMutateActivity.tsx b/static/app/components/feedback/useMutateActivity.tsx
index b53dff3cb1654e..6b456c902d2223 100644
--- a/static/app/components/feedback/useMutateActivity.tsx
+++ b/static/app/components/feedback/useMutateActivity.tsx
@@ -9,10 +9,10 @@ import type {RequestError} from 'sentry/utils/requestError/requestError';
type TPayload = {activity: GroupActivity[]; note?: NoteType; noteId?: string};
type TMethod = 'PUT' | 'POST' | 'DELETE';
-export type TData = GroupActivity;
-export type TError = RequestError;
-export type TVariables = [TPayload, TMethod];
-export type TContext = unknown;
+type TData = GroupActivity;
+type TError = RequestError;
+type TVariables = [TPayload, TMethod];
+type TContext = unknown;
type DeleteCommentCallback = (
noteId: string,
diff --git a/static/app/components/forms/formField/controlState.tsx b/static/app/components/forms/formField/controlState.tsx
index 10b6fe05bae464..ec9f231cc519aa 100644
--- a/static/app/components/forms/formField/controlState.tsx
+++ b/static/app/components/forms/formField/controlState.tsx
@@ -2,7 +2,7 @@ import {Observer} from 'mobx-react-lite';
import {ControlState} from 'sentry/components/forms/fieldGroup/controlState';
import type {FormModel} from 'sentry/components/forms/model';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
type Props = {
model: FormModel;
diff --git a/static/app/components/forms/formField/index.tsx b/static/app/components/forms/formField/index.tsx
index 4b8967e5598415..7a76ed94968131 100644
--- a/static/app/components/forms/formField/index.tsx
+++ b/static/app/components/forms/formField/index.tsx
@@ -17,7 +17,7 @@ import type {FieldGroupProps} from 'sentry/components/forms/fieldGroup/types';
import {FormContext} from 'sentry/components/forms/formContext';
import type {FormModel} from 'sentry/components/forms/model';
import {MockModel} from 'sentry/components/forms/model';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
import type {FieldValue} from 'sentry/components/forms/types';
import {PanelAlert} from 'sentry/components/panels/panelAlert';
import {t} from 'sentry/locale';
diff --git a/static/app/components/forms/model.tsx b/static/app/components/forms/model.tsx
index e92ff45f9733a1..9a6efbfbc13204 100644
--- a/static/app/components/forms/model.tsx
+++ b/static/app/components/forms/model.tsx
@@ -5,7 +5,7 @@ import {action, computed, makeObservable, observable} from 'mobx';
import {addErrorMessage} from 'sentry/actionCreators/indicator';
import {Client} from 'sentry/api';
import {addUndoableFormChangeMessage} from 'sentry/components/forms/formIndicators';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
import {t} from 'sentry/locale';
import type {Choice} from 'sentry/types/core';
import {defined} from 'sentry/utils';
diff --git a/static/app/components/forms/state.tsx b/static/app/components/forms/state.tsx
index 4602f21746327e..2ad48b6acf9cbe 100644
--- a/static/app/components/forms/state.tsx
+++ b/static/app/components/forms/state.tsx
@@ -1,4 +1,4 @@
-enum FormState {
+export enum FormState {
HOVER = 'Hover',
DISABLED = 'Disabled',
LOADING = 'Loading',
@@ -7,5 +7,3 @@ enum FormState {
ERROR = 'Error',
INCOMPLETE = 'Incomplete',
}
-
-export default FormState;
diff --git a/static/app/components/globalDrawer/index.tsx b/static/app/components/globalDrawer/index.tsx
index 271d527d9b322e..cc0b5c354aed76 100644
--- a/static/app/components/globalDrawer/index.tsx
+++ b/static/app/components/globalDrawer/index.tsx
@@ -13,7 +13,7 @@ import type {Location} from 'history';
import {useScrollLock} from '@sentry/scraps/useScrollLock';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {DrawerComponents} from 'sentry/components/globalDrawer/components';
import {t} from 'sentry/locale';
import {defined} from 'sentry/utils';
diff --git a/static/app/components/group/externalIssuesList/index.tsx b/static/app/components/group/externalIssuesList/index.tsx
index ac0d0108a11e73..e4d42b65a56583 100644
--- a/static/app/components/group/externalIssuesList/index.tsx
+++ b/static/app/components/group/externalIssuesList/index.tsx
@@ -7,7 +7,7 @@ import {Tooltip} from '@sentry/scraps/tooltip';
import {DropdownButton} from 'sentry/components/dropdownButton';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import type {ExternalIssueAction} from 'sentry/components/group/externalIssuesList/hooks/types';
import {useGroupExternalIssues} from 'sentry/components/group/externalIssuesList/hooks/useGroupExternalIssues';
import {Placeholder} from 'sentry/components/placeholder';
diff --git a/static/app/components/groupHeaderRow.tsx b/static/app/components/groupHeaderRow.tsx
index 8dd17a27fd8c8e..b7d8174b14afa0 100644
--- a/static/app/components/groupHeaderRow.tsx
+++ b/static/app/components/groupHeaderRow.tsx
@@ -4,7 +4,7 @@ import {useHover} from '@react-aria/interactions';
import {Link} from '@sentry/scraps/link';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventMessage} from 'sentry/components/events/eventMessage';
import {GroupTitle} from 'sentry/components/groupTitle';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
diff --git a/static/app/components/idBadge/index.tsx b/static/app/components/idBadge/index.tsx
index eea81d80d035e3..c4c273addd5d3f 100644
--- a/static/app/components/idBadge/index.tsx
+++ b/static/app/components/idBadge/index.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import type {GetBadgeProps} from './getBadge';
import {getBadge} from './getBadge';
diff --git a/static/app/components/layouts/thirds.tsx b/static/app/components/layouts/thirds.tsx
index 39d78237619e95..906ccffcf23c5a 100644
--- a/static/app/components/layouts/thirds.tsx
+++ b/static/app/components/layouts/thirds.tsx
@@ -5,6 +5,7 @@ import styled from '@emotion/styled';
import {Container, Stack, type FlexProps} from '@sentry/scraps/layout';
import {Tabs} from '@sentry/scraps/tabs';
+import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
import {SecondaryNavigationContext} from 'sentry/views/navigation/secondaryNavigationContext';
import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
@@ -13,6 +14,7 @@ import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFea
*/
export function Page(props: FlexProps<'main'> & {withPadding?: boolean}) {
const hasPageFrame = useHasPageFrameFeature();
+ const primaryNavigation = usePrimaryNavigation();
const secondaryNavigation = useContext(SecondaryNavigationContext);
const {withPadding, ...rest} = props;
@@ -22,10 +24,29 @@ export function Page(props: FlexProps<'main'> & {withPadding?: boolean}) {
@@ -37,9 +58,9 @@ export function Page(props: FlexProps<'main'> & {withPadding?: boolean}) {
);
}
-const StyledPageFrameStack = styled(Stack)`
+const StyledPageFrameStack = styled(Stack)<{roundedCorner: boolean}>`
> :first-child {
- border-top-left-radius: ${p => p.theme.radius.lg};
+ border-top-left-radius: ${p => (p.roundedCorner ? p.theme.radius.lg : undefined)};
}
`;
diff --git a/static/app/components/links/externalLink.tsx b/static/app/components/links/externalLink.tsx
index 12e1ceb35f03f4..2a1b9bcf34c86b 100644
--- a/static/app/components/links/externalLink.tsx
+++ b/static/app/components/links/externalLink.tsx
@@ -1,6 +1,8 @@
import {ExternalLink} from '@sentry/scraps/link';
-/**
- * @deprecated Use `ExternalLink` from `@sentry/scraps/link` instead.
- */
-export default ExternalLink;
+export {
+ /**
+ * @deprecated Use `ExternalLink` from `@sentry/scraps/link` instead.
+ */
+ ExternalLink,
+};
diff --git a/static/app/components/modals/dataWidgetViewerModal.spec.tsx b/static/app/components/modals/dataWidgetViewerModal.spec.tsx
index 87a102ef2c42f9..9c6b9f841921a1 100644
--- a/static/app/components/modals/dataWidgetViewerModal.spec.tsx
+++ b/static/app/components/modals/dataWidgetViewerModal.spec.tsx
@@ -24,7 +24,7 @@ import {ProjectsStore} from 'sentry/stores/projectsStore';
import type {DashboardFilters, Widget, WidgetQuery} from 'sentry/views/dashboards/types';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import {performanceScoreTooltip} from 'sentry/views/dashboards/utils';
-import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
jest.mock('echarts-for-react/lib/core', () => {
return jest.fn(({style}) => {
diff --git a/static/app/components/modals/dataWidgetViewerModal.tsx b/static/app/components/modals/dataWidgetViewerModal.tsx
index 13a1c9f18d41cc..d759c0c0f782aa 100644
--- a/static/app/components/modals/dataWidgetViewerModal.tsx
+++ b/static/app/components/modals/dataWidgetViewerModal.tsx
@@ -31,8 +31,7 @@ import {defined} from 'sentry/utils';
import {CAN_MARK, trackAnalytics} from 'sentry/utils/analytics';
import {getUtcDateString} from 'sentry/utils/dates';
import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {MetaType} from 'sentry/utils/discover/eventView';
+import type {EventView, MetaType} from 'sentry/utils/discover/eventView';
import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
import type {Sort} from 'sentry/utils/discover/fields';
import {
@@ -105,7 +104,7 @@ import {ReleaseWidgetQueries} from 'sentry/views/dashboards/widgetCard/releaseWi
import {VisualizationWidget} from 'sentry/views/dashboards/widgetCard/visualizationWidget';
import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer';
import {WidgetQueries} from 'sentry/views/dashboards/widgetCard/widgetQueries';
-import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import {AgentsTracesTableWidgetVisualization} from 'sentry/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization';
import {ALLOWED_CELL_ACTIONS} from 'sentry/views/dashboards/widgets/common/settings';
import {TableWidgetVisualization} from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';
diff --git a/static/app/components/modals/featureTourModal.spec.tsx b/static/app/components/modals/featureTourModal.spec.tsx
index 862b279e1dee7e..f74651fe510eef 100644
--- a/static/app/components/modals/featureTourModal.spec.tsx
+++ b/static/app/components/modals/featureTourModal.spec.tsx
@@ -3,7 +3,7 @@ import {Fragment} from 'react';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {GlobalModal} from 'sentry/components/globalModal';
-import FeatureTourModal from 'sentry/components/modals/featureTourModal';
+import {FeatureTourModal} from 'sentry/components/modals/featureTourModal';
const steps = [
{
diff --git a/static/app/components/modals/featureTourModal.tsx b/static/app/components/modals/featureTourModal.tsx
index 66518ef59f0e84..3394909dcc4ece 100644
--- a/static/app/components/modals/featureTourModal.tsx
+++ b/static/app/components/modals/featureTourModal.tsx
@@ -72,7 +72,7 @@ const defaultProps = {
* trigger re-renders in the modal contents. This requires a bit of duplicate state
* to be managed around the current step.
*/
-class FeatureTourModal extends Component {
+export class FeatureTourModal extends Component {
static defaultProps = defaultProps;
state: State = {
@@ -122,8 +122,6 @@ class FeatureTourModal extends Component {
}
}
-export default FeatureTourModal;
-
type ContentsProps = ModalRenderProps &
Pick &
Pick;
diff --git a/static/app/components/modals/generateDashboardFromSeerModal.tsx b/static/app/components/modals/generateDashboardFromSeerModal.tsx
deleted file mode 100644
index 93d4d1da6b5fc4..00000000000000
--- a/static/app/components/modals/generateDashboardFromSeerModal.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import {Fragment, useCallback, useState} from 'react';
-import type {Location} from 'history';
-
-import {Button} from '@sentry/scraps/button';
-import {Flex} from '@sentry/scraps/layout';
-import {TextArea} from '@sentry/scraps/textarea';
-
-import {addErrorMessage} from 'sentry/actionCreators/indicator';
-import type {ModalRenderProps} from 'sentry/actionCreators/modal';
-import {IconSeer} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import type {Organization} from 'sentry/types/organization';
-import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import {fetchMutation} from 'sentry/utils/queryClient';
-import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
-import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate';
-
-export interface GenerateDashboardFromSeerModalProps {
- location: Location;
- navigate: ReactRouter3Navigate;
- organization: Organization;
-}
-
-function GenerateDashboardFromSeerModal({
- Header,
- Body,
- Footer,
- closeModal,
- organization,
- location,
- navigate,
-}: ModalRenderProps & GenerateDashboardFromSeerModalProps) {
- const [prompt, setPrompt] = useState('');
- const [isGenerating, setIsGenerating] = useState(false);
-
- const handleGenerate = useCallback(async () => {
- if (!prompt.trim()) {
- return;
- }
-
- setIsGenerating(true);
-
- try {
- const url = getApiUrl('/organizations/$organizationIdOrSlug/dashboards/generate/', {
- path: {
- organizationIdOrSlug: organization.slug,
- },
- });
- const response = await fetchMutation<{run_id: string}>({
- url,
- method: 'POST',
- data: {prompt: prompt.trim()},
- });
-
- const runId = response.run_id;
- if (!runId) {
- addErrorMessage(t('Failed to start dashboard generation'));
- setIsGenerating(false);
- return;
- }
-
- closeModal();
-
- navigate(
- normalizeUrl({
- pathname: `/organizations/${organization.slug}/dashboards/new/from-seer/`,
- query: {...location.query, seerRunId: String(runId)},
- })
- );
- } catch (error) {
- setIsGenerating(false);
- addErrorMessage(t('Failed to start dashboard generation'));
- }
- }, [prompt, organization.slug, location.query, closeModal, navigate]);
-
- return (
-
-
- {t('Create Dashboard with Agent')}
-
-
- {t('Describe the dashboard you would like to be generated for you.')}
-
- );
-}
-
-export default GenerateDashboardFromSeerModal;
diff --git a/static/app/components/modals/inviteMembersModal/index.tsx b/static/app/components/modals/inviteMembersModal/index.tsx
index 1af8c20d916059..bf8db823c4b839 100644
--- a/static/app/components/modals/inviteMembersModal/index.tsx
+++ b/static/app/components/modals/inviteMembersModal/index.tsx
@@ -2,7 +2,7 @@ import {css} from '@emotion/react';
import styled from '@emotion/styled';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {
diff --git a/static/app/components/modals/textWidgetViewerModal.spec.tsx b/static/app/components/modals/textWidgetViewerModal.spec.tsx
index acfff6760650e3..5db4a7ff8040f6 100644
--- a/static/app/components/modals/textWidgetViewerModal.spec.tsx
+++ b/static/app/components/modals/textWidgetViewerModal.spec.tsx
@@ -13,7 +13,7 @@ import {ConfigStore} from 'sentry/stores/configStore';
import {trackAnalytics} from 'sentry/utils/analytics';
import {DisplayType} from 'sentry/views/dashboards/types';
import type {DashboardPermissions, Widget} from 'sentry/views/dashboards/types';
-import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
jest.mock('sentry/utils/analytics');
diff --git a/static/app/components/modals/textWidgetViewerModal.tsx b/static/app/components/modals/textWidgetViewerModal.tsx
index fdef1349dd3171..757f133064c9a7 100644
--- a/static/app/components/modals/textWidgetViewerModal.tsx
+++ b/static/app/components/modals/textWidgetViewerModal.tsx
@@ -21,7 +21,7 @@ import type {
} from 'sentry/views/dashboards/types';
import {checkUserHasEditAccess} from 'sentry/views/dashboards/utils/checkUserHasEditAccess';
import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer';
-import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
interface TextWidgetViewerModalOptions {
organization: Organization;
diff --git a/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx b/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
index 61173a94b813b8..603661dfe74285 100644
--- a/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
+++ b/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx
@@ -70,7 +70,7 @@ import {convertWidgetToQueryParams} from 'sentry/views/dashboards/widgetBuilder/
import WidgetCard from 'sentry/views/dashboards/widgetCard';
import {DashboardsMEPProvider} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext';
import {WidgetLegendNameEncoderDecoder} from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
-import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import {getTopNConvertedDefaultWidgets} from 'sentry/views/dashboards/widgetLibrary/data';
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
import {MetricsDataSwitcher} from 'sentry/views/performance/landing/metricsDataSwitcher';
diff --git a/static/app/components/onboarding/consoleModal.tsx b/static/app/components/onboarding/consoleModal.tsx
index e02f1ead309e7b..4b42ca411b2280 100644
--- a/static/app/components/onboarding/consoleModal.tsx
+++ b/static/app/components/onboarding/consoleModal.tsx
@@ -8,7 +8,7 @@ import {Flex} from '@sentry/scraps/layout';
import {Heading} from '@sentry/scraps/text';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
-import ExternalLink from 'sentry/components/links/externalLink';
+import {ExternalLink} from 'sentry/components/links/externalLink';
import {ConsolePlatform} from 'sentry/constants/consolePlatforms';
import {t, tct} from 'sentry/locale';
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx
index 03e34771f0ff58..7cc88cc023067a 100644
--- a/static/app/components/onboarding/productSelection.tsx
+++ b/static/app/components/onboarding/productSelection.tsx
@@ -26,7 +26,7 @@ interface DisabledProduct {
export type DisabledProducts = Partial>;
-function getDisabledProducts(organization: Organization): DisabledProducts {
+export function getDisabledProducts(organization: Organization): DisabledProducts {
const disabledProducts: DisabledProducts = {};
const hasSessionReplay = organization.features.includes('session-replay');
const hasPerformance = organization.features.includes('performance-view');
diff --git a/static/app/components/performance/searchBar.spec.tsx b/static/app/components/performance/searchBar.spec.tsx
index f0a4a982fe78db..2fa974e554916c 100644
--- a/static/app/components/performance/searchBar.spec.tsx
+++ b/static/app/components/performance/searchBar.spec.tsx
@@ -6,7 +6,7 @@ import {textWithMarkupMatcher} from 'sentry-test/utils';
import type {SearchBarProps} from 'sentry/components/performance/searchBar';
import {SearchBar} from 'sentry/components/performance/searchBar';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
describe('SearchBar', () => {
diff --git a/static/app/components/performance/searchBar.tsx b/static/app/components/performance/searchBar.tsx
index d88d931fe2099e..19c6db2a2682a7 100644
--- a/static/app/components/performance/searchBar.tsx
+++ b/static/app/components/performance/searchBar.tsx
@@ -11,7 +11,7 @@ import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
diff --git a/static/app/components/profiling/profileEventsTable.tsx b/static/app/components/profiling/profileEventsTable.tsx
index 7cb526aa7aa858..23aa26d2648507 100644
--- a/static/app/components/profiling/profileEventsTable.tsx
+++ b/static/app/components/profiling/profileEventsTable.tsx
@@ -20,7 +20,7 @@ import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getTimeStampFromTableDateField} from 'sentry/utils/dates';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DURATION_UNITS} from 'sentry/utils/discover/fieldRenderers';
import {Container, NumberContainer} from 'sentry/utils/discover/styles';
import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
diff --git a/static/app/components/profiling/suspectFunctions/suspectFunctionsTable.tsx b/static/app/components/profiling/suspectFunctions/suspectFunctionsTable.tsx
index 9b405c63b580a9..43f2a3a46965a2 100644
--- a/static/app/components/profiling/suspectFunctions/suspectFunctionsTable.tsx
+++ b/static/app/components/profiling/suspectFunctions/suspectFunctionsTable.tsx
@@ -16,7 +16,7 @@ import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
import {FIELD_FORMATTERS} from 'sentry/utils/discover/fieldRenderers';
import {getShortEventId} from 'sentry/utils/events';
diff --git a/static/app/components/quickTrace/utils.tsx b/static/app/components/quickTrace/utils.tsx
index 33cfd85624b097..31b74c3006f1f4 100644
--- a/static/app/components/quickTrace/utils.tsx
+++ b/static/app/components/quickTrace/utils.tsx
@@ -8,7 +8,7 @@ import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
import type {Event, EventTransaction} from 'sentry/types/event';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
import type {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
diff --git a/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx b/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx
index 25c7fced9525ac..a84feeb02bc3a5 100644
--- a/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx
+++ b/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx
@@ -3,7 +3,7 @@ import {isValidElement, useEffect, useRef} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {BreadcrumbCodeSnippet} from 'sentry/components/replays/breadcrumbs/breadcrumbCodeSnippet';
import {BreadcrumbComparisonButton} from 'sentry/components/replays/breadcrumbs/breadcrumbComparisonButton';
import {BreadcrumbDescription} from 'sentry/components/replays/breadcrumbs/breadcrumbDescription';
diff --git a/static/app/components/replays/header/replayMetaData.tsx b/static/app/components/replays/header/replayMetaData.tsx
index e022d49c40006a..da2a938dea56bb 100644
--- a/static/app/components/replays/header/replayMetaData.tsx
+++ b/static/app/components/replays/header/replayMetaData.tsx
@@ -8,7 +8,7 @@ import {ErrorCounts} from 'sentry/components/replays/header/errorCounts';
import {ReplayViewers} from 'sentry/components/replays/header/replayViewers';
import {IconCursorArrow} from 'sentry/icons';
import {t} from 'sentry/locale';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getRouteStringFromRoutes} from 'sentry/utils/getRouteStringFromRoutes';
import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
import type {RawReplayError} from 'sentry/utils/replays/types';
diff --git a/static/app/components/replays/list/__stories__/replayList.tsx b/static/app/components/replays/list/__stories__/replayList.tsx
index 18e628a06fd310..80904b65388bf6 100644
--- a/static/app/components/replays/list/__stories__/replayList.tsx
+++ b/static/app/components/replays/list/__stories__/replayList.tsx
@@ -7,7 +7,7 @@ import {Container} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
import type {ApiResult} from 'sentry/api';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {InfiniteListItems} from 'sentry/components/infiniteList/infiniteListItems';
import {InfiniteListState} from 'sentry/components/infiniteList/infiniteListState';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/components/replays/replayTagsTableRow.tsx b/static/app/components/replays/replayTagsTableRow.tsx
index 84b6f31152dced..c8497ad428afc6 100644
--- a/static/app/components/replays/replayTagsTableRow.tsx
+++ b/static/app/components/replays/replayTagsTableRow.tsx
@@ -6,7 +6,7 @@ import type {LocationDescriptor} from 'history';
import {Link} from '@sentry/scraps/link';
import {Tooltip} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
import {KeyValueTableRow} from 'sentry/components/keyValueTable';
import {ReleaseDropdownFilter} from 'sentry/components/replays/releaseDropdownFilter';
diff --git a/static/app/components/replays/replayView.tsx b/static/app/components/replays/replayView.tsx
index fd1386d56fd3d6..c53301fba79f7a 100644
--- a/static/app/components/replays/replayView.tsx
+++ b/static/app/components/replays/replayView.tsx
@@ -6,7 +6,7 @@ import {ExternalLink} from '@sentry/scraps/link';
import {Tooltip} from '@sentry/scraps/tooltip';
import {NegativeSpaceContainer} from 'sentry/components/container/negativeSpaceContainer';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
import {CanvasSupportNotice} from 'sentry/components/replays/canvasSupportNotice';
import {
diff --git a/static/app/components/replays/table/deleteReplays.tsx b/static/app/components/replays/table/deleteReplays.tsx
index 7d385133410829..4d00ed547ff87f 100644
--- a/static/app/components/replays/table/deleteReplays.tsx
+++ b/static/app/components/replays/table/deleteReplays.tsx
@@ -13,7 +13,7 @@ import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicato
import {useAnalyticsArea} from 'sentry/components/analyticsArea';
import {openConfirmModal} from 'sentry/components/confirm';
import {Duration} from 'sentry/components/duration/duration';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {KeyValueData} from 'sentry/components/keyValueData';
import {useReplayBulkDeleteAuditLogQueryKey} from 'sentry/components/replays/bulkDelete/useReplayBulkDeleteAuditLog';
import {SimpleTable} from 'sentry/components/tables/simpleTable';
diff --git a/static/app/components/replays/usePlaylistQuery.tsx b/static/app/components/replays/usePlaylistQuery.tsx
index 1ab31798a5c3eb..77c45e1f02861b 100644
--- a/static/app/components/replays/usePlaylistQuery.tsx
+++ b/static/app/components/replays/usePlaylistQuery.tsx
@@ -1,7 +1,7 @@
import type {Query} from 'history';
import {parseStatsPeriod} from 'sentry/components/timeRangeSelector/utils';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import type {ReplayListQueryReferrer} from 'sentry/views/replays/types';
diff --git a/static/app/components/reprocessedBox.tsx b/static/app/components/reprocessedBox.tsx
deleted file mode 100644
index e184368aeeef31..00000000000000
--- a/static/app/components/reprocessedBox.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import {useState} from 'react';
-import styled from '@emotion/styled';
-
-import {Link} from '@sentry/scraps/link';
-
-import {BannerContainer, BannerSummary} from 'sentry/components/events/styles';
-import {IconCheckmark, IconClose} from 'sentry/icons';
-import {t, tct, tn} from 'sentry/locale';
-import type {GroupActivityReprocess} from 'sentry/types/group';
-import type {Organization} from 'sentry/types/organization';
-import {localStorageWrapper} from 'sentry/utils/localStorage';
-
-type Props = {
- groupCount: number;
- groupId: string;
- orgSlug: Organization['slug'];
- reprocessActivity: GroupActivityReprocess;
- className?: string;
-};
-
-export function ReprocessedBox({
- orgSlug,
- reprocessActivity,
- groupCount,
- className,
- groupId,
-}: Props) {
- const getBannerUniqueId = () => {
- const {id} = reprocessActivity;
-
- return `reprocessed-activity-${id}-banner-dismissed`;
- };
-
- const [isBannerHidden, setIsBannerHidden] = useState(
- localStorageWrapper.getItem(getBannerUniqueId()) === 'true'
- );
-
- const handleBannerDismiss = () => {
- localStorageWrapper.setItem(getBannerUniqueId(), 'true');
- setIsBannerHidden(true);
- };
-
- const renderMessage = () => {
- const {data} = reprocessActivity;
- const {eventCount, oldGroupId, newGroupId} = data;
-
- const reprocessedEventsRoute = `/organizations/${orgSlug}/issues/?query=reprocessing.original_issue_id:${oldGroupId}&referrer=reprocessed-activity`;
-
- if (groupCount === 0) {
- return tct('All events in this issue were moved during reprocessing. [link]', {
- link: (
-
- {tn('See %s new event', 'See %s new events', eventCount)}
-
- ),
- });
- }
-
- return tct('Events in this issue were successfully reprocessed. [link]', {
- link: (
-
- {newGroupId === Number(groupId)
- ? tn('See %s reprocessed event', 'See %s reprocessed events', eventCount)
- : tn('See %s new event', 'See %s new events', eventCount)}
-
- ),
- });
- };
-
- if (isBannerHidden) {
- return null;
- }
-
- return (
-
-
-
- {renderMessage()}
-
-
-
- );
-}
-
-const StyledBannerSummary = styled(BannerSummary)`
- & > svg:last-child {
- margin-right: 0;
- margin-left: ${p => p.theme.space.md};
- }
-`;
-
-const StyledIconClose = styled(IconClose)`
- cursor: pointer;
-`;
diff --git a/static/app/components/search/index.tsx b/static/app/components/search/index.tsx
index 0a0f19b5f27537..09aad3eee1b9d7 100644
--- a/static/app/components/search/index.tsx
+++ b/static/app/components/search/index.tsx
@@ -4,7 +4,7 @@ import debounce from 'lodash/debounce';
import {addErrorMessage} from 'sentry/actionCreators/indicator';
import {navigateTo} from 'sentry/actionCreators/navigation';
-import AutoComplete from 'sentry/components/autoComplete';
+import {AutoComplete} from 'sentry/components/autoComplete';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {Fuse} from 'sentry/utils/fuzzySearch';
diff --git a/static/app/components/search/list.tsx b/static/app/components/search/list.tsx
index 3c28812c815fde..cbb47f58a2d35f 100644
--- a/static/app/components/search/list.tsx
+++ b/static/app/components/search/list.tsx
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import {Flex} from '@sentry/scraps/layout';
-import type AutoComplete from 'sentry/components/autoComplete';
+import type {AutoComplete} from 'sentry/components/autoComplete';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
diff --git a/static/app/components/stackTrace/displayOptions.tsx b/static/app/components/stackTrace/displayOptions.tsx
new file mode 100644
index 00000000000000..80e875fa8bdcca
--- /dev/null
+++ b/static/app/components/stackTrace/displayOptions.tsx
@@ -0,0 +1,121 @@
+import {CompactSelect} from '@sentry/scraps/compactSelect';
+import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
+
+import {useStackTraceViewState} from 'sentry/components/stackTrace/stackTraceContext';
+import {IconSettings} from 'sentry/icons';
+import {t} from 'sentry/locale';
+
+const VIEW_OPTION_VALUES = [
+ 'most-relevant',
+ 'full-stack-trace',
+ 'raw-stack-trace',
+] as const;
+const SORT_OPTION_VALUES = ['newest', 'oldest'] as const;
+
+/**
+ * A single dropdown that consolidates view, sort, and display toggles.
+ */
+export function DisplayOptions() {
+ const {
+ view,
+ setView,
+ hasMinifiedStacktrace,
+ isMinified,
+ setIsMinified,
+ isNewestFirst,
+ setIsNewestFirst,
+ platform,
+ } = useStackTraceViewState();
+
+ const isJavaScriptPlatform =
+ platform?.startsWith('javascript') || platform?.startsWith('node');
+ const minifiedLabel = isJavaScriptPlatform ? t('Minified') : t('Unsymbolicated');
+ const minifiedUnavailableTooltip = isJavaScriptPlatform
+ ? t('Minified version not available')
+ : t('Unsymbolicated version not available');
+
+ const currentViewVal =
+ view === 'raw'
+ ? 'raw-stack-trace'
+ : view === 'full'
+ ? 'full-stack-trace'
+ : 'most-relevant';
+ const currentSortVal = isNewestFirst ? 'newest' : 'oldest';
+
+ const value = [currentViewVal, currentSortVal, ...(isMinified ? ['minified'] : [])];
+
+ function handleChange(opts: Array<{value: string}>) {
+ const vals = opts.map(o => o.value);
+
+ // Mutually exclusive view selection: pick the newly added view option
+ const newViewVals = vals.filter(v =>
+ VIEW_OPTION_VALUES.includes(v as (typeof VIEW_OPTION_VALUES)[number])
+ );
+ const newViewVal =
+ newViewVals.find(v => v !== currentViewVal) ?? newViewVals[0] ?? currentViewVal;
+ if (newViewVal === 'raw-stack-trace') {
+ setView('raw');
+ } else if (newViewVal === 'full-stack-trace') {
+ setView('full');
+ } else {
+ setView('app');
+ }
+
+ // Mutually exclusive sort selection: pick the newly added sort option
+ const newSortVals = vals.filter(v =>
+ SORT_OPTION_VALUES.includes(v as (typeof SORT_OPTION_VALUES)[number])
+ );
+ const newSortVal =
+ newSortVals.find(v => v !== currentSortVal) ?? newSortVals[0] ?? currentSortVal;
+ setIsNewestFirst(newSortVal === 'newest');
+
+ setIsMinified(vals.includes('minified'));
+ }
+
+ return (
+ (
+ }
+ aria-label={t('Display options')}
+ >
+ {t('Display')}
+
+ )}
+ multiple
+ position="bottom-end"
+ value={value}
+ onChange={handleChange}
+ options={[
+ {
+ label: t('View'),
+ options: [
+ {label: t('Most Relevant'), value: 'most-relevant'},
+ {label: t('Full Stack Trace'), value: 'full-stack-trace'},
+ {label: t('Raw Stack Trace'), value: 'raw-stack-trace'},
+ ],
+ },
+ {
+ label: t('Sort'),
+ options: [
+ {label: t('Newest'), value: 'newest'},
+ {label: t('Oldest'), value: 'oldest'},
+ ],
+ },
+ {
+ label: t('Display'),
+ options: [
+ {
+ label: minifiedLabel,
+ value: 'minified',
+ disabled: !hasMinifiedStacktrace,
+ tooltip: hasMinifiedStacktrace ? undefined : minifiedUnavailableTooltip,
+ },
+ ],
+ },
+ ]}
+ />
+ );
+}
diff --git a/static/app/components/stackTrace/exceptionGroup.spec.tsx b/static/app/components/stackTrace/exceptionGroup.spec.tsx
new file mode 100644
index 00000000000000..d6abc9b8d9f349
--- /dev/null
+++ b/static/app/components/stackTrace/exceptionGroup.spec.tsx
@@ -0,0 +1,140 @@
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import type {ExceptionValue} from 'sentry/types/event';
+
+import {
+ RelatedExceptionsTree,
+ ToggleRelatedExceptionsButton,
+ useHiddenExceptions,
+} from './exceptionGroup';
+
+/**
+ * Tree structure:
+ * ExceptionGroup (id=0, root)
+ * ├── ValueError (id=1)
+ * └── ExceptionGroup (id=2, nested)
+ * ├── TypeError (id=3)
+ * └── KeyError (id=4)
+ */
+function makeValues(): ExceptionValue[] {
+ const stub = {
+ stacktrace: null,
+ module: null,
+ threadId: null,
+ rawStacktrace: null,
+ };
+ return [
+ {
+ ...stub,
+ type: 'ExceptionGroup',
+ value: 'root',
+ mechanism: {
+ handled: true,
+ type: '',
+ exception_id: 0,
+ is_exception_group: true,
+ },
+ },
+ {
+ ...stub,
+ type: 'ValueError',
+ value: 'bad value',
+ mechanism: {handled: true, type: '', exception_id: 1, parent_id: 0},
+ },
+ {
+ ...stub,
+ type: 'ExceptionGroup',
+ value: 'nested',
+ mechanism: {
+ handled: true,
+ type: '',
+ exception_id: 2,
+ parent_id: 0,
+ is_exception_group: true,
+ },
+ },
+ {
+ ...stub,
+ type: 'TypeError',
+ value: 'type err',
+ mechanism: {handled: true, type: '', exception_id: 3, parent_id: 2},
+ },
+ {
+ ...stub,
+ type: 'KeyError',
+ value: 'key err',
+ mechanism: {handled: true, type: '', exception_id: 4, parent_id: 2},
+ },
+ ];
+}
+
+function TestHarness({values}: {values: ExceptionValue[]}) {
+ const {hiddenExceptions, toggleRelatedExceptions, expandException} =
+ useHiddenExceptions(values);
+
+ return (
+
+ {values.map(exc => {
+ const id = exc.mechanism?.exception_id;
+ const parentId = exc.mechanism?.parent_id;
+
+ if (parentId !== undefined && hiddenExceptions[parentId]) {
+ return null;
+ }
+
+ return (
+
+ {exc.type}
+
+
+
+ );
+ })}
+
+ );
+}
+
+describe('exceptionGroup', () => {
+ it('hides nested group children by default, reveals on toggle, and expands via tree link', async () => {
+ render();
+
+ // Root group and its direct children are visible
+ expect(screen.getByTestId('exc-0')).toBeInTheDocument();
+ expect(screen.getByTestId('exc-1')).toBeInTheDocument();
+ expect(screen.getByTestId('exc-2')).toBeInTheDocument();
+
+ // Nested group's children are hidden
+ expect(screen.queryByTestId('exc-3')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('exc-4')).not.toBeInTheDocument();
+
+ // Toggle reveals nested group's children
+ await userEvent.click(
+ screen.getByRole('button', {name: 'Show 2 related exceptions'})
+ );
+ expect(screen.getByTestId('exc-3')).toBeInTheDocument();
+ expect(screen.getByTestId('exc-4')).toBeInTheDocument();
+
+ // Toggle hides them again
+ await userEvent.click(
+ screen.getByRole('button', {name: 'Hide 2 related exceptions'})
+ );
+ expect(screen.queryByTestId('exc-3')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('exc-4')).not.toBeInTheDocument();
+
+ // Clicking a child link in the nested group's tree calls expandException,
+ // which un-hides the parent group's children
+ await userEvent.click(screen.getByRole('button', {name: 'TypeError: type err'}));
+ expect(screen.getByTestId('exc-3')).toBeInTheDocument();
+ expect(screen.getByTestId('exc-4')).toBeInTheDocument();
+ });
+});
diff --git a/static/app/components/stackTrace/exceptionGroup.tsx b/static/app/components/stackTrace/exceptionGroup.tsx
new file mode 100644
index 00000000000000..49c0af15046efc
--- /dev/null
+++ b/static/app/components/stackTrace/exceptionGroup.tsx
@@ -0,0 +1,263 @@
+import {useCallback, useState} from 'react';
+import styled from '@emotion/styled';
+
+import {Button} from '@sentry/scraps/button';
+import {Flex} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
+
+import {t, tn} from 'sentry/locale';
+import type {ExceptionValue} from 'sentry/types/event';
+import {defined} from 'sentry/utils';
+
+type HiddenExceptionsState = Record;
+
+/**
+ * Manages collapse/expand state for exception group hierarchies.
+ * Non-root exception groups (those with a parent_id) start with their
+ * children hidden. Users can toggle visibility or click a link in the
+ * related exceptions tree to reveal a specific branch.
+ */
+export function useHiddenExceptions(values: ExceptionValue[]) {
+ const [hiddenExceptions, setHiddenExceptions] = useState(() =>
+ values
+ .filter(
+ ({mechanism}) => mechanism?.is_exception_group && defined(mechanism.parent_id)
+ )
+ .reduce(
+ (acc, next) => ({...acc, [next.mechanism?.exception_id ?? -1]: true}),
+ {}
+ )
+ );
+
+ const toggleRelatedExceptions = useCallback((exceptionId: number) => {
+ setHiddenExceptions(old => {
+ if (!defined(old[exceptionId])) {
+ return old;
+ }
+ return {...old, [exceptionId]: !old[exceptionId]};
+ });
+ }, []);
+
+ const expandException = useCallback(
+ (exceptionId: number) => {
+ setHiddenExceptions(old => {
+ const exceptionValue = values.find(
+ value => value.mechanism?.exception_id === exceptionId
+ );
+ const exceptionGroupId = exceptionValue?.mechanism?.parent_id;
+ if (!defined(exceptionGroupId) || !defined(old[exceptionGroupId])) {
+ return old;
+ }
+ return {...old, [exceptionGroupId]: false};
+ });
+ },
+ [values]
+ );
+
+ return {hiddenExceptions, toggleRelatedExceptions, expandException};
+}
+
+function getExceptionName(exception: ExceptionValue) {
+ if (exception.type) {
+ return exception.value ? `${exception.type}: ${exception.value}` : exception.type;
+ }
+ return exception.value ?? t('Exception');
+}
+
+interface ToggleRelatedExceptionsButtonProps {
+ exception: ExceptionValue;
+ hiddenExceptions: HiddenExceptionsState;
+ toggleRelatedExceptions: (exceptionId: number) => void;
+ values: ExceptionValue[];
+}
+
+export function ToggleRelatedExceptionsButton({
+ exception,
+ hiddenExceptions,
+ toggleRelatedExceptions,
+ values,
+}: ToggleRelatedExceptionsButtonProps) {
+ const exceptionId = exception.mechanism?.exception_id;
+ if (!defined(exceptionId) || !defined(hiddenExceptions[exceptionId])) {
+ return null;
+ }
+
+ const collapsed = hiddenExceptions[exceptionId];
+ const numChildren = values.filter(
+ ({mechanism}) => mechanism?.parent_id === exceptionId
+ ).length;
+
+ return (
+ toggleRelatedExceptions(exceptionId)}
+ data-test-id="toggle-related-exceptions"
+ >
+ {collapsed
+ ? tn('Show %s related exception', 'Show %s related exceptions', numChildren)
+ : tn('Hide %s related exception', 'Hide %s related exceptions', numChildren)}
+
+ );
+}
+
+const MonoButton = styled(Button)`
+ font-family: ${p => p.theme.font.family.mono};
+ font-size: ${p => p.theme.font.size.sm};
+`;
+
+interface RelatedExceptionsTreeProps {
+ allExceptions: ExceptionValue[];
+ exception: ExceptionValue;
+ newestFirst: boolean;
+ onExceptionClick: (exceptionId: number) => void;
+}
+
+export function RelatedExceptionsTree({
+ exception,
+ allExceptions,
+ newestFirst,
+ onExceptionClick,
+}: RelatedExceptionsTreeProps) {
+ const mechanism = exception.mechanism;
+ if (!mechanism?.is_exception_group) {
+ return null;
+ }
+
+ const parentException = allExceptions.find(
+ exc => exc.mechanism?.exception_id === mechanism.parent_id
+ );
+ const currentException = allExceptions.find(
+ exc => exc.mechanism?.exception_id === mechanism.exception_id
+ );
+ const childExceptions = allExceptions.filter(
+ exc => exc.mechanism?.parent_id === mechanism.exception_id
+ );
+
+ if (newestFirst) {
+ childExceptions.reverse();
+ }
+
+ if (!currentException) {
+ return null;
+ }
+
+ return (
+
+
+ {t('Related Exceptions')}
+
+
+ {parentException && (
+
+ )}
+
+ {childExceptions.map((child, i) => (
+
+ ))}
+
+
+ );
+}
+
+function ExceptionTreeItem({
+ exception,
+ level,
+ firstChild,
+ link = true,
+ onExceptionClick,
+}: {
+ exception: ExceptionValue;
+ level: number;
+ onExceptionClick: (exceptionId: number) => void;
+ firstChild?: boolean;
+ link?: boolean;
+}) {
+ const exceptionId = exception.mechanism?.exception_id;
+ const name = getExceptionName(exception);
+
+ return (
+
+ {level > 0 && }
+
+ {link && defined(exceptionId) ? (
+
+ ) : (
+ {name}
+ )}
+
+ );
+}
+
+function TreeChildLine({firstChild}: {firstChild?: boolean}) {
+ return (
+
+
+
+
+ );
+}
+
+const TreePre = styled('pre')`
+ margin: 0;
+ overflow-x: auto;
+`;
+
+const TreeChildLineSvg = styled('svg')`
+ position: absolute;
+ left: 6px;
+ bottom: 50%;
+`;
+
+const TreeItem = styled('div')<{level: number}>`
+ position: relative;
+ display: grid;
+ align-items: center;
+ grid-template-columns: auto auto 1fr;
+ gap: ${p => p.theme.space.md};
+ padding-left: ${p => (p.level > 0 ? 20 : 0)}px;
+ margin-left: ${p => Math.max((p.level - 1) * 20, 0)}px;
+ height: 24px;
+ white-space: nowrap;
+`;
+
+const Circle = styled('div')`
+ border-radius: 50%;
+ height: 12px;
+ width: 12px;
+ border: 1px solid ${p => p.theme.tokens.border.primary};
+`;
diff --git a/static/app/components/stackTrace/exceptionHeader.tsx b/static/app/components/stackTrace/exceptionHeader.tsx
new file mode 100644
index 00000000000000..2857d6b6047080
--- /dev/null
+++ b/static/app/components/stackTrace/exceptionHeader.tsx
@@ -0,0 +1,70 @@
+import styled from '@emotion/styled';
+
+import {Flex} from '@sentry/scraps/layout';
+import {Heading} from '@sentry/scraps/text';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import {Mechanism} from 'sentry/components/events/interfaces/crashContent/exception/mechanism';
+import {renderLinksInText} from 'sentry/components/events/interfaces/crashContent/exception/utils';
+import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
+import {t} from 'sentry/locale';
+import type {StackTraceMechanism} from 'sentry/types/stacktrace';
+
+interface ExceptionHeaderProps {
+ module: string | null;
+ type: string;
+}
+
+const ExceptionHeaderHeading = styled(Heading)`
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+export function ExceptionHeader({type, module}: ExceptionHeaderProps) {
+ return (
+
+
+ {type}
+
+
+ );
+}
+
+interface ExceptionDescriptionProps {
+ mechanism: StackTraceMechanism | null;
+ value: string | null;
+ gap?: 'sm' | 'md' | 'lg';
+ meta?: Record;
+}
+
+export function ExceptionDescription({
+ value,
+ mechanism,
+ gap = 'sm',
+ meta,
+}: ExceptionDescriptionProps) {
+ const valueMeta = meta?.value?.[''];
+
+ return (
+
+ {valueMeta && !value ? (
+
+
+
+ ) : value ? (
+ {renderLinksInText({exceptionText: value})}
+ ) : null}
+ {mechanism && }
+
+ );
+}
+
+const ExceptionValue = styled('pre')`
+ background: none;
+ margin: 0;
+ padding: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+`;
diff --git a/static/app/components/stackTrace/frame/actions/chevron.tsx b/static/app/components/stackTrace/frame/actions/chevron.tsx
new file mode 100644
index 00000000000000..7bbfaf268f9d8d
--- /dev/null
+++ b/static/app/components/stackTrace/frame/actions/chevron.tsx
@@ -0,0 +1,38 @@
+import styled from '@emotion/styled';
+
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {IconChevron} from 'sentry/icons';
+
+const CHEVRON_SLOT_SIZE = 24;
+
+export function ChevronAction() {
+ const {hasAnyExpandableFrames} = useStackTraceContext();
+ const {isExpandable, isExpanded} = useStackTraceFrameContext();
+
+ if (!hasAnyExpandableFrames) {
+ return null;
+ }
+
+ return (
+
+ {isExpandable ? (
+
+ ) : null}
+
+ );
+}
+
+const ChevronSlot = styled('span')`
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: ${CHEVRON_SLOT_SIZE}px;
+ height: ${CHEVRON_SLOT_SIZE}px;
+ min-width: ${CHEVRON_SLOT_SIZE}px;
+ min-height: ${CHEVRON_SLOT_SIZE}px;
+ color: inherit;
+ flex-shrink: 0;
+`;
diff --git a/static/app/components/stackTrace/frame/actions/default.tsx b/static/app/components/stackTrace/frame/actions/default.tsx
new file mode 100644
index 00000000000000..4ee96f87177ee0
--- /dev/null
+++ b/static/app/components/stackTrace/frame/actions/default.tsx
@@ -0,0 +1,45 @@
+import {Fragment} from 'react';
+
+import {Tag} from '@sentry/scraps/badge';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {IconRefresh} from 'sentry/icons';
+import {t, tn} from 'sentry/locale';
+
+import {ChevronAction} from './chevron';
+import {HiddenFramesToggleAction} from './hiddenFramesToggle';
+
+/**
+ * Default trailing actions rendered for every frame row:
+ * hidden-frames toggle, repeated-frame badge, in-app badge, and chevron.
+ */
+export function DefaultFrameActions() {
+ const {hasAnyExpandableFrames} = useStackTraceContext();
+ const {frame, hiddenFrameCount, timesRepeated} = useStackTraceFrameContext();
+
+ return (
+
+ {hiddenFrameCount ? : null}
+ {timesRepeated > 0 ? (
+
+ }
+ variant="muted"
+ data-test-id="core-stacktrace-repeats-tag"
+ >
+ {timesRepeated}
+
+
+ ) : null}
+ {frame.inApp ? {t('In App')} : null}
+ {hasAnyExpandableFrames ? : null}
+
+ );
+}
diff --git a/static/app/components/stackTrace/frame/actions/hiddenFramesToggle.tsx b/static/app/components/stackTrace/frame/actions/hiddenFramesToggle.tsx
new file mode 100644
index 00000000000000..8ab21bf35e711c
--- /dev/null
+++ b/static/app/components/stackTrace/frame/actions/hiddenFramesToggle.tsx
@@ -0,0 +1,31 @@
+import {Button} from '@sentry/scraps/button';
+import {Text} from '@sentry/scraps/text';
+
+import {useStackTraceFrameContext} from 'sentry/components/stackTrace/stackTraceContext';
+import {tn} from 'sentry/locale';
+
+export function HiddenFramesToggleAction() {
+ const {hiddenFrameCount, hiddenFramesExpanded, toggleHiddenFrames} =
+ useStackTraceFrameContext();
+
+ if (!hiddenFrameCount) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/static/app/components/stackTrace/frame/actions/utils.tsx b/static/app/components/stackTrace/frame/actions/utils.tsx
new file mode 100644
index 00000000000000..d40b19ac012893
--- /dev/null
+++ b/static/app/components/stackTrace/frame/actions/utils.tsx
@@ -0,0 +1,9 @@
+export const VALID_SOURCE_MAP_DEBUGGER_FILE_EXTENSIONS = [
+ '.js',
+ '.mjs',
+ '.cjs',
+ '.jsbundle',
+ '.bundle',
+ '.hbc',
+ '.js.gz',
+];
diff --git a/static/app/components/stackTrace/frame/frameContent.tsx b/static/app/components/stackTrace/frame/frameContent.tsx
new file mode 100644
index 00000000000000..b6552716614124
--- /dev/null
+++ b/static/app/components/stackTrace/frame/frameContent.tsx
@@ -0,0 +1,254 @@
+import {Activity, useRef} from 'react';
+import {css} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import {Container, Grid} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import {Assembly} from 'sentry/components/events/interfaces/frame/assembly';
+import {FrameRegisters} from 'sentry/components/events/interfaces/frame/frameRegisters';
+import {usePrismTokensSourceContext} from 'sentry/components/events/interfaces/frame/usePrismTokensSourceContext';
+import {
+ hasAssembly,
+ hasContextRegisters,
+} from 'sentry/components/events/interfaces/frame/utils';
+import {parseAssembly} from 'sentry/components/events/interfaces/utils';
+import {FrameVariablesGrid} from 'sentry/components/stackTrace/frame/frameVariablesGrid';
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {t} from 'sentry/locale';
+import {Coverage} from 'sentry/types/integrations';
+import {getFileExtension} from 'sentry/utils/fileExtension';
+
+const COVERAGE_TEXT: Record = {
+ [Coverage.NOT_COVERED]: t('Line uncovered by tests'),
+ [Coverage.COVERED]: t('Line covered by tests'),
+ [Coverage.PARTIAL]: t('Line partially covered by tests'),
+ [Coverage.NOT_APPLICABLE]: undefined,
+};
+
+interface FrameContentProps {
+ sourceLineCoverage?: Array;
+}
+
+export function FrameContent({sourceLineCoverage = []}: FrameContentProps) {
+ const {event, frame, frameContextId, frameIndex, isExpanded, platform} =
+ useStackTraceFrameContext();
+ const {frames, lastFrameIndex, meta, stacktrace} = useStackTraceContext();
+
+ // Lazy: don't mount until first expanded, then preserve via Activity.
+ // A ref is sufficient — the re-render is already triggered by isExpanded changing.
+ const hasBeenExpandedRef = useRef(isExpanded);
+ if (isExpanded) {
+ hasBeenExpandedRef.current = true;
+ }
+
+ const contextLines = isExpanded ? (frame.context ?? []) : [];
+ const maxLineNumber = contextLines.reduce(
+ (max, [lineNo]) => Math.max(max, lineNo ?? 0),
+ 0
+ );
+ const lineNumberDigits = String(maxLineNumber).length;
+ const fileExtension = isExpanded ? (getFileExtension(frame.filename ?? '') ?? '') : '';
+ const prismLines = usePrismTokensSourceContext({
+ contextLines,
+ lineNo: frame.lineNo,
+ fileExtension,
+ });
+ const frameRegisters = frameIndex === frames.length - 1 ? stacktrace.registers : null;
+ const expandedFrameRegisters =
+ frameRegisters && hasContextRegisters(frameRegisters) ? frameRegisters : null;
+ const frameVariables = frame.vars;
+ const hasFrameAssembly = hasAssembly(frame, platform);
+ const hasSourceContext = contextLines.length > 0;
+ const hasFrameVariables = !!frameVariables && Object.keys(frameVariables).length > 0;
+ const hasFrameRegisters = !!expandedFrameRegisters;
+ const hasAnyFrameDetails =
+ hasSourceContext || hasFrameVariables || hasFrameRegisters || hasFrameAssembly;
+ const shouldShowNoDetails =
+ frameIndex === lastFrameIndex && frameIndex === 0 && !hasAnyFrameDetails;
+
+ if (!hasBeenExpandedRef.current) {
+ return null;
+ }
+
+ return (
+
+
+ {hasSourceContext ? (
+
+ {contextLines.map(([lineNumber, lineValue], lineIndex) => (
+
+
+
+ {lineNumber}
+
+
+
+ {(
+ prismLines[lineIndex] ?? [
+ {children: lineValue ?? '', className: 'token'},
+ ]
+ ).map((token, tokenIndex) => (
+
+ {token.children}
+
+ ))}
+
+
+ ))}
+
+ ) : shouldShowNoDetails ? (
+
+
+ {t('No additional details are available for this frame.')}
+
+
+ ) : null}
+ {hasFrameVariables ? (
+
+ ) : null}
+ {hasFrameRegisters ? (
+
+
+
+ ) : null}
+ {hasFrameAssembly ? (
+
+
+
+ ) : null}
+
+
+ );
+}
+
+const FrameSourceGrid = styled('div')`
+ display: grid;
+ width: 100%;
+ min-width: 0;
+`;
+
+const FrameSourceRow = styled(Grid)<{isActive: boolean; lineNumberDigits: number}>`
+ grid-template-columns:
+ calc(${p => Math.max(p.lineNumberDigits, 3) + 1}ch)
+ 1fr;
+ align-items: start;
+ min-width: 0;
+ background: ${p => (p.isActive ? p.theme.tokens.background.secondary : 'transparent')};
+`;
+
+const FrameSourceLineNumber = styled('div')<{
+ coverage: Coverage;
+ isActive: boolean;
+}>`
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ min-height: 1.8em;
+ font-family: ${p => p.theme.font.family.mono};
+ font-size: ${p => p.theme.font.size.sm};
+ color: ${p => p.theme.tokens.content.secondary};
+ line-height: 1.8;
+ text-align: right;
+ user-select: none;
+ padding-left: ${p => p.theme.space.xs};
+ padding-right: ${p => p.theme.space.xs};
+ border-right: 3px solid transparent;
+
+ ${p =>
+ p.coverage === Coverage.COVERED &&
+ css`
+ background: ${p.theme.colors.green100};
+ border-right-color: ${p.theme.tokens.border.success.vibrant};
+ `}
+
+ ${p =>
+ p.coverage === Coverage.NOT_COVERED &&
+ css`
+ background: ${p.theme.colors.red100};
+ border-right-color: ${p.theme.tokens.border.danger.vibrant};
+ `}
+
+ ${p =>
+ p.coverage === Coverage.PARTIAL &&
+ css`
+ background: ${p.theme.colors.yellow100};
+ border-right-style: dashed;
+ border-right-color: ${p.theme.tokens.border.warning.vibrant};
+ `}
+
+ ${p =>
+ p.isActive &&
+ p.coverage === Coverage.PARTIAL &&
+ css`
+ background: ${p.theme.colors.yellow200};
+ `}
+
+ ${p =>
+ p.isActive &&
+ p.coverage === Coverage.COVERED &&
+ css`
+ background: ${p.theme.colors.green200};
+ `}
+
+ ${p =>
+ p.isActive &&
+ p.coverage === Coverage.NOT_COVERED &&
+ css`
+ background: ${p.theme.colors.red200};
+ `}
+`;
+
+// overrides code[class*='language-'] in global.tsx
+const FrameSourceCode = styled('code')`
+ color: ${p => p.theme.tokens.content.primary};
+ font-family: ${p => p.theme.font.family.mono};
+ font-size: ${p => p.theme.font.size.sm};
+ line-height: 1.8;
+ display: block;
+ min-width: 0;
+ background: transparent;
+ padding: 0;
+ border-radius: 0;
+
+ && {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ background: transparent;
+ }
+`;
diff --git a/static/app/components/stackTrace/frame/frameHeader.tsx b/static/app/components/stackTrace/frame/frameHeader.tsx
new file mode 100644
index 00000000000000..748d4570faaaa3
--- /dev/null
+++ b/static/app/components/stackTrace/frame/frameHeader.tsx
@@ -0,0 +1,356 @@
+import {Fragment, useState} from 'react';
+import styled from '@emotion/styled';
+
+import {Text} from '@sentry/scraps/text';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import {
+ getLeadHint,
+ getPlatform,
+ isDotnet,
+} from 'sentry/components/events/interfaces/frame/utils';
+import {useStackTraceFrameContext} from 'sentry/components/stackTrace/stackTraceContext';
+import {t} from 'sentry/locale';
+import type {Frame} from 'sentry/types/event';
+import type {PlatformKey} from 'sentry/types/project';
+import {defined} from 'sentry/utils';
+import {isUrl} from 'sentry/utils/string/isUrl';
+
+function getFrameDisplayPath(frame: Frame, platform: PlatformKey) {
+ const framePlatform = getPlatform(frame.platform, platform);
+
+ if (framePlatform === 'java') {
+ return frame.module ?? frame.filename ?? '';
+ }
+
+ return frame.filename ?? frame.module ?? '';
+}
+
+function formatFrameLocation(
+ path: string,
+ lineNo: number | null | undefined,
+ colNo: number | null | undefined
+): string {
+ if (!defined(lineNo) || lineNo < 0) {
+ return path;
+ }
+
+ if (!defined(colNo) || colNo < 0) {
+ return `${path}:${lineNo}`;
+ }
+
+ return `${path}:${lineNo}:${colNo}`;
+}
+
+interface FrameHeaderProps {
+ /**
+ * Custom trailing actions for this frame. Pass a ReactNode, or a render
+ * function that receives `isHovering`.
+ */
+ actions?: React.ReactNode | ((props: {isHovering: boolean}) => React.ReactNode);
+}
+
+export function FrameHeader({actions}: FrameHeaderProps) {
+ const [isHovering, setIsHovering] = useState(false);
+ const {
+ frame,
+ frameContextId,
+ isExpandable,
+ isExpanded,
+ nextFrame,
+ platform,
+ toggleExpansion,
+ } = useStackTraceFrameContext();
+
+ const resolvedActions = typeof actions === 'function' ? actions({isHovering}) : actions;
+ const leadsToApp = !frame.inApp && (nextFrame?.inApp || !nextFrame);
+ const hasLeadHint = !isExpanded && leadsToApp;
+
+ return (
+ {
+ const selectedText = window.getSelection()?.toString();
+ if (isExpandable && !selectedText) {
+ toggleExpansion();
+ }
+ }}
+ onMouseEnter={() => setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ >
+
+
+
+
+
+
+
+ {resolvedActions}
+
+
+
+ );
+}
+
+function FrameLocation({
+ frame,
+ platform,
+ isExpanded,
+ leadsToApp,
+ hasNextFrame,
+}: {
+ frame: Frame;
+ hasNextFrame: boolean;
+ isExpanded: boolean;
+ leadsToApp: boolean;
+ platform: PlatformKey;
+}) {
+ const {event} = useStackTraceFrameContext();
+ const frameDisplayPath = getFrameDisplayPath(frame, platform);
+ const frameLocationSuffix = formatFrameLocation('', frame.lineNo, frame.colNo);
+
+ return (
+
+ {!isExpanded && leadsToApp ? (
+
+ {getLeadHint({event, hasNextFrame})}
+ {': '}
+
+ ) : null}
+
+
+
+ {frameDisplayPath}
+ {frameLocationSuffix ? {frameLocationSuffix} : null}
+
+
+
+
+ );
+}
+
+function FrameContext({frame, platform}: {frame: Frame; platform: PlatformKey}) {
+ const frameFunctionName = frame.function ?? frame.rawFunction;
+ const hasFrameFunction = !!frameFunctionName;
+ const framePlatform = getPlatform(frame.platform, platform);
+ const showPackage = !!frame.package && !isDotnet(framePlatform);
+
+ if (!hasFrameFunction && !showPackage) {
+ return null;
+ }
+
+ return (
+
+ {hasFrameFunction ? (
+
+
+ {t('in')}
+
+ {frameFunctionName}
+
+ ) : null}
+ {showPackage ? (
+
+
+ {t('within')}
+
+ {frame.package}
+
+ ) : null}
+
+ );
+}
+
+function FrameLocationTooltip({
+ frame,
+ frameDisplayPath,
+ children,
+}: {
+ children: React.ReactNode;
+ frame: Frame;
+ frameDisplayPath: string;
+}) {
+ const absPath =
+ frame.absPath && frame.absPath !== frameDisplayPath
+ ? formatFrameLocation(frame.absPath, frame.lineNo, frame.colNo)
+ : undefined;
+
+ const sourceMapInfoText = frame.mapUrl ?? frame.map;
+ const showSourceMap = !!frame.origAbsPath && !!sourceMapInfoText;
+ const externalUrl = frame.absPath && isUrl(frame.absPath) ? frame.absPath : undefined;
+
+ const hasContent = !!absPath || showSourceMap || !!externalUrl;
+
+ return (
+
+ {absPath ? (
+
+ {t('File')}
+ {absPath}
+
+ ) : null}
+ {showSourceMap ? (
+
+ {t('Source Map')}
+ {sourceMapInfoText}
+
+ ) : null}
+ {externalUrl ? (
+
+ {t('URL')}
+ e.stopPropagation()}
+ >
+ {externalUrl}
+
+
+ ) : null}
+
+ }
+ disabled={!hasContent}
+ maxWidth={450}
+ skipWrapper
+ delay={1000}
+ isHoverable
+ >
+ {children}
+
+ );
+}
+
+const HeaderGrid = styled('div')<{isExpandable: boolean; hasLeadHint?: boolean}>`
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) min-content;
+ gap: ${p => p.theme.space.sm};
+ align-items: center;
+ width: 100%;
+ cursor: ${p => (p.isExpandable ? 'pointer' : 'default')};
+ text-align: left;
+ padding: ${p => p.theme.space.sm} ${p => p.theme.space.md};
+ background: ${p => p.theme.tokens.background.secondary};
+
+ &:hover {
+ background: ${p => p.theme.tokens.background.tertiary};
+ }
+`;
+
+const MainContent = styled('div')<{isExpanded: boolean; isMuted: boolean}>`
+ display: flex;
+ flex-wrap: ${p => (p.isExpanded && !p.isMuted ? 'wrap' : 'nowrap')};
+ row-gap: ${p => p.theme.space['2xs']};
+ column-gap: ${p => p.theme.space.sm};
+ align-items: baseline;
+ color: ${p =>
+ p.isMuted ? p.theme.tokens.content.secondary : p.theme.tokens.content.primary};
+ font-size: ${p => p.theme.font.size.sm};
+ line-height: 1.5;
+ min-width: 0;
+`;
+
+const ActionArea = styled('div')`
+ display: flex;
+ align-items: center;
+ gap: ${p => p.theme.space.xs};
+ min-width: 0;
+`;
+
+const TrailingActions = styled('div')`
+ display: flex;
+ align-items: center;
+ gap: ${p => p.theme.space.xs};
+ margin-left: auto;
+`;
+
+const LocationWrapper = styled('span')`
+ display: inline-flex;
+ align-items: baseline;
+ min-width: 0;
+ max-width: 100%;
+ flex: 0 1 auto;
+ overflow: hidden;
+`;
+
+const LeadHint = styled('span')`
+ font-size: inherit;
+ line-height: inherit;
+ white-space: nowrap;
+ flex-shrink: 0;
+ margin-right: ${p => p.theme.space['2xs']};
+`;
+
+const Path = styled('span')`
+ display: inline-block;
+ vertical-align: baseline;
+ flex: 0 1 auto;
+ max-width: 100%;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ direction: rtl;
+ text-align: left;
+
+ > span {
+ direction: ltr;
+ unicode-bidi: isolate;
+ }
+`;
+
+const ContextWrapper = styled('span')`
+ display: inline-flex;
+ align-items: baseline;
+ flex: 0 999 auto;
+ gap: ${p => p.theme.space.sm};
+ max-width: 100%;
+ min-width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+`;
+
+const FuncName = styled('span')`
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const PkgName = styled('span')`
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ display: inline-block;
+ vertical-align: baseline;
+ min-width: 0;
+ max-width: min(45vw, 420px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const TooltipContent = styled('div')`
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ align-items: baseline;
+ column-gap: ${p => p.theme.space.sm};
+ row-gap: ${p => p.theme.space.md};
+ word-break: break-all;
+ text-align: left;
+`;
diff --git a/static/app/components/stackTrace/frame/frameRow.tsx b/static/app/components/stackTrace/frame/frameRow.tsx
new file mode 100644
index 00000000000000..d7c2632b5bbf75
--- /dev/null
+++ b/static/app/components/stackTrace/frame/frameRow.tsx
@@ -0,0 +1,115 @@
+import {memo, useId, useMemo, useState} from 'react';
+import styled from '@emotion/styled';
+
+import {Flex} from '@sentry/scraps/layout';
+
+import {isExpandable as frameHasExpandableDetails} from 'sentry/components/events/interfaces/frame/utils';
+import {
+ StackTraceFrameContext,
+ useStackTraceContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import type {StackTraceFrameContextValue} from 'sentry/components/stackTrace/stackTraceContext';
+import type {FrameRow} from 'sentry/components/stackTrace/types';
+
+import {ChevronAction} from './actions/chevron';
+import {DefaultFrameActions} from './actions/default';
+import {HiddenFramesToggleAction} from './actions/hiddenFramesToggle';
+import {FrameContent} from './frameContent';
+import {FrameHeader} from './frameHeader';
+
+interface StackTraceFrameRowProps {
+ children: React.ReactNode;
+ row: FrameRow;
+}
+
+const StackTraceFrameRowRoot = memo(function StackTraceFrameRowRoot({
+ row,
+ children,
+}: StackTraceFrameRowProps) {
+ const {
+ event,
+ frames,
+ lastFrameIndex,
+ platform,
+ stacktrace,
+ hiddenFrameToggleMap,
+ toggleHiddenFrames,
+ } = useStackTraceContext();
+
+ const registers = row.frameIndex === frames.length - 1 ? stacktrace.registers : {};
+ const [isExpanded, setIsExpanded] = useState(() => row.frameIndex === lastFrameIndex);
+
+ const isFrameExpandable = frameHasExpandableDetails({
+ frame: row.frame,
+ registers,
+ platform,
+ });
+
+ const frameContextId = useId();
+
+ const value = useMemo(
+ () => ({
+ event,
+ frame: row.frame,
+ frameContextId,
+ frameIndex: row.frameIndex,
+ hiddenFrameCount: row.hiddenFrameCount,
+ hiddenFramesExpanded: !!hiddenFrameToggleMap[row.frameIndex],
+ isExpandable: isFrameExpandable,
+ isExpanded,
+ nextFrame: row.nextFrame,
+ platform,
+ timesRepeated: row.timesRepeated,
+ toggleExpansion: () => {
+ setIsExpanded(prevState => !prevState);
+ },
+ toggleHiddenFrames: () => {
+ toggleHiddenFrames(row.frameIndex);
+ },
+ }),
+ [
+ event,
+ frameContextId,
+ hiddenFrameToggleMap,
+ isExpanded,
+ isFrameExpandable,
+ platform,
+ row.frame,
+ row.frameIndex,
+ row.hiddenFrameCount,
+ row.nextFrame,
+ row.timesRepeated,
+ toggleHiddenFrames,
+ ]
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+});
+
+function FrameRowActionsContainer({children}: {children: React.ReactNode}) {
+ return (
+
+ {children}
+
+ );
+}
+
+const FrameRowActions = Object.assign(FrameRowActionsContainer, {
+ Chevron: ChevronAction,
+ Default: DefaultFrameActions,
+ HiddenFramesToggle: HiddenFramesToggleAction,
+});
+
+export const StackTraceFrameRow = Object.assign(StackTraceFrameRowRoot, {
+ Context: FrameContent,
+ Header: FrameHeader,
+ Actions: FrameRowActions,
+});
+
+const FrameRowContainer = styled('div')``;
diff --git a/static/app/components/stackTrace/frame/frameVariablesGrid.spec.tsx b/static/app/components/stackTrace/frame/frameVariablesGrid.spec.tsx
new file mode 100644
index 00000000000000..5ac6300fb63598
--- /dev/null
+++ b/static/app/components/stackTrace/frame/frameVariablesGrid.spec.tsx
@@ -0,0 +1,152 @@
+import {DataScrubbingRelayPiiConfigFixture} from 'sentry-fixture/dataScrubbingRelayPiiConfig';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
+import {textWithMarkupMatcher} from 'sentry-test/utils';
+
+import {FrameVariablesGrid} from 'sentry/components/stackTrace/frame/frameVariablesGrid';
+import {ProjectsStore} from 'sentry/stores/projectsStore';
+
+describe('FrameVariablesGrid', () => {
+ it('renders variables sorted alphabetically', () => {
+ render(
+
+ );
+
+ const keys = screen.getAllByText(/^(alpha|middle|zebra)$/);
+ expect(keys[0]).toHaveTextContent('alpha');
+ expect(keys[1]).toHaveTextContent('middle');
+ expect(keys[2]).toHaveTextContent('zebra');
+ });
+
+ it('strips quotes from variable keys', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('quoted')).toBeInTheDocument();
+ expect(screen.getByText('unquoted')).toBeInTheDocument();
+ });
+
+ it('renders null when data is null', () => {
+ const {container} = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('renders meta annotations with tooltips for filtered values', async () => {
+ const organization = OrganizationFixture();
+ const project = ProjectFixture({id: '0'});
+ const projectDetails = ProjectFixture({
+ ...project,
+ relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/projects/org-slug/${project.slug}/`,
+ body: projectDetails,
+ });
+ ProjectsStore.loadInitialData([project]);
+
+ const initialRouterConfig = {
+ location: {
+ pathname: `/organizations/org-slug/issues/1/`,
+ query: {project: project.id},
+ },
+ route: '/organizations/:orgId/issues/:groupId/',
+ };
+
+ render(
+ ,
+ {organization, initialRouterConfig}
+ );
+
+ expect(screen.getByText(/redacted/)).toBeInTheDocument();
+
+ await userEvent.hover(screen.getByText(/redacted/));
+
+ expect(
+ await screen.findByText(
+ textWithMarkupMatcher(
+ 'Replaced because of the data scrubbing rule [Replace] [Password fields] with [Scrubbed] from [password] in the settings of the project project-slug'
+ )
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('renders python variables correctly', () => {
+ render(
+ ',
+ }}
+ platform="python"
+ />
+ );
+
+ expect(
+ within(screen.getByTestId('value-null')).getByText('None')
+ ).toBeInTheDocument();
+ expect(
+ within(screen.getByTestId('value-boolean')).getByText('True')
+ ).toBeInTheDocument();
+ expect(
+ within(screen.getByTestId('value-string')).getByText('"string"')
+ ).toBeInTheDocument();
+ expect(
+ within(screen.getByTestId('value-number')).getByText('123.45')
+ ).toBeInTheDocument();
+ expect(
+ within(screen.getByTestId('value-unformatted')).getByText('')
+ ).toBeInTheDocument();
+ });
+
+ it('does not mutate the data prop', () => {
+ const data = {
+ zebra: 'last',
+ alpha: 'first',
+ middle: 'middle',
+ };
+ const originalKeys = Object.keys(data);
+
+ render();
+
+ expect(Object.keys(data)).toEqual(originalKeys);
+ });
+});
diff --git a/static/app/components/stackTrace/frame/frameVariablesGrid.tsx b/static/app/components/stackTrace/frame/frameVariablesGrid.tsx
new file mode 100644
index 00000000000000..0f48f6618939eb
--- /dev/null
+++ b/static/app/components/stackTrace/frame/frameVariablesGrid.tsx
@@ -0,0 +1,103 @@
+import {useMemo} from 'react';
+import styled from '@emotion/styled';
+
+import {Text} from '@sentry/scraps/text';
+
+import {ClippedBox} from 'sentry/components/clippedBox';
+import {StructuredEventData} from 'sentry/components/structuredEventData';
+import type {PlatformKey} from 'sentry/types/project';
+
+import {getStructuredDataConfig} from './getStructuredDataConfig';
+
+const QUOTED_KEY_REGEX = /^['"](.*)['"]$/;
+
+function formatVariableKey(key: string): string {
+ return key.replace(QUOTED_KEY_REGEX, '$1');
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null;
+}
+
+interface FrameVariablesGridProps {
+ data: Record | null;
+ meta?: Record;
+ platform?: PlatformKey;
+}
+
+export function FrameVariablesGrid({data, meta, platform}: FrameVariablesGridProps) {
+ const config = useMemo(() => getStructuredDataConfig({platform}), [platform]);
+ const rows = useMemo(() => (data ? Object.keys(data).sort() : []), [data]);
+
+ if (!data) {
+ return null;
+ }
+
+ return (
+
+
+ {rows.map(rawKey => (
+
+
+
+ {formatVariableKey(rawKey)}
+
+
+
+ {/*
+ StructuredEventData expects record-like meta for each value; skip invalid meta entries.
+ */}
+
+
+
+ ))}
+
+
+ );
+}
+
+const StyledClippedBox = styled(ClippedBox)`
+ padding: 0;
+ border-top: 1px solid ${p => p.theme.tokens.border.primary};
+`;
+
+const VariablesGrid = styled('div')`
+ display: grid;
+ grid-template-columns: 150px minmax(0, 1fr);
+ align-items: baseline;
+`;
+
+const VariableRow = styled('div')`
+ display: grid;
+ grid-template-columns: subgrid;
+ grid-column: 1 / -1;
+ align-items: baseline;
+ column-gap: ${p => p.theme.space.md};
+
+ &:not(:last-child) {
+ border-bottom: 1px solid ${p => p.theme.tokens.border.secondary};
+ }
+`;
+
+const VariableKey = styled('div')`
+ overflow-wrap: anywhere;
+ padding: ${p => p.theme.space.md} 0 ${p => p.theme.space.md} ${p => p.theme.space.md};
+`;
+
+const VariablesValue = styled('div')`
+ min-width: 0;
+ align-self: stretch;
+ padding: ${p => p.theme.space.md};
+ border-left: 1px solid ${p => p.theme.tokens.border.secondary};
+
+ > pre {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ }
+`;
diff --git a/static/app/components/stackTrace/frame/getStructuredDataConfig.spec.tsx b/static/app/components/stackTrace/frame/getStructuredDataConfig.spec.tsx
new file mode 100644
index 00000000000000..0c02fe62dfd4aa
--- /dev/null
+++ b/static/app/components/stackTrace/frame/getStructuredDataConfig.spec.tsx
@@ -0,0 +1,52 @@
+import {getStructuredDataConfig} from './getStructuredDataConfig';
+
+describe('getStructuredDataConfig', () => {
+ it('formats python values with sdk-specific conventions', () => {
+ const config = getStructuredDataConfig({platform: 'python'});
+
+ expect(config.isNull?.('None')).toBe(true);
+ expect(config.isNull?.(null)).toBe(true);
+ expect(config.renderNull?.(null)).toBe('None');
+
+ expect(config.isBoolean?.('True')).toBe(true);
+ expect(config.isBoolean?.(true)).toBe(true);
+ expect(config.renderBoolean?.(true)).toBe('True');
+
+ expect(config.isString?.("'string'")).toBe(true);
+ expect(config.renderString?.("'string'")).toBe('string');
+
+ expect(config.isNumber?.('123.45')).toBe(true);
+ expect(config.isNumber?.(123.45)).toBe(true);
+ expect(config.isNumber?.('')).toBe(false);
+ });
+
+ it('formats node sentinels for null and undefined', () => {
+ const config = getStructuredDataConfig({platform: 'node'});
+
+ expect(config.isNull?.('')).toBe(true);
+ expect(config.isNull?.('')).toBe(true);
+ expect(config.renderNull?.('')).toBe('null');
+ expect(config.renderNull?.('')).toBe('undefined');
+ });
+
+ it('formats ruby null and booleans', () => {
+ const config = getStructuredDataConfig({platform: 'ruby'});
+
+ expect(config.isNull?.('nil')).toBe(true);
+ expect(config.renderNull?.(null)).toBe('nil');
+ expect(config.isBoolean?.('true')).toBe(true);
+ expect(config.isBoolean?.(false)).toBe(true);
+ });
+
+ it('formats php null and booleans', () => {
+ const config = getStructuredDataConfig({platform: 'php'});
+
+ expect(config.isNull?.('null')).toBe(true);
+ expect(config.isBoolean?.('true')).toBe(true);
+ expect(config.isBoolean?.(true)).toBe(true);
+ });
+
+ it('returns an empty config for unsupported platforms', () => {
+ expect(getStructuredDataConfig({platform: 'java'})).toEqual({});
+ });
+});
diff --git a/static/app/components/stackTrace/frame/getStructuredDataConfig.tsx b/static/app/components/stackTrace/frame/getStructuredDataConfig.tsx
new file mode 100644
index 00000000000000..fede695d9c944b
--- /dev/null
+++ b/static/app/components/stackTrace/frame/getStructuredDataConfig.tsx
@@ -0,0 +1,70 @@
+import type {StructedEventDataConfig} from 'sentry/components/structuredEventData';
+import type {PlatformKey} from 'sentry/types/project';
+
+const PYTHON_STRING_REGEX = /^['"](.*)['"]$/;
+const NUMERIC_STRING_REGEX = /^-?\d+(\.\d+)?$/;
+
+const renderPythonBoolean = (value: unknown) => {
+ if (typeof value === 'string') {
+ return value;
+ }
+
+ return value ? 'True' : 'False';
+};
+
+const renderNodeNull = (value: unknown) => {
+ if (value === '') {
+ return 'null';
+ }
+
+ if (value === '') {
+ return 'undefined';
+ }
+
+ return String(value);
+};
+
+export const getStructuredDataConfig = ({
+ platform,
+}: {
+ platform?: PlatformKey;
+}): StructedEventDataConfig => {
+ switch (platform) {
+ case 'python':
+ return {
+ isBoolean: value =>
+ typeof value === 'boolean' || value === 'True' || value === 'False',
+ isNull: value => value === null || value === 'None',
+ renderBoolean: renderPythonBoolean,
+ renderNull: () => 'None',
+ // Python SDK wraps string values in single quotes.
+ isString: value => typeof value === 'string' && PYTHON_STRING_REGEX.test(value),
+ // Strip quote wrapping for display purposes.
+ renderString: value => value.replace(PYTHON_STRING_REGEX, '$1'),
+ // Python SDK can emit numbers as strings.
+ isNumber: value =>
+ typeof value === 'number' ||
+ (typeof value === 'string' && NUMERIC_STRING_REGEX.test(value)),
+ };
+ case 'ruby':
+ return {
+ isBoolean: value =>
+ typeof value === 'boolean' || value === 'true' || value === 'false',
+ isNull: value => value === null || value === 'nil',
+ renderNull: () => 'nil',
+ };
+ case 'php':
+ return {
+ isBoolean: value =>
+ typeof value === 'boolean' || value === 'true' || value === 'false',
+ isNull: value => value === null || value === 'null',
+ };
+ case 'node':
+ return {
+ isNull: value => value === null || value === '' || value === '',
+ renderNull: renderNodeNull,
+ };
+ default:
+ return {};
+ }
+};
diff --git a/static/app/components/stackTrace/getRows.spec.tsx b/static/app/components/stackTrace/getRows.spec.tsx
new file mode 100644
index 00000000000000..48efd0f1b502fa
--- /dev/null
+++ b/static/app/components/stackTrace/getRows.spec.tsx
@@ -0,0 +1,152 @@
+import {getLastFrameIndex} from 'sentry/components/events/interfaces/utils';
+import type {Frame} from 'sentry/types/event';
+
+import {createInitialHiddenFrameToggleMap, getFrameCountMap, getRows} from './getRows';
+
+let frameSerial = 0;
+
+function makeFrame(overrides: Partial = {}): Frame {
+ frameSerial += 1;
+
+ return {
+ absPath: null,
+ colNo: null,
+ context: [],
+ filename: 'frame.py',
+ function: `fn-${frameSerial}`,
+ inApp: false,
+ instructionAddr: `0x${frameSerial}`,
+ lineNo: frameSerial,
+ module: `mod-${frameSerial}`,
+ package: `pkg-${frameSerial}`,
+ platform: null,
+ rawFunction: null,
+ symbol: null,
+ symbolAddr: null,
+ trust: null,
+ vars: null,
+ ...overrides,
+ };
+}
+
+describe('stackTrace rows utils', () => {
+ it('returns last in-app frame index and falls back to last frame', () => {
+ expect(
+ getLastFrameIndex([
+ makeFrame({inApp: false}),
+ makeFrame({inApp: true}),
+ makeFrame({inApp: false}),
+ ])
+ ).toBe(1);
+
+ expect(
+ getLastFrameIndex([makeFrame({inApp: false}), makeFrame({inApp: false})])
+ ).toBe(1);
+ });
+
+ it('builds hidden toggle and count maps for app-frame view', () => {
+ const frames = [
+ makeFrame({inApp: false, filename: 'hidden.py'}),
+ makeFrame({inApp: false, filename: 'lead.py'}),
+ makeFrame({inApp: true, filename: 'app.py'}),
+ makeFrame({inApp: false, filename: 'tail.py'}),
+ ];
+
+ expect(createInitialHiddenFrameToggleMap(frames, false)).toEqual({
+ 1: false,
+ 3: false,
+ });
+ expect(getFrameCountMap(frames, false)).toEqual({
+ 1: 1,
+ 3: 0,
+ });
+ });
+
+ it('expands hidden system frames when toggle is enabled', () => {
+ const frames = [
+ makeFrame({inApp: false, filename: 'hidden.py'}),
+ makeFrame({inApp: false, filename: 'lead.py'}),
+ makeFrame({inApp: true, filename: 'app.py'}),
+ makeFrame({inApp: false, filename: 'tail.py'}),
+ ];
+ const frameCountMap = getFrameCountMap(frames, false);
+
+ const rows = getRows({
+ frames,
+ includeSystemFrames: false,
+ hiddenFrameToggleMap: {1: true, 3: false},
+ frameCountMap,
+ newestFirst: false,
+ framesOmitted: null,
+ maxDepth: undefined,
+ });
+
+ expect(rows).toHaveLength(4);
+ expect(rows[0]).toMatchObject({kind: 'frame', frameIndex: 0, isSubFrame: true});
+ expect(rows[1]).toMatchObject({kind: 'frame', frameIndex: 1, hiddenFrameCount: 1});
+ });
+
+ it('collapses repeated frames and carries repeat count to the visible frame', () => {
+ const repeatedFrameBase = {
+ inApp: false,
+ lineNo: 112,
+ instructionAddr: '0x1',
+ package: 'pkg',
+ module: 'mod',
+ function: 'fn',
+ };
+ const frames = [
+ makeFrame(repeatedFrameBase),
+ makeFrame(repeatedFrameBase),
+ makeFrame({inApp: true, lineNo: 113, instructionAddr: '0x2', function: 'next'}),
+ ];
+
+ const rows = getRows({
+ frames,
+ includeSystemFrames: true,
+ hiddenFrameToggleMap: {},
+ frameCountMap: getFrameCountMap(frames, true),
+ newestFirst: false,
+ framesOmitted: null,
+ maxDepth: undefined,
+ });
+
+ expect(rows).toHaveLength(2);
+ expect(rows[0]).toMatchObject({kind: 'frame', frameIndex: 1, timesRepeated: 1});
+ });
+
+ it('inserts omitted rows and respects maxDepth and newestFirst', () => {
+ const frames = [
+ makeFrame({inApp: true, filename: '0.py'}),
+ makeFrame({inApp: true, filename: '1.py'}),
+ makeFrame({inApp: true, filename: '2.py'}),
+ makeFrame({inApp: true, filename: '3.py'}),
+ ];
+
+ const rowsWithOmitted = getRows({
+ frames,
+ includeSystemFrames: true,
+ hiddenFrameToggleMap: {},
+ frameCountMap: getFrameCountMap(frames, true),
+ newestFirst: false,
+ framesOmitted: [1, 3],
+ maxDepth: undefined,
+ });
+
+ expect(rowsWithOmitted.some(row => row.kind === 'omitted')).toBe(true);
+
+ const newestRows = getRows({
+ frames,
+ includeSystemFrames: true,
+ hiddenFrameToggleMap: {},
+ frameCountMap: getFrameCountMap(frames, true),
+ newestFirst: true,
+ framesOmitted: null,
+ maxDepth: 2,
+ });
+
+ expect(newestRows).toHaveLength(2);
+ expect(newestRows[0]).toMatchObject({kind: 'frame', frameIndex: 3});
+ expect(newestRows[1]).toMatchObject({kind: 'frame', frameIndex: 2});
+ });
+});
diff --git a/static/app/components/stackTrace/getRows.tsx b/static/app/components/stackTrace/getRows.tsx
new file mode 100644
index 00000000000000..f6a2ce5b9e4f06
--- /dev/null
+++ b/static/app/components/stackTrace/getRows.tsx
@@ -0,0 +1,195 @@
+import {isRepeatedFrame} from 'sentry/components/events/interfaces/utils';
+import type {FrameRow, OmittedFramesRow, Row} from 'sentry/components/stackTrace/types';
+import type {Frame} from 'sentry/types/event';
+
+function frameIsVisible(
+ frame: Frame,
+ nextFrame: Frame | undefined,
+ includeSystemFrames: boolean
+) {
+ return (
+ includeSystemFrames ||
+ frame.inApp ||
+ nextFrame?.inApp ||
+ // Include the last non-app frame to keep the call chain understandable.
+ (!frame.inApp && !nextFrame)
+ );
+}
+
+export function createInitialHiddenFrameToggleMap(
+ frames: Frame[],
+ includeSystemFrames: boolean
+) {
+ const indexMap: Record = {};
+
+ frames.forEach((frame, frameIdx) => {
+ const nextFrame = frames[frameIdx + 1];
+ const repeatedFrame = isRepeatedFrame(frame, nextFrame);
+
+ if (
+ frameIsVisible(frame, nextFrame, includeSystemFrames) &&
+ !repeatedFrame &&
+ !frame.inApp
+ ) {
+ indexMap[frameIdx] = false;
+ }
+ });
+
+ return indexMap;
+}
+
+export function getFrameCountMap(frames: Frame[], includeSystemFrames: boolean) {
+ let count = 0;
+ const countMap: Record = {};
+
+ frames.forEach((frame, frameIdx) => {
+ const nextFrame = frames[frameIdx + 1];
+ const repeatedFrame = isRepeatedFrame(frame, nextFrame);
+
+ if (
+ frameIsVisible(frame, nextFrame, includeSystemFrames) &&
+ !repeatedFrame &&
+ !frame.inApp
+ ) {
+ countMap[frameIdx] = count;
+ count = 0;
+ } else if (!repeatedFrame && !frame.inApp) {
+ count += 1;
+ }
+ });
+
+ return countMap;
+}
+
+function getHiddenFrameIndices({
+ frames,
+ hiddenFrameToggleMap,
+ frameCountMap,
+}: {
+ frameCountMap: Record;
+ frames: Frame[];
+ hiddenFrameToggleMap: Record;
+}) {
+ const repeatedIndices = new Set();
+
+ frames.forEach((frame, frameIdx) => {
+ const nextFrame = frames[frameIdx + 1];
+ if (isRepeatedFrame(frame, nextFrame)) {
+ repeatedIndices.add(frameIdx);
+ }
+ });
+
+ const hiddenFrameIndices = new Set();
+
+ Object.entries(hiddenFrameToggleMap).forEach(([indexString, isExpanded]) => {
+ if (!isExpanded) {
+ return;
+ }
+
+ const index = Number(indexString);
+ let i = 1;
+ let numHidden = frameCountMap[index] ?? 0;
+
+ while (numHidden > 0) {
+ if (!repeatedIndices.has(index - i)) {
+ hiddenFrameIndices.add(index - i);
+ numHidden -= 1;
+ }
+ i += 1;
+ }
+ });
+
+ return hiddenFrameIndices;
+}
+
+export function getRows({
+ frames,
+ includeSystemFrames,
+ hiddenFrameToggleMap,
+ frameCountMap,
+ newestFirst,
+ framesOmitted,
+ maxDepth,
+}: {
+ frameCountMap: Record;
+ frames: Frame[];
+ framesOmitted: [number, number] | null | undefined;
+ hiddenFrameToggleMap: Record;
+ includeSystemFrames: boolean;
+ maxDepth: number | undefined;
+ newestFirst: boolean;
+}): Row[] {
+ const hiddenFrameIndices = getHiddenFrameIndices({
+ frames,
+ hiddenFrameToggleMap,
+ frameCountMap,
+ });
+
+ let nRepeats = 0;
+
+ let rows = frames
+ .map((frame, frameIndex) => {
+ const nextFrame = frames[frameIndex + 1];
+ const repeatedFrame = isRepeatedFrame(frame, nextFrame);
+
+ if (repeatedFrame) {
+ nRepeats += 1;
+ }
+
+ if (
+ (frameIsVisible(frame, nextFrame, includeSystemFrames) && !repeatedFrame) ||
+ hiddenFrameIndices.has(frameIndex)
+ ) {
+ const row: FrameRow = {
+ kind: 'frame',
+ frame,
+ frameIndex,
+ nextFrame,
+ timesRepeated: nRepeats,
+ isSubFrame: hiddenFrameIndices.has(frameIndex),
+ hiddenFrameCount: frameCountMap[frameIndex],
+ };
+
+ nRepeats = 0;
+
+ if (frameIndex === framesOmitted?.[0]) {
+ return [
+ row,
+ {
+ kind: 'omitted',
+ omittedFrames: framesOmitted,
+ rowKey: `omitted-${framesOmitted[0]}-${framesOmitted[1]}`,
+ } satisfies OmittedFramesRow,
+ ];
+ }
+
+ return row;
+ }
+
+ if (!repeatedFrame) {
+ nRepeats = 0;
+ }
+
+ if (frameIndex !== framesOmitted?.[0]) {
+ return null;
+ }
+
+ return {
+ kind: 'omitted',
+ omittedFrames: framesOmitted,
+ rowKey: `omitted-${framesOmitted[0]}-${framesOmitted[1]}`,
+ } satisfies OmittedFramesRow;
+ })
+ .flatMap((row): Row[] => {
+ if (!row) {
+ return [];
+ }
+ return Array.isArray(row) ? row : [row];
+ });
+
+ if (maxDepth !== undefined) {
+ rows = rows.slice(-maxDepth);
+ }
+
+ return newestFirst ? [...rows].reverse() : rows;
+}
diff --git a/static/app/components/stackTrace/issueStackTrace/index.spec.tsx b/static/app/components/stackTrace/issueStackTrace/index.spec.tsx
new file mode 100644
index 00000000000000..01c4c2be111c5f
--- /dev/null
+++ b/static/app/components/stackTrace/issueStackTrace/index.spec.tsx
@@ -0,0 +1,662 @@
+import {EventFixture} from 'sentry-fixture/event';
+import {EventEntryStacktraceFixture} from 'sentry-fixture/eventEntryStacktrace';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {IssueStackTrace} from 'sentry/components/stackTrace/issueStackTrace';
+import {ProjectsStore} from 'sentry/stores/projectsStore';
+import {CodecovStatusCode, Coverage} from 'sentry/types/integrations';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+
+type StacktraceWithFrames = StacktraceType & {
+ frames: NonNullable;
+};
+
+function makeStackTraceData(): {
+ event: ReturnType;
+ stacktrace: StacktraceWithFrames;
+} {
+ const entry = EventEntryStacktraceFixture();
+
+ return {
+ event: EventFixture({
+ platform: 'python',
+ projectID: '1',
+ entries: [entry],
+ }),
+ stacktrace: {
+ ...entry.data,
+ hasSystemFrames: true,
+ frames:
+ entry.data.frames?.map((frame, index) => ({
+ ...frame,
+ inApp: index >= 2,
+ })) ?? [],
+ } as StacktraceWithFrames,
+ };
+}
+
+describe('IssueStackTrace', () => {
+ beforeEach(() => {
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/prompts-activity/',
+ body: {dismissed_ts: undefined, snoozed_ts: undefined},
+ });
+ MockApiClient.addMockResponse({
+ url: '/projects/org-slug/project-slug/stacktrace-link/',
+ body: {config: null, sourceUrl: null, integrations: []},
+ });
+ });
+
+ it('does not render when event has threads', () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const eventWithThreads = EventFixture({
+ ...event,
+ entries: [
+ ...event.entries,
+ {type: 'threads' as const, data: {values: [{id: 0, current: true}]}},
+ ],
+ });
+
+ const {container} = render(
+
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('shares display options across chained issue exceptions', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+ );
+
+ expect(await screen.findAllByTestId('core-stacktrace-frame-row')).toHaveLength(8);
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Full Stack Trace'}));
+
+ expect(await screen.findAllByTestId('core-stacktrace-frame-row')).toHaveLength(10);
+ });
+
+ it('renders chained exceptions in newest-first order by default and reverses on sort toggle', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+ );
+
+ const headings = screen.getAllByRole('heading', {level: 5});
+ expect(headings[0]).toHaveTextContent('LeafError');
+ expect(headings[1]).toHaveTextContent('MiddleError');
+ expect(headings[2]).toHaveTextContent('RootError');
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Oldest'}));
+
+ const reorderedHeadings = screen.getAllByRole('heading', {level: 5});
+ expect(reorderedHeadings[0]).toHaveTextContent('RootError');
+ expect(reorderedHeadings[1]).toHaveTextContent('MiddleError');
+ expect(reorderedHeadings[2]).toHaveTextContent('LeafError');
+ });
+
+ it('renders coverage tooltip from issue-level coverage request', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const organization = OrganizationFixture({slug: 'org-slug', codecovAccess: true});
+ const project = ProjectFixture({
+ id: event.projectID,
+ slug: 'project-slug',
+ });
+ ProjectsStore.loadInitialData([project]);
+ const coverageRequest = MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/stacktrace-coverage/`,
+ body: {
+ status: CodecovStatusCode.COVERAGE_EXISTS,
+ lineCoverage: [
+ [110, Coverage.COVERED],
+ [111, Coverage.PARTIAL],
+ [112, Coverage.NOT_COVERED],
+ ],
+ },
+ });
+
+ render(
+ ,
+ {organization}
+ );
+
+ expect(
+ await screen.findByTestId('core-stacktrace-frame-context')
+ ).toBeInTheDocument();
+ expect(coverageRequest).toHaveBeenCalled();
+
+ await userEvent.hover(screen.getByLabelText('Line 112'));
+
+ expect(await screen.findByText('Line uncovered by tests')).toBeInTheDocument();
+ });
+
+ it('renders annotated text when exception value has PII scrubbing metadata', async () => {
+ const {stacktrace} = makeStackTraceData();
+ const entryIndex = 0;
+ const event = EventFixture({
+ platform: 'python',
+ projectID: '1',
+ entries: [{type: 'exception' as const, data: {values: []}}],
+ _meta: {
+ entries: {
+ [entryIndex]: {
+ data: {
+ values: {
+ 0: {
+ value: {
+ '': {
+ rem: [['project:0', 's', 0, 0]],
+ len: 18,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ render(
+
+ );
+
+ expect(await screen.findByText('')).toBeInTheDocument();
+ });
+
+ it('shows raw exception type/value/module when minified toggle is active', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const rawStacktrace: StacktraceWithFrames = {
+ ...stacktrace,
+ frames: stacktrace.frames.map(f => ({
+ ...f,
+ function: f.function ? `_min_${f.function}` : f.function,
+ })),
+ };
+
+ render(
+
+ );
+
+ expect(await screen.findByText('ValueError')).toBeInTheDocument();
+ expect(screen.getByText('symbolicated value')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Unsymbolicated'}));
+
+ expect(await screen.findByText('RawError')).toBeInTheDocument();
+ expect(screen.getByText('raw value')).toBeInTheDocument();
+ });
+
+ it('falls back to symbolicated values when raw fields are missing', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+ );
+
+ expect(await screen.findByText('ValueError')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Unsymbolicated'}));
+
+ expect(await screen.findByText('ValueError')).toBeInTheDocument();
+ expect(screen.getByText('original value')).toBeInTheDocument();
+ });
+
+ it('copies raw stacktrace when unsymbolicated toggle is active for chained exceptions', async () => {
+ Object.assign(navigator, {
+ clipboard: {writeText: jest.fn().mockResolvedValue(undefined)},
+ });
+ const {event, stacktrace} = makeStackTraceData();
+ const rawStacktrace: StacktraceWithFrames = {
+ ...stacktrace,
+ frames: stacktrace.frames.map(f => ({
+ ...f,
+ filename: f.filename ? `minified_${f.filename}` : f.filename,
+ })),
+ };
+
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Unsymbolicated'}));
+
+ await userEvent.click(screen.getByRole('button', {name: 'Copy as'}));
+ await userEvent.click(await screen.findByRole('menuitemradio', {name: 'Text'}));
+
+ const copiedText = (navigator.clipboard.writeText as jest.Mock).mock.calls[0][0];
+ expect(copiedText).toContain('minified_');
+ expect(copiedText).not.toContain('File "raven/');
+ });
+
+ it('renders raw view for a single exception', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+ );
+
+ expect(await screen.findByText('ValueError')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Raw Stack Trace'}));
+
+ // Exception header should not render in raw view
+ expect(screen.queryByText('ValueError')).not.toBeInTheDocument();
+ // Raw text should be in a pre element
+ expect(screen.getByText(/raven\/base\.py/)).toBeInTheDocument();
+ });
+
+ it('renders raw view as flat text for chained exceptions', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+ );
+
+ expect(await screen.findByText('RootError')).toBeInTheDocument();
+ expect(screen.getByText('NestedError')).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Raw Stack Trace'}));
+
+ expect(screen.queryByText(/chained exception/)).not.toBeInTheDocument();
+ expect(screen.getByText(/RootError: root cause/)).toBeInTheDocument();
+ expect(screen.getByText(/NestedError: nested cause/)).toBeInTheDocument();
+ });
+
+ it('does not reverse exception order in raw view', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+ );
+
+ await userEvent.click(await screen.findByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Raw Stack Trace'}));
+
+ const rawText = await screen.findByText(/FirstError: first/);
+ const pre = rawText.closest('pre')!;
+ const firstIdx = pre.textContent.indexOf('FirstError: first');
+ const secondIdx = pre.textContent.indexOf('SecondError: second');
+ expect(firstIdx).toBeLessThan(secondIdx);
+ });
+
+ describe('standalone stacktrace prop', () => {
+ it('renders frame rows for a standalone stacktrace', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ render(
+
+ );
+
+ expect(await screen.findByText('Stack Trace')).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.getAllByTestId('core-stacktrace-frame-row').length).toBeGreaterThan(
+ 0
+ );
+ });
+ });
+
+ it('returns null when stacktrace has no frames', () => {
+ const {event} = makeStackTraceData();
+ const emptyStacktrace: StacktraceType = {
+ frames: [],
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ };
+
+ const {container} = render(
+
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+ });
+
+ describe('exception groups', () => {
+ function makeExceptionGroupValues(): {
+ event: ReturnType;
+ values: Extract[0], {values: unknown}>['values'];
+ } {
+ const {stacktrace, event} = makeStackTraceData();
+ const minimalStacktrace: StacktraceWithFrames = {
+ ...stacktrace,
+ frames: [stacktrace.frames[stacktrace.frames.length - 1]!],
+ };
+
+ return {
+ event,
+ values: [
+ {
+ type: 'ExceptionGroup',
+ value: 'root group',
+ module: null,
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 0,
+ is_exception_group: true,
+ },
+ stacktrace: minimalStacktrace,
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'ValueError',
+ value: 'value error',
+ module: null,
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 1,
+ parent_id: 0,
+ },
+ stacktrace: minimalStacktrace,
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'NestedGroup',
+ value: 'nested group',
+ module: null,
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 2,
+ parent_id: 0,
+ is_exception_group: true,
+ },
+ stacktrace: minimalStacktrace,
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'TypeError',
+ value: 'type error',
+ module: null,
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 3,
+ parent_id: 2,
+ },
+ stacktrace: minimalStacktrace,
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'KeyError',
+ value: 'key error',
+ module: null,
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 4,
+ parent_id: 2,
+ },
+ stacktrace: minimalStacktrace,
+ threadId: null,
+ rawStacktrace: null,
+ },
+ ],
+ };
+ }
+
+ it('hides children of non-root exception groups by default', async () => {
+ const {event, values} = makeExceptionGroupValues();
+ render();
+
+ expect(await screen.findByText('ExceptionGroup')).toBeInTheDocument();
+ expect(screen.getByText('ValueError')).toBeInTheDocument();
+ expect(screen.getByText('NestedGroup')).toBeInTheDocument();
+
+ expect(screen.queryByText('TypeError')).not.toBeInTheDocument();
+ expect(screen.queryByText('KeyError')).not.toBeInTheDocument();
+ });
+
+ it('toggles hidden exception group children on button click', async () => {
+ const {event, values} = makeExceptionGroupValues();
+ render();
+
+ expect(await screen.findByText('ExceptionGroup')).toBeInTheDocument();
+ expect(screen.queryByText('TypeError')).not.toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole('button', {name: 'Show 2 related exceptions'})
+ );
+
+ expect(screen.getByText('TypeError')).toBeInTheDocument();
+ expect(screen.getByText('KeyError')).toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole('button', {name: 'Hide 2 related exceptions'})
+ );
+
+ expect(screen.queryByText('TypeError')).not.toBeInTheDocument();
+ expect(screen.queryByText('KeyError')).not.toBeInTheDocument();
+ });
+
+ it('renders related exceptions tree for exception groups', async () => {
+ const {event, values} = makeExceptionGroupValues();
+ render();
+
+ expect(await screen.findByText('ExceptionGroup')).toBeInTheDocument();
+ expect(screen.getAllByTestId('related-exceptions-tree')).toHaveLength(2);
+ });
+ });
+});
diff --git a/static/app/components/stackTrace/issueStackTrace/index.tsx b/static/app/components/stackTrace/issueStackTrace/index.tsx
new file mode 100644
index 00000000000000..6024439c2e90c6
--- /dev/null
+++ b/static/app/components/stackTrace/issueStackTrace/index.tsx
@@ -0,0 +1,389 @@
+import {useMemo} from 'react';
+
+import {Disclosure} from '@sentry/scraps/disclosure';
+import {Container, Flex} from '@sentry/scraps/layout';
+import {Separator} from '@sentry/scraps/separator';
+import {Text} from '@sentry/scraps/text';
+
+import {CommitRow} from 'sentry/components/commitRow';
+import {CopyAsDropdown} from 'sentry/components/copyAsDropdown';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
+import {StacktraceBanners} from 'sentry/components/events/interfaces/crashContent/exception/banners/stacktraceBanners';
+import {
+ LineCoverageProvider,
+ useLineCoverageContext,
+} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageContext';
+import {LineCoverageLegend} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageLegend';
+import {displayRawContent as rawStacktraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/rawContent';
+import {SuspectCommits} from 'sentry/components/events/suspectCommits';
+import {Panel} from 'sentry/components/panels/panel';
+import {DisplayOptions} from 'sentry/components/stackTrace/displayOptions';
+import {
+ RelatedExceptionsTree,
+ ToggleRelatedExceptionsButton,
+ useHiddenExceptions,
+} from 'sentry/components/stackTrace/exceptionGroup';
+import {
+ ExceptionDescription,
+ ExceptionHeader,
+} from 'sentry/components/stackTrace/exceptionHeader';
+import {RawStackTraceText} from 'sentry/components/stackTrace/rawStackTrace';
+import {
+ StackTraceViewStateProvider,
+ useStackTraceViewState,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {StackTraceFrames} from 'sentry/components/stackTrace/stackTraceFrames';
+import {StackTraceProvider} from 'sentry/components/stackTrace/stackTraceProvider';
+import {t, tn} from 'sentry/locale';
+import type {Event, ExceptionValue} from 'sentry/types/event';
+import {EntryType} from 'sentry/types/event';
+import type {Group} from 'sentry/types/group';
+import type {Project} from 'sentry/types/project';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+import {defined} from 'sentry/utils';
+import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
+import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
+
+import {IssueFrameActions} from './issueFrameActions';
+import {IssueStackTraceFrameContext} from './issueStackTraceFrameContext';
+
+interface IssueStackTraceBaseProps {
+ event: Event;
+ group?: Group;
+ projectSlug?: Project['slug'];
+}
+
+/** Exception stack traces with chaining, type/value metadata, and minified variants. */
+interface ExceptionStackTraceProps extends IssueStackTraceBaseProps {
+ values: ExceptionValue[];
+ stacktrace?: never;
+}
+
+/** Bare stack trace with no exception metadata (e.g. log/message events). */
+interface StandaloneStackTraceProps extends IssueStackTraceBaseProps {
+ stacktrace: StacktraceType;
+ values?: never;
+}
+
+type IssueStackTraceProps = ExceptionStackTraceProps | StandaloneStackTraceProps;
+
+interface IndexedExceptionValue extends ExceptionValue {
+ exceptionIndex: number;
+ stacktrace: StacktraceType;
+}
+
+/** Resolves symbolicated vs raw (minified) exception fields. */
+function resolveExceptionFields(exc: IndexedExceptionValue, isMinified: boolean) {
+ return {
+ type: isMinified ? (exc.rawType ?? exc.type) : exc.type,
+ module: isMinified ? (exc.rawModule ?? exc.module) : exc.module,
+ value: isMinified ? (exc.rawValue ?? exc.value) : exc.value,
+ };
+}
+
+function IssueStackTraceLineCoverageLegend() {
+ const {hasCoverageData} = useLineCoverageContext();
+
+ if (!hasCoverageData) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export function IssueStackTrace(props: IssueStackTraceProps) {
+ const {event, group, projectSlug} = props;
+ const eventHasThreads = event.entries?.some(entry => entry.type === EntryType.THREADS);
+ if (eventHasThreads) {
+ return null;
+ }
+
+ const isStandalone = 'stacktrace' in props && !!props.stacktrace;
+
+ let values: ExceptionValue[];
+ if (isStandalone) {
+ if (!(props.stacktrace.frames ?? []).length) {
+ return null;
+ }
+ values = [
+ {
+ stacktrace: props.stacktrace,
+ type: '',
+ value: null,
+ module: null,
+ mechanism: null,
+ threadId: null,
+ rawStacktrace: null,
+ },
+ ];
+ } else {
+ values = props.values;
+ }
+
+ const hasMinifiedStacktrace =
+ !isStandalone && values.some(v => v.rawStacktrace !== null);
+
+ return (
+
+
+
+
+
+ );
+}
+
+function IssueStackTraceContent({
+ event,
+ values,
+ group,
+ projectSlug,
+ isStandalone,
+}: IssueStackTraceBaseProps & {isStandalone: boolean; values: ExceptionValue[]}) {
+ const {isMinified, isNewestFirst, view} = useStackTraceViewState();
+ const {hiddenExceptions, toggleRelatedExceptions, expandException} =
+ useHiddenExceptions(values);
+
+ const entryType = isStandalone ? EntryType.STACKTRACE : EntryType.EXCEPTION;
+ const entryIndex = event.entries?.findIndex(entry => entry.type === entryType);
+ const rawEntryMeta = event._meta?.entries?.[entryIndex ?? -1]?.data;
+ const exceptionValuesMeta = isStandalone ? undefined : rawEntryMeta?.values;
+
+ const exceptions = useMemo(() => {
+ const indexed = values
+ .map((exc, exceptionIndex) => ({...exc, exceptionIndex}))
+ .filter((exc): exc is IndexedExceptionValue => exc.stacktrace !== null);
+ return isNewestFirst && view !== 'raw' ? indexed.reverse() : indexed;
+ }, [values, isNewestFirst, view]);
+
+ const firstVisibleExceptionIndex = exceptions.findIndex(
+ exc =>
+ exc.mechanism?.parent_id === undefined || !hiddenExceptions[exc.mechanism.parent_id]
+ );
+
+ if (exceptions.length === 0) {
+ return null;
+ }
+
+ const sectionKey = isStandalone ? SectionKey.STACKTRACE : SectionKey.EXCEPTION;
+
+ const copyItems = CopyAsDropdown.makeDefaultCopyAsOptions({
+ text: () =>
+ exceptions
+ .map(exc =>
+ rawStacktraceContent({
+ data: isMinified ? (exc.rawStacktrace ?? exc.stacktrace) : exc.stacktrace,
+ platform: event.platform,
+ })
+ )
+ .join('\n\n'),
+ json: undefined,
+ markdown: undefined,
+ });
+
+ const sectionActions = (
+
+
+
+
+ );
+
+ if (view === 'raw') {
+ return (
+
+
+
+
+ {exceptions
+ .map(exc =>
+ rawStacktraceContent({
+ data: isMinified
+ ? (exc.rawStacktrace ?? exc.stacktrace)
+ : exc.stacktrace,
+ platform: event.platform,
+ exception: exc,
+ isMinified,
+ })
+ )
+ .join('\n\n')}
+
+
+
+
+
+ );
+ }
+
+ if (exceptions.length === 1) {
+ const exc = exceptions[0]!;
+ const {type, module, value} = resolveExceptionFields(exc, isMinified);
+ const hasExceptionInfo = Boolean(type || value);
+
+ const excMeta = exceptionValuesMeta?.[exc.exceptionIndex];
+
+ return (
+
+ {hasExceptionInfo && (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {tn(
+ 'There is %s chained exception in this event.',
+ 'There are %s chained exceptions in this event.',
+ exceptions.length
+ )}
+
+
+ {exceptions.map((exc, idx) => {
+ if (
+ exc.mechanism?.parent_id !== undefined &&
+ hiddenExceptions[exc.mechanism.parent_id]
+ ) {
+ return null;
+ }
+
+ const exceptionId = exc.mechanism?.exception_id;
+ const {
+ type: excType,
+ module: excModule,
+ value: excValue,
+ } = resolveExceptionFields(exc, isMinified);
+
+ return (
+
+
+ }
+ >
+
+
+
+
+
+
+ {idx === firstVisibleExceptionIndex ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+function IssueStackTraceSuspectCommits({
+ event,
+ group,
+ projectSlug,
+}: IssueStackTraceBaseProps) {
+ if (!group || !projectSlug) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/static/app/components/stackTrace/issueStackTrace/issueFrameActions.tsx b/static/app/components/stackTrace/issueStackTrace/issueFrameActions.tsx
new file mode 100644
index 00000000000000..10fae4a7be790b
--- /dev/null
+++ b/static/app/components/stackTrace/issueStackTrace/issueFrameActions.tsx
@@ -0,0 +1,49 @@
+import {Fragment} from 'react';
+
+import {Tag} from '@sentry/scraps/badge';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import {ChevronAction} from 'sentry/components/stackTrace/frame/actions/chevron';
+import {HiddenFramesToggleAction} from 'sentry/components/stackTrace/frame/actions/hiddenFramesToggle';
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {IconRefresh} from 'sentry/icons';
+import {t, tn} from 'sentry/locale';
+
+import {IssueSourceLinkAction} from './issueSourceLinkAction';
+import {IssueSourceMapsDebuggerAction} from './issueSourceMapsDebuggerAction';
+
+interface IssueFrameActionsProps {
+ isHovering: boolean;
+}
+
+export function IssueFrameActions({isHovering}: IssueFrameActionsProps) {
+ const {hasAnyExpandableFrames} = useStackTraceContext();
+ const {frame, hiddenFrameCount, timesRepeated} = useStackTraceFrameContext();
+
+ return (
+
+
+
+ {hiddenFrameCount ? : null}
+ {timesRepeated > 0 ? (
+
+ }
+ variant="muted"
+ data-test-id="core-stacktrace-repeats-tag"
+ >
+ {timesRepeated}
+
+
+ ) : null}
+ {frame.inApp ? {t('In App')} : null}
+ {hasAnyExpandableFrames ? : null}
+
+ );
+}
diff --git a/static/app/components/stackTrace/issueStackTrace/issueSourceLinkAction.tsx b/static/app/components/stackTrace/issueStackTrace/issueSourceLinkAction.tsx
new file mode 100644
index 00000000000000..e4d0d2c49f75f3
--- /dev/null
+++ b/static/app/components/stackTrace/issueStackTrace/issueSourceLinkAction.tsx
@@ -0,0 +1,107 @@
+import {useMemo} from 'react';
+import styled from '@emotion/styled';
+
+import {Flex} from '@sentry/scraps/layout';
+
+import {OpenInContextLine} from 'sentry/components/events/interfaces/frame/openInContextLine';
+import {StacktraceLink} from 'sentry/components/events/interfaces/frame/stacktraceLink';
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import type {
+ SentryAppComponent,
+ SentryAppSchemaStacktraceLink,
+} from 'sentry/types/integrations';
+import {useSentryAppComponentsStore} from 'sentry/utils/useSentryAppComponentsStore';
+
+const HOVER_ACTIONS_SLOT_HEIGHT = 28;
+
+interface IssueSourceLinkActionProps {
+ isHovering?: boolean;
+}
+
+export function IssueSourceLinkAction({isHovering = false}: IssueSourceLinkActionProps) {
+ const {frame, event, isExpanded} = useStackTraceFrameContext();
+ const {project} = useStackTraceContext();
+
+ const storeComponents = useSentryAppComponentsStore({
+ componentType: 'stacktrace-link',
+ });
+ const components = useMemo(
+ () =>
+ storeComponents.filter(
+ (
+ component: SentryAppComponent
+ ): component is SentryAppComponent =>
+ component.type === 'stacktrace-link' &&
+ component.schema.type === 'stacktrace-link'
+ ),
+ [storeComponents]
+ );
+
+ const contextLine = frame.context?.find(([lineNumber]) => lineNumber === frame.lineNo);
+ const frameCanShowActions =
+ !!frame.filename && (frame.inApp || event.platform === 'csharp');
+ const canShowFrameActions = frameCanShowActions && (isExpanded || isHovering);
+
+ const showCodeMappingLink = canShowFrameActions && !!project;
+ const showSentryAppStacktraceLink = canShowFrameActions && components.length > 0;
+
+ const wouldShowCodeMappingLink = frameCanShowActions && !!project;
+ const wouldShowSentryAppStacktraceLink = frameCanShowActions && components.length > 0;
+ const hasContent = wouldShowCodeMappingLink || wouldShowSentryAppStacktraceLink;
+
+ return (
+
+ {showCodeMappingLink ? (
+ e.stopPropagation()}>
+
+
+ ) : null}
+
+ {showSentryAppStacktraceLink ? (
+ e.stopPropagation()}>
+
+
+ ) : null}
+
+ );
+}
+
+const FrameActionsSlot = styled(Flex)<{reserveSpace: boolean}>`
+ align-items: center;
+ gap: ${p => p.theme.space.sm};
+ justify-content: flex-end;
+ width: ${p => (p.reserveSpace ? 'max-content' : '0')};
+ flex: ${p => (p.reserveSpace ? '0 0 max-content' : '0 0 0')};
+ height: ${p => (p.reserveSpace ? `${HOVER_ACTIONS_SLOT_HEIGHT}px` : '0')};
+ min-height: ${p => (p.reserveSpace ? `${HOVER_ACTIONS_SLOT_HEIGHT}px` : '0')};
+ overflow: hidden;
+ white-space: nowrap;
+ pointer-events: none;
+
+ > * {
+ pointer-events: auto;
+ }
+
+ @media (max-width: ${p => p.theme.breakpoints.sm}) {
+ width: auto;
+ flex: 0 1 auto;
+ height: auto;
+ min-height: 0;
+ overflow: visible;
+ }
+`;
diff --git a/static/app/components/stackTrace/issueStackTrace/issueSourceMapsDebuggerAction.tsx b/static/app/components/stackTrace/issueStackTrace/issueSourceMapsDebuggerAction.tsx
new file mode 100644
index 00000000000000..f958fb1c12ff45
--- /dev/null
+++ b/static/app/components/stackTrace/issueStackTrace/issueSourceMapsDebuggerAction.tsx
@@ -0,0 +1,116 @@
+import {css} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import {Button} from '@sentry/scraps/button';
+
+import {openModal} from 'sentry/actionCreators/modal';
+import {
+ prepareSourceMapDebuggerFrameInformation,
+ useSourceMapDebuggerData,
+} from 'sentry/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData';
+import {SourceMapsDebuggerModal} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal';
+import {VALID_SOURCE_MAP_DEBUGGER_FILE_EXTENSIONS} from 'sentry/components/stackTrace/frame/actions/utils';
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {IconFix} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {useOrganization} from 'sentry/utils/useOrganization';
+
+export function IssueSourceMapsDebuggerAction() {
+ const {frame, event, frameIndex} = useStackTraceFrameContext();
+ const {exceptionIndex, hideSourceMapDebugger, project} = useStackTraceContext();
+ const organization = useOrganization({allowNull: true});
+
+ const sourceMapDebuggerData = useSourceMapDebuggerData(event, project?.slug ?? '');
+ const debuggerFrame =
+ exceptionIndex === undefined
+ ? undefined
+ : sourceMapDebuggerData?.exceptions[exceptionIndex]?.frames[frameIndex];
+ const frameSourceResolutionResults =
+ debuggerFrame && sourceMapDebuggerData
+ ? prepareSourceMapDebuggerFrameInformation(
+ sourceMapDebuggerData,
+ debuggerFrame,
+ event,
+ project?.platform
+ )
+ : undefined;
+
+ const frameHasValidFileEndingForSourceMapDebugger =
+ VALID_SOURCE_MAP_DEBUGGER_FILE_EXTENSIONS.some(
+ ending =>
+ (frame.absPath ?? '').endsWith(ending) || (frame.filename ?? '').endsWith(ending)
+ );
+ const shouldShowSourceMapDebuggerButton =
+ !frame.context?.length &&
+ !hideSourceMapDebugger &&
+ frame.inApp &&
+ frameHasValidFileEndingForSourceMapDebugger &&
+ !!frameSourceResolutionResults &&
+ !frameSourceResolutionResults.frameIsResolved;
+
+ if (!shouldShowSourceMapDebuggerButton || !frameSourceResolutionResults) {
+ return null;
+ }
+
+ const sourceMapDebuggerAnalytics = {
+ organization,
+ project_id: event.projectID,
+ event_id: event.id,
+ event_platform: event.platform,
+ sdk_name: event.sdk?.name,
+ sdk_version: event.sdk?.version,
+ };
+
+ return (
+
+ );
+}
+
+const UnminifyActionContent = styled('span')`
+ display: inline-flex;
+ align-items: center;
+ gap: ${p => p.theme.space.xs};
+`;
diff --git a/static/app/components/stackTrace/issueStackTrace/issueStackTraceFrameContext.tsx b/static/app/components/stackTrace/issueStackTrace/issueStackTraceFrameContext.tsx
new file mode 100644
index 00000000000000..2212bb4b271ad8
--- /dev/null
+++ b/static/app/components/stackTrace/issueStackTrace/issueStackTraceFrameContext.tsx
@@ -0,0 +1,70 @@
+import {useEffect} from 'react';
+
+import {useLineCoverageContext} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageContext';
+import {useStacktraceCoverage} from 'sentry/components/events/interfaces/frame/useStacktraceCoverage';
+import {FrameContent} from 'sentry/components/stackTrace/frame/frameContent';
+import {
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {
+ CodecovStatusCode,
+ type Coverage,
+ type LineCoverage,
+} from 'sentry/types/integrations';
+import {defined} from 'sentry/utils';
+import {useOrganization} from 'sentry/utils/useOrganization';
+
+function getLineCoverage(
+ lines: Array<[number, string | null]>,
+ lineCoverage: LineCoverage[]
+): Array {
+ const coverageByLine = new Map(lineCoverage);
+ return lines.map(([lineNo]) => coverageByLine.get(lineNo));
+}
+
+export function IssueStackTraceFrameContext() {
+ const {event, frame, isExpanded} = useStackTraceFrameContext();
+ const {project} = useStackTraceContext();
+ const {hasCoverageData, setHasCoverageData} = useLineCoverageContext();
+ const organization = useOrganization({allowNull: true});
+
+ const contextLines = isExpanded ? (frame.context ?? []) : [];
+
+ const {data: coverageData, isPending: isLoadingCoverage} = useStacktraceCoverage(
+ {
+ event,
+ frame,
+ orgSlug: organization?.slug || '',
+ projectSlug: project?.slug,
+ },
+ {
+ enabled:
+ isExpanded &&
+ defined(organization) &&
+ defined(project) &&
+ !!organization.codecovAccess,
+ }
+ );
+
+ const sourceLineCoverage =
+ !isLoadingCoverage &&
+ coverageData?.status === CodecovStatusCode.COVERAGE_EXISTS &&
+ coverageData.lineCoverage
+ ? getLineCoverage(contextLines, coverageData.lineCoverage)
+ : [];
+
+ useEffect(() => {
+ if (hasCoverageData) {
+ return;
+ }
+
+ const frameHasCoverageData =
+ !isLoadingCoverage && coverageData?.status === CodecovStatusCode.COVERAGE_EXISTS;
+ if (frameHasCoverageData) {
+ setHasCoverageData(true);
+ }
+ }, [coverageData, hasCoverageData, isLoadingCoverage, setHasCoverageData]);
+
+ return ;
+}
diff --git a/static/app/components/stackTrace/rawStackTrace.tsx b/static/app/components/stackTrace/rawStackTrace.tsx
new file mode 100644
index 00000000000000..a039c8c3cc9732
--- /dev/null
+++ b/static/app/components/stackTrace/rawStackTrace.tsx
@@ -0,0 +1,8 @@
+import styled from '@emotion/styled';
+
+export const RawStackTraceText = styled('pre')`
+ margin: 0;
+ padding: ${p => p.theme.space.md};
+ overflow: auto;
+ font-size: ${p => p.theme.font.size.sm};
+`;
diff --git a/static/app/components/stackTrace/stackTrace.spec.tsx b/static/app/components/stackTrace/stackTrace.spec.tsx
new file mode 100644
index 00000000000000..74c26b21d3c036
--- /dev/null
+++ b/static/app/components/stackTrace/stackTrace.spec.tsx
@@ -0,0 +1,822 @@
+import type {ComponentProps} from 'react';
+import {DataScrubbingRelayPiiConfigFixture} from 'sentry-fixture/dataScrubbingRelayPiiConfig';
+import {EventFixture} from 'sentry-fixture/event';
+import {EventEntryStacktraceFixture} from 'sentry-fixture/eventEntryStacktrace';
+import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
+import {textWithMarkupMatcher} from 'sentry-test/utils';
+
+import {DisplayOptions} from 'sentry/components/stackTrace/displayOptions';
+import {FrameContent} from 'sentry/components/stackTrace/frame/frameContent';
+import {IssueFrameActions} from 'sentry/components/stackTrace/issueStackTrace/issueFrameActions';
+import {StackTraceViewStateProvider} from 'sentry/components/stackTrace/stackTraceContext';
+import {StackTraceFrames} from 'sentry/components/stackTrace/stackTraceFrames';
+import {StackTraceProvider} from 'sentry/components/stackTrace/stackTraceProvider';
+import type {StackTraceViewStateProviderProps} from 'sentry/components/stackTrace/types';
+import {ProjectsStore} from 'sentry/stores/projectsStore';
+import {SentryAppComponentsStore} from 'sentry/stores/sentryAppComponentsStore';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+import {addQueryParamsToExistingUrl} from 'sentry/utils/queryString';
+
+type StacktraceWithFrames = StacktraceType & {
+ frames: NonNullable;
+};
+
+function makeStackTraceData(): {
+ event: ReturnType;
+ stacktrace: StacktraceWithFrames;
+} {
+ const entry = EventEntryStacktraceFixture();
+
+ return {
+ event: EventFixture({
+ platform: 'python',
+ projectID: '1',
+ entries: [entry],
+ }),
+ stacktrace: {
+ ...entry.data,
+ hasSystemFrames: true,
+ frames:
+ entry.data.frames?.map((frame, index) => ({
+ ...frame,
+ inApp: index >= 2,
+ })) ?? [],
+ } as StacktraceWithFrames,
+ };
+}
+
+type TestStackTraceProviderProps = ComponentProps &
+ Pick<
+ StackTraceViewStateProviderProps,
+ 'defaultIsMinified' | 'defaultIsNewestFirst' | 'defaultView'
+ >;
+
+function TestStackTraceProvider({
+ event,
+ children,
+ defaultIsMinified,
+ defaultIsNewestFirst,
+ defaultView,
+ minifiedStacktrace,
+ platform,
+ ...providerProps
+}: TestStackTraceProviderProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function renderStackTrace() {
+ const {event, stacktrace} = makeStackTraceData();
+
+ render(
+
+
+
+
+ );
+}
+
+describe('Core StackTrace', () => {
+ beforeEach(() => {
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/prompts-activity/',
+ body: {
+ dismissed_ts: undefined,
+ snoozed_ts: undefined,
+ },
+ });
+ MockApiClient.addMockResponse({
+ url: '/projects/org-slug/project-slug/stacktrace-link/',
+ body: {
+ config: null,
+ sourceUrl: null,
+ integrations: [],
+ },
+ });
+ });
+
+ it('switches between app and full stack views', async () => {
+ renderStackTrace();
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-row')).toHaveLength(4);
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Full Stack Trace'}));
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-row')).toHaveLength(5);
+ });
+
+ it('toggles frame ordering', async () => {
+ renderStackTrace();
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-title')[0]).toHaveTextContent(
+ 'raven/scripts/runner.py'
+ );
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Oldest'}));
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-title')[0]).toHaveTextContent(
+ 'raven/base.py'
+ );
+ });
+
+ it('supports raw stack trace view', async () => {
+ renderStackTrace();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Raw Stack Trace'}));
+
+ expect(screen.getByText(/File "raven\/scripts\/runner.py"/)).toBeInTheDocument();
+ expect(screen.queryByRole('list', {name: 'Stack frames'})).not.toBeInTheDocument();
+ });
+
+ it('toggles minified stacktrace frames when minified data is provided', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const minifiedStacktrace = {
+ ...stacktrace,
+ frames: stacktrace.frames.map((frame, index) => ({
+ ...frame,
+ filename: `minified/${index}.js`,
+ function: frame.rawFunction ?? `raw_fn_${index}`,
+ })),
+ };
+
+ render(
+
+
+
+
+ );
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-title')[0]).toHaveTextContent(
+ 'raven/scripts/runner.py'
+ );
+
+ await userEvent.click(await screen.findByRole('button', {name: 'Display options'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Unsymbolicated'}));
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-title')[0]).toHaveTextContent(
+ 'minified/4.js'
+ );
+ });
+
+ it('throws when DisplayOptions renders without stack trace view state', () => {
+ expect(() => render()).toThrow(
+ 'useStackTraceViewState must be used within StackTraceViewStateProvider'
+ );
+ });
+
+ it('toggles frame expansion', async () => {
+ renderStackTrace();
+
+ expect(screen.getByTestId('core-stacktrace-frame-context')).toBeInTheDocument();
+
+ await userEvent.click(screen.getAllByTestId('core-stacktrace-frame-title')[0]!);
+ expect(screen.getByTestId('core-stacktrace-frame-context')).not.toBeVisible();
+
+ await userEvent.click(screen.getAllByTestId('core-stacktrace-frame-title')[0]!);
+ expect(screen.getByTestId('core-stacktrace-frame-context')).toBeVisible();
+ });
+
+ it('toggles frame expansion when clicking the right trailing area', async () => {
+ renderStackTrace();
+
+ const firstTrailingArea = screen.getAllByTestId('core-stacktrace-frame-trailing')[0]!;
+
+ expect(screen.getByTestId('core-stacktrace-frame-context')).toBeVisible();
+ await userEvent.click(firstTrailingArea);
+ expect(screen.getByTestId('core-stacktrace-frame-context')).not.toBeVisible();
+ });
+
+ it('toggles frame expansion when clicking reserved actions slot space', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ render(
+
+
+
+
+ );
+
+ const firstActionsSlot = screen.getAllByTestId(
+ 'core-stacktrace-frame-actions-slot'
+ )[0]!;
+
+ expect(screen.getByTestId('core-stacktrace-frame-context')).toBeVisible();
+ await userEvent.click(firstActionsSlot);
+ expect(screen.getByTestId('core-stacktrace-frame-context')).not.toBeVisible();
+ });
+
+ it('shows and hides collapsed system frames', async () => {
+ renderStackTrace();
+
+ const toggleButton = screen.getByRole('button', {name: 'Show 1 more frame'});
+
+ await userEvent.click(toggleButton);
+
+ expect(screen.getAllByTestId('core-stacktrace-frame-row')).toHaveLength(5);
+ expect(screen.getByRole('button', {name: 'Hide 1 frame'})).toBeInTheDocument();
+ });
+
+ it('renders frame badges for in-app frames only', async () => {
+ renderStackTrace();
+
+ expect((await screen.findAllByText('In App')).length).toBeGreaterThan(0);
+ expect(screen.queryByText('System')).not.toBeInTheDocument();
+ });
+
+ it('renders captured python frame variables', async () => {
+ renderStackTrace();
+
+ expect(await screen.findByText('args')).toBeInTheDocument();
+ expect(screen.getByText('dsn')).toBeInTheDocument();
+ });
+
+ it('renders variable redaction metadata like legacy frame variables', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+ const organization = OrganizationFixture();
+ const project = ProjectFixture({id: event.projectID});
+ const projectDetails = ProjectFixture({
+ ...project,
+ relayPiiConfig: JSON.stringify(DataScrubbingRelayPiiConfigFixture()),
+ });
+ const initialRouterConfig = {
+ location: {
+ pathname: `/organizations/${organization.slug}/issues/1/`,
+ query: {project: project.id},
+ },
+ route: '/organizations/:orgId/issues/:groupId/',
+ };
+
+ ProjectsStore.loadInitialData([project]);
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/`,
+ body: projectDetails,
+ });
+
+ render(
+
+
+
+ ,
+ {
+ organization,
+ initialRouterConfig,
+ }
+ );
+
+ expect(screen.getByText(/redacted/i)).toBeInTheDocument();
+
+ await userEvent.hover(screen.getByText(/redacted/i));
+
+ expect(
+ await screen.findByText(
+ textWithMarkupMatcher(
+ 'Replaced because of the data scrubbing rule [Replace] [Password fields] with [Scrubbed] from [password] in the settings of the project project-slug'
+ )
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('renders custom frame context via StackTraceFrames slot', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ function CustomFrameContext() {
+ return ;
+ }
+
+ render(
+
+
+
+ );
+
+ expect(await screen.findAllByTestId('custom-stacktrace-frame-context')).toHaveLength(
+ 4
+ );
+ });
+
+ it('renders lead hint when non-app frame leads to app frame', async () => {
+ renderStackTrace();
+
+ expect(await screen.findByText('Called from:')).toBeInTheDocument();
+ });
+
+ it('renders crash lead hint when non-app frame has no next frame', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const singleNonAppFrame = {...stacktrace.frames[0]!, inApp: false};
+
+ render(
+
+
+
+
+ );
+
+ await userEvent.click(screen.getByTestId('core-stacktrace-frame-title'));
+ expect(screen.getByText('Crashed in non-app:')).toBeInTheDocument();
+ });
+
+ it('renders stacktrace code mapping links when project data is available', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const organization = OrganizationFixture();
+ const project = ProjectFixture({id: event.projectID});
+
+ ProjectsStore.loadInitialData([project]);
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`,
+ body: {
+ config: {provider: {key: 'github', name: 'GitHub'}},
+ sourceUrl:
+ 'https://github.com/getsentry/sentry/blob/main/raven/scripts/runner.py',
+ integrations: [],
+ },
+ });
+
+ render(
+
+
+
+ ,
+ {organization}
+ );
+
+ expect(
+ await screen.findByRole('button', {name: 'Open this line in GitHub'})
+ ).toHaveAttribute(
+ 'href',
+ 'https://github.com/getsentry/sentry/blob/main/raven/scripts/runner.py#L112'
+ );
+ });
+
+ it('renders source map info tooltip when frame map metadata exists', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+
+ render(
+
+
+
+
+ );
+
+ await userEvent.hover(screen.getByTestId('core-stacktrace-frame-location'));
+
+ expect(
+ await screen.findByText('Source Map', undefined, {timeout: 2000})
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText('https://cdn.example.com/runner.min.js.map', undefined, {
+ timeout: 2000,
+ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders unminify action when frame source map debugger data is unresolved', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const javascriptEvent = EventFixture({
+ ...event,
+ sdk: {name: 'sentry.javascript.react', version: '10.0.0'},
+ });
+ const organization = OrganizationFixture({slug: 'org-slug'});
+ const project = ProjectFixture({
+ id: javascriptEvent.projectID,
+ slug: 'project-slug',
+ platform: 'javascript',
+ });
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+ ProjectsStore.loadInitialData([project]);
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/events/${javascriptEvent.id}/source-map-debug-blue-thunder-edition/`,
+ body: {
+ dist: null,
+ exceptions: [
+ {
+ frames: [
+ {
+ debug_id_process: {
+ debug_id: null,
+ uploaded_source_file_with_correct_debug_id: false,
+ uploaded_source_map_with_correct_debug_id: false,
+ },
+ release_process: null,
+ },
+ ],
+ },
+ ],
+ has_debug_ids: false,
+ has_uploaded_some_artifact_with_a_debug_id: false,
+ project_has_some_artifact_bundle: false,
+ release: null,
+ release_has_some_artifact: false,
+ sdk_debug_id_support: 'not-supported' as const,
+ sdk_version: '10.0.0',
+ },
+ });
+
+ render(
+
+
+
+ ,
+ {organization}
+ );
+
+ expect(
+ await screen.findByRole('button', {name: 'Unminify Code'})
+ ).toBeInTheDocument();
+ });
+
+ it('renders sentry app frame links with line context', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ SentryAppComponentsStore.loadComponents([
+ {
+ uuid: 'stacktrace-component',
+ type: 'stacktrace-link',
+ schema: {
+ uri: '/frame-link',
+ url: 'https://example.com/frame-link?projectSlug=sentry',
+ type: 'stacktrace-link',
+ },
+ sentryApp: {
+ uuid: 'sentry-app-1',
+ slug: 'source-lens',
+ name: 'Source Lens',
+ avatars: [],
+ },
+ },
+ ]);
+
+ render(
+
+
+
+
+ );
+
+ expect(await screen.findByRole('link', {name: 'Source Lens'})).toHaveAttribute(
+ 'href',
+ addQueryParamsToExistingUrl('https://example.com/frame-link?projectSlug=sentry', {
+ filename: 'raven/scripts/runner.py',
+ lineNo: 112,
+ })
+ );
+ });
+
+ it('shows a tooltip with absPath when hovering filename', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frameWithAbsolutePath = {
+ ...stacktrace.frames[stacktrace.frames.length - 1]!,
+ filename: 'raven/scripts/runner.py',
+ absPath: '/home/ubuntu/raven/scripts/runner.py',
+ inApp: false,
+ };
+
+ render(
+
+
+
+
+ );
+
+ await userEvent.hover(screen.getByTestId('core-stacktrace-frame-location'));
+ expect(
+ await screen.findByText('/home/ubuntu/raven/scripts/runner.py:112', undefined, {
+ timeout: 2000,
+ })
+ ).toBeInTheDocument();
+ });
+
+ it('shows copy path and code mapping setup actions on hover for collapsed frames', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const organization = OrganizationFixture();
+ const project = ProjectFixture({id: event.projectID});
+ const integration = GitHubIntegrationFixture();
+
+ ProjectsStore.loadInitialData([project]);
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`,
+ match: [MockApiClient.matchQuery({lineNo: 112})],
+ body: {
+ config: {provider: {key: 'github', name: 'GitHub'}},
+ sourceUrl:
+ 'https://github.com/getsentry/sentry/blob/main/raven/scripts/runner.py',
+ integrations: [],
+ },
+ });
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/stacktrace-link/`,
+ match: [MockApiClient.matchQuery({lineNo: 77})],
+ body: {
+ config: null,
+ sourceUrl: null,
+ integrations: [integration],
+ },
+ });
+
+ render(
+
+
+
+ ,
+ {organization}
+ );
+
+ expect(
+ screen.queryByRole('button', {name: 'Set up Code Mapping'})
+ ).not.toBeInTheDocument();
+
+ const frameTitles = screen.getAllByTestId('core-stacktrace-frame-title');
+ await userEvent.hover(frameTitles[1]!);
+
+ expect(
+ await within(frameTitles[1]!).findByRole('button', {name: 'Set up Code Mapping'})
+ ).toBeInTheDocument();
+ expect(
+ within(frameTitles[1]!).getByRole('button', {name: 'Copy file path'})
+ ).toBeInTheDocument();
+ });
+
+ it('renders a repeat tag with tooltip in frame actions', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const recursiveFrame = {
+ ...stacktrace.frames[stacktrace.frames.length - 1]!,
+ filename: 'raven/scripts/runner.py',
+ module: 'raven.scripts.runner',
+ function: 'main',
+ lineNo: 112,
+ inApp: true,
+ package: 'raven',
+ instructionAddr: '0x00000001',
+ };
+
+ render(
+
+
+
+
+ );
+
+ expect(await screen.findAllByTestId('core-stacktrace-frame-row')).toHaveLength(1);
+ const repeatsTag = screen.getByTestId('core-stacktrace-repeats-tag');
+ expect(repeatsTag).toHaveTextContent('2');
+
+ await userEvent.hover(repeatsTag);
+ expect(await screen.findByText('Frame repeated 2 times')).toBeInTheDocument();
+ });
+
+ it('falls back to raw function and renders trimmed package in title metadata', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+
+ render(
+
+
+
+
+ );
+
+ expect(await screen.findByText('raw_runner_entrypoint')).toBeInTheDocument();
+ expect(screen.getByText('within')).toBeInTheDocument();
+ expect(screen.getByText('/opt/service/releases/libpipeline.so')).toBeInTheDocument();
+ });
+
+ it('renders registers and .NET assembly details in expanded frame context', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+
+ render(
+
+
+
+
+ );
+
+ expect(await screen.findByText('Registers')).toBeInTheDocument();
+ expect(screen.getByText('rax')).toBeInTheDocument();
+ expect(screen.getByText('Assembly:')).toBeInTheDocument();
+ expect(screen.getByText('Acme.Worker')).toBeInTheDocument();
+ expect(screen.getByText('PublicKeyToken:')).toBeInTheDocument();
+ expect(screen.getByText('abc123')).toBeInTheDocument();
+ });
+
+ it('renders empty source notation for single frame with no details', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+
+ render(
+
+
+
+
+ );
+
+ expect(
+ await screen.findByText('No additional details are available for this frame.')
+ ).toBeInTheDocument();
+ });
+
+ it('shows URL link in tooltip when absPath is an http URL', async () => {
+ const {event, stacktrace} = makeStackTraceData();
+ const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
+
+ render(
+
+
+
+ );
+
+ const location = await screen.findByTestId('core-stacktrace-frame-location');
+ await userEvent.hover(location);
+
+ expect(
+ await screen.findByRole('link', {name: 'https://example.com/static/app.js'})
+ ).toHaveAttribute('href', 'https://example.com/static/app.js');
+ });
+});
diff --git a/static/app/components/stackTrace/stackTrace.stories.tsx b/static/app/components/stackTrace/stackTrace.stories.tsx
new file mode 100644
index 00000000000000..0bca12053d6773
--- /dev/null
+++ b/static/app/components/stackTrace/stackTrace.stories.tsx
@@ -0,0 +1,1323 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import {Tag} from '@sentry/scraps/badge';
+import {Button} from '@sentry/scraps/button';
+import {Flex} from '@sentry/scraps/layout';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import {LineCoverageLegend} from 'sentry/components/events/interfaces/crashContent/exception/lineCoverageLegend';
+import {Hovercard} from 'sentry/components/hovercard';
+import {Panel} from 'sentry/components/panels/panel';
+import {ChevronAction} from 'sentry/components/stackTrace/frame/actions/chevron';
+import {HiddenFramesToggleAction} from 'sentry/components/stackTrace/frame/actions/hiddenFramesToggle';
+import {FrameContent} from 'sentry/components/stackTrace/frame/frameContent';
+import {StackTraceFrameRow} from 'sentry/components/stackTrace/frame/frameRow';
+import {IssueStackTrace} from 'sentry/components/stackTrace/issueStackTrace';
+import {
+ StackTraceViewStateProvider,
+ useStackTraceContext,
+ useStackTraceFrameContext,
+} from 'sentry/components/stackTrace/stackTraceContext';
+import {StackTraceFrames} from 'sentry/components/stackTrace/stackTraceFrames';
+import {StackTraceProvider} from 'sentry/components/stackTrace/stackTraceProvider';
+import type {StackTraceViewStateProviderProps} from 'sentry/components/stackTrace/types';
+import {IconCopy, IconGithub, IconRefresh} from 'sentry/icons';
+import {t, tn} from 'sentry/locale';
+import * as Storybook from 'sentry/stories';
+import {
+ EntryType,
+ EventOrGroupType,
+ type Event,
+ type ExceptionValue,
+ type Frame,
+} from 'sentry/types/event';
+import {Coverage} from 'sentry/types/integrations';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+
+type StacktraceWithFrames = StacktraceType & {
+ frames: NonNullable;
+};
+
+type StackTraceStoryData = {
+ event: Event;
+ stacktrace: StacktraceWithFrames;
+};
+
+function getSampleSourceLineCoverage(length: number): Coverage[] {
+ return Array.from({length}, (_, index) => {
+ if (index % 3 === 0) {
+ return Coverage.COVERED;
+ }
+ if (index % 3 === 1) {
+ return Coverage.NOT_COVERED;
+ }
+ return Coverage.PARTIAL;
+ });
+}
+
+function makeEvent(overrides: Partial = {}): Event {
+ return {
+ id: '1',
+ message: 'ApiException',
+ title: 'ApiException',
+ metadata: {},
+ entries: [],
+ projectID: '1',
+ groupID: '1',
+ eventID: '12345678901234567890123456789012',
+ dateCreated: '2019-05-21T18:01:48.762Z',
+ dateReceived: '2019-05-21T18:01:48.762Z',
+ tags: [],
+ errors: [],
+ crashFile: null,
+ size: 0,
+ dist: null,
+ fingerprints: [],
+ culprit: '',
+ user: null,
+ location: '',
+ type: EventOrGroupType.ERROR,
+ occurrence: null,
+ resolvedWith: [],
+ contexts: {},
+ ...overrides,
+ } as Event;
+}
+
+function makeFrame(overrides: Partial): Frame {
+ return {
+ absPath: '/home/ubuntu/raven/base.py',
+ colNo: null,
+ lineNo: null,
+ context: [],
+ filename: 'raven/base.py',
+ function: 'frame_function',
+ inApp: true,
+ instructionAddr: '0x0000000',
+ module: 'raven.base',
+ package: null,
+ platform: 'python',
+ rawFunction: 'frame_function',
+ symbol: 'frame_function',
+ symbolAddr: '0x0000000',
+ trust: 'none',
+ vars: {},
+ ...overrides,
+ };
+}
+
+function makeStackTraceData(): StackTraceStoryData {
+ const frames = [
+ makeFrame({
+ filename: 'raven/base.py',
+ absPath: '/home/ubuntu/raven/base.py',
+ module: 'raven.base',
+ function: 'build_msg',
+ context: [
+ [298, ' def build_msg(self, event_type, data=None, date=None,'],
+ [299, ' time_spent=None, extra=None, stack=False, **kwargs):'],
+ [300, ' data.update({'],
+ [301, " 'sentry.interfaces.Stacktrace': {"],
+ [302, " 'frames': get_stack_info(frames),"],
+ [303, ' }'],
+ [304, ' })'],
+ ],
+ lineNo: 302,
+ inApp: false,
+ vars: {
+ "'event_type'": "'raven.events.Message'",
+ "'stack'": 'True',
+ "'kwargs'": {
+ "'level'": '20',
+ "'message'": "'This is a test message generated using raven test'",
+ },
+ },
+ }),
+ makeFrame({
+ filename: 'raven/base.py',
+ absPath: '/home/ubuntu/raven/base.py',
+ module: 'raven.base',
+ function: 'capture',
+ context: [
+ [455, ' def capture(self, event_type, data=None, date=None,'],
+ [
+ 456,
+ ' time_spent=None, extra=None, stack=False, tags=None, **kwargs):',
+ ],
+ [457, ' data = self.build_msg('],
+ [458, ' event_type, data, date, time_spent, extra, stack, tags=tags,'],
+ [459, ' **kwargs)'],
+ [460, ' if data is None:'],
+ [461, ' return None'],
+ ],
+ lineNo: 459,
+ inApp: false,
+ vars: {
+ "'data'": 'None',
+ "'event_type'": "'raven.events.Message'",
+ "'time_spent'": 'None',
+ },
+ }),
+ makeFrame({
+ filename: 'raven/base.py',
+ absPath: '/home/ubuntu/raven/base.py',
+ module: 'raven.base',
+ function: 'captureMessage',
+ context: [
+ [573, ' def captureMessage(self, message, **kwargs):'],
+ [574, ' """'],
+ [575, " >>> client.captureMessage('My event just happened!')"],
+ [576, ' """'],
+ [
+ 577,
+ " return self.capture('raven.events.Message', message=message, **kwargs)",
+ ],
+ [578, ''],
+ [579, ' def captureException(self, exc_info=None, **kwargs):'],
+ ],
+ lineNo: 577,
+ inApp: true,
+ vars: {
+ "'message'": "'My event just happened!'",
+ "'kwargs'": {
+ "'stack'": 'True',
+ "'tags'": 'None',
+ },
+ },
+ }),
+ makeFrame({
+ filename: 'raven/scripts/runner.py',
+ absPath: '/home/ubuntu/raven/scripts/runner.py',
+ module: 'raven.scripts.runner',
+ function: 'send_test_message',
+ context: [
+ [73, 'def send_test_message(client, options=None):'],
+ [74, ' client.captureMessage('],
+ [75, ' extra={'],
+ [76, " 'user': get_uid(),"],
+ [77, " 'loadavg': get_loadavg(),"],
+ [78, ' },'],
+ [79, ' )'],
+ ],
+ lineNo: 77,
+ inApp: true,
+ vars: {
+ "'options'": {
+ "'data'": 'None',
+ "'tags'": 'None',
+ },
+ },
+ }),
+ makeFrame({
+ filename: 'raven/scripts/runner.py',
+ absPath: '/home/ubuntu/raven/scripts/runner.py',
+ module: 'raven.scripts.runner',
+ function: 'main',
+ context: [
+ [108, 'def main():'],
+ [109, ' opts, args = parser.parse_args()'],
+ [110, ' dsn = args[0] if args else os.environ.get("SENTRY_DSN")'],
+ [111, " client = Client(dsn, include_paths=['raven'])"],
+ [112, ' send_test_message(client, opts.__dict__)'],
+ [113, ''],
+ [114, 'if __name__ == "__main__":'],
+ ],
+ lineNo: 112,
+ inApp: true,
+ vars: {
+ "'args'": ["'test'", "'https://public@o0.ingest.sentry.io/1'"],
+ "'dsn'": "'https://public@o0.ingest.sentry.io/1'",
+ },
+ }),
+ ];
+
+ const stacktrace = {
+ framesOmitted: null,
+ hasSystemFrames: true,
+ registers: {},
+ frames,
+ } as StacktraceWithFrames;
+
+ const event = makeEvent({
+ platform: 'python',
+ projectID: '1',
+ tags: [],
+ entries: [],
+ contexts: {},
+ });
+
+ return {event, stacktrace};
+}
+
+function makeCircularStackTraceData(): StackTraceStoryData {
+ const {event, stacktrace} = makeStackTraceData();
+ const inAppRecursiveFrame = {
+ ...stacktrace.frames[stacktrace.frames.length - 1]!,
+ filename: 'raven/scripts/runner.py',
+ module: 'raven.scripts.runner',
+ function: 'main',
+ lineNo: 112,
+ inApp: true,
+ package: 'raven',
+ instructionAddr: '0x00000001',
+ };
+ const systemRecursiveFrame = {
+ ...stacktrace.frames[stacktrace.frames.length - 1]!,
+ filename: 'lib/urllib3/connectionpool.py',
+ module: 'urllib3.connectionpool',
+ function: '_make_request',
+ lineNo: 487,
+ inApp: false,
+ package: 'urllib3',
+ instructionAddr: '0x00000002',
+ };
+
+ return {
+ event,
+ stacktrace: {
+ ...stacktrace,
+ frames: [
+ {...systemRecursiveFrame},
+ {...systemRecursiveFrame},
+ {...systemRecursiveFrame},
+ {...inAppRecursiveFrame},
+ {...inAppRecursiveFrame},
+ {...inAppRecursiveFrame},
+ ],
+ } as StacktraceWithFrames,
+ };
+}
+
+function makeSourceMapTooltipStackTraceData(): StackTraceStoryData {
+ const {event, stacktrace} = makeStackTraceData();
+ const lastFrame = stacktrace.frames[stacktrace.frames.length - 1]!;
+
+ return {
+ event,
+ stacktrace: {
+ ...stacktrace,
+ frames: [
+ {
+ ...lastFrame,
+ filename: 'raven/scripts/runner.min.js',
+ absPath: '/home/ubuntu/raven/scripts/runner.min.js',
+ origAbsPath: '/home/ubuntu/raven/scripts/runner.js',
+ mapUrl: 'https://cdn.example.com/runner.min.js.map',
+ inApp: true,
+ },
+ ],
+ } as StacktraceWithFrames,
+ };
+}
+
+function makeLongPathStackTraceData(): StackTraceStoryData {
+ const {event, stacktrace} = makeStackTraceData();
+
+ return {
+ event,
+ stacktrace: {
+ ...stacktrace,
+ frames: stacktrace.frames.map((frame, index) => {
+ const longPath = `/workspace/teams/platform/very/deep/directory/for/customer/super/long/path/segment/${index}/src/services/handlers/production/error_processing_pipeline/frame_handler.py`;
+
+ return {
+ ...frame,
+ filename: longPath,
+ absPath: `/home/ubuntu${longPath}`,
+ inApp: true,
+ };
+ }),
+ } as StacktraceWithFrames,
+ };
+}
+
+function makeLongPathAndFunctionStackTraceData(): StackTraceStoryData {
+ const {event, stacktrace} = makeLongPathStackTraceData();
+
+ return {
+ event,
+ stacktrace: {
+ ...stacktrace,
+ frames: stacktrace.frames.map((frame, index) => ({
+ ...frame,
+ function:
+ `very_long_function_name_for_exception_debugging_pipeline_stage_${index}` +
+ `__with_additional_context_and_nested_handler_resolution_chain`,
+ inApp: true,
+ })),
+ } as StacktraceWithFrames,
+ };
+}
+
+function makeRawFunctionAndPackageStackTraceData(): StackTraceStoryData {
+ const {event, stacktrace} = makeStackTraceData();
+ const firstFrame = stacktrace.frames[stacktrace.frames.length - 1];
+
+ return {
+ event,
+ stacktrace: {
+ ...stacktrace,
+ frames: firstFrame
+ ? [
+ {
+ ...firstFrame,
+ function: null,
+ rawFunction: 'raw_runner_entrypoint',
+ package: '/opt/service/releases/libpipeline.so',
+ inApp: true,
+ },
+ ]
+ : [],
+ } as StacktraceWithFrames,
+ };
+}
+
+function makeMixedExpandabilityStackTraceData(): StackTraceStoryData {
+ return {
+ event: makeEvent({
+ platform: 'python',
+ projectID: '1',
+ tags: [],
+ entries: [],
+ contexts: {},
+ }),
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: {},
+ frames: [
+ makeFrame({
+ filename: 'app/expandable.py',
+ absPath: '/srv/app/expandable.py',
+ function: 'expandable_handler',
+ inApp: true,
+ lineNo: 23,
+ context: [
+ [21, 'def expandable_handler(payload):'],
+ [22, ' value = payload.get("value")'],
+ [23, ' raise RuntimeError(value)'],
+ ],
+ vars: {"'value'": "'boom'"},
+ }),
+ makeFrame({
+ filename: 'app/non_expandable.py',
+ absPath: '/srv/app/non_expandable.py',
+ function: 'non_expandable_handler',
+ inApp: true,
+ lineNo: null,
+ context: [],
+ vars: null,
+ package: null,
+ }),
+ ],
+ } as StacktraceWithFrames,
+ };
+}
+
+function makeChainedExceptionValues(): ExceptionValue[] {
+ return [
+ {
+ type: 'ValueError',
+ value: 'test',
+ mechanism: {handled: true, type: ''},
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [
+ makeFrame({
+ filename: 'file1.py',
+ absPath: 'file1.py',
+ module: 'helpers',
+ function: 'func1',
+ lineNo: 50,
+ context: [
+ [46, 'def func1(items):'],
+ [47, ' processed = []'],
+ [48, ' for item in items:'],
+ [49, ' value = item.get("value")'],
+ [50, ' raise ValueError("test")'],
+ [51, ' return processed'],
+ [52, ''],
+ ],
+ }),
+ ],
+ },
+ module: 'helpers',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'TypeError',
+ value: 'nested',
+ mechanism: {handled: true, type: ''},
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [
+ makeFrame({
+ filename: 'file2.py',
+ absPath: 'file2.py',
+ module: 'helpers',
+ function: 'func2',
+ lineNo: 50,
+ context: [
+ [46, 'def func2(raw):'],
+ [47, ' # coerce raw input to int'],
+ [48, ' if not isinstance(raw, (int, str)):'],
+ [49, ' pass'],
+ [50, ' raise TypeError("int")'],
+ [51, ''],
+ [52, 'def func3():'],
+ ],
+ }),
+ ],
+ },
+ module: 'helpers',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'RuntimeError',
+ value: 'original cause',
+ mechanism: {handled: true, type: ''},
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [
+ makeFrame({
+ filename: 'file3.py',
+ absPath: 'file3.py',
+ module: 'helpers',
+ function: 'func3',
+ lineNo: 10,
+ context: [
+ [6, 'def func3():'],
+ [7, ' conn = get_connection()'],
+ [8, ' if not conn.is_alive():'],
+ [9, ' conn.reconnect()'],
+ [10, ' raise RuntimeError("original cause")'],
+ [11, ' return conn.execute()'],
+ [12, ''],
+ ],
+ }),
+ ],
+ },
+ module: 'helpers',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ ];
+}
+
+function makeExceptionGroupValues(): ExceptionValue[] {
+ return [
+ {
+ type: 'ExceptionGroup',
+ value: '2 sub-exceptions',
+ mechanism: {
+ handled: true,
+ type: 'BaseExceptionGroup',
+ exception_id: 0,
+ is_exception_group: true,
+ },
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [
+ makeFrame({
+ filename: 'app/main.py',
+ absPath: 'app/main.py',
+ module: 'app.main',
+ function: 'run_tasks',
+ lineNo: 42,
+ context: [
+ [40, 'def run_tasks():'],
+ [41, ' errors = run_batch()'],
+ [42, ' raise ExceptionGroup("2 sub-exceptions", errors)'],
+ ],
+ }),
+ ],
+ },
+ module: 'app.main',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'ValueError',
+ value: 'invalid input: expected positive integer',
+ mechanism: {
+ handled: true,
+ type: 'BaseExceptionGroup',
+ exception_id: 1,
+ parent_id: 0,
+ },
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [
+ makeFrame({
+ filename: 'app/validators.py',
+ absPath: 'app/validators.py',
+ module: 'app.validators',
+ function: 'validate_input',
+ lineNo: 15,
+ context: [
+ [13, 'def validate_input(value):'],
+ [14, ' if value < 0:'],
+ [
+ 15,
+ ' raise ValueError("invalid input: expected positive integer")',
+ ],
+ ],
+ }),
+ ],
+ },
+ module: 'app.validators',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'TypeError',
+ value: "unsupported operand type(s) for +: 'int' and 'str'",
+ mechanism: {
+ handled: true,
+ type: 'BaseExceptionGroup',
+ exception_id: 2,
+ parent_id: 0,
+ },
+ stacktrace: {
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [
+ makeFrame({
+ filename: 'app/math.py',
+ absPath: 'app/math.py',
+ module: 'app.math',
+ function: 'add_values',
+ lineNo: 7,
+ context: [
+ [5, 'def add_values(a, b):'],
+ [6, ' # oops, b is a string'],
+ [7, ' return a + b'],
+ ],
+ }),
+ ],
+ },
+ module: 'app.math',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ ];
+}
+
+function makeChainedWithExceptionGroupValues(): ExceptionValue[] {
+ const makeGroupFrame = (filename: string, func: string, lineNo: number) =>
+ makeFrame({
+ filename,
+ absPath: filename,
+ module: 'app',
+ function: func,
+ lineNo,
+ context: [
+ [lineNo - 2, `def ${func}():`],
+ [lineNo - 1, ' try:'],
+ [lineNo, ` raise ExceptionGroup("group", errors)`],
+ [lineNo + 1, ' except Exception:'],
+ [lineNo + 2, ' pass'],
+ ],
+ });
+
+ const makeSimpleStacktrace = (
+ filename: string,
+ func: string,
+ lineNo: number
+ ): ExceptionValue['stacktrace'] => ({
+ framesOmitted: null,
+ hasSystemFrames: false,
+ registers: null,
+ frames: [makeGroupFrame(filename, func, lineNo)],
+ });
+
+ return [
+ // A plain chained exception (no exception_id, no tree structure)
+ {
+ type: 'RuntimeError',
+ value: 'task runner failed',
+ mechanism: {handled: true, type: 'chained'},
+ stacktrace: makeSimpleStacktrace('app/runner.py', 'run', 10),
+ module: 'app.runner',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ // Root exception group caused by the above
+ {
+ type: 'ExceptionGroup',
+ value: 'batch failed (2 sub-exceptions)',
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 0,
+ is_exception_group: true,
+ },
+ stacktrace: makeSimpleStacktrace('app/main.py', 'run_tasks', 42),
+ module: 'app.main',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'ValueError',
+ value: 'invalid input: expected positive integer',
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 1,
+ parent_id: 0,
+ },
+ stacktrace: makeSimpleStacktrace('app/validators.py', 'validate_input', 15),
+ module: 'app.validators',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ // Nested exception group — its children start hidden
+ {
+ type: 'ExceptionGroup',
+ value: 'nested group (2 sub-exceptions)',
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 2,
+ parent_id: 0,
+ is_exception_group: true,
+ },
+ stacktrace: makeSimpleStacktrace('app/tasks.py', 'process_batch', 88),
+ module: 'app.tasks',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'TypeError',
+ value: "unsupported operand type(s) for +: 'int' and 'str'",
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 3,
+ parent_id: 2,
+ },
+ stacktrace: makeSimpleStacktrace('app/math.py', 'add_values', 7),
+ module: 'app.math',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ {
+ type: 'KeyError',
+ value: "'missing_key'",
+ mechanism: {
+ handled: true,
+ type: 'chained',
+ exception_id: 4,
+ parent_id: 2,
+ },
+ stacktrace: makeSimpleStacktrace('app/config.py', 'get_setting', 23),
+ module: 'app.config',
+ threadId: null,
+ rawStacktrace: null,
+ },
+ ];
+}
+
+function StoryFrameActions({isHovering}: {isHovering: boolean}) {
+ const {frame, timesRepeated, isExpanded} = useStackTraceFrameContext();
+ const showHoverActions = isExpanded || isHovering;
+
+ return (
+
+
+
+ }
+ onClick={e => e.stopPropagation()}
+ />
+
+
+ }
+ onClick={e => e.stopPropagation()}
+ />
+
+
+
+ {timesRepeated > 0 ? (
+
+ }
+ variant="muted"
+ data-test-id="core-stacktrace-repeats-tag"
+ >
+ {timesRepeated}
+
+
+ ) : null}
+ {frame.inApp ? {t('In App')} : null}
+
+
+ );
+}
+
+type StoryStackTraceProviderProps = React.ComponentProps &
+ Pick<
+ StackTraceViewStateProviderProps,
+ 'defaultIsMinified' | 'defaultIsNewestFirst' | 'defaultView'
+ >;
+
+function StoryStackTraceProvider({
+ children,
+ event,
+ defaultIsMinified,
+ defaultIsNewestFirst,
+ defaultView,
+ minifiedStacktrace,
+ platform,
+ ...providerProps
+}: StoryStackTraceProviderProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export default Storybook.story('StackTrace', story => {
+ story('IssueStackTrace - Default', () => {
+ const {event, stacktrace} = makeStackTraceData();
+ return (
+
+ );
+ });
+
+ story('IssueStackTrace - Chained', () => {
+ const values = makeChainedExceptionValues();
+ return ;
+ });
+
+ story('IssueStackTrace - Exception Group', () => {
+ const values = makeExceptionGroupValues();
+ return (
+
+
+ A single exception group (Python 3.11+) with two child exceptions. The root
+ group shows a related exceptions tree with its children.
+
+
+
+ );
+ });
+
+ story('IssueStackTrace - Chained + Exception Group', () => {
+ const values = makeChainedWithExceptionGroupValues();
+ return (
+
+
+ A flat chained exception followed by an exception group. The{' '}
+ RuntimeError is a plain chained exception with no tree structure,
+ while the ExceptionGroup has child exceptions with the related
+ exceptions tree and toggle controls.
+
+
+
+ );
+ });
+
+ story('IssueStackTrace - Standalone', () => {
+ const {stacktrace} = makeStackTraceData();
+ const standaloneEvent = makeEvent({
+ platform: 'python',
+ entries: [{type: EntryType.STACKTRACE, data: stacktrace}],
+ });
+
+ return (
+
+
+ A standalone stack trace with no exception metadata. This is the typical shape
+ for log/message events that include a bare EntryType.STACKTRACE{' '}
+ entry. Uses the same IssueStackTrace component with the{' '}
+ stacktrace prop instead of values.
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - With Omitted Frames', () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ return (
+
+
+ When is set, a
+ placeholder row appears in place of the omitted frame range.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Circular Frames', () => {
+ const {event, stacktrace} = makeCircularStackTraceData();
+
+ return (
+
+
+ Identical frames (same module, function, and address) are detected as recursive
+ and collapsed into a single row with a repeat count badge.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Long Frame Paths', () => {
+ const {event, stacktrace} = makeLongPathStackTraceData();
+
+ return (
+
+
+ Very long file paths are truncated with an ellipsis on the left side, preserving
+ the most specific (rightmost) segments.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - File Path and Source Map Tooltip', () => {
+ const {event, stacktrace} = makeSourceMapTooltipStackTraceData();
+
+ return (
+
+
+ File paths use the standard filename:line or{' '}
+ filename:line:column format. Hover over the path for the full
+ absolute path and source map info (when{' '}
+ and{' '}
+ or{' '}
+ are set).
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Long Paths and Functions', () => {
+ const {event, stacktrace} = makeLongPathAndFunctionStackTraceData();
+
+ return (
+
+
+ Both the file path and function name are long here, testing two-column overflow.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Raw Function and Package', () => {
+ const {event, stacktrace} = makeRawFunctionAndPackageStackTraceData();
+
+ return (
+
+
+ When is null and{' '}
+ is set, the raw
+ symbol is shown. A path
+ appears as a secondary label.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceFrames - Single Frame Source Coverage', () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ const frameWithContext = stacktrace.frames.find(
+ frame => frame.inApp && (frame.context?.length ?? 0) > 0
+ );
+ if (!frameWithContext) {
+ return null;
+ }
+
+ const sourceLineCoverage = getSampleSourceLineCoverage(
+ frameWithContext.context?.length ?? 0
+ );
+
+ const singleFrameStacktrace = {
+ ...stacktrace,
+ frames: [frameWithContext],
+ };
+
+ function CoveredFrameContext() {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceFrames - Long Line Numbers', () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ const frameWithContext = stacktrace.frames.find(
+ frame => frame.inApp && (frame.context?.length ?? 0) > 0
+ );
+ if (!frameWithContext) {
+ return null;
+ }
+
+ const context = frameWithContext.context ?? [];
+ const lineNumberOffset = 12000;
+ const longLineNumberFrame = {
+ ...frameWithContext,
+ context: context.map<[number, string | null]>(([lineNumber, lineValue]) => [
+ lineNumber + lineNumberOffset,
+ lineValue,
+ ]),
+ lineNo:
+ typeof frameWithContext.lineNo === 'number'
+ ? frameWithContext.lineNo + lineNumberOffset
+ : frameWithContext.lineNo,
+ };
+
+ const singleFrameStacktrace = {
+ ...stacktrace,
+ frames: [longLineNumberFrame],
+ };
+
+ return (
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Mixed Expandability Alignment', () => {
+ const {event, stacktrace} = makeMixedExpandabilityStackTraceData();
+
+ return (
+
+
+ Renders one expandable frame and one non-expandable frame so trailing actions
+ stay aligned while still showing a chevron only on expandable rows.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Composed Frame API', () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ function ComposedContent() {
+ const {rows} = useStackTraceContext();
+
+ return (
+
+
+ {rows.map(row => {
+ if (row.kind === 'omitted') {
+ return (
+
+ Frames {row.omittedFrames[0]} to {row.omittedFrames[1]} were omitted.
+
+ );
+ }
+
+ return (
+
+ (
+
+ )}
+ />
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ return (
+
+
+ Use and{' '}
+ to render individual frames with
+ complete control over layout.
+
+
+
+
+
+ );
+ });
+
+ story('StackTraceProvider - Composed Frame Actions', () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ function ComposedActionsContent() {
+ const {rows} = useStackTraceContext();
+
+ return (
+
+
+ {rows.map((row, i) => {
+ if (row.kind === 'omitted') {
+ return null;
+ }
+
+ if (i === 0) {
+ return (
+
+ (
+
+ )}
+ />
+
+
+ );
+ }
+
+ if (i === 1) {
+ return (
+
+
+
+
+ }
+ />
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ }
+ />
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ return (
+
+
+ Use on{' '}
+ to compose exactly the
+ actions you need. The first frame uses all actions, the second uses only{' '}
+ , and the rest use{' '}
+ +{' '}
+ .
+
+
+
+
+
+ );
+ });
+
+ story('StackTrace - Hovercard Preview', () => {
+ const {event, stacktrace} = makeStackTraceData();
+
+ return (
+
+
+
+
+ }
+ >
+ Hovercard Trigger
+
+
+ );
+ });
+
+ story('StackTraceProvider - Minified Toggle', () => {
+ const {stacktrace} = makeStackTraceData();
+ const nodeEvent = makeEvent({platform: 'node'});
+ const stripVars = (frames: StacktraceWithFrames['frames']) =>
+ frames.map(frame => ({...frame, vars: {}}));
+ const minifiedStacktrace: StacktraceWithFrames = {
+ ...stacktrace,
+ frames: stripVars(stacktrace.frames).map(frame => ({
+ ...frame,
+ filename: frame.filename
+ ? frame.filename.replace('.py', '.min.js')
+ : frame.filename,
+ function: frame.function ? `_${frame.function}` : frame.function,
+ })),
+ };
+ stacktrace.frames = stripVars(stacktrace.frames);
+ return (
+
+
+ Provide to
+ enable the minified toggle in the Display Options (···) dropdown.
+ The label reads Minified for JS/Node and Unsymbolicated{' '}
+ elsewhere.
+
+
+
+
+
+ );
+ });
+});
+
+// Mirrors the GroupPreviewHovercard pattern: className controls the body styles,
+// so we need an intermediary to forward className → bodyClassName.
+function WideHovercardBase({className, ...rest}: React.ComponentProps) {
+ return ;
+}
+
+const StyledWideHovercard = styled(Hovercard)`
+ width: 700px;
+`;
+
+const WideHovercard = styled(WideHovercardBase)`
+ padding: 0;
+`;
+
+const HoverActionsSlot = styled(Flex)<{visible: boolean}>`
+ align-items: center;
+ gap: ${p => p.theme.space.sm};
+ opacity: ${p => (p.visible ? 1 : 0)};
+ pointer-events: ${p => (p.visible ? 'auto' : 'none')};
+`;
diff --git a/static/app/components/stackTrace/stackTraceContext.tsx b/static/app/components/stackTrace/stackTraceContext.tsx
new file mode 100644
index 00000000000000..0501edf2efba9c
--- /dev/null
+++ b/static/app/components/stackTrace/stackTraceContext.tsx
@@ -0,0 +1,144 @@
+import {createContext, useContext, useMemo, useState} from 'react';
+
+import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal';
+import type {Event, Frame} from 'sentry/types/event';
+import type {PlatformKey, Project} from 'sentry/types/project';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+
+import type {
+ Row,
+ StackTraceMeta,
+ StackTraceView,
+ StackTraceViewState,
+ StackTraceViewStateProviderProps,
+} from './types';
+
+const StackTraceViewStateContext = createContext(null);
+
+export function StackTraceViewStateProvider({
+ children,
+ defaultIsMinified = false,
+ defaultIsNewestFirst = true,
+ defaultView = 'app',
+ hasMinifiedStacktrace = false,
+ platform,
+}: StackTraceViewStateProviderProps) {
+ const [view, setView] = useState(defaultView);
+ const [isNewestFirst, setIsNewestFirst] = useState(defaultIsNewestFirst);
+ const [isMinified, setIsMinified] = useState(
+ hasMinifiedStacktrace && defaultIsMinified
+ );
+
+ const value = useMemo(
+ () => ({
+ hasMinifiedStacktrace,
+ isMinified,
+ isNewestFirst,
+ platform,
+ setIsMinified,
+ setIsNewestFirst,
+ setView,
+ view,
+ }),
+ [hasMinifiedStacktrace, isMinified, isNewestFirst, platform, view]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export interface StackTraceContextValue {
+ /** All frames regardless of system-frame filter, for Activity-based rendering. */
+ allRows: Row[];
+ /** Event payload for project/platform metadata and integrations. */
+ event: Event;
+ /** Active frame list for the selected (symbolicated/minified) stacktrace. */
+ frames: Frame[];
+ /** True when any visible frame row has expandable details. */
+ hasAnyExpandableFrames: boolean;
+ /** Hidden-system-frame expansion state keyed by frame index. */
+ hiddenFrameToggleMap: Record;
+ /** True when the "Unminify Code" source map action must be hidden. */
+ hideSourceMapDebugger: boolean;
+ /** Last in-app frame index, or the final frame index when none are in-app. */
+ lastFrameIndex: number;
+ /** Rendering platform for frame utils; always resolved before context creation. */
+ platform: PlatformKey;
+ /** Materialized rows (frames + omitted markers) for rendering. */
+ rows: Row[];
+ /** Currently active stacktrace (symbolicated or minified). */
+ stacktrace: StacktraceType;
+ /** Toggles hidden system frames adjacent to a visible row. */
+ toggleHiddenFrames: (frameIndex: number) => void;
+ /** Optional exception index in the full exception values list. */
+ exceptionIndex?: number;
+ /** Optional per-frame source map debugger resolution data. */
+ frameSourceMapDebuggerData?: FrameSourceMapDebuggerData[];
+ /** Optional redaction metadata used by variable/register renderers. */
+ meta?: StackTraceMeta;
+ /** Active project from ProjectsStore, used by frame source-link actions. */
+ project?: Project;
+}
+
+export interface StackTraceFrameContextValue {
+ /** Event payload for links, integrations, and analytics in frame actions. */
+ event: Event;
+ /** Current frame row data. Always defined for StackTrace.Frame descendants. */
+ frame: Frame;
+ /** Stable DOM id for aria-controls links between header and context. */
+ frameContextId: string;
+ /** Absolute frame index within stacktrace.frames. */
+ frameIndex: number;
+ /** Expanded/collapsed state for hidden system frames near this row. */
+ hiddenFramesExpanded: boolean;
+ /** Whether this row has expandable source/register/context details. */
+ isExpandable: boolean;
+ /** Whether source/register/context details are currently expanded. */
+ isExpanded: boolean;
+ /** Effective platform used for frame render/utility logic. */
+ platform: PlatformKey;
+ /** Number of repeated frames collapsed into this row. */
+ timesRepeated: number;
+ /** Toggle handler for source/register/context expansion. */
+ toggleExpansion: () => void;
+ /** Toggle handler for revealing or hiding collapsed system frames. */
+ toggleHiddenFrames: () => void;
+ /** Count of collapsed system frames hidden behind this row, when present. */
+ hiddenFrameCount?: number;
+ /** Next frame in call order, when one exists. */
+ nextFrame?: Frame;
+}
+
+export const StackTraceContext = createContext(null);
+export const StackTraceFrameContext = createContext(
+ null
+);
+
+export function useStackTraceContext() {
+ const context = useContext(StackTraceContext);
+ if (!context) {
+ throw new Error('StackTrace components must be used within StackTrace.Root');
+ }
+ return context;
+}
+
+export function useStackTraceViewState(): StackTraceViewState {
+ const context = useContext(StackTraceViewStateContext);
+ if (!context) {
+ throw new Error(
+ 'useStackTraceViewState must be used within StackTraceViewStateProvider'
+ );
+ }
+ return context;
+}
+
+export function useStackTraceFrameContext() {
+ const context = useContext(StackTraceFrameContext);
+ if (!context) {
+ throw new Error('StackTrace.Frame components must be used within StackTrace.Frame');
+ }
+ return context;
+}
diff --git a/static/app/components/stackTrace/stackTraceFrames.tsx b/static/app/components/stackTrace/stackTraceFrames.tsx
new file mode 100644
index 00000000000000..a0bb00470b4679
--- /dev/null
+++ b/static/app/components/stackTrace/stackTraceFrames.tsx
@@ -0,0 +1,132 @@
+import {Activity, useMemo, useRef, type ComponentType} from 'react';
+import {css} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import {Container} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
+
+import {displayRawContent as rawStacktraceContent} from 'sentry/components/events/interfaces/crashContent/stackTrace/rawContent';
+import {Panel} from 'sentry/components/panels/panel';
+import {t} from 'sentry/locale';
+
+import {StackTraceFrameRow} from './frame/frameRow';
+import {RawStackTraceText} from './rawStackTrace';
+import {useStackTraceContext, useStackTraceViewState} from './stackTraceContext';
+
+function OmittedFramesBanner({omittedFrames}: {omittedFrames: [number, number]}) {
+ const [start, end] = omittedFrames;
+ return (
+
+
+ {t('Frames %d to %d were omitted and not available.', start, end)}
+
+
+ );
+}
+
+interface StackTraceFramesProps {
+ frameContextComponent: ComponentType;
+ borderless?: boolean;
+ frameActionsComponent?: ComponentType<{isHovering: boolean}>;
+}
+
+export function StackTraceFrames({
+ borderless = false,
+ frameContextComponent: FrameContextComponent,
+ frameActionsComponent: FrameActionsComponent = StackTraceFrameRow.Actions.Default,
+}: StackTraceFramesProps) {
+ const {rows, allRows, stacktrace, event} = useStackTraceContext();
+ const {view} = useStackTraceViewState();
+
+ // Visible frame indices + current row data from a single pass over rows
+ const {visibleIndices, rowByIndex} = useMemo(() => {
+ const indices = new Set();
+ const map = new Map();
+ for (const row of rows) {
+ if (row.kind === 'frame') {
+ indices.add(row.frameIndex);
+ map.set(row.frameIndex, row);
+ }
+ }
+ return {visibleIndices: indices, rowByIndex: map};
+ }, [rows]);
+
+ // Lazy: track frames that have ever been visible so we only mount on first appearance.
+ // A ref is sufficient — the component already re-renders when `rows` changes.
+ const everVisibleRef = useRef(new Set());
+ for (const idx of visibleIndices) {
+ everVisibleRef.current.add(idx);
+ }
+
+ if (view === 'raw') {
+ return (
+
+
+ {rawStacktraceContent({data: stacktrace, platform: event.platform})}
+
+
+ );
+ }
+
+ if (allRows.length === 0) {
+ return (
+
+ {t('No stack trace available')}
+
+ );
+ }
+
+ return (
+
+ {allRows.map(row => {
+ if (row.kind === 'omitted') {
+ return (
+
+ );
+ }
+
+ if (!everVisibleRef.current.has(row.frameIndex)) {
+ return null;
+ }
+
+ const isVisible = visibleIndices.has(row.frameIndex);
+ const activeRow = rowByIndex.get(row.frameIndex) ?? row;
+
+ return (
+
+
+ (
+
+ )}
+ />
+
+
+
+ );
+ })}
+
+ );
+}
+
+const FramesPanel = styled(Panel)<{borderless: boolean}>`
+ overflow: hidden;
+ margin-bottom: 0;
+ ${p =>
+ p.borderless &&
+ css`
+ border: 0;
+ border-radius: 0;
+ `}
+
+ > * + * {
+ border-top: 1px solid ${p => p.theme.tokens.border.primary};
+ }
+`;
+
+const OmittedRow = styled(Container)`
+ border-left: 2px solid ${p => p.theme.colors.red400};
+ border-top: 1px solid ${p => p.theme.tokens.border.primary};
+ background: ${p => p.theme.colors.red100};
+ padding: ${p => `${p.theme.space.sm} ${p.theme.space.md}`};
+`;
diff --git a/static/app/components/stackTrace/stackTraceProvider.tsx b/static/app/components/stackTrace/stackTraceProvider.tsx
new file mode 100644
index 00000000000000..c19c19de17f91e
--- /dev/null
+++ b/static/app/components/stackTrace/stackTraceProvider.tsx
@@ -0,0 +1,161 @@
+import {useCallback, useMemo, useState} from 'react';
+
+import {isExpandable as frameHasExpandableDetails} from 'sentry/components/events/interfaces/frame/utils';
+import {getLastFrameIndex} from 'sentry/components/events/interfaces/utils';
+import type {Event} from 'sentry/types/event';
+import type {PlatformKey} from 'sentry/types/project';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+import {useProjects} from 'sentry/utils/useProjects';
+
+import {createInitialHiddenFrameToggleMap, getFrameCountMap, getRows} from './getRows';
+import {StackTraceContext, useStackTraceViewState} from './stackTraceContext';
+import type {StackTraceContextValue} from './stackTraceContext';
+import type {StackTraceProviderProps} from './types';
+
+function getDefaultPlatform(stacktrace: StacktraceType, event: Event): PlatformKey {
+ const framePlatform = stacktrace.frames?.find(frame => !!frame.platform)?.platform;
+ return event.platform ?? framePlatform ?? 'other';
+}
+
+export function StackTraceProvider({
+ children,
+ exceptionIndex,
+ event,
+ frameSourceMapDebuggerData,
+ hideSourceMapDebugger,
+ minifiedStacktrace,
+ stacktrace,
+ maxDepth,
+ meta,
+ platform: platformProp,
+}: StackTraceProviderProps) {
+ const {isMinified, isNewestFirst, view} = useStackTraceViewState();
+
+ const activeStacktrace =
+ isMinified && minifiedStacktrace ? minifiedStacktrace : stacktrace;
+ const frames = useMemo(() => activeStacktrace.frames ?? [], [activeStacktrace.frames]);
+ const {projects} = useProjects();
+ const project = useMemo(
+ () => projects.find(candidate => candidate.id === event.projectID),
+ [event.projectID, projects]
+ );
+ const lastFrameIndex = useMemo(
+ () => getLastFrameIndex(frames) ?? frames.length - 1,
+ [frames]
+ );
+
+ const [hiddenFrameToggleMap, setHiddenFrameToggleMap] = useState(() =>
+ createInitialHiddenFrameToggleMap(frames, view === 'full')
+ );
+
+ const platform = platformProp ?? getDefaultPlatform(activeStacktrace, event);
+ const shouldIncludeSystemFrames = view === 'full';
+
+ const frameCountMap = useMemo(
+ () => getFrameCountMap(frames, shouldIncludeSystemFrames),
+ [frames, shouldIncludeSystemFrames]
+ );
+
+ const allRows = useMemo(
+ () =>
+ getRows({
+ frames,
+ includeSystemFrames: true,
+ hiddenFrameToggleMap: {},
+ frameCountMap: {},
+ newestFirst: isNewestFirst,
+ framesOmitted: activeStacktrace.framesOmitted,
+ maxDepth,
+ }),
+ [frames, isNewestFirst, activeStacktrace.framesOmitted, maxDepth]
+ );
+
+ const rows = useMemo(
+ () =>
+ getRows({
+ frames,
+ includeSystemFrames: shouldIncludeSystemFrames,
+ hiddenFrameToggleMap,
+ frameCountMap,
+ newestFirst: isNewestFirst,
+ framesOmitted: activeStacktrace.framesOmitted,
+ maxDepth,
+ }),
+ [
+ frameCountMap,
+ frames,
+ hiddenFrameToggleMap,
+ isNewestFirst,
+ maxDepth,
+ shouldIncludeSystemFrames,
+ activeStacktrace.framesOmitted,
+ ]
+ );
+
+ const hasAnyExpandableFrames = useMemo(
+ () =>
+ rows.some(row => {
+ if (row.kind !== 'frame') {
+ return false;
+ }
+
+ const registers =
+ row.frameIndex === frames.length - 1 ? activeStacktrace.registers : {};
+
+ return frameHasExpandableDetails({
+ frame: row.frame,
+ registers,
+ platform,
+ });
+ }),
+ [rows, frames.length, activeStacktrace.registers, platform]
+ );
+
+ const toggleHiddenFrames = useCallback((frameIndex: number) => {
+ setHiddenFrameToggleMap(prevState => ({
+ ...prevState,
+ [frameIndex]: !prevState[frameIndex],
+ }));
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ allRows,
+ exceptionIndex,
+ event,
+ hasAnyExpandableFrames,
+ platform,
+ project,
+ stacktrace: activeStacktrace,
+ frameSourceMapDebuggerData,
+ frames,
+ hideSourceMapDebugger: hideSourceMapDebugger ?? false,
+ rows,
+ meta,
+ hiddenFrameToggleMap,
+ lastFrameIndex,
+ toggleHiddenFrames,
+ }),
+ [
+ allRows,
+ exceptionIndex,
+ event,
+ frameSourceMapDebuggerData,
+ frames,
+ hasAnyExpandableFrames,
+ hideSourceMapDebugger,
+ hiddenFrameToggleMap,
+ lastFrameIndex,
+ meta,
+ platform,
+ project,
+ rows,
+ activeStacktrace,
+ toggleHiddenFrames,
+ ]
+ );
+
+ return (
+ {children}
+ );
+}
diff --git a/static/app/components/stackTrace/types.tsx b/static/app/components/stackTrace/types.tsx
new file mode 100644
index 00000000000000..56f7a38e355044
--- /dev/null
+++ b/static/app/components/stackTrace/types.tsx
@@ -0,0 +1,76 @@
+import type {ReactNode} from 'react';
+
+import type {FrameSourceMapDebuggerData} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal';
+import type {Event, Frame} from 'sentry/types/event';
+import type {PlatformKey} from 'sentry/types/project';
+import type {StacktraceType} from 'sentry/types/stacktrace';
+
+export type StackTraceView = 'app' | 'full' | 'raw';
+
+export interface StackTraceViewState {
+ hasMinifiedStacktrace: boolean;
+ isMinified: boolean;
+ isNewestFirst: boolean;
+ setIsMinified: React.Dispatch>;
+ setIsNewestFirst: React.Dispatch>;
+ setView: React.Dispatch>;
+ view: StackTraceView;
+ platform?: PlatformKey;
+}
+
+export interface StackTraceViewStateProviderProps {
+ children: ReactNode;
+ defaultIsMinified?: boolean;
+ defaultIsNewestFirst?: boolean;
+ defaultView?: StackTraceView;
+ hasMinifiedStacktrace?: boolean;
+ platform?: PlatformKey;
+}
+
+export type FrameRow = {
+ frame: Frame;
+ frameIndex: number;
+ isSubFrame: boolean;
+ kind: 'frame';
+ timesRepeated: number;
+ hiddenFrameCount?: number;
+ nextFrame?: Frame;
+};
+
+export type OmittedFramesRow = {
+ kind: 'omitted';
+ omittedFrames: [number, number];
+ rowKey: string;
+};
+
+export type Row = FrameRow | OmittedFramesRow;
+
+export type StackTraceMeta = {
+ frames?: Array<{
+ vars?: Record;
+ }>;
+ registers?: Record;
+} & Record;
+
+export interface StackTraceProviderProps {
+ children: ReactNode;
+ event: Event;
+ stacktrace: StacktraceType;
+ /** Optional exception index in the full exception values list. */
+ exceptionIndex?: number;
+ /** Per-frame source map debugger data, powering the "Unminify Code" action. */
+ frameSourceMapDebuggerData?: FrameSourceMapDebuggerData[];
+ /** Hide the source maps debugger button entirely. */
+ hideSourceMapDebugger?: boolean;
+ /** Cap the number of frames rendered. Frames beyond this depth are omitted. */
+ maxDepth?: number;
+ /** Relay PII/scrubbing metadata used to render redaction annotations on frame variables. */
+ meta?: StackTraceMeta;
+ /**
+ * Enables toggling between symbolicated and minified views when present.
+ * The initial minified selection is controlled by StackTraceViewStateProvider.
+ */
+ minifiedStacktrace?: StacktraceType;
+ /** Override the platform used for frame rendering logic. Defaults to the event/frame platform. */
+ platform?: PlatformKey;
+}
diff --git a/static/app/components/stream/group.tsx b/static/app/components/stream/group.tsx
index 6eedd13afc703f..beda07ed787d5e 100644
--- a/static/app/components/stream/group.tsx
+++ b/static/app/components/stream/group.tsx
@@ -38,7 +38,7 @@ import type {NewQuery} from 'sentry/types/organization';
import type {User} from 'sentry/types/user';
import {defined, percent} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {isCtrlKeyPressed} from 'sentry/utils/isCtrlKeyPressed';
import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
diff --git a/static/app/constants/index.spec.tsx b/static/app/constants/index.spec.tsx
index 06e9a8de96409e..48e25a1040fff7 100644
--- a/static/app/constants/index.spec.tsx
+++ b/static/app/constants/index.spec.tsx
@@ -12,7 +12,11 @@ describe('DATA_CATEGORY_INFO', () => {
});
it('byte categories have correct formatting', () => {
- const byteCategories = [DataCategoryExact.ATTACHMENT, DataCategoryExact.LOG_BYTE];
+ const byteCategories = [
+ DataCategoryExact.ATTACHMENT,
+ DataCategoryExact.LOG_BYTE,
+ DataCategoryExact.TRACE_METRIC_BYTE,
+ ];
for (const category of byteCategories) {
const {formatting} = DATA_CATEGORY_INFO[category];
@@ -72,7 +76,11 @@ describe('DATA_CATEGORY_INFO', () => {
});
it('formatting unitType matches expected categories', () => {
- const bytesCategories = [DataCategoryExact.ATTACHMENT, DataCategoryExact.LOG_BYTE];
+ const bytesCategories = [
+ DataCategoryExact.ATTACHMENT,
+ DataCategoryExact.LOG_BYTE,
+ DataCategoryExact.TRACE_METRIC_BYTE,
+ ];
const durationHoursCategories = [
DataCategoryExact.PROFILE_DURATION,
DataCategoryExact.PROFILE_DURATION_UI,
diff --git a/static/app/constants/index.tsx b/static/app/constants/index.tsx
index cc07c2afe6a95f..583236a1bd4b99 100644
--- a/static/app/constants/index.tsx
+++ b/static/app/constants/index.tsx
@@ -645,6 +645,22 @@ export const DATA_CATEGORY_INFO = {
},
formatting: DEFAULT_COUNT_FORMATTING,
},
+ [DataCategoryExact.TRACE_METRIC_BYTE]: {
+ name: DataCategoryExact.TRACE_METRIC_BYTE,
+ plural: DataCategory.TRACE_METRIC_BYTE,
+ singular: 'traceMetricByte',
+ displayName: 'metric byte',
+ titleName: t('Metrics (Bytes)'),
+ productName: t('Metrics'),
+ uid: 37,
+ isBilledCategory: false,
+ statsInfo: {
+ ...DEFAULT_STATS_INFO,
+ showExternalStats: true,
+ yAxisMinInterval: 1 * KILOBYTE,
+ },
+ formatting: BYTES_FORMATTING,
+ },
[DataCategoryExact.SEER_USER]: {
name: DataCategoryExact.SEER_USER,
plural: DataCategory.SEER_USER,
diff --git a/static/app/data/languages.tsx b/static/app/data/languages.tsx
index f7cb1b094141c8..739032e8093a77 100644
--- a/static/app/data/languages.tsx
+++ b/static/app/data/languages.tsx
@@ -1,4 +1,4 @@
-export default [
+export const languages = [
['ja', 'Japanese'],
['it', 'Italian'],
['zh-tw', 'Traditional Chinese'],
diff --git a/static/app/plugins/basePlugin.tsx b/static/app/plugins/basePlugin.tsx
index 264fb4ce622f72..cb7ebfe9627f1b 100644
--- a/static/app/plugins/basePlugin.tsx
+++ b/static/app/plugins/basePlugin.tsx
@@ -1,4 +1,4 @@
-import Settings from 'sentry/plugins/components/settings';
+import {PluginSettings} from 'sentry/plugins/components/settings';
import type {Plugin} from 'sentry/types/integrations';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
@@ -8,15 +8,13 @@ type Props = {
project: Project;
};
-class BasePlugin {
+export class BasePlugin {
plugin: Plugin;
constructor(data: Plugin) {
this.plugin = data;
}
renderSettings(props: Props) {
- return ;
+ return ;
}
}
-
-export default BasePlugin;
diff --git a/static/app/plugins/components/issueActions.tsx b/static/app/plugins/components/issueActions.tsx
index 5ec769e4bd7666..8fe70355652437 100644
--- a/static/app/plugins/components/issueActions.tsx
+++ b/static/app/plugins/components/issueActions.tsx
@@ -3,12 +3,12 @@ import {Fragment} from 'react';
import {Alert} from '@sentry/scraps/alert';
import {Button, LinkButton} from '@sentry/scraps/button';
-import Form from 'sentry/components/deprecatedforms/form';
-import FormState from 'sentry/components/forms/state';
+import {Form} from 'sentry/components/deprecatedforms/form';
+import {FormState} from 'sentry/components/forms/state';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
-import PluginComponentBase from 'sentry/plugins/pluginComponentBase';
+import {PluginComponentBase} from 'sentry/plugins/pluginComponentBase';
import {GroupStore} from 'sentry/stores/groupStore';
import type {Group} from 'sentry/types/group';
import type {Plugin} from 'sentry/types/integrations';
@@ -60,7 +60,7 @@ type State = {
unlinkFieldList?: Field[];
} & PluginComponentBase['state'];
-class IssueActions extends PluginComponentBase {
+export class IssueActions extends PluginComponentBase {
constructor(props: Props) {
super(props);
@@ -572,5 +572,3 @@ class IssueActions extends PluginComponentBase {
);
}
}
-
-export default IssueActions;
diff --git a/static/app/plugins/components/settings.tsx b/static/app/plugins/components/settings.tsx
index 4572b159abb45e..1262524bcef46d 100644
--- a/static/app/plugins/components/settings.tsx
+++ b/static/app/plugins/components/settings.tsx
@@ -5,11 +5,11 @@ import {Alert} from '@sentry/scraps/alert';
import {LinkButton} from '@sentry/scraps/button';
import {Stack} from '@sentry/scraps/layout';
-import Form from 'sentry/components/deprecatedforms/form';
-import FormState from 'sentry/components/forms/state';
+import {Form} from 'sentry/components/deprecatedforms/form';
+import {FormState} from 'sentry/components/forms/state';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {t, tct} from 'sentry/locale';
-import PluginComponentBase from 'sentry/plugins/pluginComponentBase';
+import {PluginComponentBase} from 'sentry/plugins/pluginComponentBase';
import type {Plugin} from 'sentry/types/integrations';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
@@ -36,7 +36,7 @@ type State = {
wasConfiguredOnPageLoad: boolean;
} & PluginComponentBase['state'];
-class PluginSettings<
+export class PluginSettings<
P extends Props = Props,
S extends State = State,
> extends PluginComponentBase {
@@ -246,5 +246,3 @@ class PluginSettings<
);
}
}
-
-export default PluginSettings;
diff --git a/static/app/plugins/defaultIssuePlugin.tsx b/static/app/plugins/defaultIssuePlugin.tsx
index a92c461a95bc25..a38fbcbf6aff1f 100644
--- a/static/app/plugins/defaultIssuePlugin.tsx
+++ b/static/app/plugins/defaultIssuePlugin.tsx
@@ -1,5 +1,5 @@
-import BasePlugin from 'sentry/plugins/basePlugin';
-import IssueActions from 'sentry/plugins/components/issueActions';
+import {BasePlugin} from 'sentry/plugins/basePlugin';
+import {IssueActions} from 'sentry/plugins/components/issueActions';
import type {Group} from 'sentry/types/group';
import type {Plugin} from 'sentry/types/integrations';
import type {Organization} from 'sentry/types/organization';
diff --git a/static/app/plugins/defaultPlugin.tsx b/static/app/plugins/defaultPlugin.tsx
index f5d17f6d5e03ca..679eea75b9c568 100644
--- a/static/app/plugins/defaultPlugin.tsx
+++ b/static/app/plugins/defaultPlugin.tsx
@@ -1,4 +1,4 @@
-import BasePlugin from 'sentry/plugins/basePlugin';
+import {BasePlugin} from 'sentry/plugins/basePlugin';
class DefaultPlugin extends BasePlugin {
static displayName = 'DefaultPlugin';
diff --git a/static/app/plugins/index.tsx b/static/app/plugins/index.tsx
index f0b37c9d0cd767..1b828323821fc5 100644
--- a/static/app/plugins/index.tsx
+++ b/static/app/plugins/index.tsx
@@ -1,4 +1,4 @@
-import BasePlugin from 'sentry/plugins/basePlugin';
+import {BasePlugin} from 'sentry/plugins/basePlugin';
import {DefaultIssuePlugin} from 'sentry/plugins/defaultIssuePlugin';
import {Registry} from 'sentry/plugins/registry';
diff --git a/static/app/plugins/jira/components/issueActions.tsx b/static/app/plugins/jira/components/issueActions.tsx
index 19d0dd09f8752a..1f35bc5940fb95 100644
--- a/static/app/plugins/jira/components/issueActions.tsx
+++ b/static/app/plugins/jira/components/issueActions.tsx
@@ -1,9 +1,9 @@
-import Form from 'sentry/components/deprecatedforms/form';
-import FormState from 'sentry/components/forms/state';
-import DefaultIssueActions from 'sentry/plugins/components/issueActions';
+import {Form} from 'sentry/components/deprecatedforms/form';
+import {FormState} from 'sentry/components/forms/state';
+import {IssueActions as DefaultIssueActions} from 'sentry/plugins/components/issueActions';
import type {Writable} from 'sentry/types/core';
-class IssueActions extends DefaultIssueActions {
+export class IssueActions extends DefaultIssueActions {
changeField = (
action: DefaultIssueActions['props']['actionType'],
name: string,
@@ -116,5 +116,3 @@ class IssueActions extends DefaultIssueActions {
return form;
}
}
-
-export default IssueActions;
diff --git a/static/app/plugins/jira/components/settings.tsx b/static/app/plugins/jira/components/settings.tsx
index d758b46696f2a6..c3a56d9e392c71 100644
--- a/static/app/plugins/jira/components/settings.tsx
+++ b/static/app/plugins/jira/components/settings.tsx
@@ -4,11 +4,11 @@ import isEqual from 'lodash/isEqual';
import {Alert} from '@sentry/scraps/alert';
import {Button} from '@sentry/scraps/button';
-import Form from 'sentry/components/deprecatedforms/form';
-import FormState from 'sentry/components/forms/state';
+import {Form} from 'sentry/components/deprecatedforms/form';
+import {FormState} from 'sentry/components/forms/state';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
-import DefaultSettings from 'sentry/plugins/components/settings';
+import {PluginSettings as DefaultSettings} from 'sentry/plugins/components/settings';
type Field = Parameters[0]['config'];
@@ -29,7 +29,7 @@ const PAGE_FIELD_LIST = {
2: ['ignored_fields', 'default_priority', 'default_issue_type', 'auto_create'],
};
-class Settings extends DefaultSettings {
+export class Settings extends DefaultSettings {
constructor(props: Props) {
super(props);
@@ -211,5 +211,3 @@ class Settings extends DefaultSettings {
const FloatLeftButton = styled(Button)`
float: left;
`;
-
-export default Settings;
diff --git a/static/app/plugins/jira/index.tsx b/static/app/plugins/jira/index.tsx
index 1826da55fb3012..608f1d6d1576ca 100644
--- a/static/app/plugins/jira/index.tsx
+++ b/static/app/plugins/jira/index.tsx
@@ -1,8 +1,8 @@
-import type BasePlugin from 'sentry/plugins/basePlugin';
+import type {BasePlugin} from 'sentry/plugins/basePlugin';
import {DefaultIssuePlugin} from 'sentry/plugins/defaultIssuePlugin';
-import IssueActions from './components/issueActions';
-import Settings from './components/settings';
+import {IssueActions} from './components/issueActions';
+import {Settings} from './components/settings';
export class Jira extends DefaultIssuePlugin {
displayName = 'Jira';
diff --git a/static/app/plugins/pluginComponentBase.tsx b/static/app/plugins/pluginComponentBase.tsx
index c1dbf51ded96cb..abd254eee5711e 100644
--- a/static/app/plugins/pluginComponentBase.tsx
+++ b/static/app/plugins/pluginComponentBase.tsx
@@ -9,7 +9,7 @@ import {
} from 'sentry/actionCreators/indicator';
import {Client} from 'sentry/api';
import {GenericField} from 'sentry/components/deprecatedforms/genericField';
-import FormState from 'sentry/components/forms/state';
+import {FormState} from 'sentry/components/forms/state';
import {t} from 'sentry/locale';
const callbackWithArgs = function (context: any, callback: any, ...args: any) {
@@ -22,7 +22,7 @@ type Props = Record;
type State = {state: FormState};
-abstract class PluginComponentBase<
+export abstract class PluginComponentBase<
P extends Props = Props,
S extends State = State,
> extends Component {
@@ -156,5 +156,3 @@ abstract class PluginComponentBase<
return ;
}
}
-
-export default PluginComponentBase;
diff --git a/static/app/plugins/sessionstack/components/settings.tsx b/static/app/plugins/sessionstack/components/settings.tsx
index c05d5579b36875..50e5df580dfe7a 100644
--- a/static/app/plugins/sessionstack/components/settings.tsx
+++ b/static/app/plugins/sessionstack/components/settings.tsx
@@ -3,11 +3,11 @@ import isEqual from 'lodash/isEqual';
import {Alert} from '@sentry/scraps/alert';
import {Button} from '@sentry/scraps/button';
-import Form from 'sentry/components/deprecatedforms/form';
-import FormState from 'sentry/components/forms/state';
+import {Form} from 'sentry/components/deprecatedforms/form';
+import {FormState} from 'sentry/components/forms/state';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {t} from 'sentry/locale';
-import DefaultSettings from 'sentry/plugins/components/settings';
+import {PluginSettings as DefaultSettings} from 'sentry/plugins/components/settings';
type Props = DefaultSettings['props'];
@@ -19,7 +19,7 @@ type State = DefaultSettings['state'] & {
showOnPremisesConfiguration?: boolean;
};
-class Settings extends DefaultSettings {
+export class Settings extends DefaultSettings {
REQUIRED_FIELDS = ['account_email', 'api_token', 'website_id'];
ON_PREMISES_FIELDS = ['api_url', 'player_url'];
@@ -93,5 +93,3 @@ class Settings extends DefaultSettings {
);
}
}
-
-export default Settings;
diff --git a/static/app/plugins/sessionstack/index.tsx b/static/app/plugins/sessionstack/index.tsx
index 7e45706d63944d..d673e613d3e6f3 100644
--- a/static/app/plugins/sessionstack/index.tsx
+++ b/static/app/plugins/sessionstack/index.tsx
@@ -1,6 +1,6 @@
-import BasePlugin from 'sentry/plugins/basePlugin';
+import {BasePlugin} from 'sentry/plugins/basePlugin';
-import Settings from './components/settings';
+import {Settings} from './components/settings';
export class SessionStackPlugin extends BasePlugin {
displayName = 'SessionStack';
diff --git a/static/app/router/routes.tsx b/static/app/router/routes.tsx
index 334eec6063593e..6f45252d97ba68 100644
--- a/static/app/router/routes.tsx
+++ b/static/app/router/routes.tsx
@@ -37,7 +37,7 @@ import {IssueTaxonomy} from 'sentry/views/issueList/taxonomies';
import {OrganizationContainerRoute} from 'sentry/views/organizationContainer';
import {OrganizationLayout} from 'sentry/views/organizationLayout';
import {OrganizationStatsWrapper} from 'sentry/views/organizationStats/organizationStatsWrapper';
-import TransactionSummaryTab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab as TransactionSummaryTab} from 'sentry/views/performance/transactionSummary/tabs';
import {ProjectEventRedirect} from 'sentry/views/projectEventRedirect';
import {redirectDeprecatedProjectRoute} from 'sentry/views/projects/redirectDeprecatedProjectRoute';
import {RouteNotFound} from 'sentry/views/routeNotFound';
@@ -2433,7 +2433,10 @@ function buildRoutes(): RouteObject[] {
},
{
path: TabPaths[Tab.ACTIVITY],
- component: make(() => import('sentry/views/issueDetails/groupActivity')),
+ component: make(
+ () => import('sentry/views/issueDetails/groupEventDetails/groupEventDetails'),
+
+ ),
},
{
path: TabPaths[Tab.EVENTS],
@@ -2453,11 +2456,17 @@ function buildRoutes(): RouteObject[] {
},
{
path: TabPaths[Tab.DISTRIBUTIONS],
- component: make(() => import('sentry/views/issueDetails/groupTags/groupTagsTab')),
+ component: make(
+ () => import('sentry/views/issueDetails/groupEventDetails/groupEventDetails'),
+
+ ),
},
{
path: `${TabPaths[Tab.DISTRIBUTIONS]}:tagKey/`,
- component: make(() => import('sentry/views/issueDetails/groupTags/groupTagValues')),
+ component: make(
+ () => import('sentry/views/issueDetails/groupEventDetails/groupEventDetails'),
+
+ ),
},
{
path: TabPaths[Tab.USER_FEEDBACK],
@@ -2470,13 +2479,15 @@ function buildRoutes(): RouteObject[] {
{
path: TabPaths[Tab.SIMILAR_ISSUES],
component: make(
- () => import('sentry/views/issueDetails/groupSimilarIssues/groupSimilarIssuesTab')
+ () => import('sentry/views/issueDetails/groupEventDetails/groupEventDetails'),
+
),
},
{
path: TabPaths[Tab.MERGED],
component: make(
- () => import('sentry/views/issueDetails/groupMerged/groupMergedTab')
+ () => import('sentry/views/issueDetails/groupEventDetails/groupEventDetails'),
+
),
},
];
diff --git a/static/app/types/core.tsx b/static/app/types/core.tsx
index 9c5ae3990cd33c..a2317839f2de0b 100644
--- a/static/app/types/core.tsx
+++ b/static/app/types/core.tsx
@@ -98,6 +98,7 @@ export enum DataCategory {
SEER_USER = 'seerUsers',
USER_REPORT_V2 = 'feedback',
TRACE_METRICS = 'traceMetrics',
+ TRACE_METRIC_BYTE = 'traceMetricBytes',
SIZE_ANALYSIS = 'sizeAnalyses',
INSTALLABLE_BUILD = 'installableBuilds',
}
@@ -132,6 +133,7 @@ export enum DataCategoryExact {
SEER_USER = 'seer_user',
USER_REPORT_V2 = 'feedback',
TRACE_METRIC = 'trace_metric',
+ TRACE_METRIC_BYTE = 'trace_metric_byte',
SIZE_ANALYSIS = 'size_analysis',
INSTALLABLE_BUILD = 'installable_build',
}
diff --git a/static/app/types/event.tsx b/static/app/types/event.tsx
index 90f3247a3c2687..d26f0ead43b5a3 100644
--- a/static/app/types/event.tsx
+++ b/static/app/types/event.tsx
@@ -204,7 +204,7 @@ export type ExceptionValue = {
stacktrace: StacktraceType | null;
threadId: number | null;
type: string;
- value: string;
+ value: string | null;
frames?: Frame[] | null;
rawModule?: string | null;
rawType?: string | null;
@@ -390,6 +390,11 @@ export type Entry =
| EntryGeneric
| EntryResources;
+/** Maps each EntryType to its corresponding Entry subtype. */
+export type EntryMap = {
+ [E in Entry as E['type']]: E;
+};
+
// Contexts: https://develop.sentry.dev/sdk/event-payloads/contexts/
interface BaseContext {
diff --git a/static/app/utils/analytics/dashboardsAnalyticsEvents.tsx b/static/app/utils/analytics/dashboardsAnalyticsEvents.tsx
index c4c4add60a32b2..2560fa499e2c2f 100644
--- a/static/app/utils/analytics/dashboardsAnalyticsEvents.tsx
+++ b/static/app/utils/analytics/dashboardsAnalyticsEvents.tsx
@@ -95,6 +95,7 @@ export type DashboardsEventParameters = {
'dashboards_manage.create.start': Record;
'dashboards_manage.delete': {dashboard_id: number; view_type: DashboardsLayout};
'dashboards_manage.duplicate': {dashboard_id: number; view_type: DashboardsLayout};
+ 'dashboards_manage.generate.start': Record;
'dashboards_manage.paginate': Record;
'dashboards_manage.search': Record;
'dashboards_manage.templates.add': {
@@ -196,6 +197,8 @@ export const dashboardsEventMap: Record = {
'dashboards_manage.change_sort': 'Dashboards Manager: Sort By Changed',
'dashboards_manage.change_view_type': 'Dashboards Manager: View Type Toggled',
'dashboards_manage.create.start': 'Dashboards Manager: Dashboard Create Started',
+ 'dashboards_manage.generate.start':
+ 'Dashboards Manager: Dashboard Seer Generate Started',
'dashboards_manage.delete': 'Dashboards Manager: Dashboard Deleted',
'dashboards_manage.duplicate': 'Dashboards Manager: Dashboard Duplicated',
'dashboards_manage.paginate': 'Dashboards Manager: Paginate',
diff --git a/static/app/utils/cursorPoller.tsx b/static/app/utils/cursorPoller.tsx
index 911461657ccb09..98289cfd3c5668 100644
--- a/static/app/utils/cursorPoller.tsx
+++ b/static/app/utils/cursorPoller.tsx
@@ -11,7 +11,7 @@ type Options = {
const BASE_DELAY = 3000;
const MAX_DELAY = 60000;
-class CursorPoller {
+export class CursorPoller {
constructor(options: Options) {
this.options = options;
this.setEndpoint(options.linkPreviousHref);
@@ -117,5 +117,3 @@ class CursorPoller {
});
}
}
-
-export default CursorPoller;
diff --git a/static/app/utils/dashboards/issueFieldRenderers.tsx b/static/app/utils/dashboards/issueFieldRenderers.tsx
index 4e4b1e45c612cd..e6143e3fbc7524 100644
--- a/static/app/utils/dashboards/issueFieldRenderers.tsx
+++ b/static/app/utils/dashboards/issueFieldRenderers.tsx
@@ -13,7 +13,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {IssueAssignee} from 'sentry/utils/dashboards/issueAssignee';
import type {EventData, MetaType} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {FieldFormatterRenderFunctionPartial} from 'sentry/utils/discover/fieldRenderers';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {Container, FieldShortId, OverflowLink} from 'sentry/utils/discover/styles';
diff --git a/static/app/utils/discover/discoverQuery.spec.tsx b/static/app/utils/discover/discoverQuery.spec.tsx
index e90d13274448ed..f476177d559ca1 100644
--- a/static/app/utils/discover/discoverQuery.spec.tsx
+++ b/static/app/utils/discover/discoverQuery.spec.tsx
@@ -1,7 +1,7 @@
import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
describe('DiscoverQuery', () => {
let location: any, eventView: any;
diff --git a/static/app/utils/discover/eventView.spec.tsx b/static/app/utils/discover/eventView.spec.tsx
index df0f1d395f85ba..0a985d6119d3f0 100644
--- a/static/app/utils/discover/eventView.spec.tsx
+++ b/static/app/utils/discover/eventView.spec.tsx
@@ -8,7 +8,8 @@ import {ConfigStore} from 'sentry/stores/configStore';
import type {NewQuery, SavedQuery} from 'sentry/types/organization';
import type {Config} from 'sentry/types/system';
import type {MetaType} from 'sentry/utils/discover/eventView';
-import EventView, {
+import {
+ EventView,
isAPIPayloadSimilar,
pickRelevantLocationQueryStrings,
} from 'sentry/utils/discover/eventView';
diff --git a/static/app/utils/discover/eventView.tsx b/static/app/utils/discover/eventView.tsx
index 7376d518d52747..d7855bfc166070 100644
--- a/static/app/utils/discover/eventView.tsx
+++ b/static/app/utils/discover/eventView.tsx
@@ -294,7 +294,7 @@ export type EventViewOptions = {
yAxis?: string | string[] | undefined;
};
-class EventView {
+export class EventView {
id: string | undefined;
name: string | undefined;
fields: readonly Field[];
@@ -1581,5 +1581,3 @@ export function pickRelevantLocationQueryStrings(location: Location) {
return picked;
}
-
-export default EventView;
diff --git a/static/app/utils/discover/fieldRenderers.spec.tsx b/static/app/utils/discover/fieldRenderers.spec.tsx
index 784cf4c006aeec..ab1f7b057aeddf 100644
--- a/static/app/utils/discover/fieldRenderers.spec.tsx
+++ b/static/app/utils/discover/fieldRenderers.spec.tsx
@@ -7,7 +7,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {SPAN_OP_RELATIVE_BREAKDOWN_FIELD} from 'sentry/utils/discover/fields';
import {WidgetType, type DashboardFilters} from 'sentry/views/dashboards/types';
diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx
index e45f9b9176e7da..ab05167b223f9b 100644
--- a/static/app/utils/discover/fieldRenderers.tsx
+++ b/static/app/utils/discover/fieldRenderers.tsx
@@ -32,8 +32,7 @@ import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {toArray} from 'sentry/utils/array/toArray';
import {browserHistory} from 'sentry/utils/browserHistory';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {EventData, MetaType} from 'sentry/utils/discover/eventView';
+import type {EventData, EventView, MetaType} from 'sentry/utils/discover/eventView';
import type {RateUnit} from 'sentry/utils/discover/fields';
import {
ABYTE_UNITS,
@@ -64,7 +63,11 @@ import {
findLinkedDashboardForField,
getLinkedDashboardUrl,
} from 'sentry/views/dashboards/utils/getLinkedDashboardUrl';
-import {NUMBER_MAX_FRACTION_DIGITS} from 'sentry/views/dashboards/widgets/common/settings';
+import {
+ NUMBER_MAX_FRACTION_DIGITS,
+ NUMBER_MIN_VALUE,
+} from 'sentry/views/dashboards/widgets/common/settings';
+import {formatTooltipValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue';
import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
import type {TraceItemDetailsMeta} from 'sentry/views/explore/hooks/useTraceItemDetails';
@@ -303,18 +306,30 @@ export const FIELD_FORMATTERS: FieldFormatters = {
},
number: {
isSortable: true,
- renderFunc: (field, data) => (
-
- {typeof data[field] === 'number'
- ? formatFloat(data[field], NUMBER_MAX_FRACTION_DIGITS).toLocaleString(
- undefined,
- {
- maximumFractionDigits: NUMBER_MAX_FRACTION_DIGITS,
- }
- )
- : emptyValue}
-
- ),
+ renderFunc: (field, data) => {
+ if (typeof data[field] !== 'number') {
+ return {emptyValue};
+ }
+ if (data[field] > 0 && data[field] < NUMBER_MIN_VALUE) {
+ return (
+
+
+ {`<${NUMBER_MIN_VALUE}`}
+
+
+ );
+ }
+ return (
+
+ {formatFloat(data[field], NUMBER_MAX_FRACTION_DIGITS).toLocaleString(
+ undefined,
+ {
+ maximumFractionDigits: NUMBER_MAX_FRACTION_DIGITS,
+ }
+ )}
+
+ );
+ },
},
percentage: {
isSortable: true,
diff --git a/static/app/utils/discover/genericDiscoverQuery.tsx b/static/app/utils/discover/genericDiscoverQuery.tsx
index d04dd5796c9254..f621325d8ecced 100644
--- a/static/app/utils/discover/genericDiscoverQuery.tsx
+++ b/static/app/utils/discover/genericDiscoverQuery.tsx
@@ -6,8 +6,11 @@ import type {EventQuery} from 'sentry/actionCreators/events';
import type {ResponseMeta} from 'sentry/api';
import {Client} from 'sentry/api';
import {t} from 'sentry/locale';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {ImmutableEventView, LocationQuery} from 'sentry/utils/discover/eventView';
+import type {
+ EventView,
+ ImmutableEventView,
+ LocationQuery,
+} from 'sentry/utils/discover/eventView';
import {isAPIPayloadSimilar} from 'sentry/utils/discover/eventView';
import {PerformanceEventViewContext} from 'sentry/utils/performance/contexts/performanceEventViewContext';
import type {UseQueryOptions} from 'sentry/utils/queryClient';
diff --git a/static/app/utils/discover/urls.tsx b/static/app/utils/discover/urls.tsx
index 21dc6975425f08..6ae69b04acb2c3 100644
--- a/static/app/utils/discover/urls.tsx
+++ b/static/app/utils/discover/urls.tsx
@@ -10,7 +10,7 @@ import type {TraceLayoutTabKeys} from 'sentry/views/performance/newTraceDetails/
import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
import type {EventData} from './eventView';
-import EventView from './eventView';
+import {EventView} from './eventView';
/**
* Create a slug that can be used with discover details views
diff --git a/static/app/utils/performance/contexts/metricsCardinality.tsx b/static/app/utils/performance/contexts/metricsCardinality.tsx
index 73599395bce7e5..462a67cb67abfd 100644
--- a/static/app/utils/performance/contexts/metricsCardinality.tsx
+++ b/static/app/utils/performance/contexts/metricsCardinality.tsx
@@ -4,7 +4,7 @@ import type {Location} from 'history';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
import {canUseMetricsData} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
diff --git a/static/app/utils/performance/contexts/performanceEventViewContext.tsx b/static/app/utils/performance/contexts/performanceEventViewContext.tsx
index 035fde381a48b6..7e1e31ad83d769 100644
--- a/static/app/utils/performance/contexts/performanceEventViewContext.tsx
+++ b/static/app/utils/performance/contexts/performanceEventViewContext.tsx
@@ -1,4 +1,4 @@
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {createDefinedContext} from './utils';
diff --git a/static/app/utils/performance/histogram/histogramQuery.spec.tsx b/static/app/utils/performance/histogram/histogramQuery.spec.tsx
index c2acb4a0970b70..ed90b64e6aea7c 100644
--- a/static/app/utils/performance/histogram/histogramQuery.spec.tsx
+++ b/static/app/utils/performance/histogram/histogramQuery.spec.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import {render, screen} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {HistogramQuery} from 'sentry/utils/performance/histogram/histogramQuery';
function renderHistogram({isLoading, error, histograms}: any) {
diff --git a/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuery.tsx b/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuery.tsx
index 5922dbc5fa46d9..cab2cc054bbd14 100644
--- a/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuery.tsx
+++ b/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuery.tsx
@@ -1,6 +1,6 @@
import omit from 'lodash/omit';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {
DiscoverQueryProps,
GenericChildrenProps,
diff --git a/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuerySums.tsx b/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuerySums.tsx
index a307684ef37e7f..3916d26af6c665 100644
--- a/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuerySums.tsx
+++ b/static/app/utils/performance/metricsEnhanced/metricsCompatibilityQuerySums.tsx
@@ -1,6 +1,6 @@
import omit from 'lodash/omit';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {
DiscoverQueryProps,
GenericChildrenProps,
diff --git a/static/app/utils/platform.tsx b/static/app/utils/platform.tsx
index 00976ce106dd2e..ffdcafecc274bf 100644
--- a/static/app/utils/platform.tsx
+++ b/static/app/utils/platform.tsx
@@ -72,7 +72,7 @@ export function isDisabledGamingPlatform({
platform,
enabledConsolePlatforms,
}: {
- platform: Platform;
+ platform: Pick;
enabledConsolePlatforms?: string[];
}) {
return platform.type === 'console' && !enabledConsolePlatforms?.includes(platform.id);
diff --git a/static/app/utils/replays/fetchReplayList.tsx b/static/app/utils/replays/fetchReplayList.tsx
index 338d48ded3a2f7..0a4aba9cb3ac09 100644
--- a/static/app/utils/replays/fetchReplayList.tsx
+++ b/static/app/utils/replays/fetchReplayList.tsx
@@ -5,7 +5,7 @@ import type {Client} from 'sentry/api';
import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants';
import type {PageFilters} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils';
import type {RequestError} from 'sentry/utils/requestError/requestError';
import type {ReplayListQueryReferrer, ReplayListRecord} from 'sentry/views/replays/types';
diff --git a/static/app/utils/replays/hooks/useReplayList.tsx b/static/app/utils/replays/hooks/useReplayList.tsx
index c4924cb72fcf6b..20844a6ebd0c6b 100644
--- a/static/app/utils/replays/hooks/useReplayList.tsx
+++ b/static/app/utils/replays/hooks/useReplayList.tsx
@@ -3,7 +3,7 @@ import type {Location} from 'history';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {fetchReplayList} from 'sentry/utils/replays/fetchReplayList';
import {useApi} from 'sentry/utils/useApi';
import type {
diff --git a/static/app/views/alerts/create.tsx b/static/app/views/alerts/create.tsx
index 7ba1d7fe4efe8d..8230782cf304cf 100644
--- a/static/app/views/alerts/create.tsx
+++ b/static/app/views/alerts/create.tsx
@@ -5,7 +5,7 @@ import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {uniqueId} from 'sentry/utils/guid';
import {decodeScalar} from 'sentry/utils/queryString';
import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
diff --git a/static/app/views/alerts/list/incidents/index.tsx b/static/app/views/alerts/list/incidents/index.tsx
index fbd6a6b0af8522..b3d75f8a7cf9ca 100644
--- a/static/app/views/alerts/list/incidents/index.tsx
+++ b/static/app/views/alerts/list/incidents/index.tsx
@@ -9,7 +9,7 @@ import {ExternalLink} from '@sentry/scraps/link';
import {promptsCheck, promptsUpdate} from 'sentry/actionCreators/prompts';
import Feature from 'sentry/components/acl/feature';
import {CreateAlertButton} from 'sentry/components/createAlertButton';
-import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
+import {DeprecatedAsyncComponent} from 'sentry/components/deprecatedAsyncComponent';
import * as Layout from 'sentry/components/layouts/thirds';
import {PageFiltersContainer} from 'sentry/components/pageFilters/container';
import {Pagination} from 'sentry/components/pagination';
diff --git a/static/app/views/alerts/list/incidents/row.tsx b/static/app/views/alerts/list/incidents/row.tsx
index db4bd4968aad76..352c1ac1390d1b 100644
--- a/static/app/views/alerts/list/incidents/row.tsx
+++ b/static/app/views/alerts/list/incidents/row.tsx
@@ -7,7 +7,7 @@ import {Tag} from '@sentry/scraps/badge';
import {Link} from '@sentry/scraps/link';
import {Duration} from 'sentry/components/duration';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {IdBadge} from 'sentry/components/idBadge';
import {TimeSince} from 'sentry/components/timeSince';
import {t} from 'sentry/locale';
diff --git a/static/app/views/alerts/list/rules/row.tsx b/static/app/views/alerts/list/rules/row.tsx
index e3f7df49782a33..89278ddf473f0d 100644
--- a/static/app/views/alerts/list/rules/row.tsx
+++ b/static/app/views/alerts/list/rules/row.tsx
@@ -13,7 +13,7 @@ import {Access} from 'sentry/components/acl/access';
import {openConfirmModal} from 'sentry/components/confirm';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {IdBadge} from 'sentry/components/idBadge';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {TextOverflow} from 'sentry/components/textOverflow';
diff --git a/static/app/views/alerts/rules/issue/details/ruleDetails.tsx b/static/app/views/alerts/rules/issue/details/ruleDetails.tsx
index ab3e46ee4d4441..1e202c80134802 100644
--- a/static/app/views/alerts/rules/issue/details/ruleDetails.tsx
+++ b/static/app/views/alerts/rules/issue/details/ruleDetails.tsx
@@ -12,7 +12,7 @@ import {Access} from 'sentry/components/acl/access';
import {SnoozeAlert} from 'sentry/components/alerts/snoozeAlert';
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
import type {DateTimeObject} from 'sentry/components/charts/utils';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {IdBadge} from 'sentry/components/idBadge';
import * as Layout from 'sentry/components/layouts/thirds';
import {LoadingError} from 'sentry/components/loadingError';
diff --git a/static/app/views/alerts/rules/issue/index.tsx b/static/app/views/alerts/rules/issue/index.tsx
index e86d7a1da70556..cd63e0aea725a4 100644
--- a/static/app/views/alerts/rules/issue/index.tsx
+++ b/static/app/views/alerts/rules/issue/index.tsx
@@ -23,8 +23,8 @@ import {
} from 'sentry/actionCreators/indicator';
import {hasEveryAccess} from 'sentry/components/acl/access';
import {Confirm} from 'sentry/components/confirm';
-import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {DeprecatedAsyncComponent} from 'sentry/components/deprecatedAsyncComponent';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {components} from 'sentry/components/forms/controls/reactSelectWrapper';
import {FieldGroup} from 'sentry/components/forms/fieldGroup';
import {FieldHelp} from 'sentry/components/forms/fieldGroup/fieldHelp';
@@ -80,7 +80,7 @@ import {
} from 'sentry/views/alerts/utils/constants';
import {ProjectPermissionAlert} from 'sentry/views/settings/project/projectPermissionAlert';
-import RuleNodeList from './ruleNodeList';
+import {RuleNodeList} from './ruleNodeList';
const FREQUENCY_OPTIONS = [
{value: '5', label: t('5 minutes')},
diff --git a/static/app/views/alerts/rules/issue/memberTeamFields.tsx b/static/app/views/alerts/rules/issue/memberTeamFields.tsx
index 368f9210ca3f12..5eadf4b5094bba 100644
--- a/static/app/views/alerts/rules/issue/memberTeamFields.tsx
+++ b/static/app/views/alerts/rules/issue/memberTeamFields.tsx
@@ -27,7 +27,7 @@ type Props = {
teamValue: string | number;
};
-class MemberTeamFields extends Component {
+export class MemberTeamFields extends Component {
handleChange = (attribute: 'targetType' | 'targetIdentifier', newValue: string) => {
const {onChange, ruleData} = this.props;
if (newValue === ruleData[attribute]) {
@@ -131,5 +131,3 @@ const PanelItemGrid = styled(PanelItem)`
const SelectWrapper = styled('div')`
width: 200px;
`;
-
-export default MemberTeamFields;
diff --git a/static/app/views/alerts/rules/issue/ruleNode.tsx b/static/app/views/alerts/rules/issue/ruleNode.tsx
index 63c3bc5e29d7de..fcafbd0f5a158a 100644
--- a/static/app/views/alerts/rules/issue/ruleNode.tsx
+++ b/static/app/views/alerts/rules/issue/ruleNode.tsx
@@ -32,7 +32,7 @@ import type {IssueCategory} from 'sentry/types/group';
import {VALID_ISSUE_CATEGORIES} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import MemberTeamFields from 'sentry/views/alerts/rules/issue/memberTeamFields';
+import {MemberTeamFields} from 'sentry/views/alerts/rules/issue/memberTeamFields';
import {SentryAppRuleModal} from 'sentry/views/alerts/rules/issue/sentryAppRuleModal';
interface FieldProps {
diff --git a/static/app/views/alerts/rules/issue/ruleNodeList.tsx b/static/app/views/alerts/rules/issue/ruleNodeList.tsx
index 4dd1ea914570b9..c30a7813fa6889 100644
--- a/static/app/views/alerts/rules/issue/ruleNodeList.tsx
+++ b/static/app/views/alerts/rules/issue/ruleNodeList.tsx
@@ -135,7 +135,7 @@ const groupSelectOptions = (actions: IssueAlertRuleActionTemplate[]) => {
});
};
-class RuleNodeList extends Component {
+export class RuleNodeList extends Component {
componentWillUnmount() {
window.clearTimeout(this.propertyChangeTimeout);
}
@@ -308,8 +308,6 @@ class RuleNodeList extends Component {
}
}
-export default RuleNodeList;
-
const StyledSelectControl = styled(Select)`
width: 100%;
`;
diff --git a/static/app/views/alerts/rules/metric/constants.spec.tsx b/static/app/views/alerts/rules/metric/constants.spec.tsx
index 1bec8e6b819a4f..61b62621e155f6 100644
--- a/static/app/views/alerts/rules/metric/constants.spec.tsx
+++ b/static/app/views/alerts/rules/metric/constants.spec.tsx
@@ -1,7 +1,7 @@
import {UserFixture} from 'sentry-fixture/user';
import type {EventViewOptions} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {createRuleFromEventView} from 'sentry/views/alerts/rules/metric/constants';
import {Dataset, EventTypes} from 'sentry/views/alerts/rules/metric/types';
diff --git a/static/app/views/alerts/rules/metric/constants.tsx b/static/app/views/alerts/rules/metric/constants.tsx
index 5ed365d50789b2..8dfdd49efdd9c2 100644
--- a/static/app/views/alerts/rules/metric/constants.tsx
+++ b/static/app/views/alerts/rules/metric/constants.tsx
@@ -1,7 +1,7 @@
import pick from 'lodash/pick';
import {t, tct} from 'sentry/locale';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {AggregationKeyWithAlias, LooseFieldKey} from 'sentry/utils/discover/fields';
import {parseFunction, SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/discover/fields';
import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours';
diff --git a/static/app/views/alerts/rules/metric/create.spec.tsx b/static/app/views/alerts/rules/metric/create.spec.tsx
index b7b821d6f0c593..6282608588b2ea 100644
--- a/static/app/views/alerts/rules/metric/create.spec.tsx
+++ b/static/app/views/alerts/rules/metric/create.spec.tsx
@@ -5,7 +5,7 @@ import {RouteComponentPropsFixture} from 'sentry-fixture/routeComponentPropsFixt
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, waitFor} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MetricRulesCreate} from 'sentry/views/alerts/rules/metric/create';
describe('Incident Rules Create', () => {
diff --git a/static/app/views/alerts/rules/metric/create.tsx b/static/app/views/alerts/rules/metric/create.tsx
index 088bd8add5c414..4e83dce41f2e7d 100644
--- a/static/app/views/alerts/rules/metric/create.tsx
+++ b/static/app/views/alerts/rules/metric/create.tsx
@@ -4,7 +4,7 @@ import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {metric} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar} from 'sentry/utils/queryString';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {makeAlertsPathname} from 'sentry/views/alerts/pathnames';
diff --git a/static/app/views/alerts/rules/metric/duplicate.tsx b/static/app/views/alerts/rules/metric/duplicate.tsx
index 8fc815e75865dd..3ee24466d24ae8 100644
--- a/static/app/views/alerts/rules/metric/duplicate.tsx
+++ b/static/app/views/alerts/rules/metric/duplicate.tsx
@@ -6,7 +6,7 @@ import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
import type {Project} from 'sentry/types/project';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {uniqueId} from 'sentry/utils/guid';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/alerts/rules/metric/incompatibleAlertQuery.spec.tsx b/static/app/views/alerts/rules/metric/incompatibleAlertQuery.spec.tsx
index 921dfd0e6e9bbe..9ad5a1ea30f279 100644
--- a/static/app/views/alerts/rules/metric/incompatibleAlertQuery.spec.tsx
+++ b/static/app/views/alerts/rules/metric/incompatibleAlertQuery.spec.tsx
@@ -2,7 +2,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {IncompatibleAlertQuery} from 'sentry/views/alerts/rules/metric/incompatibleAlertQuery';
import {DEFAULT_EVENT_VIEW, getAllViews} from 'sentry/views/discover/results/data';
diff --git a/static/app/views/alerts/rules/metric/incompatibleAlertQuery.tsx b/static/app/views/alerts/rules/metric/incompatibleAlertQuery.tsx
index 809bcaec0aa7a9..d10ee144ca1be0 100644
--- a/static/app/views/alerts/rules/metric/incompatibleAlertQuery.tsx
+++ b/static/app/views/alerts/rules/metric/incompatibleAlertQuery.tsx
@@ -6,7 +6,7 @@ import {Button} from '@sentry/scraps/button';
import {IconClose} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {Aggregation} from 'sentry/utils/discover/fields';
import {AGGREGATIONS, explodeFieldString} from 'sentry/utils/discover/fields';
import {
diff --git a/static/app/views/alerts/rules/metric/ruleForm.tsx b/static/app/views/alerts/rules/metric/ruleForm.tsx
index 3be17fbbdd2c93..712860f42f9375 100644
--- a/static/app/views/alerts/rules/metric/ruleForm.tsx
+++ b/static/app/views/alerts/rules/metric/ruleForm.tsx
@@ -21,7 +21,7 @@ import {hasEveryAccess} from 'sentry/components/acl/access';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
import {CircleIndicator} from 'sentry/components/circleIndicator';
import {Confirm} from 'sentry/components/confirm';
-import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
+import {DeprecatedAsyncComponent} from 'sentry/components/deprecatedAsyncComponent';
import type {FormProps} from 'sentry/components/forms/form';
import {Form} from 'sentry/components/forms/form';
import {FormModel} from 'sentry/components/forms/model';
@@ -42,7 +42,7 @@ import type {
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {metric, trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {parseFunction, prettifyParsedFunction} from 'sentry/utils/discover/fields';
import {AggregationKey} from 'sentry/utils/fields';
import {isOnDemandQueryString} from 'sentry/utils/onDemandMetrics';
@@ -57,7 +57,7 @@ import {IncompatibleAlertQuery} from 'sentry/views/alerts/rules/metric/incompati
import {OnDemandThresholdChecker} from 'sentry/views/alerts/rules/metric/onDemandThresholdChecker';
import {RuleNameOwnerForm} from 'sentry/views/alerts/rules/metric/ruleNameOwnerForm';
import {ThresholdTypeForm} from 'sentry/views/alerts/rules/metric/thresholdTypeForm';
-import Triggers from 'sentry/views/alerts/rules/metric/triggers';
+import {Triggers} from 'sentry/views/alerts/rules/metric/triggers';
import TriggersChart, {ErrorChart} from 'sentry/views/alerts/rules/metric/triggers/chart';
import type {SeriesSamplingInfo} from 'sentry/views/alerts/rules/metric/utils/determineSeriesSampleCount';
import {determineSeriesSampleCountAndIsSampled} from 'sentry/views/alerts/rules/metric/utils/determineSeriesSampleCount';
diff --git a/static/app/views/alerts/rules/metric/triggers/anomalyAlertsForm.tsx b/static/app/views/alerts/rules/metric/triggers/anomalyAlertsForm.tsx
index f25f54924be13b..a438c4d7ca1e6f 100644
--- a/static/app/views/alerts/rules/metric/triggers/anomalyAlertsForm.tsx
+++ b/static/app/views/alerts/rules/metric/triggers/anomalyAlertsForm.tsx
@@ -110,7 +110,7 @@ function DirectionFormItem({
);
}
-class AnomalyDetectionFormField extends Component {
+export class AnomalyDetectionFormField extends Component {
render() {
const {sensitivity, onSensitivityChange, thresholdType, onThresholdTypeChange} =
this.props;
@@ -140,4 +140,3 @@ const StyledField = styled(FieldGroup)`
const SelectContainer = styled('div')`
flex: 1;
`;
-export default AnomalyDetectionFormField;
diff --git a/static/app/views/alerts/rules/metric/triggers/chart/index.tsx b/static/app/views/alerts/rules/metric/triggers/chart/index.tsx
index aaad25192a316c..d77fd9e577e1c8 100644
--- a/static/app/views/alerts/rules/metric/triggers/chart/index.tsx
+++ b/static/app/views/alerts/rules/metric/triggers/chart/index.tsx
@@ -12,12 +12,13 @@ import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
import {fetchTotalCount} from 'sentry/actionCreators/events';
import {Client} from 'sentry/api';
import {ErrorPanel} from 'sentry/components/charts/errorPanel';
-import EventsRequest, {
+import {
+ EventsRequest,
type EventsRequestProps,
} from 'sentry/components/charts/eventsRequest';
import type {LineChartSeries} from 'sentry/components/charts/lineChart';
import {OnDemandMetricRequest} from 'sentry/components/charts/onDemandMetricRequest';
-import SessionsRequest from 'sentry/components/charts/sessionsRequest';
+import {SessionsRequest} from 'sentry/components/charts/sessionsRequest';
import {
ChartControls,
InlineContainer,
diff --git a/static/app/views/alerts/rules/metric/triggers/form.tsx b/static/app/views/alerts/rules/metric/triggers/form.tsx
index 3d80bfe3e1b3c2..d118e05098d285 100644
--- a/static/app/views/alerts/rules/metric/triggers/form.tsx
+++ b/static/app/views/alerts/rules/metric/triggers/form.tsx
@@ -14,7 +14,7 @@ import type {Config} from 'sentry/types/system';
import {withApi} from 'sentry/utils/withApi';
import {withConfig} from 'sentry/utils/withConfig';
import {getThresholdUnits} from 'sentry/views/alerts/rules/metric/constants';
-import ThresholdControl from 'sentry/views/alerts/rules/metric/triggers/thresholdControl';
+import {ThresholdControl} from 'sentry/views/alerts/rules/metric/triggers/thresholdControl';
import type {
AlertRuleThresholdType,
ThresholdControlValue,
diff --git a/static/app/views/alerts/rules/metric/triggers/index.tsx b/static/app/views/alerts/rules/metric/triggers/index.tsx
index 2a77c322b4037e..be6d193830a9e3 100644
--- a/static/app/views/alerts/rules/metric/triggers/index.tsx
+++ b/static/app/views/alerts/rules/metric/triggers/index.tsx
@@ -7,7 +7,7 @@ import type {Project} from 'sentry/types/project';
import {removeAtArrayIndex} from 'sentry/utils/array/removeAtArrayIndex';
import {replaceAtArrayIndex} from 'sentry/utils/array/replaceAtArrayIndex';
import ActionsPanel from 'sentry/views/alerts/rules/metric/triggers/actionsPanel';
-import AnomalyDetectionFormField from 'sentry/views/alerts/rules/metric/triggers/anomalyAlertsForm';
+import {AnomalyDetectionFormField} from 'sentry/views/alerts/rules/metric/triggers/anomalyAlertsForm';
import {DynamicAlertsFeedbackButton} from 'sentry/views/alerts/rules/metric/triggers/dynamicAlertsFeedbackButton';
import TriggerForm from 'sentry/views/alerts/rules/metric/triggers/form';
import {
@@ -51,7 +51,7 @@ type Props = {
/**
* A list of forms to add, edit, and delete triggers.
*/
-class Triggers extends Component {
+export class Triggers extends Component {
handleDeleteTrigger = (index: number) => {
const {triggers, onChange} = this.props;
const updatedTriggers = removeAtArrayIndex(triggers, index);
@@ -169,5 +169,3 @@ class Triggers extends Component {
);
}
}
-
-export default Triggers;
diff --git a/static/app/views/alerts/rules/metric/triggers/thresholdControl.tsx b/static/app/views/alerts/rules/metric/triggers/thresholdControl.tsx
index 5adf711b8dd554..27d5efcb62ba74 100644
--- a/static/app/views/alerts/rules/metric/triggers/thresholdControl.tsx
+++ b/static/app/views/alerts/rules/metric/triggers/thresholdControl.tsx
@@ -27,7 +27,7 @@ type State = {
currentValue: string | null;
};
-class ThresholdControl extends Component {
+export class ThresholdControl extends Component {
state: State = {
currentValue: null,
};
@@ -186,5 +186,3 @@ const ThresholdContainer = styled('div')<{comparisonType: AlertRuleComparisonTyp
const PercentWrapper = styled('div')`
margin-left: ${p => p.theme.space.md};
`;
-
-export default ThresholdControl;
diff --git a/static/app/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree.tsx b/static/app/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree.tsx
index 0fd445e1ddf57d..f0883010e8f13f 100644
--- a/static/app/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree.tsx
+++ b/static/app/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree.tsx
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import {Container, Flex} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import type {UptimeAssertion} from 'sentry/views/alerts/rules/uptime/types';
diff --git a/static/app/views/alerts/utils/getMetricRuleDiscoverUrl.tsx b/static/app/views/alerts/utils/getMetricRuleDiscoverUrl.tsx
index 9616c1fa55a1d6..ae1b6766b7ecd8 100644
--- a/static/app/views/alerts/utils/getMetricRuleDiscoverUrl.tsx
+++ b/static/app/views/alerts/utils/getMetricRuleDiscoverUrl.tsx
@@ -1,6 +1,6 @@
import type {NewQuery, Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getAggregateAlias} from 'sentry/utils/discover/fields';
import type {SavedQueryDatasets} from 'sentry/utils/discover/types';
import type {TimePeriodType} from 'sentry/views/alerts/rules/metric/details/constants';
diff --git a/static/app/views/app/index.tsx b/static/app/views/app/index.tsx
index 8904d05be8fee7..7e0137300581bf 100644
--- a/static/app/views/app/index.tsx
+++ b/static/app/views/app/index.tsx
@@ -9,7 +9,7 @@ import {
import {fetchGuides} from 'sentry/actionCreators/guides';
import {fetchOrganizations} from 'sentry/actionCreators/organizations';
import {initApiClientErrorHandling} from 'sentry/api';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {GlobalModal} from 'sentry/components/globalModal';
import Hook from 'sentry/components/hook';
import Indicators from 'sentry/components/indicators';
diff --git a/static/app/views/automations/detail.tsx b/static/app/views/automations/detail.tsx
index 19b9898e9453c6..1f63be36ac67dd 100644
--- a/static/app/views/automations/detail.tsx
+++ b/static/app/views/automations/detail.tsx
@@ -7,7 +7,7 @@ import {Flex} from '@sentry/scraps/layout';
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
import {DateTime} from 'sentry/components/dateTime';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {KeyValueTable, KeyValueTableRow} from 'sentry/components/keyValueTable';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/views/dashboards/create.tsx b/static/app/views/dashboards/create.tsx
index 93467950a9615a..6b97b7d333370c 100644
--- a/static/app/views/dashboards/create.tsx
+++ b/static/app/views/dashboards/create.tsx
@@ -3,7 +3,7 @@ import {useState} from 'react';
import {Alert} from '@sentry/scraps/alert';
import Feature from 'sentry/components/acl/feature';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import * as Layout from 'sentry/components/layouts/thirds';
import {t} from 'sentry/locale';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/dashboards/createFromSeer.tsx b/static/app/views/dashboards/createFromSeer.tsx
index 4eef01b2177a67..b0f7fe4fbca714 100644
--- a/static/app/views/dashboards/createFromSeer.tsx
+++ b/static/app/views/dashboards/createFromSeer.tsx
@@ -5,7 +5,7 @@ import {Alert} from '@sentry/scraps/alert';
import {validateDashboard} from 'sentry/actionCreators/dashboards';
import {addErrorMessage} from 'sentry/actionCreators/indicator';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import * as Layout from 'sentry/components/layouts/thirds';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
@@ -14,6 +14,7 @@ import {fetchMutation, useApiQuery, useQueryClient} from 'sentry/utils/queryClie
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
import {CreateFromSeerLoading} from 'sentry/views/dashboards/createFromSeerLoading';
+import {CreateFromSeerPrompt} from 'sentry/views/dashboards/createFromSeerPrompt';
import type {SeerExplorerResponse} from 'sentry/views/seerExplorer/hooks/useSeerExplorer';
import {makeSeerExplorerQueryKey} from 'sentry/views/seerExplorer/utils';
@@ -286,7 +287,7 @@ export default function CreateFromSeer() {
}
if (!seerRunId) {
- return null;
+ return ;
}
if (isLoading && !isUpdating) {
diff --git a/static/app/views/dashboards/createFromSeerLoading.tsx b/static/app/views/dashboards/createFromSeerLoading.tsx
index 35826f191e3114..aa2c81c2f19f1b 100644
--- a/static/app/views/dashboards/createFromSeerLoading.tsx
+++ b/static/app/views/dashboards/createFromSeerLoading.tsx
@@ -16,7 +16,7 @@ export function CreateFromSeerLoading({blocks, seerRunId}: CreateFromSeerLoading
return (
-
+
{t('Generating Dashboard')}
{t('Stay on this page while we get this made for you')}
diff --git a/static/app/views/dashboards/createFromSeerPrompt.tsx b/static/app/views/dashboards/createFromSeerPrompt.tsx
new file mode 100644
index 00000000000000..7bbc2eb68c4007
--- /dev/null
+++ b/static/app/views/dashboards/createFromSeerPrompt.tsx
@@ -0,0 +1,105 @@
+import {useCallback, useState} from 'react';
+
+import {Button} from '@sentry/scraps/button';
+import {Container, Flex} from '@sentry/scraps/layout';
+import {Heading} from '@sentry/scraps/text';
+import {TextArea} from '@sentry/scraps/textarea';
+
+import {addErrorMessage} from 'sentry/actionCreators/indicator';
+import * as Layout from 'sentry/components/layouts/thirds';
+import {t} from 'sentry/locale';
+import {getApiUrl} from 'sentry/utils/api/getApiUrl';
+import {fetchMutation} from 'sentry/utils/queryClient';
+import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import {useOrganization} from 'sentry/utils/useOrganization';
+
+export function CreateFromSeerPrompt() {
+ const organization = useOrganization();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const [prompt, setPrompt] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+
+ const handleGenerate = useCallback(async () => {
+ if (!prompt.trim()) {
+ return;
+ }
+
+ setIsGenerating(true);
+
+ try {
+ const url = getApiUrl('/organizations/$organizationIdOrSlug/dashboards/generate/', {
+ path: {
+ organizationIdOrSlug: organization.slug,
+ },
+ });
+ const response = await fetchMutation<{run_id: string}>({
+ url,
+ method: 'POST',
+ data: {prompt: prompt.trim()},
+ });
+
+ const runId = response.run_id;
+ if (!runId) {
+ addErrorMessage(t('Failed to start dashboard generation'));
+ setIsGenerating(false);
+ return;
+ }
+
+ navigate(
+ normalizeUrl({
+ pathname: `/organizations/${organization.slug}/dashboards/new/from-seer/`,
+ query: {...location.query, seerRunId: String(runId)},
+ })
+ );
+ } catch (error) {
+ setIsGenerating(false);
+ addErrorMessage(t('Failed to start dashboard generation'));
+ }
+ }, [prompt, organization.slug, location.query, navigate]);
+
+ return (
+
+
+
+ {t('Describe your Dashboard')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/static/app/views/dashboards/dashboard.spec.tsx b/static/app/views/dashboards/dashboard.spec.tsx
index 40d2407a3212a0..5148eb102c8c26 100644
--- a/static/app/views/dashboards/dashboard.spec.tsx
+++ b/static/app/views/dashboards/dashboard.spec.tsx
@@ -19,7 +19,7 @@ import type {DashboardDetails, Widget} from 'sentry/views/dashboards/types';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import {getSavedFiltersAsPageFilters} from 'sentry/views/dashboards/utils';
-import WidgetLegendSelectionState from './widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from './widgetLegendSelectionState';
jest.mock('sentry/components/lazyRender', () => ({
LazyRender: ({children}: {children: React.ReactNode}) => children,
diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx
index 89fa85c68ef3bf..12e4f0534f7916 100644
--- a/static/app/views/dashboards/dashboard.tsx
+++ b/static/app/views/dashboards/dashboard.tsx
@@ -50,7 +50,7 @@ import {SortableWidget} from './sortableWidget';
import type {DashboardDetails, Widget} from './types';
import {DashboardFilterKeys, WidgetType} from './types';
import {connectDashboardCharts, getDashboardFiltersFromURL} from './utils';
-import type WidgetLegendSelectionState from './widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from './widgetLegendSelectionState';
export const DRAG_HANDLE_CLASS = 'widget-drag';
const DRAG_RESIZE_CLASS = 'widget-resize';
diff --git a/static/app/views/dashboards/datasetConfig/errors.spec.tsx b/static/app/views/dashboards/datasetConfig/errors.spec.tsx
index d638341b070286..e8e9f9d9a55478 100644
--- a/static/app/views/dashboards/datasetConfig/errors.spec.tsx
+++ b/static/app/views/dashboards/datasetConfig/errors.spec.tsx
@@ -7,7 +7,7 @@ import {UserFixture} from 'sentry-fixture/user';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import type {EventViewOptions} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {ErrorsConfig} from 'sentry/views/dashboards/datasetConfig/errors';
const theme = ThemeFixture();
diff --git a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.spec.tsx b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.spec.tsx
index b2c11f74777f19..47c45d0d9c6e1f 100644
--- a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.spec.tsx
+++ b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.spec.tsx
@@ -9,7 +9,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
import type {EventViewOptions} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {
getCustomEventsFieldRenderer,
transformEventsResponseToTable,
diff --git a/static/app/views/dashboards/datasetConfig/spans.spec.tsx b/static/app/views/dashboards/datasetConfig/spans.spec.tsx
index 5d261e3bda5fd8..6c3d98a91d4645 100644
--- a/static/app/views/dashboards/datasetConfig/spans.spec.tsx
+++ b/static/app/views/dashboards/datasetConfig/spans.spec.tsx
@@ -13,7 +13,7 @@ import type {
Organization,
} from 'sentry/types/organization';
import type {EventViewOptions} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
import {SpansConfig} from 'sentry/views/dashboards/datasetConfig/spans';
import {DisplayType, type WidgetQuery} from 'sentry/views/dashboards/types';
diff --git a/static/app/views/dashboards/detail.tsx b/static/app/views/dashboards/detail.tsx
index 90c232b54b392c..f093953a3d9c34 100644
--- a/static/app/views/dashboards/detail.tsx
+++ b/static/app/views/dashboards/detail.tsx
@@ -40,7 +40,7 @@ import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {browserHistory} from 'sentry/utils/browserHistory';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
import {MetricsResultsMetaProvider} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
@@ -102,7 +102,7 @@ import type {
Widget,
} from './types';
import {DashboardFilterKeys, DashboardState, MAX_WIDGETS, WidgetType} from './types';
-import WidgetLegendSelectionState from './widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from './widgetLegendSelectionState';
const UNSAVED_MESSAGE = t('You have unsaved changes, are you sure you want to leave?');
@@ -429,12 +429,20 @@ class DashboardDetail extends Component {
onEdit = () => {
const {dashboard, organization} = this.props;
+ const start = performance.now();
trackAnalytics('dashboards2.edit.start', {organization});
this.setState({
dashboardState: DashboardState.EDIT,
modifiedDashboard: cloneDashboard(dashboard),
});
+
+ scheduleMicroTask(() => {
+ const duration = performance.now() - start;
+ Sentry.metrics.distribution('dashboards.dashboard.onEdit', duration, {
+ unit: 'millisecond',
+ });
+ });
};
onDelete = (dashboard: State['modifiedDashboard']) => () => {
@@ -1092,7 +1100,7 @@ class DashboardDetail extends Component {
dashboardState !== DashboardState.CREATE &&
hasUnsavedFilterChanges(dashboard, location);
- const eventView = generatePerformanceEventView(location, projects, {}, organization);
+ const eventView = generatePerformanceEventView(location, projects, {});
const isDashboardUsingTransaction = dashboard.widgets.some(
isWidgetUsingTransactionName
diff --git a/static/app/views/dashboards/index.tsx b/static/app/views/dashboards/index.tsx
index 13d901a651ec53..d6e2757acf7753 100644
--- a/static/app/views/dashboards/index.tsx
+++ b/static/app/views/dashboards/index.tsx
@@ -1,7 +1,7 @@
import {Fragment} from 'react';
import {Outlet} from 'react-router-dom';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {NotFound} from 'sentry/components/errors/notFound';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/dashboards/manage/index.tsx b/static/app/views/dashboards/manage/index.tsx
index cd330ba7d09bdc..53ff00990f0b39 100644
--- a/static/app/views/dashboards/manage/index.tsx
+++ b/static/app/views/dashboards/manage/index.tsx
@@ -16,13 +16,10 @@ import {Switch} from '@sentry/scraps/switch';
import {createDashboard} from 'sentry/actionCreators/dashboards';
import {addLoadingMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
-import {
- openGenerateDashboardFromSeerModal,
- openImportDashboardFromFileModal,
-} from 'sentry/actionCreators/modal';
+import {openImportDashboardFromFileModal} from 'sentry/actionCreators/modal';
import Feature from 'sentry/components/acl/feature';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
import * as Layout from 'sentry/components/layouts/thirds';
import {NoProjectMessage} from 'sentry/components/noProjectMessage';
@@ -597,6 +594,17 @@ function ManageDashboards() {
);
}
+ function onGenerateDashboard() {
+ trackAnalytics('dashboards_manage.generate.start', {
+ organization,
+ });
+ navigate(
+ normalizeUrl({
+ pathname: `/organizations/${organization.slug}/dashboards/new/from-seer/`,
+ })
+ );
+ }
+
return (
onCreate(),
disabled:
hasReachedDashboardLimit ||
@@ -671,16 +679,11 @@ function ManageDashboards() {
key: 'create-dashboard-agent',
label: (
- {t('Create with Agent')}
+ {t('Generate dashboard')}
),
- onAction: () =>
- openGenerateDashboardFromSeerModal({
- organization,
- location,
- navigate,
- }),
+ onAction: () => onGenerateDashboard(),
},
]}
trigger={triggerProps => (
diff --git a/static/app/views/dashboards/sortableWidget.tsx b/static/app/views/dashboards/sortableWidget.tsx
index 70cfe61f91f4cc..6af739810b4459 100644
--- a/static/app/views/dashboards/sortableWidget.tsx
+++ b/static/app/views/dashboards/sortableWidget.tsx
@@ -25,7 +25,7 @@ import {
type Widget,
type WidgetQuery,
} from './types';
-import type WidgetLegendSelectionState from './widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from './widgetLegendSelectionState';
const TABLE_ITEM_LIMIT = 20;
diff --git a/static/app/views/dashboards/utils.tsx b/static/app/views/dashboards/utils.tsx
index f6b3c21091152c..67f9202ce9898b 100644
--- a/static/app/views/dashboards/utils.tsx
+++ b/static/app/views/dashboards/utils.tsx
@@ -22,7 +22,7 @@ import type {Organization} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import {browserHistory} from 'sentry/utils/browserHistory';
import {getUtcDateString} from 'sentry/utils/dates';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DURATION_UNITS} from 'sentry/utils/discover/fieldRenderers';
import {
ABYTE_UNITS,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts
index 6d4ad3d84b22a6..bc306c7a870497 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts
@@ -4,9 +4,9 @@ import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {DisplayType, WidgetType, type Widget} from 'sentry/views/dashboards/types';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {DASHBOARD_TITLE} from 'sentry/views/dashboards/utils/prebuiltConfigs/backendOverview/settings';
+import {BASE_FILTER_STRING} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings';
import {TABLE_MIN_HEIGHT} from 'sentry/views/dashboards/utils/prebuiltConfigs/settings';
import {spaceWidgetsEquallyOnRow} from 'sentry/views/dashboards/utils/prebuiltConfigs/utils/spaceWidgetsEquallyOnRow';
-import {SupportedDatabaseSystem} from 'sentry/views/insights/database/utils/constants';
import {OVERVIEW_PAGE_ALLOWED_OPS} from 'sentry/views/insights/pages/backend/settings';
import {
OVERVIEW_PAGE_ALLOWED_OPS as FRONTEND_OVERVIEW_PAGE_OPS,
@@ -152,7 +152,7 @@ export const BACKEND_OVERVIEW_SECOND_ROW_WIDGETS = spaceWidgetsEquallyOnRow(
aggregates: [`p75(${SpanFields.SPAN_DURATION})`],
columns: [SpanFields.NORMALIZED_DESCRIPTION],
fieldAliases: [''],
- conditions: `${SpanFields.DB_SYSTEM}:[${Object.values(SupportedDatabaseSystem).join(',')}]`,
+ conditions: BASE_FILTER_STRING,
orderby: `-sum(${SpanFields.SPAN_DURATION})`,
linkedDashboards: [
{
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts
index a84797d74e0349..f1a34d35e0c785 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/queries/queries.ts
@@ -1,23 +1,16 @@
import {t} from 'sentry/locale';
import {RATE_UNIT_TITLE, RateUnit} from 'sentry/utils/discover/fields';
import {FieldKind} from 'sentry/utils/fields';
-import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
AVERAGE_DURATION_TEXT,
QUERIES_PER_MINUTE_TEXT,
} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/constants';
+import {BASE_FILTER_STRING} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings';
import {DataTitles} from 'sentry/views/insights/common/views/spans/types';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-export const BASE_FILTERS = {
- [SpanFields.SPAN_CATEGORY]: ModuleName.DB,
- has: SpanFields.NORMALIZED_DESCRIPTION,
-};
-
-const FILTER_STRING = MutableSearch.fromQueryObject(BASE_FILTERS).formatString();
-
export const QUERIES_PREBUILT_CONFIG: PrebuiltDashboard = {
dateCreated: '',
projects: [],
@@ -63,7 +56,7 @@ export const QUERIES_PREBUILT_CONFIG: PrebuiltDashboard = {
queries: [
{
name: QUERIES_PER_MINUTE_TEXT,
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
fields: ['epm()'],
aggregates: ['epm()'],
columns: [],
@@ -87,7 +80,7 @@ export const QUERIES_PREBUILT_CONFIG: PrebuiltDashboard = {
queries: [
{
name: AVERAGE_DURATION_TEXT,
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
fields: [`avg(${SpanFields.SPAN_DURATION})`],
aggregates: [`avg(${SpanFields.SPAN_DURATION})`],
columns: [],
@@ -112,7 +105,7 @@ export const QUERIES_PREBUILT_CONFIG: PrebuiltDashboard = {
queries: [
{
name: '',
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
fields: [
SpanFields.NORMALIZED_DESCRIPTION,
'epm()',
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts
index fd20801da63d7a..c50b2665ab3171 100644
--- a/static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/queries/querySummary.ts
@@ -1,17 +1,14 @@
import {t} from 'sentry/locale';
import {FieldKind} from 'sentry/utils/fields';
-import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import type {PrebuiltDashboard} from 'sentry/views/dashboards/utils/prebuiltConfigs';
import {
AVERAGE_DURATION_TEXT,
QUERIES_PER_MINUTE_TEXT,
} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/constants';
-import {BASE_FILTERS} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/queries';
+import {BASE_FILTER_STRING} from 'sentry/views/dashboards/utils/prebuiltConfigs/queries/settings';
import {ModuleName, SpanFields} from 'sentry/views/insights/types';
-const FILTER_STRING = MutableSearch.fromQueryObject(BASE_FILTERS).formatString();
-
export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
dateCreated: '',
projects: [],
@@ -43,7 +40,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
fields: ['epm()'],
aggregates: ['epm()'],
columns: [],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: '',
isHidden: false,
},
@@ -64,7 +61,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
fields: [`avg(${SpanFields.SPAN_DURATION})`],
aggregates: [`avg(${SpanFields.SPAN_DURATION})`],
columns: [],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: '',
isHidden: false,
},
@@ -85,7 +82,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
fields: [`sum(${SpanFields.SPAN_DURATION})`],
aggregates: [`sum(${SpanFields.SPAN_DURATION})`],
columns: [],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: '',
isHidden: false,
},
@@ -107,7 +104,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
aggregates: [],
columns: ['id', 'span.op', 'span.group', 'span.description', 'span.category'],
fieldAliases: [],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: 'id',
onDemand: [],
linkedDashboards: [],
@@ -131,7 +128,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
aggregates: ['epm()', `sum(${SpanFields.SPAN_DURATION})`],
columns: [SpanFields.TRANSACTION],
fieldAliases: [t('Found In'), t('Queries Per Minute'), t('Time Spent')],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: `-sum(${SpanFields.SPAN_DURATION})`,
onDemand: [],
isHidden: false,
@@ -155,7 +152,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
aggregates: ['epm()'],
columns: [],
fieldAliases: [],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: 'epm()',
onDemand: [],
isHidden: false,
@@ -180,7 +177,7 @@ export const QUERIES_SUMMARY_PREBUILT_CONFIG: PrebuiltDashboard = {
aggregates: [`avg(${SpanFields.SPAN_DURATION})`],
columns: [],
fieldAliases: [],
- conditions: FILTER_STRING,
+ conditions: BASE_FILTER_STRING,
orderby: `avg(${SpanFields.SPAN_DURATION})`,
onDemand: [],
isHidden: false,
diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts b/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts
new file mode 100644
index 00000000000000..ab2adbd97636f4
--- /dev/null
+++ b/static/app/views/dashboards/utils/prebuiltConfigs/queries/settings.ts
@@ -0,0 +1,10 @@
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {ModuleName, SpanFields} from 'sentry/views/insights/types';
+
+const BASE_FILTERS = {
+ [SpanFields.SPAN_CATEGORY]: ModuleName.DB,
+ has: SpanFields.NORMALIZED_DESCRIPTION,
+};
+
+export const BASE_FILTER_STRING =
+ MutableSearch.fromQueryObject(BASE_FILTERS).formatString();
diff --git a/static/app/views/dashboards/utils/usePrebuiltDashboardUrl.tsx b/static/app/views/dashboards/utils/usePrebuiltDashboardUrl.tsx
new file mode 100644
index 00000000000000..d1e3086fc2fbb7
--- /dev/null
+++ b/static/app/views/dashboards/utils/usePrebuiltDashboardUrl.tsx
@@ -0,0 +1,65 @@
+import * as qs from 'query-string';
+
+import {pageFiltersToQueryParams} from 'sentry/components/pageFilters/parse';
+import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
+import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
+import {useOrganization} from 'sentry/utils/useOrganization';
+import type {DashboardFilters, GlobalFilter} from 'sentry/views/dashboards/types';
+import {PrebuiltDashboardId} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {useGetPrebuiltDashboard} from 'sentry/views/dashboards/utils/usePopulateLinkedDashboards';
+import {hasPlatformizedInsights} from 'sentry/views/insights/common/utils/useHasPlatformizedInsights';
+
+export interface PrebuiltDashboardUrlOptions {
+ bare?: boolean;
+ /**
+ * Dashboard-specific filters (release, global filters) to apply
+ * when navigating to a prebuilt dashboard URL.
+ * Only applied to dashboard URLs, not module URL fallbacks.
+ */
+ filters?: DashboardFilters;
+}
+
+function applyDashboardFilters(
+ queryParams: Record,
+ filters?: DashboardFilters
+) {
+ if (filters?.release?.length) {
+ queryParams.release = filters.release;
+ }
+ if (filters?.globalFilter?.length) {
+ queryParams.globalFilter = filters.globalFilter.map(filter =>
+ JSON.stringify({...filter, isTemporary: true} satisfies GlobalFilter)
+ );
+ }
+}
+
+export function usePrebuiltDashboardUrl(
+ prebuiltId: PrebuiltDashboardId,
+ options: PrebuiltDashboardUrlOptions = {}
+): string | undefined {
+ const {bare = false, filters} = options;
+ const organization = useOrganization({allowNull: true});
+ const {selection} = usePageFilters();
+ const isPlatformized = organization ? hasPlatformizedInsights(organization) : false;
+ const {dashboard: prebuiltDashboard} = useGetPrebuiltDashboard(
+ isPlatformized ? prebuiltId : undefined
+ );
+
+ const queryParams = pageFiltersToQueryParams(selection);
+
+ if (!organization) {
+ return undefined;
+ }
+
+ const {slug} = organization;
+
+ if (isPlatformized && prebuiltDashboard.id) {
+ applyDashboardFilters(queryParams, filters);
+ const query = Object.keys(queryParams).length ? `?${qs.stringify(queryParams)}` : '';
+ return bare
+ ? `dashboard/${prebuiltDashboard.id}/${query}`
+ : normalizeUrl(`/organizations/${slug}/dashboard/${prebuiltDashboard.id}/${query}`);
+ }
+
+ return undefined;
+}
diff --git a/static/app/views/dashboards/view.tsx b/static/app/views/dashboards/view.tsx
index eb5ba3f8066e00..77a79023d2395b 100644
--- a/static/app/views/dashboards/view.tsx
+++ b/static/app/views/dashboards/view.tsx
@@ -4,7 +4,7 @@ import {Alert} from '@sentry/scraps/alert';
import {updateDashboardVisit} from 'sentry/actionCreators/dashboards';
import Feature from 'sentry/components/acl/feature';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {NotFound} from 'sentry/components/errors/notFound';
import * as Layout from 'sentry/components/layouts/thirds';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/views/dashboards/widgetBuilder/components/datasetSelector.tsx b/static/app/views/dashboards/widgetBuilder/components/datasetSelector.tsx
index 7edba8cc9deb02..ec7323589be034 100644
--- a/static/app/views/dashboards/widgetBuilder/components/datasetSelector.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/datasetSelector.tsx
@@ -4,7 +4,7 @@ import * as Sentry from '@sentry/react';
import {CompactSelect} from '@sentry/scraps/compactSelect';
-import ExternalLink from 'sentry/components/links/externalLink';
+import {ExternalLink} from 'sentry/components/links/externalLink';
import {t, tct, tctCode} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
diff --git a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
index 8ea4b28f8423f2..3434106587d9d2 100644
--- a/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/newWidgetBuilder.tsx
@@ -17,7 +17,7 @@ import {Flex} from '@sentry/scraps/layout';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {useDimensions} from 'sentry/utils/useDimensions';
diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx
index cefa373b20479e..b17494c071277b 100644
--- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx
@@ -18,7 +18,7 @@ import {SlideOverPanel} from '@sentry/scraps/slideOverPanel';
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
import {openConfirmModal} from 'sentry/components/confirm';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {Placeholder} from 'sentry/components/placeholder';
import {IconClose} from 'sentry/icons';
import {t, tctCode} from 'sentry/locale';
diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx
index 4f20daa8b3ae48..5f4909a8c4b49f 100644
--- a/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx
+++ b/static/app/views/dashboards/widgetBuilder/components/widgetPreview.tsx
@@ -20,7 +20,7 @@ import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder
import type {OnDataFetchedParams} from 'sentry/views/dashboards/widgetCard';
import WidgetCard from 'sentry/views/dashboards/widgetCard';
import {WidgetLegendNameEncoderDecoder} from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
-import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
interface WidgetPreviewProps {
diff --git a/static/app/views/dashboards/widgetCard/chart.tsx b/static/app/views/dashboards/widgetCard/chart.tsx
index 0dd6def32bb091..b7de3f86229c70 100644
--- a/static/app/views/dashboards/widgetCard/chart.tsx
+++ b/static/app/views/dashboards/widgetCard/chart.tsx
@@ -12,7 +12,7 @@ import {getFormatter} from 'sentry/components/charts/components/tooltip';
import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import {LineChart} from 'sentry/components/charts/lineChart';
import ReleaseSeries from 'sentry/components/charts/releaseSeries';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {getSeriesSelection, isChartHovered} from 'sentry/components/charts/utils';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
@@ -72,7 +72,7 @@ import {getBucketSize} from 'sentry/views/dashboards/utils/getBucketSize';
import {getWidgetTableRowExploreUrlFunction} from 'sentry/views/dashboards/utils/getWidgetExploreUrl';
import {getSelectedAggregateIndex} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';
import {WidgetLegendNameEncoderDecoder} from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
-import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import {AgentsTracesTableWidgetVisualization} from 'sentry/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization';
import {BigNumberWidgetVisualization} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization';
import {CategoricalSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/categoricalSeriesWidget/categoricalSeriesWidgetVisualization';
diff --git a/static/app/views/dashboards/widgetCard/index.spec.tsx b/static/app/views/dashboards/widgetCard/index.spec.tsx
index 94fb2ca11196f1..29c2e5e85d3cef 100644
--- a/static/app/views/dashboards/widgetCard/index.spec.tsx
+++ b/static/app/views/dashboards/widgetCard/index.spec.tsx
@@ -25,7 +25,7 @@ import {
} from 'sentry/views/dashboards/types';
import WidgetCard from 'sentry/views/dashboards/widgetCard';
import {ReleaseWidgetQueries} from 'sentry/views/dashboards/widgetCard/releaseWidgetQueries';
-import WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import {TableWidgetVisualization} from 'sentry/views/dashboards/widgets/tableWidget/tableWidgetVisualization';
import {DashboardsMEPProvider} from './dashboardsMEPContext';
diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx
index 5b3af83e7c6f43..f2186e76a10c06 100644
--- a/static/app/views/dashboards/widgetCard/index.tsx
+++ b/static/app/views/dashboards/widgetCard/index.tsx
@@ -7,7 +7,7 @@ import omit from 'lodash/omit';
import {openWidgetViewerModal} from 'sentry/actionCreators/modal';
import type {Client} from 'sentry/api';
import {DateTime} from 'sentry/components/dateTime';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
isWidgetViewerPath,
WidgetViewerQueryField,
@@ -49,7 +49,7 @@ import {
} from 'sentry/views/dashboards/types';
import {widgetCanUseTimeSeriesVisualization} from 'sentry/views/dashboards/utils/widgetCanUseTimeSeriesVisualization';
import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widgetCardChartContainer';
-import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
diff --git a/static/app/views/dashboards/widgetCard/matchTimeSeriesToTableRowValue.spec.tsx b/static/app/views/dashboards/widgetCard/matchTimeSeriesToTableRowValue.spec.tsx
index a1940aa4ff8712..fd6f213f92122a 100644
--- a/static/app/views/dashboards/widgetCard/matchTimeSeriesToTableRowValue.spec.tsx
+++ b/static/app/views/dashboards/widgetCard/matchTimeSeriesToTableRowValue.spec.tsx
@@ -82,6 +82,21 @@ describe('matchTimeSeriesToTableRowValue', () => {
expect(result).toBe(50);
});
+ it('matches numeric table values to numeric groupBy values', () => {
+ const result = matchTimeSeriesToTableRowValue({
+ tableDataRows: [
+ {id: '1', 'http.response_status_code': 200, 'count()': 50},
+ {id: '2', 'http.response_status_code': 404, 'count()': 3},
+ ],
+ timeSeries: TimeSeriesFixture({
+ yAxis: 'count()',
+ groupBy: [{key: 'http.response_status_code', value: 200}],
+ }),
+ });
+
+ expect(result).toBe(50);
+ });
+
it('matches array groupBy values using Python str() format', () => {
const result = matchTimeSeriesToTableRowValue({
tableDataRows: [
diff --git a/static/app/views/dashboards/widgetCard/visualizationWidget.tsx b/static/app/views/dashboards/widgetCard/visualizationWidget.tsx
index 8f7782deae18c6..d43e2cd2fcec6b 100644
--- a/static/app/views/dashboards/widgetCard/visualizationWidget.tsx
+++ b/static/app/views/dashboards/widgetCard/visualizationWidget.tsx
@@ -31,14 +31,17 @@ import {
import {getChartType} from 'sentry/views/dashboards/utils/getWidgetExploreUrl';
import {matchTimeSeriesToTableRowValue} from 'sentry/views/dashboards/widgetCard/matchTimeSeriesToTableRowValue';
import {transformWidgetSeriesToTimeSeries} from 'sentry/views/dashboards/widgetCard/transformWidgetSeriesToTimeSeries';
-import {MISSING_DATA_MESSAGE} from 'sentry/views/dashboards/widgets/common/settings';
+import {
+ MISSING_DATA_MESSAGE,
+ NUMBER_MIN_VALUE,
+} from 'sentry/views/dashboards/widgets/common/settings';
import type {
LegendSelection,
TabularColumn,
TimeSeries,
TimeSeriesGroupBy,
} from 'sentry/views/dashboards/widgets/common/types';
-import {formatTooltipValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue';
+import {formatBreakdownLegendValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatBreakdownLegendValue';
import {createPlottableFromTimeSeries} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/createPlottableFromTimeSeries';
import type {Plottable} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/plottable';
import {Thresholds} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/thresholds';
@@ -359,7 +362,16 @@ function VisualizationWidgetContent({
{labelContent}
- {value === null ? '—' : formatTooltipValue(value, dataType, dataUnit)}
+ {dataType === 'number' &&
+ value !== null &&
+ value > 0 &&
+ value < NUMBER_MIN_VALUE ? (
+
+ {formatBreakdownLegendValue(value, dataType, dataUnit)}
+
+ ) : (
+ formatBreakdownLegendValue(value, dataType, dataUnit)
+ )}
);
diff --git a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx
index 42738a7cc43e1c..a5d48de2727bb7 100644
--- a/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx
+++ b/static/app/views/dashboards/widgetCard/widgetCardChartContainer.tsx
@@ -18,7 +18,7 @@ import type {DashboardFilters, Widget as TWidget} from 'sentry/views/dashboards/
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import {usesTimeSeriesData, widgetFetchesOwnData} from 'sentry/views/dashboards/utils';
import {WidgetLegendNameEncoderDecoder} from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
-import type WidgetLegendSelectionState from 'sentry/views/dashboards/widgetLegendSelectionState';
+import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
diff --git a/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx b/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx
index 1ddf951eeda5b0..1e7e4b824db3d0 100644
--- a/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx
+++ b/static/app/views/dashboards/widgetCard/widgetCardContextMenu.tsx
@@ -306,7 +306,7 @@ export function getMenuOptions(
const search = new MutableSearch(baseQuery);
for (const group of timeSeries.groupBy ?? []) {
if (group.value !== null && !Array.isArray(group.value)) {
- search.addFilterValue(group.key, group.value);
+ search.addFilterValue(group.key, String(group.value));
}
}
diff --git a/static/app/views/dashboards/widgetLegendSelectionState.spec.tsx b/static/app/views/dashboards/widgetLegendSelectionState.spec.tsx
index 24da8df7b394c9..2d64c4e5ba7df9 100644
--- a/static/app/views/dashboards/widgetLegendSelectionState.spec.tsx
+++ b/static/app/views/dashboards/widgetLegendSelectionState.spec.tsx
@@ -7,7 +7,7 @@ import type {Organization} from 'sentry/types/organization';
import type {DashboardDetails, Widget} from 'sentry/views/dashboards/types';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
-import WidgetLegendSelectionState from './widgetLegendSelectionState';
+import {WidgetLegendSelectionState} from './widgetLegendSelectionState';
const WIDGET_ID_DELIMITER = ':';
const SERIES_NAME_DELIMITER = '|~|';
diff --git a/static/app/views/dashboards/widgetLegendSelectionState.tsx b/static/app/views/dashboards/widgetLegendSelectionState.tsx
index 5404b6936a095d..88dd694fbacd33 100644
--- a/static/app/views/dashboards/widgetLegendSelectionState.tsx
+++ b/static/app/views/dashboards/widgetLegendSelectionState.tsx
@@ -21,7 +21,7 @@ const WIDGET_ID_DELIMITER = ':';
const SERIES_NAME_DELIMITER = '|~|';
-class WidgetLegendSelectionState {
+export class WidgetLegendSelectionState {
dashboard: DashboardDetails | null;
location: Location;
organization: Organization;
@@ -270,5 +270,3 @@ class WidgetLegendSelectionState {
return unselectedSeries;
}
}
-
-export default WidgetLegendSelectionState;
diff --git a/static/app/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization.tsx b/static/app/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization.tsx
index a6366b25156a2f..4df837ab9af181 100644
--- a/static/app/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization.tsx
+++ b/static/app/views/dashboards/widgets/agentsTracesTableWidget/agentsTracesTableWidgetVisualization.tsx
@@ -1,6 +1,5 @@
import type {DashboardFilters} from 'sentry/views/dashboards/types';
import {DEFAULT_TRACES_TABLE_WIDTHS} from 'sentry/views/dashboards/utils/prebuiltConfigs/ai/aiAgentsOverview';
-import {useTraceViewDrawer} from 'sentry/views/insights/pages/agents/components/drawer';
import {TracesTable} from 'sentry/views/insights/pages/agents/components/tracesTable';
interface AgentsTracesTableWidgetVisualizationProps {
@@ -16,15 +15,13 @@ export function AgentsTracesTableWidgetVisualization({
dashboardFilters,
frameless,
}: AgentsTracesTableWidgetVisualizationProps) {
- const {openTraceViewDrawer} = useTraceViewDrawer();
-
return (
);
}
diff --git a/static/app/views/dashboards/widgets/categoricalSeriesWidget/formatters/formatCategoricalSeriesLabel.tsx b/static/app/views/dashboards/widgets/categoricalSeriesWidget/formatters/formatCategoricalSeriesLabel.tsx
index 980331beaf111d..0ef06c7ab2b4f1 100644
--- a/static/app/views/dashboards/widgets/categoricalSeriesWidget/formatters/formatCategoricalSeriesLabel.tsx
+++ b/static/app/views/dashboards/widgets/categoricalSeriesWidget/formatters/formatCategoricalSeriesLabel.tsx
@@ -19,7 +19,7 @@ export function formatCategoricalSeriesLabel(series: CategoricalSeries): string
return t('(no value)');
}
- return groupBy.value;
+ return String(groupBy.value);
})
.join(',');
}
diff --git a/static/app/views/dashboards/widgets/common/settings.tsx b/static/app/views/dashboards/widgets/common/settings.tsx
index 1a647c475a2142..00d749e2e9e3d6 100644
--- a/static/app/views/dashboards/widgets/common/settings.tsx
+++ b/static/app/views/dashboards/widgets/common/settings.tsx
@@ -28,6 +28,7 @@ export const NON_FINITE_NUMBER_MESSAGE = t('Value is not a finite number.');
// Currently we lose precision for actual number fields because we
// can't distinguish them from percentage-like fields that need capping.
export const NUMBER_MAX_FRACTION_DIGITS = 4;
+export const NUMBER_MIN_VALUE = 10 ** -NUMBER_MAX_FRACTION_DIGITS;
export const ALLOWED_CELL_ACTIONS = [
Actions.OPEN_INTERNAL_LINK,
diff --git a/static/app/views/dashboards/widgets/common/types.tsx b/static/app/views/dashboards/widgets/common/types.tsx
index 4fa5264054a551..462144a3acbc83 100644
--- a/static/app/views/dashboards/widgets/common/types.tsx
+++ b/static/app/views/dashboards/widgets/common/types.tsx
@@ -78,7 +78,7 @@ type IncompleteReason = 'INCOMPLETE_BUCKET';
*/
type GroupBy = {
key: string;
- value: string | null | Array | Array;
+ value: string | number | boolean | null | Array | Array;
};
// Aliases - allows divergence later if unique cases arise
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatBreakdownLegendValue.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatBreakdownLegendValue.tsx
new file mode 100644
index 00000000000000..6a7aeddd73e29f
--- /dev/null
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatBreakdownLegendValue.tsx
@@ -0,0 +1,33 @@
+import {ECHARTS_MISSING_DATA_VALUE} from 'sentry/utils/timeSeries/timeSeriesItemToEChartsDataPoint';
+import {NUMBER_MIN_VALUE} from 'sentry/views/dashboards/widgets/common/settings';
+
+import {formatTooltipValue} from './formatTooltipValue';
+
+/**
+ * Format a value for the breakdown legend table under a chart.
+ *
+ * Similar to formatTooltipValue, but purpose-built for the legend context.
+ * For small "number" values, shows a threshold indicator (e.g. "<0.0001")
+ * rather than the full precision shown in chart tooltips.
+ * Also handles null values, which are displayed as an em dash.
+ */
+export function formatBreakdownLegendValue(
+ value: number | null | typeof ECHARTS_MISSING_DATA_VALUE,
+ type: string,
+ unit?: string
+): string {
+ if (value === null) {
+ return '—';
+ }
+
+ if (
+ type === 'number' &&
+ typeof value === 'number' &&
+ value > 0 &&
+ value < NUMBER_MIN_VALUE
+ ) {
+ return `<${NUMBER_MIN_VALUE}`;
+ }
+
+ return formatTooltipValue(value, type, unit);
+}
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx
index 60e3c26c6ed3e7..8897947291d8ed 100644
--- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx
@@ -19,7 +19,7 @@ export function formatTimeSeriesLabel(timeSeries: TimeSeries): string {
return JSON.stringify(groupBy.value);
}
- if (groupBy.key === 'release' && groupBy.value) {
+ if (groupBy.key === 'release' && typeof groupBy.value === 'string') {
return formatVersion(groupBy.value);
}
@@ -27,7 +27,7 @@ export function formatTimeSeriesLabel(timeSeries: TimeSeries): string {
return t('(no value)');
}
- return groupBy.value;
+ return String(groupBy.value);
})
.join(',');
}
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.spec.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.spec.tsx
index 8fe367f7bbc818..4c7ec96079cec0 100644
--- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.spec.tsx
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.spec.tsx
@@ -16,8 +16,8 @@ describe('formatTooltipValue', () => {
describe('number', () => {
it.each([
- [0.000033452, '0'],
- [0.00003, '0'],
+ [0.000033452, '0.00003345'],
+ [0.00003, '0.00003'],
[0.001234, '0.0012'],
[17.1238, '17.1238'],
[170, '170'],
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.tsx
index 1fb84d32eb3cce..3a5bfe55dc8225 100644
--- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.tsx
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue.tsx
@@ -12,7 +12,10 @@ import {formatPercentage} from 'sentry/utils/number/formatPercentage';
import {ECHARTS_MISSING_DATA_VALUE} from 'sentry/utils/timeSeries/timeSeriesItemToEChartsDataPoint';
import {convertDuration} from 'sentry/utils/unitConversion/convertDuration';
import {convertSize} from 'sentry/utils/unitConversion/convertSize';
-import {NUMBER_MAX_FRACTION_DIGITS} from 'sentry/views/dashboards/widgets/common/settings';
+import {
+ NUMBER_MAX_FRACTION_DIGITS,
+ NUMBER_MIN_VALUE,
+} from 'sentry/views/dashboards/widgets/common/settings';
import {
isADurationUnit,
isASizeUnit,
@@ -36,7 +39,15 @@ export function formatTooltipValue(
switch (type) {
case 'integer':
+ return value.toLocaleString(undefined, {
+ maximumFractionDigits: NUMBER_MAX_FRACTION_DIGITS,
+ });
case 'number':
+ if (value > 0 && value < NUMBER_MIN_VALUE) {
+ return value.toLocaleString(undefined, {
+ maximumSignificantDigits: NUMBER_MAX_FRACTION_DIGITS,
+ });
+ }
return value.toLocaleString(undefined, {
maximumFractionDigits: NUMBER_MAX_FRACTION_DIGITS,
});
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx
index 1d3ceef6da6926..7e3c2f4b8500f4 100644
--- a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx
@@ -172,16 +172,15 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
const plottablesByType = groupBy(props.plottables, plottable => plottable.dataType);
- // Count up the field types of all the plottables
- const fieldTypeCounts = mapValues(plottablesByType, plottables => plottables.length);
-
- // Sort the field types by how many plottables use each one
- const axisTypes = Object.keys(fieldTypeCounts)
- .toSorted(
- // `dataTypes` is extracted from `dataTypeCounts`, so the counts are guaranteed to exist
- (a, b) => fieldTypeCounts[b]! - fieldTypeCounts[a]!
- )
- .filter(axisType => !!axisType); // `TimeSeries` allows for a `null` data type , though it's not likely
+ // Get unique axis types in order of first appearance, treating the first
+ // aggregate as primary. This avoids axis flipping when thresholds or other
+ // plottables inflate the count of a particular data type.
+ const axisTypes: string[] = [];
+ for (const plottable of props.plottables) {
+ if (plottable.dataType && !axisTypes.includes(plottable.dataType)) {
+ axisTypes.push(plottable.dataType);
+ }
+ }
// Partition the types between the two axes
let leftYAxisDataTypes: string[] = [];
@@ -195,11 +194,11 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
leftYAxisDataTypes = axisTypes.slice(0, 1);
rightYAxisDataTypes = axisTypes.slice(1, 2);
} else if (axisTypes.length > 2 && axisTypes.at(0) === FALLBACK_TYPE) {
- // There are multiple types, and the most popular one is the fallback. Don't
+ // There are multiple types, and the first one is the fallback. Don't
// bother creating a second fallback axis, plot everything on the left
leftYAxisDataTypes = axisTypes;
} else {
- // There are multiple types. Assign the most popular type to the left axis,
+ // There are multiple types. Assign the first type to the left axis,
// the rest to the right axis
leftYAxisDataTypes = axisTypes.slice(0, 1);
rightYAxisDataTypes = axisTypes.slice(1);
@@ -381,7 +380,9 @@ export function TimeSeriesWidgetVisualization(props: TimeSeriesWidgetVisualizati
const fieldType = correspondingPlottable?.dataType ?? FALLBACK_TYPE;
- return formatTooltipValue(value, fieldType, unitForType[fieldType] ?? undefined);
+ return escape(
+ formatTooltipValue(value, fieldType, unitForType[fieldType] ?? undefined)
+ );
},
truncate: false,
utc: utc ?? false,
diff --git a/static/app/views/dashboards/widgets/widget/widget.tsx b/static/app/views/dashboards/widgets/widget/widget.tsx
index 4505edc0c917dc..393d2f7ad89df1 100644
--- a/static/app/views/dashboards/widgets/widget/widget.tsx
+++ b/static/app/views/dashboards/widgets/widget/widget.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import {Container, Flex} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {defined} from 'sentry/utils';
import {MIN_HEIGHT, MIN_WIDTH} from 'sentry/views/dashboards/widgets/common/settings';
diff --git a/static/app/views/detectors/components/details/common/automations.tsx b/static/app/views/detectors/components/details/common/automations.tsx
index f32be03a250349..73ebcc26931249 100644
--- a/static/app/views/detectors/components/details/common/automations.tsx
+++ b/static/app/views/detectors/components/details/common/automations.tsx
@@ -7,7 +7,7 @@ import {Link} from '@sentry/scraps/link';
import {Tooltip} from '@sentry/scraps/tooltip';
import {addLoadingMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {useDrawer} from 'sentry/components/globalDrawer';
import {LoadingError} from 'sentry/components/loadingError';
import {Pagination} from 'sentry/components/pagination';
diff --git a/static/app/views/detectors/components/details/common/ongoingIssues.tsx b/static/app/views/detectors/components/details/common/ongoingIssues.tsx
index 3726aa1974d915..33b75319de9e63 100644
--- a/static/app/views/detectors/components/details/common/ongoingIssues.tsx
+++ b/static/app/views/detectors/components/details/common/ongoingIssues.tsx
@@ -1,6 +1,6 @@
import {LinkButton} from '@sentry/scraps/button';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {GroupList} from 'sentry/components/issues/groupList';
import {Section} from 'sentry/components/workflowEngine/ui/section';
import {t} from 'sentry/locale';
diff --git a/static/app/views/detectors/components/details/common/openPeriodIssues.tsx b/static/app/views/detectors/components/details/common/openPeriodIssues.tsx
index 0258f37d37cc1d..11d22a2e8b37c0 100644
--- a/static/app/views/detectors/components/details/common/openPeriodIssues.tsx
+++ b/static/app/views/detectors/components/details/common/openPeriodIssues.tsx
@@ -7,7 +7,7 @@ import {Text} from '@sentry/scraps/text';
import {DateTime} from 'sentry/components/dateTime';
import {Duration} from 'sentry/components/duration';
import {EmptyStateWarning} from 'sentry/components/emptyStateWarning';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
AssigneeSelector,
useHandleAssigneeChange,
diff --git a/static/app/views/detectors/components/details/cron/index.tsx b/static/app/views/detectors/components/details/cron/index.tsx
index b1a3f4f8994c41..48e0b653a9c973 100644
--- a/static/app/views/detectors/components/details/cron/index.tsx
+++ b/static/app/views/detectors/components/details/cron/index.tsx
@@ -7,7 +7,7 @@ import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {useDrawer} from 'sentry/components/globalDrawer';
import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
import {KeyValueTableRow} from 'sentry/components/keyValueTable';
diff --git a/static/app/views/detectors/components/details/fallback.tsx b/static/app/views/detectors/components/details/fallback.tsx
index 9d52761e47b394..dea5b6c533403f 100644
--- a/static/app/views/detectors/components/details/fallback.tsx
+++ b/static/app/views/detectors/components/details/fallback.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {DetailLayout} from 'sentry/components/workflowEngine/layout/detail';
import type {Project} from 'sentry/types/project';
import type {Detector} from 'sentry/types/workflowEngine/detectors';
diff --git a/static/app/views/detectors/components/details/metric/getDetectorOpenInDestination.tsx b/static/app/views/detectors/components/details/metric/getDetectorOpenInDestination.tsx
index 416fce8f7778bb..d04ae272c6f8f7 100644
--- a/static/app/views/detectors/components/details/metric/getDetectorOpenInDestination.tsx
+++ b/static/app/views/detectors/components/details/metric/getDetectorOpenInDestination.tsx
@@ -4,7 +4,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {SnubaQuery} from 'sentry/types/workflowEngine/detectors';
import {defined} from 'sentry/utils';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getAggregateAlias, parseFunction} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
diff --git a/static/app/views/detectors/components/details/metric/index.tsx b/static/app/views/detectors/components/details/metric/index.tsx
index 6c1289c965b237..45c23628c6e105 100644
--- a/static/app/views/detectors/components/details/metric/index.tsx
+++ b/static/app/views/detectors/components/details/metric/index.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {DetailLayout} from 'sentry/components/workflowEngine/layout/detail';
import {t} from 'sentry/locale';
import type {Project} from 'sentry/types/project';
diff --git a/static/app/views/detectors/components/details/metric/sidebar.tsx b/static/app/views/detectors/components/details/metric/sidebar.tsx
index 5b251048e4bbfc..c5bbfea5880e0a 100644
--- a/static/app/views/detectors/components/details/metric/sidebar.tsx
+++ b/static/app/views/detectors/components/details/metric/sidebar.tsx
@@ -3,7 +3,7 @@ import {Link} from 'react-router-dom';
import {Tooltip} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {Section} from 'sentry/components/workflowEngine/ui/section';
import {t} from 'sentry/locale';
import type {MetricDetector} from 'sentry/types/workflowEngine/detectors';
diff --git a/static/app/views/detectors/components/details/mobileBuild/index.tsx b/static/app/views/detectors/components/details/mobileBuild/index.tsx
index c968a157240149..69f2fcab22c9bf 100644
--- a/static/app/views/detectors/components/details/mobileBuild/index.tsx
+++ b/static/app/views/detectors/components/details/mobileBuild/index.tsx
@@ -1,4 +1,4 @@
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {DetailLayout} from 'sentry/components/workflowEngine/layout/detail';
import {t} from 'sentry/locale';
import type {Project} from 'sentry/types/project';
diff --git a/static/app/views/detectors/components/details/mobileBuild/sidebar.tsx b/static/app/views/detectors/components/details/mobileBuild/sidebar.tsx
index e969c8c709bd25..e7cc965febc072 100644
--- a/static/app/views/detectors/components/details/mobileBuild/sidebar.tsx
+++ b/static/app/views/detectors/components/details/mobileBuild/sidebar.tsx
@@ -1,6 +1,6 @@
import {Fragment} from 'react';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {Section} from 'sentry/components/workflowEngine/ui/section';
import {t} from 'sentry/locale';
import type {PreprodDetector} from 'sentry/types/workflowEngine/detectors';
diff --git a/static/app/views/detectors/components/detectorLink.tsx b/static/app/views/detectors/components/detectorLink.tsx
index 0404116762bdb0..c3cdb03a011eb1 100644
--- a/static/app/views/detectors/components/detectorLink.tsx
+++ b/static/app/views/detectors/components/detectorLink.tsx
@@ -3,7 +3,7 @@ import {css} from '@emotion/react';
import styled from '@emotion/styled';
import pick from 'lodash/pick';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import {TitleCell} from 'sentry/components/workflowEngine/gridCell/titleCell';
import {t, tct} from 'sentry/locale';
diff --git a/static/app/views/detectors/components/forms/common/detectorIssuePreview.tsx b/static/app/views/detectors/components/forms/common/detectorIssuePreview.tsx
index 70cdb7def3956c..2ac92d5e510b24 100644
--- a/static/app/views/detectors/components/forms/common/detectorIssuePreview.tsx
+++ b/static/app/views/detectors/components/forms/common/detectorIssuePreview.tsx
@@ -1,19 +1,18 @@
-import {useTheme} from '@emotion/react';
-
-import {Flex, Grid} from '@sentry/scraps/layout';
+import {ActorAvatar} from '@sentry/scraps/avatar';
+import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
-import {ErrorLevel} from 'sentry/components/events/errorLevel';
import {ShortId} from 'sentry/components/group/inboxBadges/shortId';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
-import {Container} from 'sentry/components/workflowEngine/ui/container';
-import {Section} from 'sentry/components/workflowEngine/ui/section';
+import {SimpleTable} from 'sentry/components/tables/simpleTable';
import {t} from 'sentry/locale';
+import type {Actor} from 'sentry/types/core';
import type {AvatarProject} from 'sentry/types/project';
interface DetectorIssuePreviewProps {
issueTitle: string;
subtitle: string;
+ assignee?: Actor;
project?: AvatarProject;
}
@@ -21,77 +20,44 @@ export function DetectorIssuePreview({
issueTitle,
project,
subtitle,
+ assignee,
}: DetectorIssuePreviewProps) {
- const theme = useTheme();
const projectSlug = project?.slug ?? 'project';
const shortId = `${projectSlug.toUpperCase()}-D3M0`;
- return (
-
-
-
-
-
- {t('Issue')}
-
-
-
- {t('Last Seen')}
-
-
- {t('Age')}
-
-
- {t('Events')}
-
-
- {t('Users')}
-
-
- {t('Assignee')}
-
-
-
- {issueTitle}
-
-
- {subtitle}
-
-
- {project && }
-
-
-
-
+ return (
+
+
+ {t('Issue')}
+ {t('Last Seen')}
+ {t('Age')}
+ {t('Events')}
+ {t('Users')}
+ {t('Assignee')}
+
+
+
+
+
+ {issueTitle}
+
+ {subtitle}
+
+ {project && }
+
+
+
- 4hr ago
- 2min
- 1
- 1.2k
-
-
-
+
+ 2min ago
+ 4h
+ 1.2k
+ 620
+
+ {assignee && }
+
+
+
);
}
diff --git a/static/app/views/detectors/components/forms/common/issuePreviewSection.tsx b/static/app/views/detectors/components/forms/common/issuePreviewSection.tsx
new file mode 100644
index 00000000000000..eb80448b54222f
--- /dev/null
+++ b/static/app/views/detectors/components/forms/common/issuePreviewSection.tsx
@@ -0,0 +1,18 @@
+import {Container} from 'sentry/components/workflowEngine/ui/container';
+import {Section} from 'sentry/components/workflowEngine/ui/section';
+import {t} from 'sentry/locale';
+
+export function IssuePreviewSection({children}: {children: React.ReactNode}) {
+ return (
+
+
+
+ );
+}
diff --git a/static/app/views/detectors/components/forms/common/ownerToActor.tsx b/static/app/views/detectors/components/forms/common/ownerToActor.tsx
new file mode 100644
index 00000000000000..b5b5d5ce1b48a2
--- /dev/null
+++ b/static/app/views/detectors/components/forms/common/ownerToActor.tsx
@@ -0,0 +1,15 @@
+import type {Actor} from 'sentry/types/core';
+
+/**
+ * Converts an owner string (e.g. "user:123" or "team:456") to an Actor object.
+ */
+export function ownerToActor(owner: string | undefined): Actor | undefined {
+ if (!owner) {
+ return undefined;
+ }
+ const [type, id] = owner.split(':');
+ if (!id || (type !== 'team' && type !== 'user')) {
+ return undefined;
+ }
+ return {type, id, name: ''};
+}
diff --git a/static/app/views/detectors/components/forms/metric/metric.spec.tsx b/static/app/views/detectors/components/forms/metric/metric.spec.tsx
index 10feb37d02b5e2..5764a7eaade4d1 100644
--- a/static/app/views/detectors/components/forms/metric/metric.spec.tsx
+++ b/static/app/views/detectors/components/forms/metric/metric.spec.tsx
@@ -1,9 +1,11 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
+import {UserFixture} from 'sentry-fixture/user';
-import {render, screen, within} from 'sentry-test/reactTestingLibrary';
+import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
import {selectEvent} from 'sentry-test/selectEvent';
+import {MemberListStore} from 'sentry/stores/memberListStore';
import {OrganizationStore} from 'sentry/stores/organizationStore';
import {DetectorFormProvider} from 'sentry/views/detectors/components/forms/context';
import {NewMetricDetectorForm} from 'sentry/views/detectors/components/forms/metric/metric';
@@ -18,6 +20,8 @@ describe('NewMetricDetectorForm', () => {
MockApiClient.clearMockResponses();
OrganizationStore.reset();
OrganizationStore.onUpdate(organization);
+ MemberListStore.init();
+ MemberListStore.loadInitialData([UserFixture()]);
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
@@ -63,6 +67,76 @@ describe('NewMetricDetectorForm', () => {
url: '/organizations/org-slug/projects/',
body: [project],
});
+ MockApiClient.addMockResponse({
+ url: '/organizations/org-slug/user-teams/',
+ body: [],
+ });
+ });
+
+ it('shows default issue preview and updates subtitle when threshold changes', async () => {
+ render(
+
+
+ ,
+ {organization}
+ );
+
+ // Default title and subtitle
+ expect(await screen.findByText('Monitor title')).toBeInTheDocument();
+ expect(
+ screen.getByText('Critical: Number of errors above ... in 1 hour')
+ ).toBeInTheDocument();
+
+ // Change the monitor name and verify it updates the preview
+ await userEvent.click(screen.getByTestId('editable-text-label'));
+ await userEvent.clear(screen.getByRole('textbox', {name: 'Monitor Name'}));
+ await userEvent.type(
+ screen.getByRole('textbox', {name: 'Monitor Name'}),
+ 'My Custom Monitor'
+ );
+ await userEvent.keyboard('{Enter}');
+ const preview = screen.getByTestId('issue-preview-section');
+ expect(within(preview).getByText('My Custom Monitor')).toBeInTheDocument();
+
+ // Change the high threshold
+ await userEvent.type(screen.getByRole('spinbutton', {name: 'High threshold'}), '100');
+
+ expect(
+ within(preview).getByText('Critical: Number of errors above 100 in 1 hour')
+ ).toBeInTheDocument();
+
+ // Switch to percent change detection type — threshold value carries over
+ await userEvent.click(screen.getByRole('radio', {name: /Change/i}));
+
+ expect(
+ within(preview).getByText(
+ 'Critical: Number of errors higher by 100% compared to past 1 hour'
+ )
+ ).toBeInTheDocument();
+
+ // Clear and set a new percent threshold
+ await userEvent.clear(screen.getByRole('spinbutton', {name: 'High threshold'}));
+ await userEvent.type(screen.getByRole('spinbutton', {name: 'High threshold'}), '50');
+
+ expect(
+ within(preview).getByText(
+ 'Critical: Number of errors higher by 50% compared to past 1 hour'
+ )
+ ).toBeInTheDocument();
+
+ // Switch to dynamic (anomaly) detection type
+ await userEvent.click(screen.getByRole('radio', {name: /Dynamic/i}));
+
+ expect(
+ within(preview).getByText('Detected an anomaly in the query for Number of errors')
+ ).toBeInTheDocument();
+
+ // Change the assignee and verify it shows in the preview
+ await selectEvent.select(
+ screen.getByRole('textbox', {name: 'Default assignee'}),
+ 'Foo Bar'
+ );
+ expect(within(preview).getByText('FB')).toBeInTheDocument();
});
it('removes is filters when switching away from the errors dataset', async () => {
diff --git a/static/app/views/detectors/components/forms/metric/metric.tsx b/static/app/views/detectors/components/forms/metric/metric.tsx
index 421119bcb21fb6..df95a50358d42b 100644
--- a/static/app/views/detectors/components/forms/metric/metric.tsx
+++ b/static/app/views/detectors/components/forms/metric/metric.tsx
@@ -44,6 +44,7 @@ import {
metricSavedDetectorToFormData,
useMetricDetectorFormField,
} from 'sentry/views/detectors/components/forms/metric/metricFormData';
+import {MetricIssuePreview} from 'sentry/views/detectors/components/forms/metric/metricIssuePreview';
import {MetricDetectorPreviewChart} from 'sentry/views/detectors/components/forms/metric/previewChart';
import {DetectorQueryFilterBuilder} from 'sentry/views/detectors/components/forms/metric/queryFilterBuilder';
import {ResolveSection} from 'sentry/views/detectors/components/forms/metric/resolveSection';
@@ -77,6 +78,7 @@ function MetricDetectorForm() {
+
);
diff --git a/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx b/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx
new file mode 100644
index 00000000000000..c50799b6113bde
--- /dev/null
+++ b/static/app/views/detectors/components/forms/metric/metricIssuePreview.tsx
@@ -0,0 +1,96 @@
+import {t} from 'sentry/locale';
+import {DataConditionType} from 'sentry/types/workflowEngine/dataConditions';
+import {getDuration} from 'sentry/utils/duration/getDuration';
+import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview';
+import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection';
+import {ownerToActor} from 'sentry/views/detectors/components/forms/common/ownerToActor';
+import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
+import {
+ METRIC_DETECTOR_FORM_FIELDS,
+ useMetricDetectorFormField,
+} from 'sentry/views/detectors/components/forms/metric/metricFormData';
+import {getDatasetConfig} from 'sentry/views/detectors/datasetConfig/getDatasetConfig';
+import {getMetricDetectorSuffix} from 'sentry/views/detectors/utils/metricDetectorSuffix';
+
+function useMetricIssuePreviewSubtitle() {
+ const detectionType = useMetricDetectorFormField(
+ METRIC_DETECTOR_FORM_FIELDS.detectionType
+ );
+ const aggregate = useMetricDetectorFormField(
+ METRIC_DETECTOR_FORM_FIELDS.aggregateFunction
+ );
+ const dataset = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.dataset);
+ const interval = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.interval);
+ const highThreshold = useMetricDetectorFormField(
+ METRIC_DETECTOR_FORM_FIELDS.highThreshold
+ );
+ const conditionType = useMetricDetectorFormField(
+ METRIC_DETECTOR_FORM_FIELDS.conditionType
+ );
+ const conditionComparisonAgo = useMetricDetectorFormField(
+ METRIC_DETECTOR_FORM_FIELDS.conditionComparisonAgo
+ );
+
+ const datasetConfig = getDatasetConfig(dataset);
+ const formattedAggregate =
+ datasetConfig.formatAggregateForTitle?.(aggregate) ?? aggregate;
+ const intervalLabel = getDuration(interval);
+ const thresholdLabel = highThreshold || t('...');
+
+ switch (detectionType) {
+ case 'static': {
+ const direction =
+ conditionType === DataConditionType.LESS ? t('below') : t('above');
+ const suffix = getMetricDetectorSuffix('static', aggregate);
+ return t(
+ 'Critical: %(aggregate)s %(direction)s %(threshold)s%(suffix)s in %(interval)s',
+ {
+ aggregate: formattedAggregate,
+ direction,
+ threshold: thresholdLabel,
+ suffix,
+ interval: intervalLabel,
+ }
+ );
+ }
+ case 'percent': {
+ const direction =
+ conditionType === DataConditionType.LESS ? t('lower') : t('higher');
+ return t(
+ 'Critical: %(aggregate)s %(direction)s by %(threshold)s%% compared to past %(interval)s',
+ {
+ aggregate: formattedAggregate,
+ direction,
+ threshold: thresholdLabel,
+ interval: getDuration(conditionComparisonAgo ?? interval),
+ }
+ );
+ }
+ case 'dynamic': {
+ return t('Detected an anomaly in the query for %(aggregate)s', {
+ aggregate: formattedAggregate,
+ });
+ }
+ default:
+ return t('Critical issue condition met');
+ }
+}
+
+export function MetricIssuePreview() {
+ const name = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.name);
+ const owner = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.owner);
+ const subtitle = useMetricIssuePreviewSubtitle();
+ const assignee = ownerToActor(owner);
+ const {project} = useDetectorFormContext();
+
+ return (
+
+
+
+ );
+}
diff --git a/static/app/views/detectors/components/forms/mobileBuild/previewSection.tsx b/static/app/views/detectors/components/forms/mobileBuild/previewSection.tsx
index ab812d7502b3c1..0d04f0eafcc6da 100644
--- a/static/app/views/detectors/components/forms/mobileBuild/previewSection.tsx
+++ b/static/app/views/detectors/components/forms/mobileBuild/previewSection.tsx
@@ -1,6 +1,7 @@
import {t} from 'sentry/locale';
import {useProjects} from 'sentry/utils/useProjects';
import {DetectorIssuePreview} from 'sentry/views/detectors/components/forms/common/detectorIssuePreview';
+import {IssuePreviewSection} from 'sentry/views/detectors/components/forms/common/issuePreviewSection';
import {
PREPROD_DETECTOR_FORM_FIELDS,
usePreprodDetectorFormField,
@@ -38,10 +39,12 @@ export function MobileBuildPreviewSection() {
const thresholdDisplay = highThreshold ? `${threshold} ${thresholdUnit}` : '\u2026';
return (
- %s Threshold', actualDisplay, thresholdDisplay)}
- />
+
+ %s Threshold', actualDisplay, thresholdDisplay)}
+ />
+
);
}
diff --git a/static/app/views/discover/breadcrumb.tsx b/static/app/views/discover/breadcrumb.tsx
index b8c9894f262f6a..bf243b895f1ff5 100644
--- a/static/app/views/discover/breadcrumb.tsx
+++ b/static/app/views/discover/breadcrumb.tsx
@@ -7,7 +7,7 @@ import {t} from 'sentry/locale';
import type {Event} from 'sentry/types/event';
import type {Organization} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
import {makeDiscoverPathname} from 'sentry/views/discover/pathnames';
diff --git a/static/app/views/discover/eventInputName.tsx b/static/app/views/discover/eventInputName.tsx
index a206db5fa27698..65724ee0fa4f74 100644
--- a/static/app/views/discover/eventInputName.tsx
+++ b/static/app/views/discover/eventInputName.tsx
@@ -2,7 +2,7 @@ import {EditableText} from 'sentry/components/editableText';
import * as Layout from 'sentry/components/layouts/thirds';
import {t} from 'sentry/locale';
import type {Organization, SavedQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
import {useApi} from 'sentry/utils/useApi';
import {useNavigate} from 'sentry/utils/useNavigate';
diff --git a/static/app/views/discover/homepage.spec.tsx b/static/app/views/discover/homepage.spec.tsx
index f2eff988623374..85c9609614c155 100644
--- a/static/app/views/discover/homepage.spec.tsx
+++ b/static/app/views/discover/homepage.spec.tsx
@@ -13,7 +13,7 @@ import {
import * as pageFilterUtils from 'sentry/components/pageFilters/persistence';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DEFAULT_EVENT_VIEW} from 'sentry/views/discover/results/data';
import Homepage from './homepage';
diff --git a/static/app/views/discover/homepage.tsx b/static/app/views/discover/homepage.tsx
index fb4a9c95090520..fee5d94c275c54 100644
--- a/static/app/views/discover/homepage.tsx
+++ b/static/app/views/discover/homepage.tsx
@@ -11,7 +11,7 @@ import {getPageFilterStorage} from 'sentry/components/pageFilters/persistence';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {Organization, SavedQuery} from 'sentry/types/organization';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useApiQuery, useQueryClient, type ApiQueryKey} from 'sentry/utils/queryClient';
import {useApi} from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/discover/landing.tsx b/static/app/views/discover/landing.tsx
index f87d20d9e0411a..6ba57d02acaba7 100644
--- a/static/app/views/discover/landing.tsx
+++ b/static/app/views/discover/landing.tsx
@@ -19,7 +19,7 @@ import type {SelectValue} from 'sentry/types/core';
import type {NewQuery, SavedQuery} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
import {useApiQuery} from 'sentry/utils/queryClient';
import {decodeScalar} from 'sentry/utils/queryString';
diff --git a/static/app/views/discover/miniGraph.spec.tsx b/static/app/views/discover/miniGraph.spec.tsx
index 5e90d567c8bd63..754281d4a43242 100644
--- a/static/app/views/discover/miniGraph.spec.tsx
+++ b/static/app/views/discover/miniGraph.spec.tsx
@@ -3,8 +3,8 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render} from 'sentry-test/reactTestingLibrary';
-import * as eventRequest from 'sentry/components/charts/eventsRequest';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
+import {EventView} from 'sentry/utils/discover/eventView';
import MiniGraph from 'sentry/views/discover/miniGraph';
jest.mock('sentry/components/charts/eventsRequest');
@@ -43,7 +43,7 @@ describe('Discover > MiniGraph', () => {
/>
);
- expect(eventRequest.default).toHaveBeenCalledWith(
+ expect(EventsRequest).toHaveBeenCalledWith(
expect.objectContaining({yAxis}),
expect.anything()
);
@@ -62,7 +62,7 @@ describe('Discover > MiniGraph', () => {
/>
);
- expect(eventRequest.default).toHaveBeenCalledWith(
+ expect(EventsRequest).toHaveBeenCalledWith(
expect.objectContaining({interval: '12h'}),
expect.anything()
);
diff --git a/static/app/views/discover/miniGraph.tsx b/static/app/views/discover/miniGraph.tsx
index 62cb33e9eea0ac..21f88ffeb8f3d5 100644
--- a/static/app/views/discover/miniGraph.tsx
+++ b/static/app/views/discover/miniGraph.tsx
@@ -10,7 +10,7 @@ import type {AreaChartProps} from 'sentry/components/charts/areaChart';
import {AreaChart} from 'sentry/components/charts/areaChart';
import type {BarChartProps} from 'sentry/components/charts/barChart';
import {BarChart} from 'sentry/components/charts/barChart';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {LineChart} from 'sentry/components/charts/lineChart';
import {getInterval} from 'sentry/components/charts/utils';
import {LoadingContainer} from 'sentry/components/loading/loadingContainer';
@@ -20,7 +20,7 @@ import type {Series} from 'sentry/types/echarts';
import type {Organization} from 'sentry/types/organization';
import {getUtcToLocalDateObject} from 'sentry/utils/dates';
import {axisLabelFormatter} from 'sentry/utils/discover/charts';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {PlotType} from 'sentry/utils/discover/fields';
import {aggregateOutputType} from 'sentry/utils/discover/fields';
import {DisplayModes, TOP_N} from 'sentry/utils/discover/types';
diff --git a/static/app/views/discover/queryList.tsx b/static/app/views/discover/queryList.tsx
index bfb8929ae468a1..55200f90d96fd7 100644
--- a/static/app/views/discover/queryList.tsx
+++ b/static/app/views/discover/queryList.tsx
@@ -18,7 +18,7 @@ import {t} from 'sentry/locale';
import type {NewQuery, Organization, SavedQuery} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {browserHistory} from 'sentry/utils/browserHistory';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {parseLinkHeader} from 'sentry/utils/parseLinkHeader';
import {decodeList} from 'sentry/utils/queryString';
@@ -34,7 +34,7 @@ import {
handleUpdateHomepageQuery,
} from './savedQuery/utils';
import MiniGraph from './miniGraph';
-import QueryCard from './querycard';
+import {QueryCard} from './querycard';
import {
getPrebuiltQueries,
handleAddQueryToDashboard,
diff --git a/static/app/views/discover/querycard.tsx b/static/app/views/discover/querycard.tsx
index 76fe2b5fd8c0bd..2b7c18c2d4d29a 100644
--- a/static/app/views/discover/querycard.tsx
+++ b/static/app/views/discover/querycard.tsx
@@ -5,7 +5,7 @@ import {Link} from '@sentry/scraps/link';
import {ActivityAvatar} from 'sentry/components/activity/item/avatar';
import {Card} from 'sentry/components/card';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import type {User} from 'sentry/types/user';
@@ -21,7 +21,7 @@ type Props = {
title?: string;
};
-class QueryCard extends PureComponent {
+export class QueryCard extends PureComponent {
handleClick = () => {
const {onEventClick} = this.props;
onEventClick?.();
@@ -160,5 +160,3 @@ const DateStatus = styled('span')`
const StyledErrorBoundary = styled(ErrorBoundary)`
margin-bottom: 100px;
`;
-
-export default QueryCard;
diff --git a/static/app/views/discover/results.spec.tsx b/static/app/views/discover/results.spec.tsx
index dcb4d3410bf926..b3bc4effbd38c0 100644
--- a/static/app/views/discover/results.spec.tsx
+++ b/static/app/views/discover/results.spec.tsx
@@ -8,7 +8,7 @@ import {selectEvent} from 'sentry-test/selectEvent';
import * as PageFilterPersistence from 'sentry/components/pageFilters/persistence';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {SavedSearchType} from 'sentry/types/group';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import Results from 'sentry/views/discover/results';
import {
DEFAULT_EVENT_VIEW,
diff --git a/static/app/views/discover/results.tsx b/static/app/views/discover/results.tsx
index b48698c2f8bc6b..cd36e27052ecf6 100644
--- a/static/app/views/discover/results.tsx
+++ b/static/app/views/discover/results.tsx
@@ -41,7 +41,7 @@ import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import {CustomMeasurementsContext} from 'sentry/utils/customMeasurements/customMeasurementsContext';
import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
-import EventView, {isAPIPayloadSimilar} from 'sentry/utils/discover/eventView';
+import {EventView, isAPIPayloadSimilar} from 'sentry/utils/discover/eventView';
import {formatTagKey, generateAggregateFields} from 'sentry/utils/discover/fields';
import {
DiscoverDatasets,
diff --git a/static/app/views/discover/results/chartFooter.spec.tsx b/static/app/views/discover/results/chartFooter.spec.tsx
index e8d2be35df290c..51fd103817ee31 100644
--- a/static/app/views/discover/results/chartFooter.spec.tsx
+++ b/static/app/views/discover/results/chartFooter.spec.tsx
@@ -2,7 +2,7 @@ import {ProjectFixture} from 'sentry-fixture/project';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DisplayModes} from 'sentry/utils/discover/types';
import {ChartFooter} from 'sentry/views/discover/results/chartFooter';
diff --git a/static/app/views/discover/results/chartFooter.tsx b/static/app/views/discover/results/chartFooter.tsx
index 3d541cfa1ab0fb..72ccb71418fa60 100644
--- a/static/app/views/discover/results/chartFooter.tsx
+++ b/static/app/views/discover/results/chartFooter.tsx
@@ -8,7 +8,7 @@ import {
} from 'sentry/components/charts/styles';
import {t} from 'sentry/locale';
import type {SelectValue} from 'sentry/types/core';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {TOP_EVENT_MODES} from 'sentry/utils/discover/types';
type Props = {
diff --git a/static/app/views/discover/results/resultsChart.spec.tsx b/static/app/views/discover/results/resultsChart.spec.tsx
index c2e1f8646ff407..7a13a1f602eef4 100644
--- a/static/app/views/discover/results/resultsChart.spec.tsx
+++ b/static/app/views/discover/results/resultsChart.spec.tsx
@@ -3,7 +3,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DISPLAY_MODE_OPTIONS, DisplayModes} from 'sentry/utils/discover/types';
import ResultsChart from 'sentry/views/discover/results/resultsChart';
diff --git a/static/app/views/discover/results/resultsChart.tsx b/static/app/views/discover/results/resultsChart.tsx
index 82ba94d3c0d3cb..a2ce8f9b742b39 100644
--- a/static/app/views/discover/results/resultsChart.tsx
+++ b/static/app/views/discover/results/resultsChart.tsx
@@ -6,7 +6,7 @@ import isEqual from 'lodash/isEqual';
import type {Client} from 'sentry/api';
import {AreaChart} from 'sentry/components/charts/areaChart';
import {BarChart} from 'sentry/components/charts/barChart';
-import EventsChart from 'sentry/components/charts/eventsChart';
+import {EventsChart} from 'sentry/components/charts/eventsChart';
import {getInterval, getPreviousSeriesName} from 'sentry/components/charts/utils';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
import {Panel} from 'sentry/components/panels/panel';
@@ -17,7 +17,7 @@ import type {Organization} from 'sentry/types/organization';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import {CustomMeasurementsContext} from 'sentry/utils/customMeasurements/customMeasurementsContext';
import {getUtcToLocalDateObject} from 'sentry/utils/dates';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {getAggregateArg, stripEquationPrefix} from 'sentry/utils/discover/fields';
import {
DisplayModes,
diff --git a/static/app/views/discover/results/resultsHeader.tsx b/static/app/views/discover/results/resultsHeader.tsx
index 764dc9e2c0a225..be4157d5b4a964 100644
--- a/static/app/views/discover/results/resultsHeader.tsx
+++ b/static/app/views/discover/results/resultsHeader.tsx
@@ -11,7 +11,7 @@ import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionT
import {TimeSince} from 'sentry/components/timeSince';
import {t} from 'sentry/locale';
import type {Organization, SavedQuery} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {withApi} from 'sentry/utils/withApi';
import {DiscoverBreadcrumb} from 'sentry/views/discover/breadcrumb';
diff --git a/static/app/views/discover/results/tags.spec.tsx b/static/app/views/discover/results/tags.spec.tsx
index 31113094b44855..ae37169c81d058 100644
--- a/static/app/views/discover/results/tags.spec.tsx
+++ b/static/app/views/discover/results/tags.spec.tsx
@@ -10,7 +10,7 @@ import {
waitForElementToBeRemoved,
} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {Tags} from 'sentry/views/discover/results/tags';
diff --git a/static/app/views/discover/results/tags.tsx b/static/app/views/discover/results/tags.tsx
index 0c5cca068c1a8c..81c43f94161245 100644
--- a/static/app/views/discover/results/tags.tsx
+++ b/static/app/views/discover/results/tags.tsx
@@ -19,7 +19,7 @@ import {Placeholder} from 'sentry/components/placeholder';
import {IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {isAPIPayloadSimilar} from 'sentry/utils/discover/eventView';
import {parseLinkHeader} from 'sentry/utils/parseLinkHeader';
import type {UseApiQueryResult} from 'sentry/utils/queryClient';
diff --git a/static/app/views/discover/savedQuery/datasetSelectorTabs.spec.tsx b/static/app/views/discover/savedQuery/datasetSelectorTabs.spec.tsx
index 1ec7d3473d9da3..0a2d298a17ee62 100644
--- a/static/app/views/discover/savedQuery/datasetSelectorTabs.spec.tsx
+++ b/static/app/views/discover/savedQuery/datasetSelectorTabs.spec.tsx
@@ -2,7 +2,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import EventView, {type EventViewOptions} from 'sentry/utils/discover/eventView';
+import {EventView, type EventViewOptions} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {DatasetSelectorTabs} from 'sentry/views/discover/savedQuery/datasetSelectorTabs';
diff --git a/static/app/views/discover/savedQuery/datasetSelectorTabs.tsx b/static/app/views/discover/savedQuery/datasetSelectorTabs.tsx
index 295064614c5241..82bd9435ac209d 100644
--- a/static/app/views/discover/savedQuery/datasetSelectorTabs.tsx
+++ b/static/app/views/discover/savedQuery/datasetSelectorTabs.tsx
@@ -3,7 +3,7 @@ import {TabList} from '@sentry/scraps/tabs';
import * as Layout from 'sentry/components/layouts/thirds';
import {t} from 'sentry/locale';
import type {SavedQuery} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {
ERROR_ONLY_FIELDS,
explodeField,
diff --git a/static/app/views/discover/savedQuery/index.spec.tsx b/static/app/views/discover/savedQuery/index.spec.tsx
index ff138394ce9456..0fc22fba0417e6 100644
--- a/static/app/views/discover/savedQuery/index.spec.tsx
+++ b/static/app/views/discover/savedQuery/index.spec.tsx
@@ -5,7 +5,7 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar
import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
import type {NewQuery, Organization, SavedQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DisplayModes, SavedQueryDatasets} from 'sentry/utils/discover/types';
import {getAllViews} from 'sentry/views/discover/results/data';
import SavedQueryButtonGroup from 'sentry/views/discover/savedQuery';
diff --git a/static/app/views/discover/savedQuery/index.tsx b/static/app/views/discover/savedQuery/index.tsx
index e40d19ddb807d2..50c316ae9f1d74 100644
--- a/static/app/views/discover/savedQuery/index.tsx
+++ b/static/app/views/discover/savedQuery/index.tsx
@@ -28,7 +28,7 @@ import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {browserHistory} from 'sentry/utils/browserHistory';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {getDiscoverQueriesUrl} from 'sentry/utils/discover/urls';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
diff --git a/static/app/views/discover/savedQuery/utils.spec.tsx b/static/app/views/discover/savedQuery/utils.spec.tsx
index 895434e195dc9a..eb7ba0f7e0c4e5 100644
--- a/static/app/views/discover/savedQuery/utils.spec.tsx
+++ b/static/app/views/discover/savedQuery/utils.spec.tsx
@@ -1,6 +1,6 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getAllViews} from 'sentry/views/discover/results/data';
import {
handleCreateQuery,
diff --git a/static/app/views/discover/savedQuery/utils.tsx b/static/app/views/discover/savedQuery/utils.tsx
index 45735c53ba382a..c2970dc4fd2ee6 100644
--- a/static/app/views/discover/savedQuery/utils.tsx
+++ b/static/app/views/discover/savedQuery/utils.tsx
@@ -17,7 +17,7 @@ import {t, tct} from 'sentry/locale';
import type {NewQuery, Organization, SavedQuery} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {SaveQueryEventParameters} from 'sentry/utils/analytics/discoverAnalyticsEvents';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {
DiscoverDatasets,
DisplayModes,
diff --git a/static/app/views/discover/table/cellAction.spec.tsx b/static/app/views/discover/table/cellAction.spec.tsx
index 5872c60ea38196..6da1a1162e44d0 100644
--- a/static/app/views/discover/table/cellAction.spec.tsx
+++ b/static/app/views/discover/table/cellAction.spec.tsx
@@ -3,7 +3,7 @@ import {LocationFixture} from 'sentry-fixture/locationFixture';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {Actions, CellAction, updateQuery} from 'sentry/views/discover/table/cellAction';
import type {TableColumn} from 'sentry/views/discover/table/types';
diff --git a/static/app/views/discover/table/index.tsx b/static/app/views/discover/table/index.tsx
index f57763abb4109c..72c4d2db91144e 100644
--- a/static/app/views/discover/table/index.tsx
+++ b/static/app/views/discover/table/index.tsx
@@ -4,7 +4,7 @@ import type {Location} from 'history';
import type {EventQuery} from 'sentry/actionCreators/events';
import type {Client} from 'sentry/api';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import type {CursorHandler} from 'sentry/components/pagination';
import {Pagination} from 'sentry/components/pagination';
import {t} from 'sentry/locale';
@@ -12,8 +12,7 @@ import type {Organization} from 'sentry/types/organization';
import {metric, trackAnalytics} from 'sentry/utils/analytics';
import {CustomMeasurementsContext} from 'sentry/utils/customMeasurements/customMeasurementsContext';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
-import type {LocationQuery} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView, LocationQuery} from 'sentry/utils/discover/eventView';
import {isAPIPayloadSimilar, isFieldsSimilar} from 'sentry/utils/discover/eventView';
import {SPAN_OP_BREAKDOWN_FIELDS} from 'sentry/utils/discover/fields';
import type {DiscoverDatasets, SavedQueryDatasets} from 'sentry/utils/discover/types';
diff --git a/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx b/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx
index 9a0d2dd3072503..b5e02e3e7a5228 100644
--- a/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx
+++ b/static/app/views/discover/table/quickContext/actionDropdown.spec.tsx
@@ -5,7 +5,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import type {EventData} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {ActionDropDown, ContextValueType} from './actionDropdown';
diff --git a/static/app/views/discover/table/quickContext/actionDropdown.tsx b/static/app/views/discover/table/quickContext/actionDropdown.tsx
index eb9482ebbbae2e..88bda0211f8ee4 100644
--- a/static/app/views/discover/table/quickContext/actionDropdown.tsx
+++ b/static/app/views/discover/table/quickContext/actionDropdown.tsx
@@ -10,8 +10,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {toArray} from 'sentry/utils/array/toArray';
-import type {EventData} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventData, EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useNavigate} from 'sentry/utils/useNavigate';
import {addToFilter, excludeFromFilter} from 'sentry/views/discover/table/cellAction';
diff --git a/static/app/views/discover/table/quickContext/eventContext.spec.tsx b/static/app/views/discover/table/quickContext/eventContext.spec.tsx
index 36865dec6b8dd4..91bc98288a71dd 100644
--- a/static/app/views/discover/table/quickContext/eventContext.spec.tsx
+++ b/static/app/views/discover/table/quickContext/eventContext.spec.tsx
@@ -14,8 +14,7 @@ import type {
Frame,
} from 'sentry/types/event';
import {EntryType, EventOrGroupType} from 'sentry/types/event';
-import type {EventData} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventData, EventView} from 'sentry/utils/discover/eventView';
import {EventContext} from './eventContext';
diff --git a/static/app/views/discover/table/quickContext/eventContext.tsx b/static/app/views/discover/table/quickContext/eventContext.tsx
index 6d3ca6acbc02d1..ecca549e9766d3 100644
--- a/static/app/views/discover/table/quickContext/eventContext.tsx
+++ b/static/app/views/discover/table/quickContext/eventContext.tsx
@@ -11,7 +11,7 @@ import type {Event, EventTransaction} from 'sentry/types/event';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {getDuration} from 'sentry/utils/duration/getDuration';
import {useApiQuery} from 'sentry/utils/queryClient';
diff --git a/static/app/views/discover/table/quickContext/quickContextHovercard.spec.tsx b/static/app/views/discover/table/quickContext/quickContextHovercard.spec.tsx
index 8d79882c4d7e60..12db39d7c6e77f 100644
--- a/static/app/views/discover/table/quickContext/quickContextHovercard.spec.tsx
+++ b/static/app/views/discover/table/quickContext/quickContextHovercard.spec.tsx
@@ -7,8 +7,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {ConfigStore} from 'sentry/stores/configStore';
import {EventOrGroupType} from 'sentry/types/event';
import {ReleaseStatus} from 'sentry/types/release';
-import type {EventData} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventData, EventView} from 'sentry/utils/discover/eventView';
import {QuickContextHoverWrapper} from './quickContextWrapper';
import {defaultRow, mockedCommit, mockedUser1, mockedUser2} from './testUtils';
diff --git a/static/app/views/discover/table/quickContext/quickContextHovercard.tsx b/static/app/views/discover/table/quickContext/quickContextHovercard.tsx
index 1e35e9cbe66c7a..d6482b9c4e7f14 100644
--- a/static/app/views/discover/table/quickContext/quickContextHovercard.tsx
+++ b/static/app/views/discover/table/quickContext/quickContextHovercard.tsx
@@ -11,8 +11,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type {EventData} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventData, EventView} from 'sentry/utils/discover/eventView';
import {getShortEventId} from 'sentry/utils/events';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/discover/table/tableActions.tsx b/static/app/views/discover/table/tableActions.tsx
index 5ecb56b942b4b6..8b0f6c315994d8 100644
--- a/static/app/views/discover/table/tableActions.tsx
+++ b/static/app/views/discover/table/tableActions.tsx
@@ -13,7 +13,7 @@ import {t} from 'sentry/locale';
import type {OrganizationSummary} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {downloadAsCsv} from 'sentry/views/discover/utils';
diff --git a/static/app/views/discover/table/tableView.spec.tsx b/static/app/views/discover/table/tableView.spec.tsx
index 22957abd97fc42..bf93622896bdbd 100644
--- a/static/app/views/discover/table/tableView.spec.tsx
+++ b/static/app/views/discover/table/tableView.spec.tsx
@@ -14,7 +14,7 @@ import {
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {TagStore} from 'sentry/stores/tagStore';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {TableView} from 'sentry/views/discover/table/tableView';
diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx
index 3593915baceeb7..3b579ccaac2f3d 100644
--- a/static/app/views/discover/table/tableView.tsx
+++ b/static/app/views/discover/table/tableView.tsx
@@ -20,7 +20,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import {getTimeStampFromTableDateField} from 'sentry/utils/dates';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {
DURATION_UNITS,
diff --git a/static/app/views/discover/utils.spec.tsx b/static/app/views/discover/utils.spec.tsx
index e0acb17da82739..8721f9ca337de0 100644
--- a/static/app/views/discover/utils.spec.tsx
+++ b/static/app/views/discover/utils.spec.tsx
@@ -9,7 +9,7 @@ import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
import {COL_WIDTH_UNDEFINED} from 'sentry/components/tables/gridEditable';
import type {Organization} from 'sentry/types/organization';
import type {EventViewOptions} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DisplayModes} from 'sentry/utils/discover/types';
import {
DashboardWidgetSource,
diff --git a/static/app/views/discover/utils.tsx b/static/app/views/discover/utils.tsx
index 80b38d847e95f3..d7cf4a52c7adb8 100644
--- a/static/app/views/discover/utils.tsx
+++ b/static/app/views/discover/utils.tsx
@@ -17,8 +17,7 @@ import {defined} from 'sentry/utils';
import {toArray} from 'sentry/utils/array/toArray';
import {getUtcDateString} from 'sentry/utils/dates';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {EventData, MetaType} from 'sentry/utils/discover/eventView';
+import type {EventData, EventView, MetaType} from 'sentry/utils/discover/eventView';
import type {
Aggregation,
Column,
diff --git a/static/app/views/explore/components/attributeBreakdowns/attributeDistributionContent.tsx b/static/app/views/explore/components/attributeBreakdowns/attributeDistributionContent.tsx
index 603e0af87cd55b..b4b3f37fdc8eb1 100644
--- a/static/app/views/explore/components/attributeBreakdowns/attributeDistributionContent.tsx
+++ b/static/app/views/explore/components/attributeBreakdowns/attributeDistributionContent.tsx
@@ -11,7 +11,7 @@ import {IconClose} from 'sentry/icons/iconClose';
import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {parseLinkHeader} from 'sentry/utils/parseLinkHeader';
import {useApiQuery} from 'sentry/utils/queryClient';
import {useQueryParamState} from 'sentry/utils/url/useQueryParamState';
diff --git a/static/app/views/explore/components/traceItemAttributes/attributesTreeValue.tsx b/static/app/views/explore/components/traceItemAttributes/attributesTreeValue.tsx
index 8b8be561fb0f6e..f35e567e1980ae 100644
--- a/static/app/views/explore/components/traceItemAttributes/attributesTreeValue.tsx
+++ b/static/app/views/explore/components/traceItemAttributes/attributesTreeValue.tsx
@@ -2,7 +2,7 @@ import {type Theme} from '@emotion/react';
import styled from '@emotion/styled';
import {openNavigateToExternalLinkModal} from 'sentry/actionCreators/modal';
-import ExternalLink from 'sentry/components/links/externalLink';
+import {ExternalLink} from 'sentry/components/links/externalLink';
import {type RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
import {isUrl} from 'sentry/utils/string/isUrl';
import {AnnotatedAttributeTooltip} from 'sentry/views/explore/components/annotatedAttributeTooltip';
diff --git a/static/app/views/explore/hooks/useAddToDashboard.tsx b/static/app/views/explore/hooks/useAddToDashboard.tsx
index e6a4bc778ee5bc..4f84d97e0bd934 100644
--- a/static/app/views/explore/hooks/useAddToDashboard.tsx
+++ b/static/app/views/explore/hooks/useAddToDashboard.tsx
@@ -2,7 +2,7 @@ import {useCallback} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/explore/hooks/useExploreAggregatesTable.tsx b/static/app/views/explore/hooks/useExploreAggregatesTable.tsx
index bbe8b97115aea3..f27c95c1d63d42 100644
--- a/static/app/views/explore/hooks/useExploreAggregatesTable.tsx
+++ b/static/app/views/explore/hooks/useExploreAggregatesTable.tsx
@@ -3,7 +3,7 @@ import {useCallback, useMemo} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {isGroupBy} from 'sentry/views/explore/contexts/pageParamsContext/aggregateFields';
import {formatSort} from 'sentry/views/explore/contexts/pageParamsContext/sortBys';
import type {RPCQueryExtras} from 'sentry/views/explore/hooks/useProgressiveQuery';
diff --git a/static/app/views/explore/hooks/useExploreSpansTable.tsx b/static/app/views/explore/hooks/useExploreSpansTable.tsx
index 267c10876b36b0..f14d6f19311652 100644
--- a/static/app/views/explore/hooks/useExploreSpansTable.tsx
+++ b/static/app/views/explore/hooks/useExploreSpansTable.tsx
@@ -3,7 +3,7 @@ import {useCallback, useMemo} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {
useProgressiveQuery,
type RPCQueryExtras,
diff --git a/static/app/views/explore/logs/confidenceFooter.tsx b/static/app/views/explore/logs/confidenceFooter.tsx
index bec13728d477ba..77aa7209a0d45b 100644
--- a/static/app/views/explore/logs/confidenceFooter.tsx
+++ b/static/app/views/explore/logs/confidenceFooter.tsx
@@ -77,14 +77,18 @@ function ConfidenceMessage({
const isTopN = defined(topEvents) && topEvents > 1;
const noSampling = defined(isSampled) && !isSampled;
+ const usePluralSampleCount = sampleCount !== 1;
+ const usePluralNormalLogsCount =
+ defined(rawLogCounts.normal.count) && rawLogCounts.normal.count !== 1;
+ const usePluralTotalLogsCount =
+ defined(rawLogCounts.total.count) && rawLogCounts.total.count !== 1;
// No sampling happened, so don't mention estimations.
if (noSampling) {
if (!hasUserQuery) {
- const matchingLogsCount =
- sampleCount > 1
- ? t('%s logs', )
- : t('%s log', );
+ const matchingLogsCount = usePluralSampleCount
+ ? t('%s logs', )
+ : t('%s log', );
if (isTopN) {
return tct('[matchingLogsCount] for top [topEvents] groups', {
@@ -96,13 +100,12 @@ function ConfidenceMessage({
return matchingLogsCount;
}
- const matchingLogsCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingLogsCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const totalLogsCount = defined(rawLogCounts.total.count) ? (
- rawLogCounts.total.count > 1 ? (
+ usePluralTotalLogsCount ? (
t('%s logs', )
) : (
t('%s log', )
@@ -136,13 +139,12 @@ function ConfidenceMessage({
// partial scans means that we didnt scan all the data so it's useful
// to mention the total number of logs available
if (dataScanned === 'partial') {
- const matchingLogsCount =
- sampleCount > 1
- ? t('%s samples', )
- : t('%s sample', );
+ const matchingLogsCount = usePluralSampleCount
+ ? t('%s samples', )
+ : t('%s sample', );
const totalLogsCount = defined(rawLogCounts.total.count) ? (
- rawLogCounts.total.count > 1 ? (
+ usePluralTotalLogsCount ? (
t('%s logs', )
) : (
t('%s log', )
@@ -178,10 +180,9 @@ function ConfidenceMessage({
// otherwise, a full scan was done
// full scan means we scanned all the data available so no need to repeat that information twice
- const matchingLogsCount =
- sampleCount > 1
- ? t('%s logs', )
- : t('%s log', );
+ const matchingLogsCount = usePluralSampleCount
+ ? t('%s logs', )
+ : t('%s log', );
if (isTopN) {
return tct(
@@ -209,13 +210,12 @@ function ConfidenceMessage({
// partial scans means that we didnt scan all the data so it's useful
// to mention the total number of logs available
if (dataScanned === 'partial') {
- const matchingLogsCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingLogsCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const scannedLogsCount = defined(rawLogCounts.normal.count) ? (
- rawLogCounts.normal.count > 1 ? (
+ usePluralNormalLogsCount ? (
t('%s samples', )
) : (
t('%s sample', )
@@ -225,7 +225,7 @@ function ConfidenceMessage({
);
const totalLogsCount = defined(rawLogCounts.total.count) ? (
- rawLogCounts.total.count > 1 ? (
+ usePluralTotalLogsCount ? (
t('%s logs', )
) : (
t('%s log', )
@@ -263,13 +263,12 @@ function ConfidenceMessage({
// otherwise, a full scan was done
// full scan means we scanned all the data available so no need to repeat that information twice
- const matchingLogsCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingLogsCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const totalLogsCount = defined(rawLogCounts.total.count) ? (
- rawLogCounts.total.count > 1 ? (
+ usePluralTotalLogsCount ? (
t('%s logs', )
) : (
t('%s log', )
diff --git a/static/app/views/explore/logs/logsGraph.tsx b/static/app/views/explore/logs/logsGraph.tsx
index 42fa0b67a91af6..f45cdc78d1b4d7 100644
--- a/static/app/views/explore/logs/logsGraph.tsx
+++ b/static/app/views/explore/logs/logsGraph.tsx
@@ -12,7 +12,7 @@ import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useChartInterval} from 'sentry/utils/useChartInterval';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/explore/logs/useSaveAsItems.tsx b/static/app/views/explore/logs/useSaveAsItems.tsx
index 03f5f01a24afdd..489b76644be376 100644
--- a/static/app/views/explore/logs/useSaveAsItems.tsx
+++ b/static/app/views/explore/logs/useSaveAsItems.tsx
@@ -14,7 +14,7 @@ import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {parseFunction, prettifyParsedFunction} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
diff --git a/static/app/views/explore/metrics/confidenceFooter.tsx b/static/app/views/explore/metrics/confidenceFooter.tsx
index f8c316f7ea44d4..332e5f9ed3c0de 100644
--- a/static/app/views/explore/metrics/confidenceFooter.tsx
+++ b/static/app/views/explore/metrics/confidenceFooter.tsx
@@ -77,14 +77,18 @@ function ConfidenceMessage({
const isTopN = defined(topEvents) && topEvents > 1;
const noSampling = defined(isSampled) && !isSampled;
+ const usePluralSampleCount = sampleCount !== 1;
+ const usePluralNormalMetricsCount =
+ defined(rawMetricCounts.normal.count) && rawMetricCounts.normal.count !== 1;
+ const usePluralTotalMetricsCount =
+ defined(rawMetricCounts.total.count) && rawMetricCounts.total.count !== 1;
// No sampling happened, so don't mention estimations.
if (noSampling) {
if (!hasUserQuery) {
- const matchingMetricsCount =
- sampleCount > 1
- ? t('%s data points', )
- : t('%s data point', );
+ const matchingMetricsCount = usePluralSampleCount
+ ? t('%s data points', )
+ : t('%s data point', );
if (isTopN) {
return tct('[matchingMetricsCount] for top [topEvents] groups', {
@@ -96,13 +100,12 @@ function ConfidenceMessage({
return matchingMetricsCount;
}
- const matchingMetricsCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingMetricsCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const totalMetricsCount = defined(rawMetricCounts.total.count) ? (
- rawMetricCounts.total.count > 1 ? (
+ usePluralTotalMetricsCount ? (
t('%s data points', )
) : (
t('%s data point', )
@@ -141,13 +144,12 @@ function ConfidenceMessage({
// partial scans means that we didnt scan all the data so it's useful
// to mention the total number of metrics available
if (dataScanned === 'partial') {
- const matchingMetricsCount =
- sampleCount > 1
- ? t('%s samples', )
- : t('%s sample', );
+ const matchingMetricsCount = usePluralSampleCount
+ ? t('%s samples', )
+ : t('%s sample', );
const totalMetricsCount = defined(rawMetricCounts.total.count) ? (
- rawMetricCounts.total.count > 1 ? (
+ usePluralTotalMetricsCount ? (
t('%s data points', )
) : (
t('%s data point', )
@@ -183,10 +185,9 @@ function ConfidenceMessage({
// otherwise, a full scan was done
// full scan means we scanned all the data available so no need to repeat that information twice
- const matchingMetricsCount =
- sampleCount > 1
- ? t('%s data points', )
- : t('%s data point', );
+ const matchingMetricsCount = usePluralSampleCount
+ ? t('%s data points', )
+ : t('%s data point', );
if (isTopN) {
return tct(
@@ -214,13 +215,12 @@ function ConfidenceMessage({
// partial scans means that we didnt scan all the data so it's useful
// to mention the total number of metrics available
if (dataScanned === 'partial') {
- const matchingMetricsCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingMetricsCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const scannedMetricsCount = defined(rawMetricCounts.normal.count) ? (
- rawMetricCounts.normal.count > 1 || rawMetricCounts.normal.count === 0 ? (
+ usePluralNormalMetricsCount ? (
t('%s samples', )
) : (
t('%s sample', )
@@ -230,7 +230,7 @@ function ConfidenceMessage({
);
const totalMetricsCount = defined(rawMetricCounts.total.count) ? (
- rawMetricCounts.total.count > 1 ? (
+ usePluralTotalMetricsCount ? (
t('%s data points', )
) : (
t('%s data point', )
@@ -268,13 +268,12 @@ function ConfidenceMessage({
// otherwise, a full scan was done
// full scan means we scanned all the data available so no need to repeat that information twice
- const matchingMetricsCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingMetricsCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const totalMetricsCount = defined(rawMetricCounts.total.count) ? (
- rawMetricCounts.total.count > 1 ? (
+ usePluralTotalMetricsCount ? (
t('%s data points', )
) : (
t('%s data point', )
diff --git a/static/app/views/explore/metrics/hooks/useAddMetricToDashboard.tsx b/static/app/views/explore/metrics/hooks/useAddMetricToDashboard.tsx
index bb8059203156f9..4dc0361c00e61a 100644
--- a/static/app/views/explore/metrics/hooks/useAddMetricToDashboard.tsx
+++ b/static/app/views/explore/metrics/hooks/useAddMetricToDashboard.tsx
@@ -2,7 +2,7 @@ import {useCallback} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx b/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx
index 41a64d26795828..075b823f7c2171 100644
--- a/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx
+++ b/static/app/views/explore/metrics/hooks/useMetricAggregatesTable.tsx
@@ -3,7 +3,7 @@ import {useCallback, useMemo} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {formatSort} from 'sentry/views/explore/contexts/pageParamsContext/sortBys';
import {
diff --git a/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx b/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx
index 58220b1bc7cf5f..8acee18b23bca4 100644
--- a/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx
+++ b/static/app/views/explore/metrics/hooks/useMetricSamplesTable.tsx
@@ -4,8 +4,7 @@ import moment from 'moment-timezone';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {defined} from 'sentry/utils';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {EventsMetaType} from 'sentry/utils/discover/eventView';
+import type {EventsMetaType, EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {intervalToMilliseconds} from 'sentry/utils/duration/intervalToMilliseconds';
import {useApiQuery, type ApiQueryKey} from 'sentry/utils/queryClient';
diff --git a/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx b/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx
index 33132d07a87a57..6dfa500ed5d4f0 100644
--- a/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx
+++ b/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx
@@ -4,7 +4,7 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch';
import type {NewQuery} from 'sentry/types/organization';
import {useDiscoverQuery, type TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/explore/metrics/metricGraph/index.tsx b/static/app/views/explore/metrics/metricGraph/index.tsx
index d9d8c4f8a67df6..4be1eb549ffb48 100644
--- a/static/app/views/explore/metrics/metricGraph/index.tsx
+++ b/static/app/views/explore/metrics/metricGraph/index.tsx
@@ -302,7 +302,7 @@ function Graph({
showChart && (
diff --git a/static/app/views/explore/metrics/metricsFlags.tsx b/static/app/views/explore/metrics/metricsFlags.tsx
index dbba20d77e3413..17052c30b48385 100644
--- a/static/app/views/explore/metrics/metricsFlags.tsx
+++ b/static/app/views/explore/metrics/metricsFlags.tsx
@@ -34,3 +34,10 @@ export const canUseMetricsUIRefresh = (organization: Organization) => {
organization.features.includes('tracemetrics-ui-refresh')
);
};
+
+export const canUseMetricsStatsBytesUI = (organization: Organization) => {
+ return (
+ canUseMetricsUI(organization) &&
+ organization.features.includes('tracemetrics-stats-bytes-ui')
+ );
+};
diff --git a/static/app/views/explore/multiQueryMode/hooks/useAddCompareQueryToDashboard.tsx b/static/app/views/explore/multiQueryMode/hooks/useAddCompareQueryToDashboard.tsx
index 250dce6aca2e2d..48be2adb30531b 100644
--- a/static/app/views/explore/multiQueryMode/hooks/useAddCompareQueryToDashboard.tsx
+++ b/static/app/views/explore/multiQueryMode/hooks/useAddCompareQueryToDashboard.tsx
@@ -2,7 +2,7 @@ import {useCallback} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx
index 6e67ab2793ddff..c96dda14f66b0f 100644
--- a/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx
+++ b/static/app/views/explore/multiQueryMode/hooks/useMultiQueryTable.tsx
@@ -3,7 +3,7 @@ import {useCallback, useMemo} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {formatSort} from 'sentry/views/explore/contexts/pageParamsContext/sortBys';
diff --git a/static/app/views/explore/spans/charts/confidenceFooter.tsx b/static/app/views/explore/spans/charts/confidenceFooter.tsx
index c79326f961e666..047a5082befb57 100644
--- a/static/app/views/explore/spans/charts/confidenceFooter.tsx
+++ b/static/app/views/explore/spans/charts/confidenceFooter.tsx
@@ -46,6 +46,11 @@ function confidenceMessage({
const isTopN = defined(topEvents) && topEvents > 1;
const noSampling = defined(isSampled) && !isSampled;
+ const usePluralSampleCount = sampleCount !== 1;
+ const usePluralNormalSpansCount =
+ defined(rawSpanCounts?.normal.count) && rawSpanCounts.normal.count !== 1;
+ const usePluralTotalSpansCount =
+ defined(rawSpanCounts?.total.count) && rawSpanCounts.total.count !== 1;
const maybeWarning =
confidence === 'low' ? tct('[warning] ', {warning: }) : null;
@@ -61,10 +66,9 @@ function confidenceMessage({
// The multi query mode does not fetch the raw span counts
// so make sure to have a backup when this happens.
if (!defined(rawSpanCounts)) {
- const matchingSpansCount =
- sampleCount === 1
- ? t('%s span', )
- : t('%s spans', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s spans', )
+ : t('%s span', );
if (isTopN) {
return tct(
@@ -92,10 +96,9 @@ function confidenceMessage({
noSampling
) {
if (!userQuery) {
- const matchingSpansCount =
- sampleCount > 1
- ? t('%s spans', )
- : t('%s span', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s spans', )
+ : t('%s span', );
if (isTopN) {
return tct('[matchingSpansCount] for top [topEvents] groups', {
@@ -107,13 +110,12 @@ function confidenceMessage({
return matchingSpansCount;
}
- const matchingSpansCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const totalSpansCount = defined(rawSpanCounts.total.count) ? (
- rawSpanCounts.total.count > 1 ? (
+ usePluralTotalSpansCount ? (
t('%s spans', )
) : (
t('%s span', )
@@ -142,13 +144,12 @@ function confidenceMessage({
// partial scans means that we didnt scan all the data so it's useful
// to mention the total number of spans available
if (dataScanned === 'partial') {
- const matchingSpansCount =
- sampleCount > 1
- ? t('%s samples', )
- : t('%s sample', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s samples', )
+ : t('%s sample', );
const totalSpansCount = defined(rawSpanCounts.total.count) ? (
- rawSpanCounts.total.count > 1 ? (
+ usePluralTotalSpansCount ? (
t('%s spans', )
) : (
t('%s span', )
@@ -184,10 +185,9 @@ function confidenceMessage({
// otherwise, a full scan was done
// full scan means we scanned all the data available so no need to repeat that information twice
- const matchingSpansCount =
- sampleCount > 1
- ? t('%s spans', )
- : t('%s span', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s spans', )
+ : t('%s span', );
if (isTopN) {
return tct(
@@ -215,13 +215,12 @@ function confidenceMessage({
// partial scans means that we didnt scan all the data so it's useful
// to mention the total number of spans available
if (dataScanned === 'partial') {
- const matchingSpansCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const scannedSpansCount = defined(rawSpanCounts.normal.count) ? (
- rawSpanCounts.normal.count > 1 ? (
+ usePluralNormalSpansCount ? (
t('%s samples', )
) : (
t('%s sample', )
@@ -231,7 +230,7 @@ function confidenceMessage({
);
const totalSpansCount = defined(rawSpanCounts.total.count) ? (
- rawSpanCounts.total.count > 1 ? (
+ usePluralTotalSpansCount ? (
t('%s spans', )
) : (
t('%s span', )
@@ -269,13 +268,12 @@ function confidenceMessage({
// otherwise, a full scan was done
// full scan means we scanned all the data available so no need to repeat that information twice
- const matchingSpansCount =
- sampleCount > 1
- ? t('%s matches', )
- : t('%s match', );
+ const matchingSpansCount = usePluralSampleCount
+ ? t('%s matches', )
+ : t('%s match', );
const totalSpansCount = defined(rawSpanCounts.total.count) ? (
- rawSpanCounts.total.count > 1 ? (
+ usePluralTotalSpansCount ? (
t('%s spans', )
) : (
t('%s span', )
diff --git a/static/app/views/explore/tables/fieldRenderer.spec.tsx b/static/app/views/explore/tables/fieldRenderer.spec.tsx
index 3e1794ca4952b7..6ca21d0d7df7f5 100644
--- a/static/app/views/explore/tables/fieldRenderer.spec.tsx
+++ b/static/app/views/explore/tables/fieldRenderer.spec.tsx
@@ -7,7 +7,7 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {resetMockDate, setMockDate} from 'sentry-test/utils';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SpansQueryParamsProvider} from 'sentry/views/explore/spans/spansQueryParamsProvider';
import {FieldRenderer} from 'sentry/views/explore/tables/fieldRenderer';
diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx
index bb4c2db16c70d1..67294bb6344c9a 100644
--- a/static/app/views/explore/tables/fieldRenderer.tsx
+++ b/static/app/views/explore/tables/fieldRenderer.tsx
@@ -15,7 +15,7 @@ import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
import type {EventData, MetaType} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getFieldRenderer, nullableValue} from 'sentry/utils/discover/fieldRenderers';
import {Container} from 'sentry/utils/discover/styles';
import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
diff --git a/static/app/views/explore/tables/tracesTable/spansTable.tsx b/static/app/views/explore/tables/tracesTable/spansTable.tsx
index 95f91f7b9e934d..2d00cd6337d68b 100644
--- a/static/app/views/explore/tables/tracesTable/spansTable.tsx
+++ b/static/app/views/explore/tables/tracesTable/spansTable.tsx
@@ -12,7 +12,7 @@ import {t, tct} from 'sentry/locale';
import type {NewQuery, Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {getUtcDateString} from 'sentry/utils/dates';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useQueryParamsQuery} from 'sentry/views/explore//queryParams/context';
diff --git a/static/app/views/feedback/feedbackListPage.tsx b/static/app/views/feedback/feedbackListPage.tsx
index 9b55637e128636..7ec776ba920d7c 100644
--- a/static/app/views/feedback/feedbackListPage.tsx
+++ b/static/app/views/feedback/feedbackListPage.tsx
@@ -6,7 +6,7 @@ import {Button, LinkButton} from '@sentry/scraps/button';
import {Flex, Stack} from '@sentry/scraps/layout';
import {AnalyticsArea} from 'sentry/components/analyticsArea';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackFilters} from 'sentry/components/feedback/feedbackFilters';
import {FeedbackItemLoader} from 'sentry/components/feedback/feedbackItem/feedbackItemLoader';
import {FeedbackSearch} from 'sentry/components/feedback/feedbackSearch';
diff --git a/static/app/views/insights/agentModels/views/modelsLandingPage.tsx b/static/app/views/insights/agentModels/views/modelsLandingPage.tsx
index 4085c69dabef5a..0b3162f5cd7952 100644
--- a/static/app/views/insights/agentModels/views/modelsLandingPage.tsx
+++ b/static/app/views/insights/agentModels/views/modelsLandingPage.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import {Flex} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import * as Layout from 'sentry/components/layouts/thirds';
import type {DatePageFilterProps} from 'sentry/components/pageFilters/date/datePageFilter';
import {DatePageFilter} from 'sentry/components/pageFilters/date/datePageFilter';
diff --git a/static/app/views/insights/common/components/chart.tsx b/static/app/views/insights/common/components/chart.tsx
index 6097fbcdb2dd0e..1735a313123bd4 100644
--- a/static/app/views/insights/common/components/chart.tsx
+++ b/static/app/views/insights/common/components/chart.tsx
@@ -25,7 +25,7 @@ import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import ReleaseSeries from 'sentry/components/charts/releaseSeries';
import {LineSeries} from 'sentry/components/charts/series/lineSeries';
import {ScatterSeries} from 'sentry/components/charts/series/scatterSeries';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {isChartHovered} from 'sentry/components/charts/utils';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/views/insights/common/queries/getSeriesEventView.tsx b/static/app/views/insights/common/queries/getSeriesEventView.tsx
index 20e10696820639..979d8f6c918767 100644
--- a/static/app/views/insights/common/queries/getSeriesEventView.tsx
+++ b/static/app/views/insights/common/queries/getSeriesEventView.tsx
@@ -1,5 +1,5 @@
import type {PageFilters} from 'sentry/types/core';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {getIntervalForTimeSeriesQuery} from 'sentry/utils/timeSeries/getIntervalForTimeSeriesQuery';
import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
diff --git a/static/app/views/insights/common/queries/useDiscover.ts b/static/app/views/insights/common/queries/useDiscover.ts
index c3f50f05ff225d..dd4f1038628bb3 100644
--- a/static/app/views/insights/common/queries/useDiscover.ts
+++ b/static/app/views/insights/common/queries/useDiscover.ts
@@ -1,6 +1,6 @@
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {PageFilters} from 'sentry/types/core';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
diff --git a/static/app/views/insights/common/queries/useReleases.tsx b/static/app/views/insights/common/queries/useReleases.tsx
index 7a04de01e6a371..a8ccb5c14a4444 100644
--- a/static/app/views/insights/common/queries/useReleases.tsx
+++ b/static/app/views/insights/common/queries/useReleases.tsx
@@ -7,7 +7,7 @@ import type {Release} from 'sentry/types/release';
import {parseQueryKey} from 'sentry/utils/api/apiQueryKey';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import type {ApiQueryKey} from 'sentry/utils/queryClient';
import {useApiQuery, useQueries} from 'sentry/utils/queryClient';
diff --git a/static/app/views/insights/common/queries/useSpansQuery.tsx b/static/app/views/insights/common/queries/useSpansQuery.tsx
index 84e3d8785fe36e..63444c5ee3600c 100644
--- a/static/app/views/insights/common/queries/useSpansQuery.tsx
+++ b/static/app/views/insights/common/queries/useSpansQuery.tsx
@@ -5,8 +5,7 @@ import type {CaseInsensitive} from 'sentry/components/searchQueryBuilder/hooks';
import {defined} from 'sentry/utils';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {EventsMetaType, MetaType} from 'sentry/utils/discover/eventView';
+import type {EventsMetaType, EventView, MetaType} from 'sentry/utils/discover/eventView';
import {encodeSort} from 'sentry/utils/discover/eventView';
import type {DiscoverQueryProps} from 'sentry/utils/discover/genericDiscoverQuery';
import {useGenericDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
diff --git a/static/app/views/insights/common/utils/trackResponse.tsx b/static/app/views/insights/common/utils/trackResponse.tsx
index d24e23b083dbaa..70b0d92c8e9803 100644
--- a/static/app/views/insights/common/utils/trackResponse.tsx
+++ b/static/app/views/insights/common/utils/trackResponse.tsx
@@ -1,7 +1,7 @@
import {useEffect, useState} from 'react';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {useOrganization} from 'sentry/utils/useOrganization';
export function TrackResponse(
diff --git a/static/app/views/insights/common/utils/useAddToSpanDashboard.ts b/static/app/views/insights/common/utils/useAddToSpanDashboard.ts
index c76d9295fafcc7..106bf81662df04 100644
--- a/static/app/views/insights/common/utils/useAddToSpanDashboard.ts
+++ b/static/app/views/insights/common/utils/useAddToSpanDashboard.ts
@@ -2,7 +2,7 @@ import {useCallback} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {getIntervalForTimeSeriesQuery} from 'sentry/utils/timeSeries/getIntervalForTimeSeriesQuery';
diff --git a/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx b/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx
index 57faa2e2496548..818175a29d663c 100644
--- a/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx
+++ b/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx
@@ -131,12 +131,17 @@ function getResponseCode(series: TimeSeries) {
return responseCodeGroupBy.value;
}
-function isNumeric(maybeNumber: string | number | null | undefined) {
- if (!maybeNumber) {
+function isNumeric(
+ maybeNumber: string | number | boolean | null | undefined
+): maybeNumber is string | number {
+ if (typeof maybeNumber === 'boolean') {
return false;
}
if (typeof maybeNumber === 'number') {
return true;
}
+ if (!maybeNumber) {
+ return false;
+ }
return /^\d+$/.test(maybeNumber);
}
diff --git a/static/app/views/insights/mobile/appStarts/components/eventSamples.tsx b/static/app/views/insights/mobile/appStarts/components/eventSamples.tsx
index 3da742837ec11f..9fc340af948879 100644
--- a/static/app/views/insights/mobile/appStarts/components/eventSamples.tsx
+++ b/static/app/views/insights/mobile/appStarts/components/eventSamples.tsx
@@ -1,7 +1,7 @@
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
diff --git a/static/app/views/insights/mobile/appStarts/components/samples.tsx b/static/app/views/insights/mobile/appStarts/components/samples.tsx
index c5c98b8fbcdeab..0d4a2a2219ece7 100644
--- a/static/app/views/insights/mobile/appStarts/components/samples.tsx
+++ b/static/app/views/insights/mobile/appStarts/components/samples.tsx
@@ -3,7 +3,7 @@ import {useMemo, useState} from 'react';
import {Flex} from '@sentry/scraps/layout';
import {SegmentedControl} from '@sentry/scraps/segmentedControl';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx
index 6c7b8a0b2a0c94..2b15022a4022e5 100644
--- a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx
+++ b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.tsx
@@ -2,7 +2,7 @@ import {Fragment, useEffect} from 'react';
import styled from '@emotion/styled';
import omit from 'lodash/omit';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import {defined} from 'sentry/utils';
import {DurationUnit} from 'sentry/utils/discover/fields';
diff --git a/static/app/views/insights/mobile/common/components/tables/samplesTables.tsx b/static/app/views/insights/mobile/common/components/tables/samplesTables.tsx
index 64a3fd921fe148..84f0151ccfbaf2 100644
--- a/static/app/views/insights/mobile/common/components/tables/samplesTables.tsx
+++ b/static/app/views/insights/mobile/common/components/tables/samplesTables.tsx
@@ -3,7 +3,7 @@ import {useMemo, useState} from 'react';
import {Flex} from '@sentry/scraps/layout';
import {SegmentedControl} from '@sentry/scraps/segmentedControl';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import {useReleaseSelection} from 'sentry/views/insights/common/queries/useReleases';
import {SubregionSelector} from 'sentry/views/insights/common/views/spans/selectors/subregionSelector';
diff --git a/static/app/views/insights/mobile/common/components/tables/screensTable.spec.tsx b/static/app/views/insights/mobile/common/components/tables/screensTable.spec.tsx
index 1343f8fbdf9869..188434fc5bb8a0 100644
--- a/static/app/views/insights/mobile/common/components/tables/screensTable.spec.tsx
+++ b/static/app/views/insights/mobile/common/components/tables/screensTable.spec.tsx
@@ -1,6 +1,6 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {ScreensTable} from 'sentry/views/insights/mobile/common/components/tables/screensTable';
diff --git a/static/app/views/insights/mobile/common/components/tables/screensTable.tsx b/static/app/views/insights/mobile/common/components/tables/screensTable.tsx
index 30c48dfca14552..632df623ea7f08 100644
--- a/static/app/views/insights/mobile/common/components/tables/screensTable.tsx
+++ b/static/app/views/insights/mobile/common/components/tables/screensTable.tsx
@@ -16,7 +16,7 @@ import {useQueryBasedColumnResize} from 'sentry/components/tables/gridEditable/u
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {isFieldSortable, type MetaType} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {fieldAlignment} from 'sentry/utils/discover/fields';
diff --git a/static/app/views/insights/mobile/screenload/components/eventSamples.tsx b/static/app/views/insights/mobile/screenload/components/eventSamples.tsx
index 09c57ff99e4b55..f04a76b545c85e 100644
--- a/static/app/views/insights/mobile/screenload/components/eventSamples.tsx
+++ b/static/app/views/insights/mobile/screenload/components/eventSamples.tsx
@@ -3,7 +3,7 @@ import {useMemo} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Sort} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {decodeList, decodeScalar, decodeSorts} from 'sentry/utils/queryString';
diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx
index 08ee84bcb4b80d..8c80fa2132c569 100644
--- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx
+++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.spec.tsx
@@ -3,7 +3,7 @@ import {LocationFixture} from 'sentry-fixture/locationFixture';
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {EventSamplesTable} from 'sentry/views/insights/mobile/screenload/components/tables/eventSamplesTable';
describe('EventSamplesTable', () => {
diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx
index 1c03bf9ce949d4..f51c2dc3f1891f 100644
--- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx
+++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx
@@ -19,8 +19,7 @@ import {IconProfiling} from 'sentry/icons/iconProfiling';
import {t} from 'sentry/locale';
import {defined} from 'sentry/utils';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import type {MetaType} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView, MetaType} from 'sentry/utils/discover/eventView';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import type {Sort} from 'sentry/utils/discover/fields';
diff --git a/static/app/views/insights/mobile/screenload/views/screenLoadSpansPage.tsx b/static/app/views/insights/mobile/screenload/views/screenLoadSpansPage.tsx
index 910cf0d013f00c..33966ade958339 100644
--- a/static/app/views/insights/mobile/screenload/views/screenLoadSpansPage.tsx
+++ b/static/app/views/insights/mobile/screenload/views/screenLoadSpansPage.tsx
@@ -5,7 +5,7 @@ import omit from 'lodash/omit';
import {Flex} from '@sentry/scraps/layout';
import {SegmentedControl} from '@sentry/scraps/segmentedControl';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import {DurationUnit} from 'sentry/utils/discover/fields';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/insights/mobile/screens/components/screensOverview.tsx b/static/app/views/insights/mobile/screens/components/screensOverview.tsx
index 400946b3e25c1a..99ffb4c6bfbee8 100644
--- a/static/app/views/insights/mobile/screens/components/screensOverview.tsx
+++ b/static/app/views/insights/mobile/screens/components/screensOverview.tsx
@@ -6,7 +6,7 @@ import {wrapQueryInWildcards} from 'sentry/components/performance/searchBar';
import {SearchBar} from 'sentry/components/searchBar';
import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/insights/mobile/screens/components/screensOverviewTable.spec.tsx b/static/app/views/insights/mobile/screens/components/screensOverviewTable.spec.tsx
index 55d60b479ace83..975f35284094d6 100644
--- a/static/app/views/insights/mobile/screens/components/screensOverviewTable.spec.tsx
+++ b/static/app/views/insights/mobile/screens/components/screensOverviewTable.spec.tsx
@@ -6,7 +6,7 @@ import {ProjectFixture} from 'sentry-fixture/project';
import {render, screen} from 'sentry-test/reactTestingLibrary';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import {
ScreensOverviewTable,
diff --git a/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx b/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx
index 1c06b688d2eba2..19b21d9b2e67ae 100644
--- a/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx
+++ b/static/app/views/insights/mobile/screens/components/screensOverviewTable.tsx
@@ -4,8 +4,7 @@ import * as qs from 'query-string';
import {Link} from '@sentry/scraps/link';
import {t} from 'sentry/locale';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {MetaType} from 'sentry/utils/discover/eventView';
+import type {EventView, MetaType} from 'sentry/utils/discover/eventView';
import {NumberContainer} from 'sentry/utils/discover/styles';
import {formatPercentage} from 'sentry/utils/number/formatPercentage';
import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
diff --git a/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx b/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx
index a1ac8b4a99566c..89c089c855c4ac 100644
--- a/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx
+++ b/static/app/views/insights/mobile/screens/views/screensLandingPage.tsx
@@ -2,7 +2,7 @@ import {useCallback, useEffect, useState} from 'react';
import styled from '@emotion/styled';
import omit from 'lodash/omit';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import * as Layout from 'sentry/components/layouts/thirds';
import {TabbedCodeSnippet} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet';
import {DatePageFilter} from 'sentry/components/pageFilters/date/datePageFilter';
diff --git a/static/app/views/insights/mobile/ui/components/tables/spanOperationTable.tsx b/static/app/views/insights/mobile/ui/components/tables/spanOperationTable.tsx
index fe3326caf07215..02fd1e4ba985ab 100644
--- a/static/app/views/insights/mobile/ui/components/tables/spanOperationTable.tsx
+++ b/static/app/views/insights/mobile/ui/components/tables/spanOperationTable.tsx
@@ -7,7 +7,7 @@ import {Duration} from 'sentry/components/duration';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {NumberContainer} from 'sentry/utils/discover/styles';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {decodeList, decodeScalar} from 'sentry/utils/queryString';
diff --git a/static/app/views/insights/pages/agents/components/modelCostWidget.tsx b/static/app/views/insights/pages/agents/components/modelCostWidget.tsx
index c6a2ee391d42ee..4adb472b617f5d 100644
--- a/static/app/views/insights/pages/agents/components/modelCostWidget.tsx
+++ b/static/app/views/insights/pages/agents/components/modelCostWidget.tsx
@@ -3,7 +3,7 @@ import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {openInsightChartModal} from 'sentry/actionCreators/modal';
-import ExternalLink from 'sentry/components/links/externalLink';
+import {ExternalLink} from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {useFetchSpanTimeSeries} from 'sentry/utils/timeSeries/useFetchEventsTimeSeries';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx
index ad3b54b35546de..034de120f006d2 100644
--- a/static/app/views/insights/pages/agents/components/tracesTable.tsx
+++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx
@@ -9,6 +9,7 @@ import {Link} from '@sentry/scraps/link';
import {Text} from '@sentry/scraps/text';
import {Tooltip} from '@sentry/scraps/tooltip';
+import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {Pagination} from 'sentry/components/pagination';
import {Placeholder} from 'sentry/components/placeholder';
@@ -49,6 +50,9 @@ import {Referrer} from 'sentry/views/insights/pages/agents/utils/referrers';
import {TableUrlParams} from 'sentry/views/insights/pages/agents/utils/urlParams';
import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/DurationCell';
import {NumberCell} from 'sentry/views/insights/pages/platform/shared/table/NumberCell';
+import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
+import {TraceLayoutTabKeys} from 'sentry/views/performance/newTraceDetails/useTraceLayoutTabs';
+import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
interface TableData {
agents: string[];
@@ -92,10 +96,11 @@ const rightAlignColumns = new Set([
const DEFAULT_LIMIT = 10;
interface TracesTableProps {
- openTraceViewDrawer: ReturnType['openTraceViewDrawer'];
dashboardFilters?: DashboardFilters;
frameless?: boolean;
limit?: number;
+ linkToTraceView?: boolean;
+ openTraceViewDrawer?: ReturnType['openTraceViewDrawer'];
tableWidths?: number[];
}
@@ -105,6 +110,7 @@ export function TracesTable({
dashboardFilters,
limit = DEFAULT_LIMIT,
tableWidths,
+ linkToTraceView,
}: TracesTableProps) {
const {columns: columnOrder, handleResizeColumn} = useStateBasedColumnResize({
columns:
@@ -273,10 +279,11 @@ export function TracesTable({
dataRow={dataRow}
query={combinedQuery}
openTraceViewDrawer={openTraceViewDrawer}
+ linkToTraceView={linkToTraceView}
/>
);
},
- [combinedQuery, openTraceViewDrawer]
+ [combinedQuery, openTraceViewDrawer, linkToTraceView]
);
const additionalGridProps = frameless
@@ -324,23 +331,41 @@ const BodyCell = memo(function BodyCell({
dataRow,
query,
openTraceViewDrawer,
+ linkToTraceView,
}: {
column: GridColumnHeader;
dataRow: TableData;
- openTraceViewDrawer: (traceSlug: string, spanId?: string, timestamp?: number) => void;
query: string;
+ linkToTraceView?: boolean;
+ openTraceViewDrawer?: (traceSlug: string, spanId?: string, timestamp?: number) => void;
}) {
const organization = useOrganization();
const {selection} = usePageFilters();
+ const location = useLocation();
switch (column.key) {
case 'traceId':
+ if (linkToTraceView || !openTraceViewDrawer) {
+ const traceUrl = getTraceDetailsUrl({
+ organization,
+ traceSlug: dataRow.traceId,
+ dateSelection: normalizeDateTimeParams(selection.datetime),
+ timestamp: dataRow.timestamp / 1000,
+ location: {
+ ...location,
+ query: {},
+ },
+ source: TraceViewSources.AGENT_MONITORING,
+ tab: TraceLayoutTabKeys.AI_SPANS,
+ });
+ return {dataRow.traceId.slice(0, 8)};
+ }
return (
- openTraceViewDrawer(dataRow.traceId, undefined, dataRow.timestamp / 1000)
+ openTraceViewDrawer?.(dataRow.traceId, undefined, dataRow.timestamp / 1000)
}
>
{dataRow.traceId.slice(0, 8)}
diff --git a/static/app/views/insights/pages/mobile/am1OverviewPage.tsx b/static/app/views/insights/pages/mobile/am1OverviewPage.tsx
index 7be23a8bbfa38d..18a6def8378693 100644
--- a/static/app/views/insights/pages/mobile/am1OverviewPage.tsx
+++ b/static/app/views/insights/pages/mobile/am1OverviewPage.tsx
@@ -103,7 +103,7 @@ export function Am1MobileOverviewPage({datePageFilterProps}: Am1MobileOverviewPa
const eventView = generateMobilePerformanceEventView(
location,
projects,
- generateGenericPerformanceEventView(location, withStaticFilters, organization),
+ generateGenericPerformanceEventView(location, withStaticFilters),
withStaticFilters
);
const searchBarEventView = eventView.clone();
diff --git a/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx b/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx
index 03e421a73db9b6..36abcac295d991 100644
--- a/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx
+++ b/static/app/views/insights/pages/mobile/mobileOverviewPage.tsx
@@ -125,7 +125,7 @@ function EAPMobileOverviewPage({datePageFilterProps}: EAPMobileOverviewPageProps
const eventView = generateMobilePerformanceEventView(
location,
projects,
- generateGenericPerformanceEventView(location, withStaticFilters, organization),
+ generateGenericPerformanceEventView(location, withStaticFilters),
withStaticFilters,
true
);
diff --git a/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts b/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts
index fd51875845094d..442988ae8db1bf 100644
--- a/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts
+++ b/static/app/views/insights/pages/useOverviewPageTrackAnalytics.ts
@@ -2,7 +2,7 @@ import {useEffect} from 'react';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';
diff --git a/static/app/views/issueDetails/activitySection.tsx b/static/app/views/issueDetails/activitySection.tsx
index 79628832950b39..ef5f526e3ff285 100644
--- a/static/app/views/issueDetails/activitySection.tsx
+++ b/static/app/views/issueDetails/activitySection.tsx
@@ -4,7 +4,7 @@ import {ActivityAuthor} from 'sentry/components/activity/author';
import {ActivityItem} from 'sentry/components/activity/item';
import {Note} from 'sentry/components/activity/note';
import {NoteInputWithStorage} from 'sentry/components/activity/note/inputWithStorage';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import type {NoteType} from 'sentry/types/alerts';
import type {Group, GroupActivity} from 'sentry/types/group';
import {GroupActivityType} from 'sentry/types/group';
diff --git a/static/app/views/issueDetails/allEventsTable.tsx b/static/app/views/issueDetails/allEventsTable.tsx
index 46ad1157259e6c..5eeafae94fc252 100644
--- a/static/app/views/issueDetails/allEventsTable.tsx
+++ b/static/app/views/issueDetails/allEventsTable.tsx
@@ -1,137 +1,16 @@
-import {useEffect, useMemo, useState} from 'react';
-import {useTheme} from '@emotion/react';
+import {useMemo} from 'react';
-import {getSampleEventQuery} from 'sentry/components/events/eventStatisticalDetector/eventComparison/eventDisplay';
-import {LoadingError} from 'sentry/components/loadingError';
import {
PlatformCategory,
profiling as PROFILING_PLATFORMS,
} from 'sentry/data/platformCategories';
import {t} from 'sentry/locale';
-import type {EventTransaction} from 'sentry/types/event';
import type {Group} from 'sentry/types/group';
-import {IssueCategory, IssueType} from 'sentry/types/group';
+import {IssueCategory} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import type {PlatformKey} from 'sentry/types/project';
-import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import EventView from 'sentry/utils/discover/eventView';
-import {DiscoverDatasets} from 'sentry/utils/discover/types';
-import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
import {platformToCategory} from 'sentry/utils/platform';
-import {useApiQuery} from 'sentry/utils/queryClient';
-import {decodeSorts} from 'sentry/utils/queryString';
import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useRoutes} from 'sentry/utils/useRoutes';
-import {EventsTable} from 'sentry/views/performance/transactionSummary/transactionEvents/eventsTable';
-
-interface Props {
- excludedTags: string[];
- group: Group;
- organization: Organization;
-}
-
-const makeGroupPreviewRequestUrl = ({
- orgSlug,
- groupId,
-}: {
- groupId: string;
- orgSlug: string;
-}) => {
- return getApiUrl(
- '/organizations/$organizationIdOrSlug/issues/$issueId/events/$eventId/',
- {
- path: {organizationIdOrSlug: orgSlug, issueId: groupId, eventId: 'latest'},
- }
- );
-};
-
-export function AllEventsTable({organization, excludedTags, group}: Props) {
- const location = useLocation();
- const theme = useTheme();
- const config = getConfigForIssueType(group, group.project);
- const [error, setError] = useState('');
- const routes = useRoutes();
- const {fields, columnTitles} = useEventColumns(group, organization);
- const now = useMemo(() => Date.now(), []);
-
- const endpointUrl = makeGroupPreviewRequestUrl({
- orgSlug: organization.slug,
- groupId: group.id,
- });
-
- const isRegressionIssue = group.issueType === IssueType.PERFORMANCE_ENDPOINT_REGRESSION;
- const {data, isLoading, isLoadingError} = useApiQuery([endpointUrl], {
- staleTime: 60000,
- enabled: isRegressionIssue,
- });
-
- const eventView = EventView.fromLocation(location);
- if (config.usesIssuePlatform) {
- eventView.dataset = DiscoverDatasets.ISSUE_PLATFORM;
- }
- eventView.fields = fields.map(fieldName => ({field: fieldName}));
-
- eventView.sorts = decodeSorts(location.query.sort).filter(sort =>
- fields.includes(sort.field)
- );
-
- useEffect(() => {
- setError('');
- }, [eventView.query]);
-
- if (!eventView.sorts.length) {
- eventView.sorts = [{field: 'timestamp', kind: 'desc'}];
- }
-
- eventView.statsPeriod = '90d';
-
- let idQuery = `issue.id:${group.id}`;
- if (isRegressionIssue) {
- const {transaction, aggregateRange2, breakpoint} =
- data?.occurrence?.evidenceData ?? {};
-
- // Surface the "bad" events that occur after the breakpoint
- idQuery = getSampleEventQuery({
- transaction,
- durationBaseline: aggregateRange2,
- addUpperBound: false,
- });
-
- eventView.dataset = DiscoverDatasets.DISCOVER;
- eventView.start = new Date(breakpoint * 1000).toISOString();
- eventView.end = new Date(now).toISOString();
- eventView.statsPeriod = undefined;
- }
- eventView.project = [parseInt(group.project.id, 10)];
- eventView.query = `${idQuery} ${location.query.query || ''}`;
-
- if (error || isLoadingError) {
- return (
- setError('')} />
- );
- }
-
- return (
- setError(msg ?? '')}
- transactionName=""
- columnTitles={columnTitles.slice()}
- referrer="api.issues.issue_events"
- isEventLoading={isLoading}
- />
- );
-}
type ColumnInfo = {columnTitles: string[]; fields: string[]};
diff --git a/static/app/views/issueDetails/groupActivity.tsx b/static/app/views/issueDetails/groupActivity.tsx
deleted file mode 100644
index 0eb2860ccb7e4a..00000000000000
--- a/static/app/views/issueDetails/groupActivity.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import {Fragment, useCallback, useMemo} from 'react';
-
-import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
-import type {
- TContext,
- TData,
- TError,
- TVariables,
-} from 'sentry/components/feedback/useMutateActivity';
-import {useMutateActivity} from 'sentry/components/feedback/useMutateActivity';
-import * as Layout from 'sentry/components/layouts/thirds';
-import {LoadingError} from 'sentry/components/loadingError';
-import {LoadingIndicator} from 'sentry/components/loadingIndicator';
-import {ReprocessedBox} from 'sentry/components/reprocessedBox';
-import {t} from 'sentry/locale';
-import {GroupStore} from 'sentry/stores/groupStore';
-import type {NoteType} from 'sentry/types/alerts';
-import type {
- Group,
- GroupActivityNote,
- GroupActivityReprocess,
- GroupActivity as GroupActivityType,
-} from 'sentry/types/group';
-import type {User} from 'sentry/types/user';
-import type {MutateOptions} from 'sentry/utils/queryClient';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import {useParams} from 'sentry/utils/useParams';
-import {ActivitySection} from 'sentry/views/issueDetails/activitySection';
-import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
-import {useGroup} from 'sentry/views/issueDetails/useGroup';
-import {
- getGroupMostRecentActivity,
- getGroupReprocessingStatus,
- ReprocessingStatus,
- useHasStreamlinedUI,
-} from 'sentry/views/issueDetails/utils';
-
-type MutateActivityOptions = MutateOptions;
-
-interface GroupActivityProps {
- group: Group;
-}
-
-function GroupActivity({group}: GroupActivityProps) {
- const organization = useOrganization();
- const {activity: activities, count, id: groupId} = group;
- const groupCount = Number(count);
- const mostRecentActivity = getGroupMostRecentActivity(activities);
- const reprocessingStatus = getGroupReprocessingStatus(group, mostRecentActivity);
- const mutators = useMutateActivity({
- organization,
- group,
- });
-
- const deleteOptions: MutateActivityOptions = useMemo(() => {
- return {
- onError: () => {
- addErrorMessage(t('Failed to delete comment'));
- },
- onSuccess: () => {
- addSuccessMessage(t('Comment removed'));
- },
- };
- }, []);
-
- const createOptions: MutateActivityOptions = useMemo(() => {
- return {
- onError: () => {
- addErrorMessage(t('Unable to post comment'));
- },
- onSuccess: data => {
- GroupStore.addActivity(group.id, data);
- addSuccessMessage(t('Comment posted'));
- },
- };
- }, [group.id]);
-
- const updateOptions: MutateActivityOptions = useMemo(() => {
- return {
- onError: () => {
- addErrorMessage(t('Unable to update comment'));
- },
- onSuccess: data => {
- const d = data as GroupActivityNote;
- GroupStore.updateActivity(group.id, data.id, {text: d.data.text});
- addSuccessMessage(t('Comment updated'));
- },
- };
- }, [group.id]);
-
- const handleDelete = useCallback(
- (item: GroupActivityType) => {
- const restore = group.activity.find(activity => activity.id === item.id);
- const index = GroupStore.removeActivity(group.id, item.id);
-
- if (index === -1 || restore === undefined) {
- addErrorMessage(t('Failed to delete comment'));
- return;
- }
- mutators.handleDelete(
- item.id,
- group.activity.filter(a => a.id !== item.id),
- deleteOptions
- );
- },
- [deleteOptions, group.activity, mutators, group.id]
- );
-
- const handleCreate = useCallback(
- (n: NoteType, _me: User) => {
- mutators.handleCreate(n, group.activity, createOptions);
- },
- [createOptions, group.activity, mutators]
- );
-
- const handleUpdate = useCallback(
- (item: GroupActivityType, n: NoteType) => {
- mutators.handleUpdate(n, item.id, group.activity, updateOptions);
- },
- [updateOptions, group.activity, mutators]
- );
-
- return (
-
- {(reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HASNT_EVENT ||
- reprocessingStatus === ReprocessingStatus.REPROCESSED_AND_HAS_EVENT) && (
-
-
-
- )}
-
-
-
-
-
- );
-}
-
-function GroupActivityRoute() {
- const hasStreamlinedUI = useHasStreamlinedUI();
- const params = useParams<{groupId: string}>();
-
- const {
- data: group,
- isPending: isGroupPending,
- isError: isGroupError,
- refetch: refetchGroup,
- } = useGroup({groupId: params.groupId});
-
- if (hasStreamlinedUI) {
- return ;
- }
-
- if (isGroupPending) {
- return ;
- }
-
- if (isGroupError) {
- return ;
- }
-
- return (
-
-
-
- );
-}
-
-export default GroupActivityRoute;
diff --git a/static/app/views/issueDetails/groupEventAttachments/groupEventAttachments.tsx b/static/app/views/issueDetails/groupEventAttachments/groupEventAttachments.tsx
index 2b42702f76ec92..c42d1716cd3c3b 100644
--- a/static/app/views/issueDetails/groupEventAttachments/groupEventAttachments.tsx
+++ b/static/app/views/issueDetails/groupEventAttachments/groupEventAttachments.tsx
@@ -17,7 +17,6 @@ import {useNavigate} from 'sentry/utils/useNavigate';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useEventQuery} from 'sentry/views/issueDetails/streamline/hooks/useEventQuery';
import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
import {
EventAttachmentFilter,
@@ -38,7 +37,6 @@ const DEFAULT_ATTACHMENTS_TAB = EventAttachmentFilter.ALL;
export function GroupEventAttachments({project, group}: GroupEventAttachmentsProps) {
const location = useLocation();
const organization = useOrganization();
- const hasStreamlinedUI = useHasStreamlinedUI();
const eventQuery = useEventQuery();
const eventView = useIssueDetailsEventView({group});
const navigate = useNavigate();
@@ -85,12 +83,11 @@ export function GroupEventAttachments({project, group}: GroupEventAttachmentsPro
group,
orgSlug: organization.slug,
cursor: location.query.cursor as string | undefined,
- // We only want to filter by date/query/environment if we're using the Streamlined UI
- environment: hasStreamlinedUI ? (eventView.environment as string[]) : undefined,
- start: hasStreamlinedUI ? eventView.start : undefined,
- end: hasStreamlinedUI ? eventView.end : undefined,
- statsPeriod: hasStreamlinedUI ? eventView.statsPeriod : undefined,
- eventQuery: hasStreamlinedUI ? eventQuery : undefined,
+ environment: eventView.environment as string[],
+ start: eventView.start,
+ end: eventView.end,
+ statsPeriod: eventView.statsPeriod,
+ eventQuery,
});
};
@@ -153,19 +150,15 @@ export function GroupEventAttachments({project, group}: GroupEventAttachmentsPro
return (
- {hasStreamlinedUI ? (
-
-
-
- {t('Results are filtered by the selections above.')}
-
- setPreviouslyUsedAttachmentsTab(key)}
- />
+
+
+
+ {t('Results are filtered by the selections above.')}
- ) : (
-
- )}
+ setPreviouslyUsedAttachmentsTab(key)}
+ />
+
{activeAttachmentsTab === EventAttachmentFilter.SCREENSHOT
? renderScreenshotGallery()
: renderAttachmentsTable()}
diff --git a/static/app/views/issueDetails/groupEventAttachments/index.tsx b/static/app/views/issueDetails/groupEventAttachments/index.tsx
index 86c5db26d38a9e..78ee90e532cc47 100644
--- a/static/app/views/issueDetails/groupEventAttachments/index.tsx
+++ b/static/app/views/issueDetails/groupEventAttachments/index.tsx
@@ -1,4 +1,3 @@
-import {css} from '@emotion/react';
import styled from '@emotion/styled';
import Feature from 'sentry/components/acl/feature';
@@ -10,13 +9,11 @@ import {t} from 'sentry/locale';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {useGroup} from 'sentry/views/issueDetails/useGroup';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
import {GroupEventAttachments} from './groupEventAttachments';
function GroupEventAttachmentsContainer() {
const organization = useOrganization();
- const hasStreamlinedUI = useHasStreamlinedUI();
const params = useParams();
const {
@@ -42,7 +39,7 @@ function GroupEventAttachmentsContainer() {
)}
>
-
+
@@ -51,18 +48,14 @@ function GroupEventAttachmentsContainer() {
);
}
-const StyledLayoutBody = styled(Layout.Body)<{hasStreamlinedUI?: boolean}>`
- ${p =>
- p.hasStreamlinedUI &&
- css`
- border: 1px solid ${p.theme.tokens.border.primary};
- border-radius: ${p.theme.radius.md};
- padding: ${p.theme.space.xl} 0;
+const StyledLayoutBody = styled(Layout.Body)`
+ border: 1px solid ${p => p.theme.tokens.border.primary};
+ border-radius: ${p => p.theme.radius.md};
+ padding: ${p => p.theme.space.xl} 0;
- @media (min-width: ${p.theme.breakpoints.md}) {
- padding: ${p.theme.space.xl} ${p.theme.space.xl};
- }
- `}
+ @media (min-width: ${p => p.theme.breakpoints.md}) {
+ padding: ${p => p.theme.space.xl} ${p => p.theme.space.xl};
+ }
`;
export default GroupEventAttachmentsContainer;
diff --git a/static/app/views/issueDetails/groupEventAttachments/useGroupEventAttachments.tsx b/static/app/views/issueDetails/groupEventAttachments/useGroupEventAttachments.tsx
index 66796139111a43..00b6e9b93cae9b 100644
--- a/static/app/views/issueDetails/groupEventAttachments/useGroupEventAttachments.tsx
+++ b/static/app/views/issueDetails/groupEventAttachments/useGroupEventAttachments.tsx
@@ -10,7 +10,6 @@ import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useEventQuery} from 'sentry/views/issueDetails/streamline/hooks/useEventQuery';
import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
interface UseGroupEventAttachmentsOptions {
activeAttachmentsTab: 'all' | 'onlyCrash' | 'screenshot';
@@ -108,7 +107,6 @@ export function useGroupEventAttachments({
activeAttachmentsTab,
options,
}: UseGroupEventAttachmentsOptions) {
- const hasStreamlinedUI = useHasStreamlinedUI();
const location = useLocation();
const organization = useOrganization();
const eventQuery = useEventQuery();
@@ -116,7 +114,7 @@ export function useGroupEventAttachments({
const hasSetStatsPeriod =
location.query.statsPeriod || location.query.start || location.query.end;
- const fetchAllAvailable = hasStreamlinedUI ? options?.fetchAllAvailable : true;
+ const fetchAllAvailable = options?.fetchAllAvailable;
const {
data: attachments = [],
isPending,
diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx
index f7089dffc13f5a..c84f6b9bfab7ec 100644
--- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx
+++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx
@@ -8,7 +8,7 @@ import {usePrompt} from 'sentry/actionCreators/prompts';
import Feature from 'sentry/components/acl/feature';
import {GuideAnchor} from 'sentry/components/assistant/guideAnchor';
import {CommitRow} from 'sentry/components/commitRow';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {BreadcrumbsDataSection} from 'sentry/components/events/breadcrumbs/breadcrumbsDataSection';
import {EventContexts} from 'sentry/components/events/contexts';
import {EventDevice} from 'sentry/components/events/device';
@@ -61,16 +61,21 @@ import {DataSection} from 'sentry/components/events/styles';
import {SuspectCommits} from 'sentry/components/events/suspectCommits';
import {EventUserFeedback} from 'sentry/components/events/userFeedback';
import {Placeholder} from 'sentry/components/placeholder';
+import {IssueStackTrace} from 'sentry/components/stackTrace/issueStackTrace';
import {IconChevron} from 'sentry/icons';
import {t} from 'sentry/locale';
-import type {Entry, Event, EventTransaction} from 'sentry/types/event';
+import type {Entry, EntryMap, Event, EventTransaction} from 'sentry/types/event';
import {EntryType} from 'sentry/types/event';
import type {Group} from 'sentry/types/group';
import {IssueType} from 'sentry/types/group';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
-import {isJavascriptPlatform, isMobilePlatform} from 'sentry/utils/platform';
+import {
+ isJavascriptPlatform,
+ isMobilePlatform,
+ isNativePlatform,
+} from 'sentry/utils/platform';
import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent';
import {useOrganization} from 'sentry/utils/useOrganization';
import {MetricIssuesSection} from 'sentry/views/issueDetails/metricIssues/metricIssuesSection';
@@ -103,11 +108,15 @@ export function EventDetailsContent({
}: Required>) {
const organization = useOrganization();
const hasStreamlinedUI = useHasStreamlinedUI();
+ const shouldUseNewStackTrace =
+ organization.features.includes('issue-details-new-stack-trace') &&
+ // New stack trace is currently only non-native platforms.
+ !isNativePlatform(event.platform);
const tagsRef = useRef(null);
const eventEntries = useMemo(() => {
const {entries = []} = event;
- return entries.reduce>>((entryMap, entry) => {
- entryMap[entry.type] = entry;
+ return entries.reduce>((entryMap, entry) => {
+ (entryMap as Record)[entry.type] = entry;
return entryMap;
}, {});
}, [event]);
@@ -269,24 +278,42 @@ export function EventDetailsContent({
>
{defined(eventEntries[EntryType.EXCEPTION]) && (
-
+ {shouldUseNewStackTrace ? (
+
+ ) : (
+
+ )}
)}
{issueTypeConfig.stacktrace.enabled &&
defined(eventEntries[EntryType.STACKTRACE]) && (
-
+ {shouldUseNewStackTrace ? (
+
+ ) : (
+
+ )}
)}
{defined(eventEntries[EntryType.THREADS]) && (
diff --git a/static/app/views/issueDetails/groupEvents.tsx b/static/app/views/issueDetails/groupEvents.tsx
index bf7e3da6e5cec7..7476663567470e 100644
--- a/static/app/views/issueDetails/groupEvents.tsx
+++ b/static/app/views/issueDetails/groupEvents.tsx
@@ -1,86 +1,10 @@
-import {useCallback} from 'react';
-import styled from '@emotion/styled';
-
-import * as Layout from 'sentry/components/layouts/thirds';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
-import type {Group} from 'sentry/types/group';
-import {normalizeUrl} from 'sentry/utils/url/normalizeUrl';
-import {useCleanQueryParamsOnRouteLeave} from 'sentry/utils/useCleanQueryParamsOnRouteLeave';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useNavigate} from 'sentry/utils/useNavigate';
-import {useOrganization} from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {EventList} from 'sentry/views/issueDetails/streamline/eventList';
-import {EventSearch} from 'sentry/views/issueDetails/streamline/eventSearch';
-import {ALL_EVENTS_EXCLUDED_TAGS} from 'sentry/views/issueDetails/streamline/hooks/useEventQuery';
import {useGroup} from 'sentry/views/issueDetails/useGroup';
-import {
- useEnvironmentsFromUrl,
- useHasStreamlinedUI,
-} from 'sentry/views/issueDetails/utils';
-
-import {AllEventsTable} from './allEventsTable';
-
-interface GroupEventsProps {
- group: Group;
-}
-
-function GroupEvents({group}: GroupEventsProps) {
- const location = useLocation();
- const environments = useEnvironmentsFromUrl();
- const params = useParams<{groupId: string}>();
- const organization = useOrganization();
- const navigate = useNavigate();
-
- useCleanQueryParamsOnRouteLeave({
- fieldsToClean: ['cursor', 'query'],
- shouldClean: newLocation =>
- newLocation.pathname.includes(`/issues/${params.groupId}/`),
- });
- const handleSearch = useCallback(
- (query: string) => {
- navigate(
- normalizeUrl({
- pathname: `/organizations/${organization.slug}/issues/${params.groupId}/events/`,
- query: {...location.query, query},
- })
- );
- },
- [location, organization, params.groupId, navigate]
- );
-
- const query = (location.query?.query ?? '') as string;
-
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-const AllEventsFilters = styled('div')`
- margin-bottom: ${p => p.theme.space.xl};
-`;
-
-// TODO(streamlined-ui): Remove this file completely and change route to new events list
function IssueEventsList() {
- const hasStreamlinedUI = useHasStreamlinedUI();
const params = useParams<{groupId: string}>();
const {
data: group,
@@ -97,11 +21,7 @@ function IssueEventsList() {
return ;
}
- if (hasStreamlinedUI) {
- return ;
- }
-
- return ;
+ return ;
}
export default IssueEventsList;
diff --git a/static/app/views/issueDetails/groupMerged/groupMergedTab.tsx b/static/app/views/issueDetails/groupMerged/groupMergedTab.tsx
deleted file mode 100644
index 3c97fa193fd06d..00000000000000
--- a/static/app/views/issueDetails/groupMerged/groupMergedTab.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as Layout from 'sentry/components/layouts/thirds';
-import {LoadingError} from 'sentry/components/loadingError';
-import {LoadingIndicator} from 'sentry/components/loadingIndicator';
-import type {Group} from 'sentry/types/group';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import {useParams} from 'sentry/utils/useParams';
-import {useProjectFromSlug} from 'sentry/utils/useProjectFromSlug';
-import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
-import {GroupMergedView} from 'sentry/views/issueDetails/groupMerged';
-import {useGroup} from 'sentry/views/issueDetails/useGroup';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
-
-function GroupMergedTab() {
- const params = useParams<{groupId: Group['id']}>();
- const location = useLocation();
- const hasStreamlinedUI = useHasStreamlinedUI();
- const organization = useOrganization();
-
- const {
- data: group,
- isPending: isGroupPending,
- isError: isGroupError,
- refetch: refetchGroup,
- } = useGroup({groupId: params.groupId});
- const project = useProjectFromSlug({
- organization,
- projectSlug: group?.project.slug,
- });
-
- // TODO(streamline-ui): Point router to event details page since merged issues opens in a drawer.
- if (hasStreamlinedUI) {
- return ;
- }
-
- if (isGroupPending || !project) {
- return ;
- }
-
- if (isGroupError) {
- return ;
- }
-
- return (
-
-
-
-
-
- );
-}
-
-export default GroupMergedTab;
diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.tsx
index 86a8d69646c23e..fb23253dcae55c 100644
--- a/static/app/views/issueDetails/groupReplays/groupReplays.tsx
+++ b/static/app/views/issueDetails/groupReplays/groupReplays.tsx
@@ -1,5 +1,4 @@
import {Fragment, useEffect, useMemo} from 'react';
-import {css} from '@emotion/react';
import styled from '@emotion/styled';
import type {Location, Query} from 'history';
@@ -34,7 +33,7 @@ import {IconPlay, IconUser} from 'sentry/icons';
import {t, tn} from 'sentry/locale';
import type {Group} from 'sentry/types/group';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {useReplayCountForIssues} from 'sentry/utils/replayCount/useReplayCountForIssues';
import {useLoadReplayReader} from 'sentry/utils/replays/hooks/useLoadReplayReader';
import {useReplayList} from 'sentry/utils/replays/hooks/useReplayList';
@@ -43,7 +42,6 @@ import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import {GroupReplaysPlayer} from 'sentry/views/issueDetails/groupReplays/groupReplaysPlayer';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
import {useAllMobileProj} from 'sentry/views/replays/detail/useAllMobileProj';
import type {ReplayListLocationQuery, ReplayListRecord} from 'sentry/views/replays/types';
@@ -92,7 +90,6 @@ export function GroupReplays({group}: Props) {
function GroupReplaysContent({group}: Props) {
const organization = useOrganization();
const location = useLocation();
- const hasStreamlinedUI = useHasStreamlinedUI();
const {eventView, fetchError, isFetching} = useReplaysFromIssue({
group,
@@ -121,9 +118,9 @@ function GroupReplaysContent({group}: Props) {
if (!eventView) {
// Shown on load and no replay data available
return (
-
+
- {hasStreamlinedUI ? : null}
+
{isFetching ? (
@@ -148,9 +145,9 @@ function GroupReplaysContent({group}: Props) {
return (
-
+
- {hasStreamlinedUI ? : null}
+
{replayCount > 50
@@ -328,17 +325,12 @@ function ReplayOverlay({
);
}
-const StyledLayoutPage = styled(Layout.Page)<{hasStreamlinedUI?: boolean}>`
+const StyledLayoutPage = styled(Layout.Page)`
background-color: ${p => p.theme.tokens.background.primary};
gap: ${p => p.theme.space.lg};
-
- ${p =>
- p.hasStreamlinedUI &&
- css`
- border: 1px solid ${p.theme.tokens.border.primary};
- border-radius: ${p.theme.radius.md};
- padding: ${p.theme.space.lg};
- `}
+ border: 1px solid ${p => p.theme.tokens.border.primary};
+ border-radius: ${p => p.theme.radius.md};
+ padding: ${p => p.theme.space.lg};
`;
const StyledBreak = styled('hr')`
diff --git a/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx b/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx
index efb475b6307804..6634543fff53dd 100644
--- a/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx
+++ b/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx
@@ -6,7 +6,7 @@ import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants';
import {DEFAULT_REPLAY_LIST_SORT} from 'sentry/components/replays/table/useReplayTableSort';
import {IssueCategory, type Group} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar} from 'sentry/utils/queryString';
import type {RequestError} from 'sentry/utils/requestError/requestError';
import {useApi} from 'sentry/utils/useApi';
diff --git a/static/app/views/issueDetails/groupSimilarIssues/groupSimilarIssuesTab.tsx b/static/app/views/issueDetails/groupSimilarIssues/groupSimilarIssuesTab.tsx
deleted file mode 100644
index fc23c8ffc6391e..00000000000000
--- a/static/app/views/issueDetails/groupSimilarIssues/groupSimilarIssuesTab.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as Layout from 'sentry/components/layouts/thirds';
-import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
-import {GroupSimilarIssues} from 'sentry/views/issueDetails/groupSimilarIssues/similarIssues';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
-
-function GroupSimilarIssuesTab() {
- const hasStreamlinedUI = useHasStreamlinedUI();
-
- // TODO(streamlined-ui): Remove this component and point router to GroupEventDetails
- // Similar issues will open in a drawer
- if (hasStreamlinedUI) {
- return ;
- }
-
- return (
-
-
-
-
-
- );
-}
-
-export default GroupSimilarIssuesTab;
diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx
index c3f8d0eae68d05..d242e673114908 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx
@@ -14,7 +14,7 @@ import type {Project} from 'sentry/types/project';
import {useOrganization} from 'sentry/utils/useOrganization';
import {SimilarStackTraceItem} from './item';
-import Toolbar from './toolbar';
+import {SimilarToolbar} from './toolbar';
type DefaultProps = {
filteredItems: SimilarItem[];
@@ -84,7 +84,7 @@ export function List({
)}
- {
+export class SimilarToolbar extends Component {
state: State = initialState;
componentWillUnmount() {
@@ -106,7 +106,6 @@ class SimilarToolbar extends Component {
);
}
}
-export default SimilarToolbar;
const StyledToolbarHeader = styled(ToolbarHeader)`
flex: 1;
diff --git a/static/app/views/issueDetails/groupTags/groupTagValues.spec.tsx b/static/app/views/issueDetails/groupTags/groupTagValues.spec.tsx
deleted file mode 100644
index c1c6df66741324..00000000000000
--- a/static/app/views/issueDetails/groupTags/groupTagValues.spec.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import {GroupFixture} from 'sentry-fixture/group';
-import {ProjectFixture} from 'sentry-fixture/project';
-import {TagsFixture} from 'sentry-fixture/tags';
-import {TagValuesFixture} from 'sentry-fixture/tagvalues';
-
-import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
-
-import {ProjectsStore} from 'sentry/stores/projectsStore';
-import {GroupTagValues} from 'sentry/views/issueDetails/groupTags/groupTagValues';
-
-describe('GroupTagValues', () => {
- const group = GroupFixture();
- const tags = TagsFixture();
- const project = ProjectFixture();
-
- const makeInitialRouterConfig = (tagKey: string, environment?: string[] | string) => ({
- location: {
- pathname: `/organizations/org-slug/issues/${group.id}/tags/${tagKey}/`,
- query: {
- ...(environment && {environment}),
- },
- },
- route: '/organizations/:orgId/issues/:groupId/tags/:tagKey/',
- });
-
- beforeEach(() => {
- ProjectsStore.init();
- ProjectsStore.loadInitialData([project]);
- MockApiClient.addMockResponse({
- url: `/organizations/org-slug/issues/${group.id}/`,
- body: group,
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/user/',
- body: tags.find(({key}) => key === 'user'),
- });
- });
-
- afterEach(() => {
- MockApiClient.clearMockResponses();
- });
-
- it('renders a list of tag values', async () => {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/user/values/',
- body: TagValuesFixture(),
- });
- render(, {
- initialRouterConfig: makeInitialRouterConfig('user'),
- });
-
- // Special case for user tag - column title changes to Affected Users
- expect(await screen.findByText('Affected Users')).toBeInTheDocument();
-
- // Affected user column
- expect(screen.getByText('David Cramer')).toBeInTheDocument();
- // Percent column
- expect(screen.getByText('16.67%')).toBeInTheDocument();
- // Count column
- expect(screen.getByText('3')).toBeInTheDocument();
- });
-
- it('can page through tag values', async () => {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/user/values/',
- body: TagValuesFixture(),
- headers: {
- Link:
- '; rel="previous"; results="false"; cursor="0:0:1", ' +
- '; rel="next"; results="true"; cursor="0:100:0"',
- },
- });
- const {router} = render(, {
- initialRouterConfig: makeInitialRouterConfig('user'),
- });
-
- expect(await screen.findByRole('button', {name: 'Previous'})).toBeDisabled();
- expect(screen.getByRole('button', {name: 'Next'})).toBeEnabled();
-
- // Clicking next button loads page with query param ?cursor=0:100:0
- await userEvent.click(screen.getByRole('button', {name: 'Next'}));
- await waitFor(() => {
- expect(router.location.query.cursor).toBe('0:100:0');
- });
- });
-
- it('navigates to issue details events tab with correct query params', async () => {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/user/values/',
- body: TagValuesFixture(),
- });
- const {router} = render(, {
- initialRouterConfig: makeInitialRouterConfig('user'),
- });
-
- await userEvent.click(await screen.findByRole('button', {name: 'More'}));
- await userEvent.click(
- screen.getByRole('menuitemradio', {name: 'Search All Issues with Tag Value'})
- );
-
- await waitFor(() => {
- expect(router.location.pathname).toBe('/organizations/org-slug/issues/');
- });
- expect(router.location.query.query).toBe('user.username:david');
- });
-
- it('renders an error message if tag values request fails', async () => {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/user/values/',
- statusCode: 500,
- });
-
- render(, {
- initialRouterConfig: makeInitialRouterConfig('user', 'staging'),
- });
-
- expect(
- await screen.findByText('There was an error loading tag details')
- ).toBeInTheDocument();
- });
-
- it('renders an error message if no tag values are returned because of environment selection', async () => {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/user/values/',
- body: [],
- });
-
- render(, {
- initialRouterConfig: makeInitialRouterConfig('user', 'staging'),
- });
-
- expect(
- await screen.findByText(
- 'No tags were found for the currently selected environments'
- )
- ).toBeInTheDocument();
- });
-});
diff --git a/static/app/views/issueDetails/groupTags/groupTagValues.tsx b/static/app/views/issueDetails/groupTags/groupTagValues.tsx
deleted file mode 100644
index e94662b0025d98..00000000000000
--- a/static/app/views/issueDetails/groupTags/groupTagValues.tsx
+++ /dev/null
@@ -1,426 +0,0 @@
-import {Fragment, useEffect} from 'react';
-import styled from '@emotion/styled';
-
-import {LinkButton} from '@sentry/scraps/button';
-import type {FlexProps} from '@sentry/scraps/layout';
-import {Flex, Grid} from '@sentry/scraps/layout';
-import {ExternalLink, Link} from '@sentry/scraps/link';
-
-import {useFetchIssueTag, useFetchIssueTagValues} from 'sentry/actionCreators/group';
-import {addMessage} from 'sentry/actionCreators/indicator';
-import {DataExport, ExportQueryType} from 'sentry/components/dataExport';
-import {DeviceName} from 'sentry/components/deviceName';
-import {DropdownMenu} from 'sentry/components/dropdownMenu';
-import {UserBadge} from 'sentry/components/idBadge/userBadge';
-import * as Layout from 'sentry/components/layouts/thirds';
-import {LoadingError} from 'sentry/components/loadingError';
-import {LoadingIndicator} from 'sentry/components/loadingIndicator';
-import {extractSelectionParameters} from 'sentry/components/pageFilters/parse';
-import {Pagination} from 'sentry/components/pagination';
-import {PanelTable} from 'sentry/components/panels/panelTable';
-import {TimeSince} from 'sentry/components/timeSince';
-import {IconArrow, IconEllipsis, IconMail, IconOpen} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import type {SavedQueryVersions} from 'sentry/types/organization';
-import {percent} from 'sentry/utils';
-import EventView from 'sentry/utils/discover/eventView';
-import {SavedQueryDatasets} from 'sentry/utils/discover/types';
-import {isUrl} from 'sentry/utils/string/isUrl';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import {useParams} from 'sentry/utils/useParams';
-import {useProjectFromSlug} from 'sentry/utils/useProjectFromSlug';
-import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
-import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
-import {useGroup} from 'sentry/views/issueDetails/useGroup';
-import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
-import {
- useEnvironmentsFromUrl,
- useHasStreamlinedUI,
-} from 'sentry/views/issueDetails/utils';
-
-type RouteParams = {
- groupId: string;
- orgId: string;
- tagKey?: string;
-};
-
-const DEFAULT_SORT = 'count';
-
-function useTagQueries({
- groupId,
- tagKey,
- environments,
- sort,
- cursor,
-}: {
- environments: string[];
- groupId: string;
- sort: string | string[];
- tagKey: string;
- cursor?: string;
-}) {
- const organization = useOrganization();
-
- const {
- data: tagValueList,
- isPending: tagValueListIsLoading,
- isError: tagValueListIsError,
- getResponseHeader,
- } = useFetchIssueTagValues({
- orgSlug: organization.slug,
- groupId,
- tagKey,
- environment: environments,
- sort,
- cursor,
- });
- const {data: tag, isError: tagIsError} = useFetchIssueTag({
- orgSlug: organization.slug,
- groupId,
- tagKey,
- });
-
- useEffect(() => {
- if (tagIsError) {
- addMessage(t('Failed to fetch total tag values'), 'error');
- }
- }, [tagIsError]);
-
- return {
- tagValueList,
- tag,
- isLoading: tagValueListIsLoading,
- isError: tagValueListIsError,
- pageLinks: getResponseHeader?.('Link'),
- };
-}
-
-export function GroupTagValues() {
- const organization = useOrganization();
- const location = useLocation();
- const params = useParams();
- const environments = useEnvironmentsFromUrl();
- const {baseUrl} = useGroupDetailsRoute();
- const {orgId, tagKey = ''} = useParams();
- const {cursor, page: _page, ...currentQuery} = location.query;
-
- const {
- data: group,
- isPending: isGroupPending,
- isError: isGroupError,
- refetch: refetchGroup,
- } = useGroup({groupId: params.groupId});
- const project = useProjectFromSlug({organization, projectSlug: group?.project?.slug});
-
- const title = tagKey === 'user' ? t('Affected Users') : tagKey;
- const sort = location.query.sort || DEFAULT_SORT;
- const sortArrow = ;
-
- const {tagValueList, tag, isLoading, isError, pageLinks} = useTagQueries({
- groupId: params.groupId,
- sort,
- tagKey,
- environments,
- cursor: typeof cursor === 'string' ? cursor : undefined,
- });
-
- if (isGroupPending) {
- return ;
- }
-
- if (isGroupError) {
- return (
-
- );
- }
-
- const lastSeenColumnHeader = (
-
- {t('Last Seen')} {sort === 'date' && sortArrow}
-
- );
- const countColumnHeader = (
-
- {t('Count')} {sort === 'count' && sortArrow}
-
- );
- const renderResults = () => {
- if (isError) {
- return ;
- }
-
- if (isLoading) {
- return null;
- }
-
- const discoverFields = [
- 'title',
- 'release',
- 'environment',
- 'user.display',
- 'timestamp',
- ];
-
- const globalSelectionParams = extractSelectionParameters(location.query);
- return tagValueList?.map((tagValue, tagValueIdx) => {
- const pct = tag?.totalValues
- ? `${percent(tagValue.count, tag?.totalValues).toFixed(2)}%`
- : '--';
- const key = tagValue.key ?? tagKey;
- const issuesQuery = tagValue.query || `${key}:"${tagValue.value}"`;
- const discoverView = EventView.fromSavedQuery({
- id: undefined,
- name: key ?? '',
- fields: [
- ...(key === undefined ? [] : [key]),
- ...discoverFields.filter(field => field !== key),
- ],
- orderby: '-timestamp',
- query: `issue:${group.shortId} ${issuesQuery}`,
- projects: [Number(project?.id)],
- environment: environments,
- version: 2 as SavedQueryVersions,
- range: '90d',
- });
- const issuesPath = `/organizations/${orgId}/issues/`;
- const tagName = tagValue.name === '' ? t('(empty)') : tagValue.name;
-
- return (
-
-
-
-
- {key === 'user' ? (
-
- ) : (
-
- )}
-
-
-
- {tagValue.email && (
-
-
-
- )}
- {isUrl(tagValue.value) && (
-
-
-
- )}
-
- {pct}
- {tagValue.count.toLocaleString()}
-
-
-
-
- ,
- 'aria-label': t('More'),
- }}
- items={[
- {
- key: 'open-in-discover',
- label: t('Open in Discover'),
- to: discoverView.getResultsViewUrlTarget(
- organization,
- false,
- hasDatasetSelector(organization)
- ? SavedQueryDatasets.ERRORS
- : undefined
- ),
- hidden: !organization.features.includes('discover-basic'),
- },
- {
- key: 'search-issues',
- label: t('Search All Issues with Tag Value'),
- to: {
- pathname: issuesPath,
- query: {
- ...globalSelectionParams, // preserve page filter selections
- query: issuesQuery,
- },
- },
- },
- ]}
- />
-
-
- );
- });
- };
-
- return (
-
-
-
- {t('Tag Details')}
-
-
- {t('Export Page to CSV')}
-
-
-
-
- {t('Percent')},
- countColumnHeader,
- lastSeenColumnHeader,
- '',
- ]}
- emptyMessage={t('Sorry, the tags for this issue could not be found.')}
- emptyAction={
- environments.length
- ? t('No tags were found for the currently selected environments')
- : null
- }
- >
- {renderResults()}
-
-
-
-
- );
-}
-
-function GroupTagValuesRoute() {
- const hasStreamlinedUI = useHasStreamlinedUI();
-
- // TODO(streamlined-ui): Point the router directly to group event details
- if (hasStreamlinedUI) {
- return ;
- }
-
- return ;
-}
-
-const Title = styled('h3')`
- margin: 0;
-`;
-
-const StyledPanelTable = styled(PanelTable)`
- white-space: nowrap;
- font-size: ${p => p.theme.font.size.md};
-
- overflow: auto;
- @media (min-width: ${p => p.theme.breakpoints.sm}) {
- overflow: initial;
- }
-
- & > * {
- padding: ${p => p.theme.space.md} ${p => p.theme.space.xl};
- }
-`;
-
-const StyledLoadingError = styled(LoadingError)`
- grid-column: 1 / -1;
- margin-bottom: ${p => p.theme.space['3xl']};
- border-radius: 0;
- border-width: 1px 0;
-`;
-
-const PercentColumnHeader = styled('div')`
- text-align: right;
-`;
-
-const StyledSortLink = styled(Link)`
- text-align: right;
- color: inherit;
-
- :hover {
- color: inherit;
- }
-`;
-
-const StyledExternalLink = styled(ExternalLink)`
- margin-left: ${p => p.theme.space.xs};
-`;
-
-function Column(props: FlexProps) {
- return ;
-}
-
-function RightAlignColumn(props: FlexProps) {
- return ;
-}
-
-const NameColumn = styled(Column)`
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: flex;
- min-width: 320px;
-`;
-
-const NameWrapper = styled('span')`
- display: block;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- width: auto;
-`;
-
-const StyledPagination = styled(Pagination)`
- margin: 0;
-`;
-
-export default GroupTagValuesRoute;
diff --git a/static/app/views/issueDetails/groupTags/groupTagsTab.spec.tsx b/static/app/views/issueDetails/groupTags/groupTagsTab.spec.tsx
deleted file mode 100644
index 2f62fc1aa51036..00000000000000
--- a/static/app/views/issueDetails/groupTags/groupTagsTab.spec.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import {GroupFixture} from 'sentry-fixture/group';
-import {OrganizationFixture} from 'sentry-fixture/organization';
-import {TagsFixture} from 'sentry-fixture/tags';
-
-import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
-
-import {GroupTagsTab} from './groupTagsTab';
-
-describe('GroupTagsTab', () => {
- const group = GroupFixture();
- const organization = OrganizationFixture();
- let tagsMock: jest.Mock;
-
- const makeInitialRouterConfig = () => ({
- location: {
- pathname: `/organizations/${organization.slug}/issues/${group.id}/tags/`,
- query: {
- environment: 'dev',
- },
- },
- route: '/organizations/:orgId/issues/:groupId/tags/',
- });
-
- beforeEach(() => {
- MockApiClient.clearMockResponses();
- MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/issues/${group.id}/`,
- body: group,
- });
- tagsMock = MockApiClient.addMockResponse({
- url: `/organizations/${organization.slug}/issues/${group.id}/tags/`,
- body: TagsFixture(),
- });
- });
-
- it('navigates to issue details events tab with correct query params', async () => {
- const {router} = render(, {
- initialRouterConfig: makeInitialRouterConfig(),
- organization,
- });
-
- const headers = await screen.findAllByTestId('tag-title');
-
- expect(tagsMock).toHaveBeenCalledWith(
- '/organizations/org-slug/issues/1/tags/',
- expect.objectContaining({
- query: {environment: ['dev'], limit: 10},
- })
- );
- // Check headers have been sorted alphabetically
- expect(headers.map(h => h.innerHTML)).toEqual([
- 'browser',
- 'device',
- 'environment',
- 'url',
- 'user',
- ]);
-
- await userEvent.click(screen.getByText('david'));
-
- await waitFor(() => {
- expect(router.location.pathname).toBe('/organizations/org-slug/issues/1/events/');
- });
- expect(router.location.query.query).toBe('user.username:david');
- expect(router.location.query.environment).toBe('dev');
- });
-
- it('shows an error message when the request fails', async () => {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/issues/1/tags/',
- statusCode: 500,
- });
-
- render(, {
- initialRouterConfig: makeInitialRouterConfig(),
- organization,
- });
-
- expect(
- await screen.findByText('There was an error loading issue tags.')
- ).toBeInTheDocument();
- });
-});
diff --git a/static/app/views/issueDetails/groupTags/groupTagsTab.tsx b/static/app/views/issueDetails/groupTags/groupTagsTab.tsx
deleted file mode 100644
index 39f9564d942b4f..00000000000000
--- a/static/app/views/issueDetails/groupTags/groupTagsTab.tsx
+++ /dev/null
@@ -1,245 +0,0 @@
-import styled from '@emotion/styled';
-
-import {Alert} from '@sentry/scraps/alert';
-import {ExternalLink, Link} from '@sentry/scraps/link';
-
-import {Count} from 'sentry/components/count';
-import {DeviceName} from 'sentry/components/deviceName';
-import {TAGS_DOCS_LINK} from 'sentry/components/events/eventTags/util';
-import * as Layout from 'sentry/components/layouts/thirds';
-import {LoadingError} from 'sentry/components/loadingError';
-import {LoadingIndicator} from 'sentry/components/loadingIndicator';
-import {extractSelectionParameters} from 'sentry/components/pageFilters/parse';
-import {Panel} from 'sentry/components/panels/panel';
-import {PanelBody} from 'sentry/components/panels/panelBody';
-import {Version} from 'sentry/components/version';
-import {t, tct} from 'sentry/locale';
-import {generateQueryWithTag, percent} from 'sentry/utils';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useParams} from 'sentry/utils/useParams';
-import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
-import {useGroupTags} from 'sentry/views/issueDetails/groupTags/useGroupTags';
-import {Tab, TabPaths} from 'sentry/views/issueDetails/types';
-import {useGroup} from 'sentry/views/issueDetails/useGroup';
-import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
-import {
- useEnvironmentsFromUrl,
- useHasStreamlinedUI,
-} from 'sentry/views/issueDetails/utils';
-
-type SimpleTag = {
- key: string;
- topValues: Array<{
- count: number;
- name: string;
- value: string;
- query?: string;
- }>;
- totalValues: number;
-};
-
-export function GroupTagsTab() {
- const location = useLocation();
- const environments = useEnvironmentsFromUrl();
- const {baseUrl} = useGroupDetailsRoute();
- const params = useParams<{groupId: string}>();
-
- const {
- data: group,
- isPending: isGroupPending,
- isError: isGroupError,
- refetch: refetchGroup,
- } = useGroup({groupId: params.groupId});
-
- const {data, isPending, isError, refetch} = useGroupTags({
- groupId: group?.id,
- environment: environments,
- limit: 10,
- });
-
- if (isPending || isGroupPending) {
- return ;
- }
-
- if (isError || isGroupError) {
- return (
- {
- refetch();
- refetchGroup();
- }}
- />
- );
- }
-
- const getTagKeyTarget = (tag: SimpleTag) => {
- return {
- pathname: `${baseUrl}${TabPaths[Tab.DISTRIBUTIONS]}${tag.key}/`,
- query: extractSelectionParameters(location.query),
- };
- };
-
- const alphabeticalTags = data.toSorted((a, b) => a.key.localeCompare(b.key));
- return (
-
-
-
-
- {tct(
- 'Tags are automatically indexed for searching and breakdown charts. Learn how to [link: add custom tags to issues]',
- {
- link: ,
- }
- )}
-
-
-
- {alphabeticalTags.map((tag, tagIdx) => (
-
-
-
-
-
- {tag.key}
-
-
-
- {tag.topValues.map((tagValue, tagValueIdx) => {
- const tagName = tagValue.name === '' ? t('(empty)') : tagValue.name;
- const baseQuery = tagValue.query
- ? {query: tagValue.query}
- : generateQueryWithTag(location.query, {
- key: tag.key,
- value: tagValue.value,
- });
- return (
-
-
-
-
- {tag.key === 'release' ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- );
- })}
-
-
-
-
- ))}
-
-
-
- );
-}
-
-function GroupTagsRoute() {
- const hasStreamlinedUI = useHasStreamlinedUI();
-
- // TODO(streamlined-ui): Point the router to group event details
- if (hasStreamlinedUI) {
- return ;
- }
-
- return ;
-}
-
-export default GroupTagsRoute;
-
-const Container = styled('div')`
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: ${p => p.theme.space.xl};
- margin-bottom: ${p => p.theme.space.xl};
-`;
-
-const StyledPanel = styled(Panel)`
- height: 100%;
-`;
-
-const TagHeading = styled('h5')`
- font-size: ${p => p.theme.font.size.lg};
- margin-bottom: 0;
- color: ${p => p.theme.tokens.interactive.link.accent.rest};
-`;
-
-const UnstyledUnorderedList = styled('ul')`
- list-style: none;
- padding-left: 0;
- margin-bottom: 0;
-`;
-
-const TagItem = styled('div')`
- padding: 0;
-`;
-
-const TagBarBackground = styled('div')<{widthPercent: string}>`
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- background: ${p => p.theme.colors.surface200};
- border-radius: ${p => p.theme.radius.md};
- width: ${p => p.widthPercent};
-`;
-
-const TagBarLink = styled(Link)`
- position: relative;
- display: flex;
- line-height: 2.2;
- color: ${p => p.theme.tokens.content.primary};
- margin-bottom: ${p => p.theme.space.xs};
- padding: 0 ${p => p.theme.space.md};
- background: ${p => p.theme.tokens.background.secondary};
- border-radius: ${p => p.theme.radius.md};
- overflow: hidden;
-
- &:hover {
- color: ${p => p.theme.tokens.content.primary};
- text-decoration: underline;
- ${TagBarBackground} {
- background: ${p => p.theme.tokens.background.accent.vibrant};
- }
- }
-`;
-
-const TagBarLabel = styled('div')`
- align-items: center;
- font-size: ${p => p.theme.font.size.md};
- position: relative;
- flex-grow: 1;
- display: block;
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-`;
-
-const TagBarCount = styled('div')`
- font-size: ${p => p.theme.font.size.md};
- position: relative;
- padding-left: ${p => p.theme.space.xl};
- padding-right: ${p => p.theme.space.md};
- font-variant-numeric: tabular-nums;
-`;
diff --git a/static/app/views/issueDetails/groupUserFeedback.tsx b/static/app/views/issueDetails/groupUserFeedback.tsx
index 385b155ffe4f37..73de7fcda15e0c 100644
--- a/static/app/views/issueDetails/groupUserFeedback.tsx
+++ b/static/app/views/issueDetails/groupUserFeedback.tsx
@@ -1,5 +1,4 @@
import {Fragment} from 'react';
-import {css} from '@emotion/react';
import styled from '@emotion/styled';
import {EventUserFeedback} from 'sentry/components/events/userFeedback';
@@ -14,11 +13,9 @@ import {useParams} from 'sentry/utils/useParams';
import {FeedbackEmptyState} from 'sentry/views/feedback/feedbackEmptyState';
import {useGroup} from 'sentry/views/issueDetails/useGroup';
import {useGroupUserFeedback} from 'sentry/views/issueDetails/useGroupUserFeedback';
-import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
function GroupUserFeedback() {
const organization = useOrganization();
- const hasStreamlinedUI = useHasStreamlinedUI();
const location = useLocation();
const params = useParams<{groupId: string}>();
@@ -57,7 +54,7 @@ function GroupUserFeedback() {
if (isPending || isPendingGroup) {
return (
-
+
@@ -69,9 +66,9 @@ function GroupUserFeedback() {
const hasUserFeedback = group.project.hasUserReports;
return (
-
+
- {hasStreamlinedUI && hasUserFeedback && (
+ {hasUserFeedback && (
{t('The feedback shown below is not subject to search filters.')}
@@ -101,18 +98,14 @@ const StyledEventUserFeedback = styled(EventUserFeedback)`
margin-bottom: ${p => p.theme.space.xl};
`;
-const StyledLayoutBody = styled(Layout.Body)<{hasStreamlinedUI?: boolean}>`
- ${p =>
- p.hasStreamlinedUI &&
- css`
- border: 1px solid ${p.theme.tokens.border.primary};
- border-radius: ${p.theme.radius.md};
- padding: ${p.theme.space.lg} 0;
+const StyledLayoutBody = styled(Layout.Body)`
+ border: 1px solid ${p => p.theme.tokens.border.primary};
+ border-radius: ${p => p.theme.radius.md};
+ padding: ${p => p.theme.space.lg} 0;
- @media (min-width: ${p.theme.breakpoints.md}) {
- padding: ${p.theme.space.lg};
- }
- `}
+ @media (min-width: ${p => p.theme.breakpoints.md}) {
+ padding: ${p => p.theme.space.lg};
+ }
`;
const FilterMessage = styled('div')`
diff --git a/static/app/views/issueDetails/metricKitHangProfileSection.tsx b/static/app/views/issueDetails/metricKitHangProfileSection.tsx
index d9fa7f47cab7e3..af2941e95aa6e0 100644
--- a/static/app/views/issueDetails/metricKitHangProfileSection.tsx
+++ b/static/app/views/issueDetails/metricKitHangProfileSection.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
hasFlamegraphData,
StacktraceFlamegraph,
@@ -28,7 +28,7 @@ export function getHangProfileData(event: Event): HangProfileData | null {
if (hasFlamegraphData(value.stacktrace?.frames)) {
return {
frames: value.stacktrace!.frames!,
- exceptionValue: value.value,
+ exceptionValue: value.value ?? '',
};
}
}
diff --git a/static/app/views/issueDetails/profilePreviewSection.tsx b/static/app/views/issueDetails/profilePreviewSection.tsx
index abd30c0a82fd7f..c627bfdfbc6740 100644
--- a/static/app/views/issueDetails/profilePreviewSection.tsx
+++ b/static/app/views/issueDetails/profilePreviewSection.tsx
@@ -6,7 +6,7 @@ import {LinkButton} from '@sentry/scraps/button';
import {ExternalLink} from '@sentry/scraps/link';
import {SegmentedControl} from '@sentry/scraps/segmentedControl';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {FlamegraphPreview} from 'sentry/components/profiling/flamegraph/flamegraphPreview';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
diff --git a/static/app/views/issueDetails/streamline/eventDetails.tsx b/static/app/views/issueDetails/streamline/eventDetails.tsx
index e3944a248123a1..a50e6f96e6f52b 100644
--- a/static/app/views/issueDetails/streamline/eventDetails.tsx
+++ b/static/app/views/issueDetails/streamline/eventDetails.tsx
@@ -2,7 +2,7 @@ import {useLayoutEffect, useState} from 'react';
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import type {Event} from 'sentry/types/event';
import type {Group} from 'sentry/types/group';
diff --git a/static/app/views/issueDetails/streamline/eventDetailsHeader.tsx b/static/app/views/issueDetails/streamline/eventDetailsHeader.tsx
index 231124e8a45bee..4606b6b577e783 100644
--- a/static/app/views/issueDetails/streamline/eventDetailsHeader.tsx
+++ b/static/app/views/issueDetails/streamline/eventDetailsHeader.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import {Flex, Grid} from '@sentry/scraps/layout';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EnvironmentPageFilter} from 'sentry/components/pageFilters/environment/environmentPageFilter';
import {PageFilterBar} from 'sentry/components/pageFilters/pageFilterBar';
import {
diff --git a/static/app/views/issueDetails/streamline/eventGraph.tsx b/static/app/views/issueDetails/streamline/eventGraph.tsx
index 220b371a215df7..099d7caee24206 100644
--- a/static/app/views/issueDetails/streamline/eventGraph.tsx
+++ b/static/app/views/issueDetails/streamline/eventGraph.tsx
@@ -22,7 +22,7 @@ import type {Group} from 'sentry/types/group';
import type {EventsStats, MultiSeriesEventsStats} from 'sentry/types/organization';
import type {ReleaseMetaBasic} from 'sentry/types/release';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
diff --git a/static/app/views/issueDetails/streamline/foldSection.tsx b/static/app/views/issueDetails/streamline/foldSection.tsx
index 5486d4f285510d..8d18a2c6845a33 100644
--- a/static/app/views/issueDetails/streamline/foldSection.tsx
+++ b/static/app/views/issueDetails/streamline/foldSection.tsx
@@ -13,7 +13,7 @@ import {Disclosure} from '@sentry/scraps/disclosure';
import {Separator, type SeparatorProps} from '@sentry/scraps/separator';
import {Text} from '@sentry/scraps/text';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/issueDetails/streamline/header/header.tsx b/static/app/views/issueDetails/streamline/header/header.tsx
index 5621dc0c9fea3c..556118c7da8f59 100644
--- a/static/app/views/issueDetails/streamline/header/header.tsx
+++ b/static/app/views/issueDetails/streamline/header/header.tsx
@@ -11,7 +11,7 @@ import {Tooltip} from '@sentry/scraps/tooltip';
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
import {Count} from 'sentry/components/count';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventMessage} from 'sentry/components/events/eventMessage';
import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
import {useFeedbackSDKIntegration} from 'sentry/components/feedbackButton/useFeedbackSDKIntegration';
diff --git a/static/app/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery.tsx b/static/app/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery.tsx
index bd615d6b243236..756444811cef3d 100644
--- a/static/app/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery.tsx
+++ b/static/app/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery.tsx
@@ -2,7 +2,7 @@ import {getInterval} from 'sentry/components/charts/utils';
import type {PageFilters} from 'sentry/types/core';
import type {Group} from 'sentry/types/group';
import type {NewQuery, SavedQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {
useGenericDiscoverQuery,
type DiscoverQueryProps,
diff --git a/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx
index c707d71ed10b36..6beb152354ac23 100644
--- a/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx
+++ b/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx
@@ -90,7 +90,10 @@ describe('AutofixSection', () => {
platform: 'javascript',
};
- const javascriptProject: Project = {...mockProject, platform: 'javascript'};
+ const javascriptProject: Project = {
+ ...mockProject,
+ platform: 'javascript',
+ };
render(
{
organization,
});
- expect(await screen.findByText('Implementation Plan')).toBeInTheDocument();
+ expect(await screen.findByText('Plan')).toBeInTheDocument();
expect(screen.getByText('Add null check before accessing user')).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Open Seer'})).toBeInTheDocument();
});
@@ -480,7 +483,7 @@ describe('AutofixSection', () => {
});
expect(await screen.findByText('Root Cause')).toBeInTheDocument();
- expect(screen.getByText('Implementation Plan')).toBeInTheDocument();
+ expect(screen.getByText('Plan')).toBeInTheDocument();
expect(screen.getByText('Code Changes')).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Open Seer'})).toBeInTheDocument();
});
@@ -535,7 +538,9 @@ describe('AutofixSection', () => {
});
expect(await screen.findByText('Finish Configuring Seer')).toBeInTheDocument();
- const link = screen.getByRole('button', {name: 'Set Up Seer for This Project'});
+ const link = screen.getByRole('button', {
+ name: 'Set Up Seer for This Project',
+ });
expect(link).toHaveAttribute(
'href',
`/settings/${organization.slug}/projects/${mockProject.slug}/seer/`
@@ -563,7 +568,9 @@ describe('AutofixSection', () => {
});
expect(await screen.findByText('Finish Configuring Seer')).toBeInTheDocument();
- const link = screen.getByRole('button', {name: 'Set Up Seer for This Project'});
+ const link = screen.getByRole('button', {
+ name: 'Set Up Seer for This Project',
+ });
expect(link).toHaveAttribute(
'href',
`/settings/${organization.slug}/projects/${mockProject.slug}/seer/`
@@ -588,6 +595,6 @@ describe('AutofixSection', () => {
).toBeInTheDocument();
expect(screen.getByText('Outline a plan')).toBeInTheDocument();
expect(screen.getByText('Create a code fix')).toBeInTheDocument();
- expect(screen.getByRole('button', {name: 'Fix the Issue'})).toBeInTheDocument();
+ expect(screen.getByRole('button', {name: 'Start Analysis'})).toBeInTheDocument();
});
});
diff --git a/static/app/views/issueDetails/streamline/sidebar/autofixSection.tsx b/static/app/views/issueDetails/streamline/sidebar/autofixSection.tsx
index 06534646a1af7b..cc82c9a6f407a8 100644
--- a/static/app/views/issueDetails/streamline/sidebar/autofixSection.tsx
+++ b/static/app/views/issueDetails/streamline/sidebar/autofixSection.tsx
@@ -288,15 +288,15 @@ function AutofixEmptyState({
}
- aria-label={t('Fix the Issue')}
- tooltipProps={{title: t('Fix the Issue')}}
+ aria-label={t('Start Analysis')}
+ tooltipProps={{title: t('Start Analysis')}}
priority="primary"
onClick={handleStartRootCause}
analyticsEventKey="autofix.start_fix_clicked"
analyticsEventName="Autofix: Start Fix Clicked"
analyticsParams={{group_id: group.id, mode: 'explorer', referrer}}
>
- {t('Fix the Issue')}
+ {t('Start Analysis')}
);
diff --git a/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.tsx b/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.tsx
index af6976bcc59067..abda381130301e 100644
--- a/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.tsx
+++ b/static/app/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection.tsx
@@ -8,7 +8,7 @@ import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import Feature from 'sentry/components/acl/feature';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList';
import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
diff --git a/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx b/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx
index 246c2b1e1c0452..9da52c748257fb 100644
--- a/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx
+++ b/static/app/views/issueDetails/streamline/sidebar/sidebar.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import {ExternalLink} from '@sentry/scraps/link';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import * as Layout from 'sentry/components/layouts/thirds';
import * as SidebarSection from 'sentry/components/sidebarSection';
import {t, tct} from 'sentry/locale';
diff --git a/static/app/views/issueDetails/traceTimeline/traceTimeline.tsx b/static/app/views/issueDetails/traceTimeline/traceTimeline.tsx
index 2479f9f4be99ab..2cc8606a2f6493 100644
--- a/static/app/views/issueDetails/traceTimeline/traceTimeline.tsx
+++ b/static/app/views/issueDetails/traceTimeline/traceTimeline.tsx
@@ -1,7 +1,7 @@
import {Fragment, useRef} from 'react';
import styled from '@emotion/styled';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
import {t} from 'sentry/locale';
import type {Event} from 'sentry/types/event';
diff --git a/static/app/views/issueList/groupListBody.tsx b/static/app/views/issueList/groupListBody.tsx
index fb6d08ac41d755..b7fe8cd7f099ad 100644
--- a/static/app/views/issueList/groupListBody.tsx
+++ b/static/app/views/issueList/groupListBody.tsx
@@ -13,7 +13,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
import type {IssueUpdateData} from 'sentry/views/issueList/types';
-import NoGroupsHandler from './noGroupsHandler';
+import {NoGroupsHandler} from './noGroupsHandler';
import {SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY} from './utils';
type GroupListBodyProps = {
diff --git a/static/app/views/issueList/noGroupsHandler/index.spec.tsx b/static/app/views/issueList/noGroupsHandler/index.spec.tsx
index a5edb50de5839a..923f33cb467470 100644
--- a/static/app/views/issueList/noGroupsHandler/index.spec.tsx
+++ b/static/app/views/issueList/noGroupsHandler/index.spec.tsx
@@ -2,7 +2,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen} from 'sentry-test/reactTestingLibrary';
-import NoGroupsHandler from 'sentry/views/issueList/noGroupsHandler';
+import {NoGroupsHandler} from 'sentry/views/issueList/noGroupsHandler';
describe('NoGroupsHandler', () => {
const defaultProps = {
diff --git a/static/app/views/issueList/noGroupsHandler/index.tsx b/static/app/views/issueList/noGroupsHandler/index.tsx
index 4ce48f22b30cde..0f002c6c0e4001 100644
--- a/static/app/views/issueList/noGroupsHandler/index.tsx
+++ b/static/app/views/issueList/noGroupsHandler/index.tsx
@@ -36,7 +36,7 @@ type State = {
* having no issues be returned from a query. This component will conditionally
* render one of those states.
*/
-class NoGroupsHandler extends Component {
+export class NoGroupsHandler extends Component {
state: State = {
fetchingSentFirstEvent: true,
sentFirstEvent: false,
@@ -220,5 +220,3 @@ class NoGroupsHandler extends Component {
return this.renderEmpty();
}
}
-
-export default NoGroupsHandler;
diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx
index cd5d0326f81b69..bbcb40783135f3 100644
--- a/static/app/views/issueList/overview.tsx
+++ b/static/app/views/issueList/overview.tsx
@@ -26,7 +26,7 @@ import type {BaseGroup, Group, PriorityLevel} from 'sentry/types/group';
import {GroupStatus} from 'sentry/types/group';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import CursorPoller from 'sentry/utils/cursorPoller';
+import {CursorPoller} from 'sentry/utils/cursorPoller';
import {getUtcDateString} from 'sentry/utils/dates';
import {getCurrentSentryReactRootSpan} from 'sentry/utils/getCurrentSentryReactRootSpan';
import {parseApiError} from 'sentry/utils/parseApiError';
diff --git a/static/app/views/navigation/constants.tsx b/static/app/views/navigation/constants.tsx
index 82bd803efedf5e..9a9e08f3ddc331 100644
--- a/static/app/views/navigation/constants.tsx
+++ b/static/app/views/navigation/constants.tsx
@@ -19,6 +19,7 @@ export const NAVIGATION_SIDEBAR_OPEN_DELAY_MS = 250;
export const NAVIGATION_SIDEBAR_RESET_DELAY_MS = 300;
export const NAVIGATION_MOBILE_TOPBAR_HEIGHT = 40;
+export const NAVIGATION_MOBILE_TOPBAR_HEIGHT_WITH_PAGE_FRAME = 48;
// To be passed as the `source` parameter in router navigation state
// e.g. {pathname: '/issues/', state: {source: `sidebar`}}
diff --git a/static/app/views/navigation/index.tsx b/static/app/views/navigation/index.tsx
index 30a4723178d25f..74cb26bbc0928a 100644
--- a/static/app/views/navigation/index.tsx
+++ b/static/app/views/navigation/index.tsx
@@ -13,7 +13,10 @@ import {useGlobalModal} from 'sentry/components/globalModal/useGlobalModal';
import {t} from 'sentry/locale';
import {useHotkeys} from 'sentry/utils/useHotkeys';
import {useOrganization} from 'sentry/utils/useOrganization';
-import {MobileNavigation} from 'sentry/views/navigation/mobileNavigation';
+import {
+ MobileNavigation,
+ MobilePageFrameNavigation,
+} from 'sentry/views/navigation/mobileNavigation';
import {Navigation as DesktopNavigation} from 'sentry/views/navigation/navigation';
import {
NavigationTourProvider,
@@ -22,12 +25,15 @@ import {
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
import {UserDropdown} from 'sentry/views/navigation/primary/userDropdown';
import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
+import {MobileSecondaryNavigationContextProvider} from 'sentry/views/navigation/secondaryNavigationContext';
+import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
import {useResetActiveNavigationGroup} from 'sentry/views/navigation/useResetActiveNavigationGroup';
function UserAndOrganizationNavigation() {
const organization = useOrganization();
const {layout} = usePrimaryNavigation();
const {visible} = useGlobalModal();
+ const hasPageFrame = useHasPageFrameFeature();
useGlobalCommandPaletteActions();
@@ -50,7 +56,13 @@ function UserAndOrganizationNavigation() {
return (
- {layout === 'mobile' ? : }
+ {layout === 'mobile' ? (
+
+ {hasPageFrame ? : }
+
+ ) : (
+
+ )}
);
}
@@ -72,6 +84,7 @@ function NavigationLayout({children}: {children: React.ReactNode}) {
return (
(null);
@@ -73,29 +83,16 @@ export function MobileNavigation() {
!HookStore.get('component:superuser-warning-excluded')[0]?.(organization);
return (
-
-
- {/* If the view is not closed, it will render under the full screen mobile menu */}
- setView('closed')} />
- {showSuperuserWarning && (
-
- )}
-
+
+
+
+ {/* If the view is not closed, it will render under the full screen mobile menu */}
+ setView('closed')} />
+ {showSuperuserWarning && (
+
+ )}
+
+
+
+ );
+}
+
+function MobileNavigationHeader(props: FlexProps<'header'>) {
+ const theme = useTheme();
+ return (
+
+ );
+}
+
+function MobilePrimaryNavigation() {
+ const {view} = useSecondaryNavigation();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {view === 'expanded' && (
+
+
+
+ )}
+
+ );
+}
+
+export function MobilePageFrameNavigation() {
+ const theme = useTheme();
+ const [isOpen, setIsOpen] = useState(false);
+ const navPanelRef = useRef(null);
+ const toggleButtonRef = useRef(null);
+ const {view, setView} = useSecondaryNavigation();
+ const scrollLock = useScrollLock(document.getElementById('main')!);
+
+ useEffect(() => {
+ const main = document.getElementById('main');
+ if (isOpen) {
+ main?.setAttribute('inert', '');
+ scrollLock.acquire();
+ } else {
+ main?.removeAttribute('inert');
+ if (scrollLock.held()) {
+ setView('expanded');
+ }
+ scrollLock.release();
+ }
+ return () => {
+ main?.removeAttribute('inert');
+ scrollLock.release();
+ };
+ }, [isOpen, scrollLock, setView]);
+
+ // Close the panel when the secondary nav's IconPanel button is clicked,
+ // which sets view to 'collapsed'.
+ useEffect(() => {
+ if (isOpen && view === 'collapsed') {
+ setIsOpen(false);
+ }
+ }, [isOpen, view]);
+
+ const handleClickOutside = useCallback((e: MouseEvent | TouchEvent) => {
+ if (toggleButtonRef.current?.contains(e.target as Node)) return;
+ setIsOpen(false);
+ }, []);
+
+ useOnClickOutside(navPanelRef, handleClickOutside);
+
+ return (
+
+
+
+ {
+ if (!isOpen) setView('expanded');
+ setIsOpen(v => !v);
+ }}
+ icon={}
+ aria-label={isOpen ? t('Close main menu') : t('Open main menu')}
+ />
+
+
+
+
+
+
+
+
+ {isOpen &&
+ createPortal(
+
+
+ ,
+ document.body
+ )}
+
);
}
diff --git a/static/app/views/navigation/navigation.tsx b/static/app/views/navigation/navigation.tsx
index 6bbbca98119029..922fbec904e32f 100644
--- a/static/app/views/navigation/navigation.tsx
+++ b/static/app/views/navigation/navigation.tsx
@@ -1,30 +1,67 @@
-import {Fragment, useMemo} from 'react';
+import {Fragment, type RefObject, useMemo, useRef} from 'react';
+import {mergeProps} from '@react-aria/utils';
import {motion, type MotionProps} from 'framer-motion';
+import {Stack} from '@sentry/scraps/layout';
import {Flex} from '@sentry/scraps/layout';
+import {SizeProvider} from '@sentry/scraps/sizeContext';
+import {openCommandPalette} from 'sentry/actionCreators/modal';
+import {openHelpSearchModal} from 'sentry/actionCreators/modal';
+import Feature from 'sentry/components/acl/feature';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
+import Hook from 'sentry/components/hook';
+import {IconSearch} from 'sentry/icons';
+import {
+ IconCompass,
+ IconDashboard,
+ IconGraph,
+ IconIssues,
+ IconSettings,
+ IconSiren,
+} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {useOrganization} from 'sentry/utils/useOrganization';
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
+import {getDefaultExploreRoute} from 'sentry/views/explore/utils';
import {
NAVIGATION_SIDEBAR_SECONDARY_WIDTH_LOCAL_STORAGE_KEY,
PRIMARY_SIDEBAR_WIDTH,
SECONDARY_SIDEBAR_WIDTH,
} from 'sentry/views/navigation/constants';
+import {
+ NavigationTour,
+ NavigationTourElement,
+} from 'sentry/views/navigation/navigationTour';
import {
useNavigationTour,
useNavigationTourModal,
} from 'sentry/views/navigation/navigationTour';
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
-import {PrimaryNavigationItems} from 'sentry/views/navigation/primary/index';
+import {PrimaryNavigationHelpMenu} from 'sentry/views/navigation/primary/helpMenu';
+import {PrimaryNavigationOnboarding} from 'sentry/views/navigation/primary/onboarding';
import {OrganizationDropdown} from 'sentry/views/navigation/primary/organizationDropdown';
+import {PrimaryNavigationServiceIncidents} from 'sentry/views/navigation/primary/serviceIncidents';
+import {useActivateNavigationGroupOnHover} from 'sentry/views/navigation/primary/useActivateNavigationGroupOnHover';
+import {UserDropdown} from 'sentry/views/navigation/primary/userDropdown';
+import {PrimaryNavigationWhatsNew} from 'sentry/views/navigation/primary/whatsNew';
+import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components';
import {SecondaryNavigationContent} from 'sentry/views/navigation/secondary/content';
import {useSecondaryNavigation} from 'sentry/views/navigation/secondaryNavigationContext';
import {useCollapsedNavigation} from 'sentry/views/navigation/useCollapsedNavigation';
+import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
export function Navigation() {
const collapsedNavigation = useCollapsedNavigation();
+ const hasPageFrame = useHasPageFrameFeature();
const {view} = useSecondaryNavigation();
+ const ref = useRef(null);
+
+ const {layout} = usePrimaryNavigation();
+ const isMobilePageFrame = hasPageFrame && layout === 'mobile';
+
useNavigationTourModal();
const {currentStepId} = useNavigationTour();
@@ -50,8 +87,28 @@ export function Navigation() {
-
+
+
+
+
+ {!isMobilePageFrame && layout === 'mobile' ? null : (
+
+
+
+
+
+
+
+
+
+
+ )}
+
{isCollapsed ? (
;
+}
+
+export function PrimaryNavigationItems({listRef}: PrimaryNavigationItemsProps) {
+ const organization = useOrganization();
+ const prefix = `organizations/${organization.slug}`;
+
+ const fallbackRef = useRef(null);
+ const hasPageFrame = useHasPageFrameFeature();
+
+ const makeNavigationItemProps = useActivateNavigationGroupOnHover({
+ ref: listRef ?? fallbackRef,
+ });
+
+ return (
+
+
+ {tourProps => (
+
+
+
+
+
+ )}
+
+
+
+ {tourProps => (
+
+
+
+
+
+ )}
+
+
+
+
+ {tourProps => (
+
+
+
+
+
+ )}
+
+
+
+
+
+ {tourProps => (
+
+
+
+
+
+ )}
+
+
+
+ {hasPageFrame ? null : (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {tourProps => (
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+/**
+ * Returns the list of items from the footer of the primary navigation
+ */
+export function PrimaryNavigationFooterItems() {
+ const organization = useOrganization();
+ const hasPageFrame = useHasPageFrameFeature();
+
+ return (
+
+ {hasPageFrame ? (
+ ,
+ onClick: () =>
+ organization.features.includes('cmd-k-supercharged')
+ ? openCommandPalette()
+ : openHelpSearchModal({organization}),
+ }}
+ />
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Returns the user dropdown from the footer of the primary navigation
+ */
+export function PrimaryNavigationFooterItemsUserDropdown() {
+ return ;
+}
+
const CollapsedSecondaryWrapper = motion.create(Flex);
const makeCollapsedSecondaryWrapperAnimationProps = (
open: boolean,
diff --git a/static/app/views/navigation/primary/components.tsx b/static/app/views/navigation/primary/components.tsx
index 2a9de1eeab945f..7940cc827fb69f 100644
--- a/static/app/views/navigation/primary/components.tsx
+++ b/static/app/views/navigation/primary/components.tsx
@@ -12,7 +12,7 @@ import type {ButtonBarProps, ButtonProps} from '@sentry/scraps/button';
import {Button, ButtonBar} from '@sentry/scraps/button';
import {Container, Flex, Stack, type FlexProps} from '@sentry/scraps/layout';
import {Link, type LinkProps} from '@sentry/scraps/link';
-import {SizeProvider} from '@sentry/scraps/sizeContext';
+import {SizeProvider, useSizeContext} from '@sentry/scraps/sizeContext';
import {StatusIndicator} from '@sentry/scraps/statusIndicator';
import {Text} from '@sentry/scraps/text';
import {Tooltip} from '@sentry/scraps/tooltip';
@@ -32,6 +32,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useOverlay, type UseOverlayProps} from 'sentry/utils/useOverlay';
import {
NAVIGATION_PRIMARY_LINK_DATA_ATTRIBUTE,
+ NAVIGATION_MOBILE_TOPBAR_HEIGHT_WITH_PAGE_FRAME,
PRIMARY_HEADER_HEIGHT,
PRIMARY_SIDEBAR_WIDTH,
SIDEBAR_NAVIGATION_SOURCE,
@@ -70,6 +71,7 @@ interface PrimaryNavigationSidebarHeaderProps extends Omit,
function PrimaryNavigationSidebarHeader(props: PrimaryNavigationSidebarHeaderProps) {
const theme = useTheme();
+ const {layout} = usePrimaryNavigation();
const organization = useOrganization({allowNull: true});
const showSuperuserWarning =
isActiveSuperuser() &&
@@ -87,8 +89,20 @@ function PrimaryNavigationSidebarHeader(props: PrimaryNavigationSidebarHeaderPro
justify="center"
borderBottom={hasPageFrame ? 'primary' : undefined}
width={hasPageFrame ? '100%' : undefined}
- height={hasPageFrame ? `${PRIMARY_HEADER_HEIGHT}px` : undefined}
- minHeight={hasPageFrame ? `${PRIMARY_HEADER_HEIGHT}px` : undefined}
+ minHeight={
+ hasPageFrame
+ ? layout === 'mobile'
+ ? `${NAVIGATION_MOBILE_TOPBAR_HEIGHT_WITH_PAGE_FRAME}px`
+ : `${PRIMARY_HEADER_HEIGHT}px`
+ : undefined
+ }
+ height={
+ hasPageFrame
+ ? layout === 'mobile'
+ ? `${NAVIGATION_MOBILE_TOPBAR_HEIGHT_WITH_PAGE_FRAME}px`
+ : `${PRIMARY_HEADER_HEIGHT}px`
+ : undefined
+ }
{...props}
>
{props.children}
@@ -161,6 +175,7 @@ function PrimaryNavigationLink(props: PrimaryNavigationLinkProps) {
const organization = useOrganization({allowNull: true});
const {layout} = usePrimaryNavigation();
const hasPageFrame = useHasPageFrameFeature();
+ const isMobilePageFrame = hasPageFrame && layout === 'mobile';
// Reload the page when the frontend is stale to ensure users get the latest version
const {state: appState} = useFrontendVersion();
const theme = useTheme();
@@ -184,7 +199,7 @@ function PrimaryNavigationLink(props: PrimaryNavigationLinkProps) {
[NAVIGATION_PRIMARY_LINK_DATA_ATTRIBUTE]: true,
};
- if (layout === 'mobile') {
+ if (layout === 'mobile' && !isMobilePageFrame) {
return (
{props.children}
@@ -237,6 +252,8 @@ interface PrimaryNavigationButtonProps extends PrimaryNavigationItemBaseProps {
function PrimaryNavigationButton(props: PrimaryNavigationButtonProps) {
const {layout} = usePrimaryNavigation();
const organization = useOrganization({allowNull: true});
+ const hasPageFrame = useHasPageFrameFeature();
+ const isMobilePageFrame = hasPageFrame && layout === 'mobile';
return (
- {layout === 'mobile' ? props.label : null}
+ {layout === 'mobile' && !isMobilePageFrame ? props.label : null}
{props.children}
@@ -322,6 +339,8 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) {
const theme = useTheme();
const organization = useOrganization({allowNull: true});
const {layout} = usePrimaryNavigation();
+ const hasPageFrame = useHasPageFrameFeature();
+ const isMobilePageFrame = hasPageFrame && layout === 'mobile';
const portalContainerRef = useRef(null);
@@ -329,15 +348,17 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) {
portalContainerRef.current = document.body;
}, []);
+ const sizeContext = useSizeContext();
+
return (
{
return (
@@ -374,12 +395,12 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) {
)
}
>
- {layout === 'mobile' ? (
+ {layout === 'mobile' && !isMobilePageFrame ? (
{props.label}
{props.children}
- ) : (
+ ) : layout === 'mobile' ? null : (
props.children
)}
@@ -394,21 +415,24 @@ function PrimaryNavigationMenu(props: PrimaryNavigationMenuProps) {
function NavigationButton(props: DistributedOmit) {
const {layout} = usePrimaryNavigation();
+ const hasPageFrame = useHasPageFrameFeature();
return (
{p => (
)}
@@ -417,13 +441,7 @@ function NavigationButton(props: DistributedOmit) {
}
function PrimaryNavigationButtonBar(props: ButtonBarProps) {
- const hasPageFrame = useHasPageFrameFeature();
-
- return (
-
-
-
- );
+ return ;
}
interface PrimaryNavigationFooterItemsProps {
@@ -617,23 +635,19 @@ const DesktopNavigationLink = styled((props: LinkProps) => (
}
`;
-const DesktopPageFrameNavigationLink = styled((props: LinkProps) => {
- const hasPageFrame = useHasPageFrameFeature();
-
- return (
-
- {p => }
-
- );
-})`
+const DesktopPageFrameNavigationLink = styled((props: LinkProps) => (
+
+ {p => }
+
+))`
outline: none;
box-shadow: none;
transition: none;
diff --git a/static/app/views/navigation/primary/helpMenu.tsx b/static/app/views/navigation/primary/helpMenu.tsx
index ccf5281acd8c8f..93efd4cc342b58 100644
--- a/static/app/views/navigation/primary/helpMenu.tsx
+++ b/static/app/views/navigation/primary/helpMenu.tsx
@@ -13,7 +13,6 @@ import {
IconMegaphone,
IconOpen,
IconQuestion,
- IconSearch,
IconSentry,
IconSupport,
} from 'sentry/icons';
@@ -49,18 +48,20 @@ export function PrimaryNavigationHelpMenu() {
-
-
- ) : undefined,
- onAction() {
- openHelpSearchModal({organization});
- },
- },
+ ...(hasPageFrame
+ ? // When page frame feature flag is enabled, the search menu is
+ // rendered as part of the footer items and is always visible
+ // to the user.
+ []
+ : [
+ {
+ key: 'search',
+ label: t('Search support, docs and more'),
+ onAction() {
+ openHelpSearchModal({organization});
+ },
+ },
+ ]),
...items,
{
key: 'actions',
diff --git a/static/app/views/navigation/primary/index.tsx b/static/app/views/navigation/primary/index.tsx
deleted file mode 100644
index 241fc7b4a38d45..00000000000000
--- a/static/app/views/navigation/primary/index.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import {Fragment, useRef} from 'react';
-import {mergeProps} from '@react-aria/utils';
-
-import {Stack} from '@sentry/scraps/layout';
-
-import Feature from 'sentry/components/acl/feature';
-import ErrorBoundary from 'sentry/components/errorBoundary';
-import Hook from 'sentry/components/hook';
-import {
- IconCompass,
- IconDashboard,
- IconGraph,
- IconIssues,
- IconSettings,
- IconSiren,
-} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {useOrganization} from 'sentry/utils/useOrganization';
-import {getDefaultExploreRoute} from 'sentry/views/explore/utils';
-import {
- NavigationTour,
- NavigationTourElement,
-} from 'sentry/views/navigation/navigationTour';
-import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
-import {PrimaryNavigationHelpMenu} from 'sentry/views/navigation/primary/helpMenu';
-import {PrimaryNavigationOnboarding} from 'sentry/views/navigation/primary/onboarding';
-import {PrimaryNavigationServiceIncidents} from 'sentry/views/navigation/primary/serviceIncidents';
-import {useActivateNavigationGroupOnHover} from 'sentry/views/navigation/primary/useActivateNavigationGroupOnHover';
-import {UserDropdown} from 'sentry/views/navigation/primary/userDropdown';
-import {PrimaryNavigationWhatsNew} from 'sentry/views/navigation/primary/whatsNew';
-import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
-import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
-
-export function PrimaryNavigationItems() {
- const organization = useOrganization();
- const prefix = `organizations/${organization.slug}`;
- const ref = useRef(null);
-
- const {layout} = usePrimaryNavigation();
- const hasPageFrame = useHasPageFrameFeature();
-
- const makeNavigationItemProps = useActivateNavigationGroupOnHover({ref});
-
- return (
-
-
-
- {tourProps => (
-
-
-
-
-
- )}
-
-
-
- {tourProps => (
-
-
-
-
-
- )}
-
-
-
-
- {tourProps => (
-
-
-
-
-
- )}
-
-
-
-
-
- {tourProps => (
-
-
-
-
-
- )}
-
-
-
- {hasPageFrame ? null : (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- {tourProps => (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/static/app/views/navigation/primary/organizationDropdown.tsx b/static/app/views/navigation/primary/organizationDropdown.tsx
index 96afdf03299f29..0c6e48e08260e1 100644
--- a/static/app/views/navigation/primary/organizationDropdown.tsx
+++ b/static/app/views/navigation/primary/organizationDropdown.tsx
@@ -6,6 +6,7 @@ import partition from 'lodash/partition';
import {OrganizationAvatar} from '@sentry/scraps/avatar';
import {AvatarButton} from '@sentry/scraps/avatarButton';
import {Flex, Stack} from '@sentry/scraps/layout';
+import {useSizeContext} from '@sentry/scraps/sizeContext';
import {Text} from '@sentry/scraps/text';
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
@@ -24,7 +25,6 @@ import {useNavigate} from 'sentry/utils/useNavigate';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';
import {useSessionStorage} from 'sentry/utils/useSessionStorage';
-import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
import {makeProjectsPathname} from 'sentry/views/projects/pathname';
interface OrganizationDropdownProps {
@@ -54,9 +54,6 @@ export function OrganizationDropdown(props: OrganizationDropdownProps) {
);
const {projects} = useProjects();
- const {layout} = usePrimaryNavigation();
- const hasPageFrame = organization.features.includes('page-frame');
-
const [, setReferrer] = useSessionStorage(CUSTOM_REFERRER_KEY, null);
const letterAvatarProps = {
@@ -64,6 +61,8 @@ export function OrganizationDropdown(props: OrganizationDropdownProps) {
name: organization.name || organization.slug,
};
+ const size = useSizeContext();
+
return (
{
diff --git a/static/app/views/navigation/primary/userDropdown.tsx b/static/app/views/navigation/primary/userDropdown.tsx
index 57b6a5656c16e8..ec9b5c0f1e652f 100644
--- a/static/app/views/navigation/primary/userDropdown.tsx
+++ b/static/app/views/navigation/primary/userDropdown.tsx
@@ -4,6 +4,7 @@ import {useTheme} from '@emotion/react';
import {UserAvatar} from '@sentry/scraps/avatar';
import {AvatarButton} from '@sentry/scraps/avatarButton';
import {Flex, Stack} from '@sentry/scraps/layout';
+import {useSizeContext} from '@sentry/scraps/sizeContext';
import {Text} from '@sentry/scraps/text';
import {logout} from 'sentry/actionCreators/account';
@@ -16,6 +17,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useUser} from 'sentry/utils/useUser';
import {PrimaryNavigation} from 'sentry/views/navigation/primary/components';
import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
+import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
// Stable module-level component to avoid remounts when used as `renderWrapAs`
function PassthroughWrapper({children}: {children: React.ReactNode}) {
@@ -29,6 +31,7 @@ export function UserDropdown() {
const {layout} = usePrimaryNavigation();
const portalContainerRef = useRef(null);
const theme = useTheme();
+ const hasPageFrame = useHasPageFrameFeature();
useEffect(() => {
portalContainerRef.current = document.body;
@@ -57,16 +60,19 @@ export function UserDropdown() {
}
: {type: 'letter_avatar' as const, identifier, name};
+ const size = useSizeContext();
+
return (
- layout === 'mobile' ? (
+ layout === 'mobile' && !hasPageFrame ? (
{props => (
(null);
const resizeHandleRef = useRef(null);
+ const {layout} = usePrimaryNavigation();
const [secondarySidebarWidth, setSecondarySidebarWidth] = useSyncedLocalStorageState(
NAVIGATION_SIDEBAR_SECONDARY_WIDTH_LOCAL_STORAGE_KEY,
@@ -99,6 +107,8 @@ function SecondarySidebar({children}: SecondarySidebarProps) {
});
const {activeGroup} = usePrimaryNavigation();
+ const hasPageFrame = useHasPageFrameFeature();
+ const isMobilePageFrame = hasPageFrame && layout === 'mobile';
return (
{p => (
@@ -290,7 +309,15 @@ function SecondaryNavigationHeader(props: SecondaryNavigationHeaderProps) {
- {layout === 'mobile' ? null : (
+ {isMobilePageFrame ? (
+
}
+ aria-label={isCollapsed ? t('Expand') : t('Collapse')}
+ onClick={() => setView(view === 'expanded' ? 'collapsed' : 'expanded')}
+ priority="transparent"
+ />
+ ) : layout === 'mobile' ? null : (
}
@@ -458,16 +485,6 @@ function SecondaryNavigationLink({
},
};
- if (layout === 'mobile') {
- return (
-
- {leadingItems}
- {children}
- {trailingItems}
-
- );
- }
-
if (hasPageFrame) {
return (
@@ -480,6 +497,16 @@ function SecondaryNavigationLink({
);
}
+ if (layout === 'mobile') {
+ return (
+
+ {leadingItems}
+ {children}
+ {trailingItems}
+
+ );
+ }
+
return (
{leadingItems}
@@ -939,17 +966,17 @@ function SecondaryNavigationReorderableLink({
);
- if (layout === 'mobile') {
+ if (hasPageFrame) {
return (
- {content}
+
+ {content}
+
);
}
- if (hasPageFrame) {
+ if (layout === 'mobile') {
return (
-
- {content}
-
+ {content}
);
}
diff --git a/static/app/views/navigation/secondary/sections/dashboards/dashboardsSecondaryNavigation.tsx b/static/app/views/navigation/secondary/sections/dashboards/dashboardsSecondaryNavigation.tsx
index 30cb495c54aea4..ee9c3484821c60 100644
--- a/static/app/views/navigation/secondary/sections/dashboards/dashboardsSecondaryNavigation.tsx
+++ b/static/app/views/navigation/secondary/sections/dashboards/dashboardsSecondaryNavigation.tsx
@@ -1,7 +1,7 @@
import {Fragment} from 'react';
import * as Sentry from '@sentry/react';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {t} from 'sentry/locale';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
diff --git a/static/app/views/navigation/secondaryNavigationContext.tsx b/static/app/views/navigation/secondaryNavigationContext.tsx
index aa431d103a70cf..6c088a0528794f 100644
--- a/static/app/views/navigation/secondaryNavigationContext.tsx
+++ b/static/app/views/navigation/secondaryNavigationContext.tsx
@@ -67,3 +67,32 @@ export function SecondaryNavigationContextProvider(
);
}
+
+/**
+ * Mobile-only secondary navigation context provider. Unlike the desktop
+ * provider, state is stored entirely in memory (no localStorage) and only
+ * supports 'expanded' | 'collapsed' — mobile has no hover-based 'peek' state.
+ *
+ * Rendering the mobile navigation tree inside this provider ensures mobile
+ * open/close interactions never bleed into the desktop navigation's persisted
+ * collapsed preference.
+ */
+export function MobileSecondaryNavigationContextProvider(
+ props: SecondaryNavigationContextProviderProps
+) {
+ const [view, setViewState] = useState<'expanded' | 'collapsed'>('expanded');
+
+ const setView = useCallback((nextView: SecondaryNavState) => {
+ if (nextView !== 'peek') {
+ setViewState(nextView);
+ }
+ }, []);
+
+ const value = useMemo(() => ({view, setView}), [view, setView]);
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/static/app/views/navigation/topBar.tsx b/static/app/views/navigation/topBar.tsx
index 695d6a59cfa4fc..3f48b6ff23dac3 100644
--- a/static/app/views/navigation/topBar.tsx
+++ b/static/app/views/navigation/topBar.tsx
@@ -1,17 +1,27 @@
import {useContext, useEffect, useRef} from 'react';
import {useTheme} from '@emotion/react';
+import {Button} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
+import {SizeProvider} from '@sentry/scraps/sizeContext';
+import {IconSeer} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {useOrganization} from 'sentry/utils/useOrganization';
+import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext';
import {SecondaryNavigationContext} from 'sentry/views/navigation/secondaryNavigationContext';
import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
+import {useExplorerPanel} from 'sentry/views/seerExplorer/useExplorerPanel';
+import {isSeerExplorerEnabled} from 'sentry/views/seerExplorer/utils';
import {PRIMARY_HEADER_HEIGHT} from './constants';
export function TopBar() {
const theme = useTheme();
- const secondaryNavigation = useContext(SecondaryNavigationContext);
const flexRef = useRef(null);
+ const organization = useOrganization({allowNull: true});
+ const primaryNavigation = usePrimaryNavigation();
+ const secondaryNavigation = useContext(SecondaryNavigationContext);
const hasPageFrame = useHasPageFrameFeature();
useEffect(() => {
@@ -19,6 +29,10 @@ export function TopBar() {
return undefined;
}
+ if (primaryNavigation.layout === 'mobile') {
+ return undefined;
+ }
+
if (secondaryNavigation?.view !== 'expanded') {
flexRef.current.style.borderBottom = `1px solid ${theme.tokens.border.primary}`;
return undefined;
@@ -29,6 +43,10 @@ export function TopBar() {
return;
}
+ if (primaryNavigation.layout === 'mobile') {
+ return;
+ }
+
// @TODO(JonasBadalic): For the nicest transition possible, we should probably lerp the
// alpha color channel of the border color betweeen 0 and border radius distance. This would make the
// two blend nicely together without requiring us to approximate it usign the transition duration.
@@ -42,7 +60,9 @@ export function TopBar() {
handleScroll();
window.addEventListener('scroll', handleScroll, {passive: true});
return () => window.removeEventListener('scroll', handleScroll);
- }, [theme.tokens.border.primary, secondaryNavigation?.view]);
+ }, [theme.tokens.border.primary, secondaryNavigation?.view, primaryNavigation.layout]);
+
+ const {openExplorerPanel} = useExplorerPanel();
if (!hasPageFrame) {
return null;
@@ -57,6 +77,7 @@ export function TopBar() {
align="center"
padding="md lg"
position="sticky"
+ borderBottom={primaryNavigation.layout === 'mobile' ? undefined : 'primary'}
top={0}
// Keep the top bar in a cascade slightly below the sidebar panel so that when the sidebar panel
// is in the hover preview state, the top bar does not sit over it.
@@ -64,6 +85,18 @@ export function TopBar() {
zIndex: theme.zIndex.sidebarPanel - 1,
transition: `border-bottom ${theme.motion.enter.slow}`,
}}
- />
+ >
+
+ {/* @TODO(JonasBadalic): Implement breadcrumbs here */}
+
+
+ {organization && isSeerExplorerEnabled(organization) ? (
+ } onClick={openExplorerPanel}>
+ {t('Ask Seer')}
+
+ ) : null}
+
+
+
);
}
diff --git a/static/app/views/onboarding/components/fallingError.tsx b/static/app/views/onboarding/components/fallingError.tsx
index d74d6e3bda1a97..74b816d92f86ba 100644
--- a/static/app/views/onboarding/components/fallingError.tsx
+++ b/static/app/views/onboarding/components/fallingError.tsx
@@ -20,7 +20,7 @@ type State = {
isFalling: boolean;
};
-class FallingError extends Component {
+export class FallingError extends Component {
state: State = {
isFalling: false,
fallCount: 0,
@@ -140,5 +140,3 @@ class FallingError extends Component {
});
}
}
-
-export default FallingError;
diff --git a/static/app/views/onboarding/components/scmCardButton.tsx b/static/app/views/onboarding/components/scmCardButton.tsx
new file mode 100644
index 00000000000000..ccf749fbe8a1a0
--- /dev/null
+++ b/static/app/views/onboarding/components/scmCardButton.tsx
@@ -0,0 +1,21 @@
+import styled from '@emotion/styled';
+
+/**
+ * A button with all default browser styling removed.
+ * Use when wrapping a Container or other visual primitive that
+ * provides its own appearance but needs click/keyboard semantics.
+ */
+export const ScmCardButton = styled('button')`
+ appearance: none;
+ background: transparent;
+ border: none;
+ padding: 0;
+ text-align: left;
+ cursor: pointer;
+ width: 100%;
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ }
+`;
diff --git a/static/app/views/onboarding/components/scmFeatureCard.tsx b/static/app/views/onboarding/components/scmFeatureCard.tsx
new file mode 100644
index 00000000000000..7ee232291bd82b
--- /dev/null
+++ b/static/app/views/onboarding/components/scmFeatureCard.tsx
@@ -0,0 +1,65 @@
+import type {ComponentType, ReactNode} from 'react';
+
+import {Checkbox} from '@sentry/scraps/checkbox';
+import {Container, Flex} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
+import {Tooltip} from '@sentry/scraps/tooltip';
+
+import type {SVGIconProps} from 'sentry/icons/svgIcon';
+
+import {ScmCardButton} from './scmCardButton';
+
+interface ScmFeatureCardProps {
+ description: string;
+ icon: ComponentType;
+ isSelected: boolean;
+ label: string;
+ onClick: () => void;
+ disabled?: boolean;
+ disabledReason?: ReactNode;
+}
+
+export function ScmFeatureCard({
+ icon: Icon,
+ label,
+ description,
+ isSelected,
+ disabled,
+ disabledReason,
+ onClick,
+}: ScmFeatureCardProps) {
+ return (
+
+
+
+
+
+ {containerProps => }
+
+
+
+ {label}
+
+
+
+ {description}
+
+
+
+
+
+
+ );
+}
diff --git a/static/app/views/onboarding/components/scmFeatureSelectionCards.spec.tsx b/static/app/views/onboarding/components/scmFeatureSelectionCards.spec.tsx
new file mode 100644
index 00000000000000..dadccbd0953aa4
--- /dev/null
+++ b/static/app/views/onboarding/components/scmFeatureSelectionCards.spec.tsx
@@ -0,0 +1,148 @@
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import type {DisabledProducts} from 'sentry/components/onboarding/productSelection';
+
+import {ScmFeatureSelectionCards} from './scmFeatureSelectionCards';
+
+const NO_DISABLED: DisabledProducts = {};
+
+const ALL_FEATURES = [
+ ProductSolution.ERROR_MONITORING,
+ ProductSolution.PERFORMANCE_MONITORING,
+ ProductSolution.SESSION_REPLAY,
+ ProductSolution.PROFILING,
+ ProductSolution.LOGS,
+ ProductSolution.METRICS,
+];
+
+describe('ScmFeatureSelectionCards', () => {
+ it('renders all available features', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Error monitoring')).toBeInTheDocument();
+ expect(screen.getByText('Tracing')).toBeInTheDocument();
+ expect(screen.getByText('Session replay')).toBeInTheDocument();
+ expect(screen.getByText('Profiling')).toBeInTheDocument();
+ expect(screen.getByText('Logging')).toBeInTheDocument();
+ expect(screen.getByText('Metrics')).toBeInTheDocument();
+ });
+
+ it('renders only passed features', () => {
+ render(
+
+ );
+
+ expect(screen.getAllByRole('checkbox')).toHaveLength(2);
+ });
+
+ it('shows correct selected count', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('3 of 6 selected')).toBeInTheDocument();
+ });
+
+ it('error monitoring card is always disabled', async () => {
+ const onToggleFeature = jest.fn();
+
+ render(
+
+ );
+
+ const errorMonitoringCard = screen.getByRole('checkbox', {
+ name: /Error monitoring/,
+ });
+ expect(errorMonitoringCard).toBeDisabled();
+
+ await userEvent.click(errorMonitoringCard);
+ expect(onToggleFeature).not.toHaveBeenCalled();
+ });
+
+ it('clicking a non-disabled feature calls onToggleFeature', async () => {
+ const onToggleFeature = jest.fn();
+
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole('checkbox', {name: /Tracing/}));
+ expect(onToggleFeature).toHaveBeenCalledWith(ProductSolution.PERFORMANCE_MONITORING);
+ });
+
+ it('plan-disabled features render as disabled', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('checkbox', {name: /Session replay/})).toBeDisabled();
+ expect(screen.getByRole('checkbox', {name: /Profiling/})).toBeDisabled();
+ expect(screen.getByRole('checkbox', {name: /Tracing/})).toBeEnabled();
+ });
+
+ it('error monitoring checkbox is always checked', () => {
+ render(
+
+ );
+
+ const errorMonitoringCard = screen.getByRole('checkbox', {
+ name: /Error monitoring/,
+ });
+ expect(errorMonitoringCard).toBeChecked();
+ });
+});
diff --git a/static/app/views/onboarding/components/scmFeatureSelectionCards.tsx b/static/app/views/onboarding/components/scmFeatureSelectionCards.tsx
new file mode 100644
index 00000000000000..43380260432e75
--- /dev/null
+++ b/static/app/views/onboarding/components/scmFeatureSelectionCards.tsx
@@ -0,0 +1,115 @@
+import type {ComponentType} from 'react';
+
+import {Flex, Grid} from '@sentry/scraps/layout';
+import {Heading, Text} from '@sentry/scraps/text';
+
+import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import type {DisabledProducts} from 'sentry/components/onboarding/productSelection';
+import {
+ IconGraph,
+ IconProfiling,
+ IconSpan,
+ IconTerminal,
+ IconTimer,
+ IconWarning,
+} from 'sentry/icons';
+import type {SVGIconProps} from 'sentry/icons/svgIcon';
+import {t} from 'sentry/locale';
+
+import {ScmFeatureCard} from './scmFeatureCard';
+
+type FeatureMeta = {
+ description: string;
+ icon: ComponentType;
+ label: string;
+ alwaysEnabled?: boolean;
+};
+
+const FEATURE_META: Record = {
+ [ProductSolution.ERROR_MONITORING]: {
+ label: t('Error monitoring'),
+ icon: IconWarning,
+ description: t('Automatically capture exceptions and stack traces'),
+ alwaysEnabled: true,
+ },
+ [ProductSolution.PERFORMANCE_MONITORING]: {
+ label: t('Tracing'),
+ icon: IconSpan,
+ description: t(
+ 'Find bottlenecks, broken requests, and understand application flow end-to-end'
+ ),
+ },
+ [ProductSolution.SESSION_REPLAY]: {
+ label: t('Session replay'),
+ icon: IconTimer,
+ description: t('Watch real user sessions to see what went wrong'),
+ },
+ [ProductSolution.LOGS]: {
+ label: t('Logging'),
+ icon: IconTerminal,
+ description: t('See logs in context with errors and performance issues'),
+ },
+ [ProductSolution.PROFILING]: {
+ label: t('Profiling'),
+ icon: IconProfiling,
+ description: t(
+ 'Pinpoint the functions and lines of code responsible for performance issues'
+ ),
+ },
+ [ProductSolution.METRICS]: {
+ label: t('Metrics'),
+ icon: IconGraph,
+ description: t(
+ 'Track application performance and usage over time with custom metrics'
+ ),
+ },
+};
+
+interface ScmFeatureSelectionCardsProps {
+ availableFeatures: ProductSolution[];
+ disabledProducts: DisabledProducts;
+ onToggleFeature: (feature: ProductSolution) => void;
+ selectedFeatures: ProductSolution[];
+}
+
+export function ScmFeatureSelectionCards({
+ availableFeatures,
+ selectedFeatures,
+ disabledProducts,
+ onToggleFeature,
+}: ScmFeatureSelectionCardsProps) {
+ const selectedCount = availableFeatures.filter(
+ f => selectedFeatures.includes(f) || FEATURE_META[f].alwaysEnabled
+ ).length;
+ const totalCount = availableFeatures.length;
+
+ return (
+
+
+ {t('What do you want to set up?')}
+ {t('%s of %s selected', selectedCount, totalCount)}
+
+
+ {availableFeatures.map(feature => {
+ const meta = FEATURE_META[feature];
+ const disabledProduct = disabledProducts[feature];
+ const disabledReason = meta.alwaysEnabled
+ ? t('Error monitoring is always enabled')
+ : disabledProduct?.reason;
+ return (
+ onToggleFeature(feature)}
+ />
+ );
+ })}
+
+
+ );
+}
diff --git a/static/app/views/onboarding/components/scmPlatformCard.tsx b/static/app/views/onboarding/components/scmPlatformCard.tsx
new file mode 100644
index 00000000000000..50ae12d4626719
--- /dev/null
+++ b/static/app/views/onboarding/components/scmPlatformCard.tsx
@@ -0,0 +1,40 @@
+import {PlatformIcon} from 'platformicons';
+
+import {Container, Flex, Stack} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
+
+import type {PlatformKey} from 'sentry/types/project';
+
+import {ScmCardButton} from './scmCardButton';
+
+interface ScmPlatformCardProps {
+ isSelected: boolean;
+ name: string;
+ onClick: () => void;
+ platform: PlatformKey;
+ type: string;
+}
+
+export function ScmPlatformCard({
+ platform,
+ name,
+ type,
+ isSelected,
+ onClick,
+}: ScmPlatformCardProps) {
+ return (
+
+
+
+
+
+ {name}
+
+ {type}
+
+
+
+
+
+ );
+}
diff --git a/static/app/views/onboarding/components/useScmPlatformDetection.spec.tsx b/static/app/views/onboarding/components/useScmPlatformDetection.spec.tsx
new file mode 100644
index 00000000000000..5626f76b25d7cf
--- /dev/null
+++ b/static/app/views/onboarding/components/useScmPlatformDetection.spec.tsx
@@ -0,0 +1,83 @@
+import {DetectedPlatformFixture} from 'sentry-fixture/detectedPlatform';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {useScmPlatformDetection} from './useScmPlatformDetection';
+
+describe('useScmPlatformDetection', () => {
+ const organization = OrganizationFixture();
+
+ afterEach(() => {
+ MockApiClient.clearMockResponses();
+ });
+
+ it('returns detected platforms from API response', async () => {
+ const mockPlatforms = [
+ DetectedPlatformFixture(),
+ DetectedPlatformFixture({
+ platform: 'python-django',
+ language: 'Python',
+ confidence: 'medium',
+ bytes: 30000,
+ priority: 2,
+ }),
+ ];
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {platforms: mockPlatforms},
+ });
+
+ const {result} = renderHookWithProviders(() => useScmPlatformDetection('42'), {
+ organization,
+ });
+
+ await waitFor(() => {
+ expect(result.current.detectedPlatforms).toEqual(mockPlatforms);
+ });
+ });
+
+ it('returns empty array when repoId is undefined', () => {
+ const apiMock = MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/undefined/platforms/`,
+ body: {platforms: []},
+ });
+
+ const {result} = renderHookWithProviders(() => useScmPlatformDetection(undefined), {
+ organization,
+ });
+
+ expect(result.current.detectedPlatforms).toEqual([]);
+ expect(apiMock).not.toHaveBeenCalled();
+ });
+
+ it('returns isPending while loading', () => {
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {platforms: []},
+ });
+
+ const {result} = renderHookWithProviders(() => useScmPlatformDetection('42'), {
+ organization,
+ });
+
+ expect(result.current.isPending).toBe(true);
+ });
+
+ it('returns isError on API failure', async () => {
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ statusCode: 500,
+ body: {detail: 'Internal Error'},
+ });
+
+ const {result} = renderHookWithProviders(() => useScmPlatformDetection('42'), {
+ organization,
+ });
+
+ await waitFor(() => {
+ expect(result.current.isError).toBe(true);
+ });
+ });
+});
diff --git a/static/app/views/onboarding/components/useScmPlatformDetection.ts b/static/app/views/onboarding/components/useScmPlatformDetection.ts
new file mode 100644
index 00000000000000..e9d9b6d871a398
--- /dev/null
+++ b/static/app/views/onboarding/components/useScmPlatformDetection.ts
@@ -0,0 +1,45 @@
+import type {PlatformKey} from 'sentry/types/project';
+import {getApiUrl} from 'sentry/utils/api/getApiUrl';
+import {fetchDataQuery, useQuery} from 'sentry/utils/queryClient';
+import {useOrganization} from 'sentry/utils/useOrganization';
+
+export interface DetectedPlatform {
+ bytes: number;
+ confidence: string;
+ language: string;
+ platform: PlatformKey;
+ priority: number;
+}
+
+interface PlatformDetectionResponse {
+ platforms: DetectedPlatform[];
+}
+
+export function useScmPlatformDetection(repoId: string | undefined) {
+ const organization = useOrganization();
+
+ const query = useQuery({
+ queryKey: [
+ getApiUrl(`/organizations/$organizationIdOrSlug/repos/$repoId/platforms/`, {
+ path: {
+ organizationIdOrSlug: organization.slug,
+ repoId: repoId!,
+ },
+ }),
+ {method: 'GET'},
+ ] as const,
+ queryFn: async context => {
+ return fetchDataQuery(context);
+ },
+ staleTime: 30_000,
+ enabled: !!repoId,
+ });
+
+ const {data} = query;
+
+ return {
+ detectedPlatforms: data?.[0]?.platforms ?? [],
+ isPending: query.isPending,
+ isError: query.isError,
+ };
+}
diff --git a/static/app/views/onboarding/onboarding.spec.tsx b/static/app/views/onboarding/onboarding.spec.tsx
index dfb8be8ae3d17b..b44ab22bec65eb 100644
--- a/static/app/views/onboarding/onboarding.spec.tsx
+++ b/static/app/views/onboarding/onboarding.spec.tsx
@@ -13,6 +13,7 @@ import {
waitFor,
} from 'sentry-test/reactTestingLibrary';
+import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
import {OnboardingContextProvider} from 'sentry/components/onboarding/onboardingContext';
import * as useRecentCreatedProjectHook from 'sentry/components/onboarding/useRecentCreatedProject';
import {OnboardingDrawerStore} from 'sentry/stores/onboardingDrawerStore';
@@ -576,9 +577,14 @@ describe('Onboarding', () => {
});
});
- function renderOnboarding(step: string) {
+ function renderOnboarding(
+ step: string,
+ options?: {
+ initialContext?: Parameters[0]['initialValue'];
+ }
+ ) {
return render(
-
+
,
{
@@ -659,7 +665,19 @@ describe('Onboarding', () => {
});
it('renders scm-platform-features step and advances to scm-project-details', async () => {
- const {router} = renderOnboarding('scm-platform-features');
+ const {router} = renderOnboarding('scm-platform-features', {
+ initialContext: {
+ selectedPlatform: {
+ key: 'javascript',
+ name: 'JavaScript',
+ language: 'javascript',
+ link: 'https://docs.sentry.io/platforms/javascript/',
+ type: 'language',
+ category: 'popular',
+ },
+ selectedFeatures: [ProductSolution.ERROR_MONITORING],
+ },
+ });
expect(screen.getByText('Platform & features')).toBeInTheDocument();
diff --git a/static/app/views/onboarding/scmConnect.tsx b/static/app/views/onboarding/scmConnect.tsx
index 8f360a57f7232b..fd27e5addec098 100644
--- a/static/app/views/onboarding/scmConnect.tsx
+++ b/static/app/views/onboarding/scmConnect.tsx
@@ -15,6 +15,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {ScmProviderPills} from './components/scmProviderPills';
import {ScmRepoSelector} from './components/scmRepoSelector';
+import {useScmPlatformDetection} from './components/useScmPlatformDetection';
import {useScmProviders} from './components/useScmProviders';
import type {StepProps} from './types';
@@ -35,6 +36,9 @@ export function ScmConnect({onComplete}: StepProps) {
activeIntegrationExisting,
} = useScmProviders();
+ // Pre-warm platform detection so results are cached when the user advances
+ useScmPlatformDetection(selectedRepository?.id);
+
// Derive integration from explicit selection, falling back to existing
const effectiveIntegration = selectedIntegration ?? activeIntegrationExisting;
diff --git a/static/app/views/onboarding/scmPlatformFeatures.spec.tsx b/static/app/views/onboarding/scmPlatformFeatures.spec.tsx
new file mode 100644
index 00000000000000..83228d33399641
--- /dev/null
+++ b/static/app/views/onboarding/scmPlatformFeatures.spec.tsx
@@ -0,0 +1,378 @@
+import {DetectedPlatformFixture} from 'sentry-fixture/detectedPlatform';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {RepositoryFixture} from 'sentry-fixture/repository';
+
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {openConsoleModal, openModal} from 'sentry/actionCreators/modal';
+import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {
+ OnboardingContextProvider,
+ type OnboardingSessionState,
+} from 'sentry/components/onboarding/onboardingContext';
+import {sessionStorageWrapper} from 'sentry/utils/sessionStorage';
+
+import {ScmPlatformFeatures} from './scmPlatformFeatures';
+
+jest.mock('sentry/actionCreators/modal');
+
+// Provide a small platform list so CompactSelect stays below the
+// virtualizeThreshold (50) and renders all options in JSDOM.
+jest.mock('sentry/data/platforms', () => {
+ const actual = jest.requireActual('sentry/data/platforms');
+ return {
+ ...actual,
+ platforms: actual.platforms.filter(
+ (p: {id: string}) =>
+ p.id === 'javascript' ||
+ p.id === 'javascript-nextjs' ||
+ p.id === 'python' ||
+ p.id === 'python-django' ||
+ p.id === 'nintendo-switch'
+ ),
+ };
+});
+
+function makeOnboardingWrapper(initialState?: OnboardingSessionState) {
+ return function OnboardingWrapper({children}: {children?: React.ReactNode}) {
+ return (
+
+ {children}
+
+ );
+ };
+}
+
+const mockRepository = RepositoryFixture({id: '42'});
+
+describe('ScmPlatformFeatures', () => {
+ const organization = OrganizationFixture({
+ features: ['performance-view', 'session-replay', 'profiling-view'],
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ sessionStorageWrapper.clear();
+ });
+
+ afterEach(() => {
+ MockApiClient.clearMockResponses();
+ });
+
+ it('renders detected platforms when repository is in context', async () => {
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {
+ platforms: [
+ DetectedPlatformFixture(),
+ DetectedPlatformFixture({
+ platform: 'python-django',
+ language: 'Python',
+ priority: 2,
+ }),
+ ],
+ },
+ });
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper({
+ selectedRepository: mockRepository,
+ }),
+ }
+ );
+
+ expect(await screen.findByText('Next.js')).toBeInTheDocument();
+ expect(screen.getByText('Django')).toBeInTheDocument();
+ });
+
+ it('auto-selects first detected platform', async () => {
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {
+ platforms: [
+ DetectedPlatformFixture(),
+ DetectedPlatformFixture({
+ platform: 'python-django',
+ language: 'Python',
+ priority: 2,
+ }),
+ ],
+ },
+ });
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper({
+ selectedRepository: mockRepository,
+ }),
+ }
+ );
+
+ expect(await screen.findByText('What do you want to set up?')).toBeInTheDocument();
+ });
+
+ it('clicking "Change platform" shows manual picker', async () => {
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {
+ platforms: [
+ DetectedPlatformFixture(),
+ DetectedPlatformFixture({
+ platform: 'python-django',
+ language: 'Python',
+ priority: 2,
+ }),
+ ],
+ },
+ });
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper({
+ selectedRepository: mockRepository,
+ }),
+ }
+ );
+
+ const changeButton = await screen.findByRole('button', {
+ name: "Doesn't look right? Change platform",
+ });
+ await userEvent.click(changeButton);
+
+ expect(screen.getByRole('heading', {name: 'Select a platform'})).toBeInTheDocument();
+ });
+
+ it('renders manual picker when no repository in context', async () => {
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper(),
+ }
+ );
+
+ expect(
+ await screen.findByRole('heading', {name: 'Select a platform'})
+ ).toBeInTheDocument();
+ expect(screen.queryByText('Recommended SDK')).not.toBeInTheDocument();
+ });
+
+ it('continue button is disabled when no platform selected', async () => {
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper(),
+ }
+ );
+
+ // Wait for the component to fully settle (CompactSelect triggers async popper updates)
+ await screen.findByRole('heading', {name: 'Select a platform'});
+
+ expect(screen.getByRole('button', {name: 'Continue'})).toBeDisabled();
+ });
+
+ it('continue button is enabled when platform is selected', async () => {
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {
+ platforms: [DetectedPlatformFixture()],
+ },
+ });
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper({
+ selectedRepository: mockRepository,
+ }),
+ }
+ );
+
+ // Wait for auto-select of first detected platform
+ await waitFor(() => {
+ expect(screen.getByRole('button', {name: 'Continue'})).toBeEnabled();
+ });
+ });
+
+ it('enabling profiling auto-enables tracing', async () => {
+ const pythonPlatform = DetectedPlatformFixture({
+ platform: 'python',
+ language: 'Python',
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {platforms: [pythonPlatform]},
+ });
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper({
+ selectedRepository: mockRepository,
+ selectedFeatures: [ProductSolution.ERROR_MONITORING],
+ }),
+ }
+ );
+
+ // Wait for feature cards to appear
+ await screen.findByText('What do you want to set up?');
+
+ // Neither profiling nor tracing should be checked initially
+ expect(screen.getByRole('checkbox', {name: /Profiling/})).not.toBeChecked();
+ expect(screen.getByRole('checkbox', {name: /Tracing/})).not.toBeChecked();
+
+ // Enable profiling — tracing should auto-enable
+ await userEvent.click(screen.getByRole('checkbox', {name: /Profiling/}));
+
+ expect(screen.getByRole('checkbox', {name: /Profiling/})).toBeChecked();
+ expect(screen.getByRole('checkbox', {name: /Tracing/})).toBeChecked();
+ });
+
+ it('shows framework suggestion modal when selecting a base language', async () => {
+ const mockOpenModal = openModal as jest.Mock;
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper(),
+ }
+ );
+
+ await screen.findByRole('heading', {name: 'Select a platform'});
+
+ // Open the CompactSelect and select a base language
+ await userEvent.click(screen.getByRole('button', {name: 'None'}));
+ await userEvent.click(
+ await screen.findByRole('option', {name: 'Browser JavaScript'})
+ );
+
+ await waitFor(() => {
+ expect(mockOpenModal).toHaveBeenCalled();
+ });
+ });
+
+ it('opens console modal when selecting a disabled gaming platform', async () => {
+ const mockOpenConsoleModal = openConsoleModal as jest.Mock;
+
+ render(
+ null}
+ />,
+ {
+ // No enabledConsolePlatforms — all console platforms are blocked
+ organization: OrganizationFixture({
+ features: ['performance-view', 'session-replay', 'profiling-view'],
+ }),
+ additionalWrapper: makeOnboardingWrapper(),
+ }
+ );
+
+ await screen.findByRole('heading', {name: 'Select a platform'});
+
+ // Open the CompactSelect and select a console platform
+ await userEvent.click(screen.getByRole('button', {name: 'None'}));
+ await userEvent.click(await screen.findByRole('option', {name: 'Nintendo Switch'}));
+
+ await waitFor(() => {
+ expect(mockOpenConsoleModal).toHaveBeenCalled();
+ });
+ });
+
+ it('disabling tracing auto-disables profiling', async () => {
+ const pythonPlatform = DetectedPlatformFixture({
+ platform: 'python',
+ language: 'Python',
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/repos/42/platforms/`,
+ body: {platforms: [pythonPlatform]},
+ });
+
+ render(
+ null}
+ />,
+ {
+ organization,
+ additionalWrapper: makeOnboardingWrapper({
+ selectedRepository: mockRepository,
+ selectedPlatform: {
+ key: 'python',
+ name: 'Python',
+ language: 'python',
+ type: 'language',
+ link: 'https://docs.sentry.io/platforms/python/',
+ category: 'popular',
+ },
+ selectedFeatures: [
+ ProductSolution.ERROR_MONITORING,
+ ProductSolution.PERFORMANCE_MONITORING,
+ ProductSolution.PROFILING,
+ ],
+ }),
+ }
+ );
+
+ // Wait for feature cards to appear
+ await screen.findByText('What do you want to set up?');
+
+ // Both should be checked initially
+ expect(screen.getByRole('checkbox', {name: /Tracing/})).toBeChecked();
+ expect(screen.getByRole('checkbox', {name: /Profiling/})).toBeChecked();
+
+ // Disable tracing — profiling should auto-disable
+ await userEvent.click(screen.getByRole('checkbox', {name: /Tracing/}));
+
+ expect(screen.getByRole('checkbox', {name: /Tracing/})).not.toBeChecked();
+ expect(screen.getByRole('checkbox', {name: /Profiling/})).not.toBeChecked();
+ });
+});
diff --git a/static/app/views/onboarding/scmPlatformFeatures.tsx b/static/app/views/onboarding/scmPlatformFeatures.tsx
index b124eaefe21650..540270129500c7 100644
--- a/static/app/views/onboarding/scmPlatformFeatures.tsx
+++ b/static/app/views/onboarding/scmPlatformFeatures.tsx
@@ -1,16 +1,363 @@
+import {useCallback, useMemo, useState} from 'react';
+import {PlatformIcon} from 'platformicons';
+
import {Button} from '@sentry/scraps/button';
-import {Flex} from '@sentry/scraps/layout';
-import {Heading} from '@sentry/scraps/text';
+import {CompactSelect} from '@sentry/scraps/compactSelect';
+import {Flex, Stack} from '@sentry/scraps/layout';
+import {Heading, Text} from '@sentry/scraps/text';
+import {closeModal, openConsoleModal, openModal} from 'sentry/actionCreators/modal';
+import {LoadingIndicator} from 'sentry/components/loadingIndicator';
+import {SupportedLanguages} from 'sentry/components/onboarding/frameworkSuggestionModal';
+import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {useOnboardingContext} from 'sentry/components/onboarding/onboardingContext';
+import {
+ getDisabledProducts,
+ platformProductAvailability,
+} from 'sentry/components/onboarding/productSelection';
+import {platforms} from 'sentry/data/platforms';
import {t} from 'sentry/locale';
+import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
+import type {PlatformIntegration, PlatformKey} from 'sentry/types/project';
+import {isDisabledGamingPlatform} from 'sentry/utils/platform';
+import {useOrganization} from 'sentry/utils/useOrganization';
+import {ScmFeatureSelectionCards} from 'sentry/views/onboarding/components/scmFeatureSelectionCards';
+import {ScmPlatformCard} from 'sentry/views/onboarding/components/scmPlatformCard';
+import {
+ useScmPlatformDetection,
+ type DetectedPlatform,
+} from './components/useScmPlatformDetection';
import type {StepProps} from './types';
+interface ResolvedPlatform extends DetectedPlatform {
+ info: PlatformIntegration;
+}
+
+const platformsByKey = new Map(platforms.map(p => [p.id, p]));
+
+const getPlatformInfo = (key: PlatformKey) => platformsByKey.get(key);
+
+const platformOptions = platforms.map(platform => ({
+ value: platform.id,
+ label: platform.name,
+ textValue: `${platform.name} ${platform.id}`,
+ leadingItems: () => ,
+}));
+
+function toSelectedSdk(info: PlatformIntegration): OnboardingSelectedSDK {
+ return {
+ key: info.id,
+ name: info.name,
+ language: info.language,
+ type: info.type,
+ link: info.link,
+ // PlatformIntegration doesn't carry a category — 'all' is the most
+ // neutral value and avoids implying a specific picker category.
+ category: 'all',
+ };
+}
+
+function shouldSuggestFramework(platformKey: PlatformKey): boolean {
+ const info = getPlatformInfo(platformKey);
+ return (
+ info?.type === 'language' &&
+ Object.values(SupportedLanguages).includes(info.language as SupportedLanguages)
+ );
+}
+
export function ScmPlatformFeatures({onComplete}: StepProps) {
+ const organization = useOrganization();
+ const {
+ selectedRepository,
+ selectedPlatform,
+ setSelectedPlatform,
+ selectedFeatures,
+ setSelectedFeatures,
+ } = useOnboardingContext();
+
+ const [showManualPicker, setShowManualPicker] = useState(false);
+
+ const setPlatform = useCallback(
+ (platformKey: PlatformKey) => {
+ const info = getPlatformInfo(platformKey);
+ if (info) {
+ setSelectedPlatform(toSelectedSdk(info));
+ }
+ },
+ [setSelectedPlatform]
+ );
+
+ const hasScmConnected = !!selectedRepository;
+
+ const {detectedPlatforms, isPending: isDetecting} = useScmPlatformDetection(
+ hasScmConnected ? selectedRepository.id : undefined
+ );
+
+ const currentFeatures = useMemo(
+ () => selectedFeatures ?? [ProductSolution.ERROR_MONITORING],
+ [selectedFeatures]
+ );
+
+ const resolvedPlatforms = useMemo(
+ () =>
+ detectedPlatforms.reduce((acc, detected) => {
+ const info = getPlatformInfo(detected.platform);
+ if (info) {
+ acc.push({...detected, info});
+ }
+ return acc;
+ }, []),
+ [detectedPlatforms]
+ );
+
+ const detectedPlatformKey = resolvedPlatforms[0]?.platform;
+ // Derive platform from explicit selection, falling back to first detected
+ const currentPlatformKey = selectedPlatform?.key ?? detectedPlatformKey;
+
+ const availableFeatures = useMemo(
+ () =>
+ currentPlatformKey
+ ? [
+ ...new Set([
+ ProductSolution.ERROR_MONITORING,
+ ...(platformProductAvailability[currentPlatformKey] ?? []),
+ ]),
+ ]
+ : [],
+ [currentPlatformKey]
+ );
+
+ const disabledProducts = useMemo(
+ () => getDisabledProducts(organization),
+ [organization]
+ );
+
+ const handleToggleFeature = useCallback(
+ (feature: ProductSolution) => {
+ if (disabledProducts[feature]) {
+ disabledProducts[feature]?.onClick?.();
+ return;
+ }
+
+ const newFeatures = new Set(
+ currentFeatures.includes(feature)
+ ? currentFeatures.filter(f => f !== feature)
+ : [...currentFeatures, feature]
+ );
+
+ // Profiling requires tracing — mirror the constraint from ProductSelection
+ if (availableFeatures.includes(ProductSolution.PROFILING)) {
+ if (
+ feature === ProductSolution.PROFILING &&
+ newFeatures.has(ProductSolution.PROFILING)
+ ) {
+ newFeatures.add(ProductSolution.PERFORMANCE_MONITORING);
+ } else if (
+ feature === ProductSolution.PERFORMANCE_MONITORING &&
+ !newFeatures.has(ProductSolution.PERFORMANCE_MONITORING)
+ ) {
+ newFeatures.delete(ProductSolution.PROFILING);
+ }
+ }
+
+ setSelectedFeatures(Array.from(newFeatures));
+ },
+ [currentFeatures, setSelectedFeatures, disabledProducts, availableFeatures]
+ );
+
+ const applyPlatformSelection = useCallback(
+ (sdk: OnboardingSelectedSDK) => {
+ setSelectedPlatform(sdk);
+ setSelectedFeatures([ProductSolution.ERROR_MONITORING]);
+ },
+ [setSelectedPlatform, setSelectedFeatures]
+ );
+
+ const handleManualPlatformSelect = useCallback(
+ async (option: {value: string}) => {
+ const platformKey = option.value as PlatformKey;
+ if (platformKey === selectedPlatform?.key) {
+ return;
+ }
+
+ // Block disabled gaming/console platforms
+ const platformInfo = getPlatformInfo(platformKey);
+ if (
+ platformInfo &&
+ isDisabledGamingPlatform({
+ platform: platformInfo,
+ enabledConsolePlatforms: organization.enabledConsolePlatforms,
+ })
+ ) {
+ openConsoleModal({
+ organization,
+ selectedPlatform: toSelectedSdk(platformInfo),
+ origin: 'onboarding',
+ });
+ return;
+ }
+
+ // For base languages (JavaScript, Python, etc.), show a modal suggesting
+ // specific frameworks — matching the legacy onboarding behavior.
+ if (platformInfo && shouldSuggestFramework(platformKey)) {
+ const baseSdk = toSelectedSdk(platformInfo);
+
+ const {FrameworkSuggestionModal, modalCss} =
+ await import('sentry/components/onboarding/frameworkSuggestionModal');
+
+ openModal(
+ deps => (
+ {
+ applyPlatformSelection(selectedFramework);
+ closeModal();
+ }}
+ onSkip={() => {
+ applyPlatformSelection(baseSdk);
+ closeModal();
+ }}
+ newOrg
+ />
+ ),
+ {modalCss}
+ );
+ return;
+ }
+
+ setPlatform(platformKey);
+ setSelectedFeatures([ProductSolution.ERROR_MONITORING]);
+ },
+ [
+ selectedPlatform?.key,
+ setPlatform,
+ setSelectedFeatures,
+ applyPlatformSelection,
+ organization,
+ ]
+ );
+
+ const handleSelectDetectedPlatform = useCallback(
+ (platformKey: PlatformKey) => {
+ if (platformKey === selectedPlatform?.key) {
+ return;
+ }
+ setPlatform(platformKey);
+ setSelectedFeatures([ProductSolution.ERROR_MONITORING]);
+ },
+ [selectedPlatform?.key, setPlatform, setSelectedFeatures]
+ );
+
+ // If the user previously selected a platform manually (not in the detected
+ // list), show the manual picker so their selection is visible.
+ const currentPlatformIsDetected = resolvedPlatforms.some(
+ p => p.platform === currentPlatformKey
+ );
+ const hasDetectedPlatforms = resolvedPlatforms.length > 0 || isDetecting;
+ const showDetectedPlatforms =
+ hasScmConnected &&
+ !showManualPicker &&
+ hasDetectedPlatforms &&
+ (!currentPlatformKey || currentPlatformIsDetected);
+
return (
-
- {t('Platform & features')}
- onComplete()}>
+
+
+ {t('Platform & features')}
+
+ {t('Select your SDK first, then choose the features to enable.')}
+
+
+
+
+ {showDetectedPlatforms ? (
+
+ {t('Recommended SDK')}
+ {isDetecting ? (
+
+
+
+ ) : (
+
+
+ {resolvedPlatforms.map(({platform, info}) => (
+ handleSelectDetectedPlatform(platform)}
+ />
+ ))}
+
+ setShowManualPicker(true)}
+ >
+ {t("Doesn't look right? Change platform")}
+
+
+ )}
+
+ ) : (
+
+ {t('Select a platform')}
+
+ {hasScmConnected && (
+ {
+ setShowManualPicker(false);
+ if (detectedPlatformKey) {
+ setPlatform(detectedPlatformKey);
+ setSelectedFeatures([ProductSolution.ERROR_MONITORING]);
+ }
+ }}
+ >
+ {t('Back to recommended platforms')}
+
+ )}
+
+ )}
+
+ {availableFeatures.length > 0 && (
+
+ )}
+
+
+ {
+ // Persist derived defaults to context if user accepted them
+ if (currentPlatformKey && !selectedPlatform?.key) {
+ setPlatform(currentPlatformKey);
+ }
+ if (!selectedFeatures) {
+ setSelectedFeatures(currentFeatures);
+ }
+ onComplete();
+ }}
+ disabled={!currentPlatformKey}
+ >
{t('Continue')}
diff --git a/static/app/views/onboarding/welcome.tsx b/static/app/views/onboarding/welcome.tsx
index becd5c5a687d71..783898f783f779 100644
--- a/static/app/views/onboarding/welcome.tsx
+++ b/static/app/views/onboarding/welcome.tsx
@@ -11,7 +11,7 @@ import {Text} from '@sentry/scraps/text';
import {t} from 'sentry/locale';
import {testableTransition} from 'sentry/utils/testableTransition';
-import FallingError from 'sentry/views/onboarding/components/fallingError';
+import {FallingError} from 'sentry/views/onboarding/components/fallingError';
import {WelcomeBackground} from 'sentry/views/onboarding/components/welcomeBackground';
import {WelcomeSkipButton} from 'sentry/views/onboarding/components/welcomeSkipButton';
import {useWelcomeAnalyticsEffect} from 'sentry/views/onboarding/useWelcomeAnalyticsEffect';
diff --git a/static/app/views/organizationLayout/body.tsx b/static/app/views/organizationLayout/body.tsx
index dea5adde028636..089ba45f69ddf3 100644
--- a/static/app/views/organizationLayout/body.tsx
+++ b/static/app/views/organizationLayout/body.tsx
@@ -6,7 +6,7 @@ import {Button} from '@sentry/scraps/button';
import {Flex, Stack} from '@sentry/scraps/layout';
import {Heading, Text} from '@sentry/scraps/text';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {LogoSentry} from 'sentry/components/logoSentry';
import {t, tct} from 'sentry/locale';
import {AlertStore} from 'sentry/stores/alertStore';
diff --git a/static/app/views/organizationStats/index.tsx b/static/app/views/organizationStats/index.tsx
index 120e184d3ade85..44c190e5091d3b 100644
--- a/static/app/views/organizationStats/index.tsx
+++ b/static/app/views/organizationStats/index.tsx
@@ -10,7 +10,7 @@ import {Flex} from '@sentry/scraps/layout';
import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
import type {DateTimeObject} from 'sentry/components/charts/utils';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {HookOrDefault} from 'sentry/components/hookOrDefault';
import * as Layout from 'sentry/components/layouts/thirds';
import {NoProjectMessage} from 'sentry/components/noProjectMessage';
@@ -31,7 +31,10 @@ import {decodeScalar} from 'sentry/utils/queryString';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate, type ReactRouter3Navigate} from 'sentry/utils/useNavigate';
import {useOrganization} from 'sentry/utils/useOrganization';
-import {canUseMetricsStatsUI} from 'sentry/views/explore/metrics/metricsFlags';
+import {
+ canUseMetricsStatsBytesUI,
+ canUseMetricsStatsUI,
+} from 'sentry/views/explore/metrics/metricsFlags';
import {StatsHeader as HeaderTabs} from 'sentry/views/organizationStats/header';
import {getPerformanceBaseUrl} from 'sentry/views/performance/utils';
import {makeProjectsPathname} from 'sentry/views/projects/pathname';
@@ -277,6 +280,9 @@ export class OrganizationStatsInner extends Component {
if ([DataCategory.TRACE_METRICS].includes(opt.value)) {
return canUseMetricsStatsUI(organization);
}
+ if ([DataCategory.TRACE_METRIC_BYTE].includes(opt.value)) {
+ return canUseMetricsStatsBytesUI(organization);
+ }
if (
[DataCategory.PROFILE_DURATION, DataCategory.PROFILE_DURATION_UI].includes(
opt.value
diff --git a/static/app/views/organizationStats/teamInsights/teamMisery.tsx b/static/app/views/organizationStats/teamInsights/teamMisery.tsx
index 64f50d7b87a4d3..2850f670bbc2a0 100644
--- a/static/app/views/organizationStats/teamInsights/teamMisery.tsx
+++ b/static/app/views/organizationStats/teamInsights/teamMisery.tsx
@@ -17,7 +17,7 @@ import type {Organization, SavedQueryVersions} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
diff --git a/static/app/views/organizationStats/utils.tsx b/static/app/views/organizationStats/utils.tsx
index 33957e1ec1ff99..21d274767bb4d2 100644
--- a/static/app/views/organizationStats/utils.tsx
+++ b/static/app/views/organizationStats/utils.tsx
@@ -34,7 +34,8 @@ export function formatUsageWithUnits(
): string {
if (
dataCategory === DATA_CATEGORY_INFO.attachment.plural ||
- dataCategory === DATA_CATEGORY_INFO.log_byte.plural
+ dataCategory === DATA_CATEGORY_INFO.log_byte.plural ||
+ dataCategory === DATA_CATEGORY_INFO.trace_metric_byte.plural
) {
if (options.useUnitScaling) {
return formatBytesBase10(usageQuantity);
@@ -70,10 +71,12 @@ export function getFormatUsageOptions(dataCategory: DataCategory): FormatOptions
return {
isAbbreviated:
dataCategory !== DATA_CATEGORY_INFO.attachment.plural &&
- dataCategory !== DATA_CATEGORY_INFO.log_byte.plural,
+ dataCategory !== DATA_CATEGORY_INFO.log_byte.plural &&
+ dataCategory !== DATA_CATEGORY_INFO.trace_metric_byte.plural,
useUnitScaling:
dataCategory === DATA_CATEGORY_INFO.attachment.plural ||
- dataCategory === DATA_CATEGORY_INFO.log_byte.plural,
+ dataCategory === DATA_CATEGORY_INFO.log_byte.plural ||
+ dataCategory === DATA_CATEGORY_INFO.trace_metric_byte.plural,
};
}
diff --git a/static/app/views/performance/breadcrumb.tsx b/static/app/views/performance/breadcrumb.tsx
index 9505ba24703740..145310367b75e0 100644
--- a/static/app/views/performance/breadcrumb.tsx
+++ b/static/app/views/performance/breadcrumb.tsx
@@ -8,7 +8,7 @@ import type {SpanSlug} from 'sentry/utils/performance/suspectSpans/types';
import {DOMAIN_VIEW_BASE_TITLE} from 'sentry/views/insights/pages/settings';
import type {DomainView} from 'sentry/views/insights/pages/useFilters';
-import type Tab from './transactionSummary/tabs';
+import type {Tab} from './transactionSummary/tabs';
import {transactionSummaryRouteWithQuery} from './transactionSummary/utils';
type Props = {
diff --git a/static/app/views/performance/data.spec.tsx b/static/app/views/performance/data.spec.tsx
index 14e12b87be58c5..eba38161ca2883 100644
--- a/static/app/views/performance/data.spec.tsx
+++ b/static/app/views/performance/data.spec.tsx
@@ -1,5 +1,4 @@
import {LocationFixture} from 'sentry-fixture/locationFixture';
-import {OrganizationFixture} from 'sentry-fixture/organization';
import {
MEPState,
@@ -11,15 +10,8 @@ import {
} from 'sentry/views/performance/data';
describe('generatePerformanceEventView()', () => {
- const organization = OrganizationFixture();
-
it('generates default values', () => {
- const result = generatePerformanceEventView(
- LocationFixture({query: {}}),
- [],
- {},
- organization
- );
+ const result = generatePerformanceEventView(LocationFixture({query: {}}), [], {});
expect(result.id).toBeUndefined();
expect(result.name).toBe('Performance');
@@ -34,8 +26,7 @@ describe('generatePerformanceEventView()', () => {
const result = generatePerformanceEventView(
LocationFixture({query: {sort: ['-p50', '-count']}}),
[],
- {},
- organization
+ {}
);
expect(result.sorts).toEqual([{kind: 'desc', field: 'p50'}]);
@@ -46,8 +37,7 @@ describe('generatePerformanceEventView()', () => {
const result = generatePerformanceEventView(
LocationFixture({query: {statsPeriod: ['90d', '45d']}}),
[],
- {},
- organization
+ {}
);
expect(result.start).toBeUndefined();
expect(result.end).toBeUndefined();
@@ -60,8 +50,7 @@ describe('generatePerformanceEventView()', () => {
query: {start: '2020-04-25T12:00:00', end: '2020-05-25T12:00:00'},
}),
[],
- {},
- organization
+ {}
);
expect(result.start).toBe('2020-04-25T12:00:00.000');
expect(result.end).toBe('2020-05-25T12:00:00.000');
@@ -72,8 +61,7 @@ describe('generatePerformanceEventView()', () => {
const result = generatePerformanceEventView(
LocationFixture({query: {query: 'things.update'}}),
[],
- {},
- organization
+ {}
);
expect(result.query).toEqual(expect.stringContaining('transaction:*things.update*'));
expect(result.getQueryWithAdditionalConditions()).toEqual(
@@ -85,8 +73,7 @@ describe('generatePerformanceEventView()', () => {
const result = generatePerformanceEventView(
LocationFixture({query: {query: 'things.update transaction:thing.gone'}}),
[],
- {},
- organization
+ {}
);
expect(result.query).toEqual(expect.stringContaining('transaction:*things.update*'));
expect(result.getQueryWithAdditionalConditions()).toEqual(
@@ -99,8 +86,7 @@ describe('generatePerformanceEventView()', () => {
const result = generatePerformanceEventView(
LocationFixture({query: {query: 'key:value tag:value'}}),
[],
- {},
- organization
+ {}
);
expect(result.query).toEqual(expect.stringContaining('key:value'));
expect(result.query).toEqual(expect.stringContaining('tag:value'));
@@ -113,8 +99,7 @@ describe('generatePerformanceEventView()', () => {
const result = generatePerformanceEventView(
LocationFixture({query: {query: 'key:value tag:value'}}),
[],
- {},
- organization
+ {}
);
expect(result.fields).toEqual(
expect.arrayContaining([expect.objectContaining({field: 'user_misery()'})])
@@ -136,8 +121,7 @@ describe('generatePerformanceEventView()', () => {
},
}),
[],
- {withStaticFilters: true},
- organization
+ {withStaticFilters: true}
);
expect(result.query).toBe('transaction:*auth*');
});
diff --git a/static/app/views/performance/data.tsx b/static/app/views/performance/data.tsx
index e63839f5130e34..9538c47b5d113d 100644
--- a/static/app/views/performance/data.tsx
+++ b/static/app/views/performance/data.tsx
@@ -8,11 +8,9 @@ import {COL_WIDTH_UNDEFINED} from 'sentry/components/tables/gridEditable';
import {t, tct} from 'sentry/locale';
import type {NewQuery, Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import EventView from 'sentry/utils/discover/eventView';
-import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
+import {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
-import {getCurrentTrendParameter} from 'sentry/views/performance/trends/utils';
import {getCurrentLandingDisplay, LandingDisplayField} from './landing/utils';
@@ -181,8 +179,7 @@ export function prepareQueryForLandingPage(searchQuery: any, withStaticFilters:
export function generateGenericPerformanceEventView(
location: Location,
- withStaticFilters: boolean,
- organization: Organization
+ withStaticFilters: boolean
): EventView {
const {query} = location;
@@ -225,19 +222,6 @@ export function generateGenericPerformanceEventView(
const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
- if (query.trendParameter) {
- // projects and projectIds are not necessary here since trendParameter will always
- // be present in location and will not be determined based on the project type
- const trendParameter = getCurrentTrendParameter(location, [], []);
- if (
- // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- WEB_VITAL_DETAILS[trendParameter.column] &&
- !organization.features.includes('performance-new-trends')
- ) {
- eventView.additionalConditions.addFilterValues('has', [trendParameter.column]);
- }
- }
-
return eventView;
}
@@ -472,14 +456,9 @@ export function generateFrontendOtherPerformanceEventView(
export function generatePerformanceEventView(
location: Location,
projects: Project[],
- {isTrends = false, withStaticFilters = false} = {},
- organization: Organization
+ {isTrends = false, withStaticFilters = false} = {}
) {
- const eventView = generateGenericPerformanceEventView(
- location,
- withStaticFilters,
- organization
- );
+ const eventView = generateGenericPerformanceEventView(location, withStaticFilters);
if (isTrends) {
return eventView;
}
diff --git a/static/app/views/performance/eap/overviewSpansTable.tsx b/static/app/views/performance/eap/overviewSpansTable.tsx
index 28169a9551cd82..94db3235e32514 100644
--- a/static/app/views/performance/eap/overviewSpansTable.tsx
+++ b/static/app/views/performance/eap/overviewSpansTable.tsx
@@ -10,9 +10,9 @@ import {GridEditable} from 'sentry/components/tables/gridEditable';
import {IconPlay, IconProfiling} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {EventsMetaType} from 'sentry/utils/discover/eventView';
+import type {EventsMetaType, EventView} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
+import {decodeScalar} from 'sentry/utils/queryString';
import type {Theme} from 'sentry/utils/theme';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
@@ -51,13 +51,15 @@ export function OverviewSpansTable({eventView, totalValues, transactionName}: Pr
const projectSlug = projects.find(p => p.id === `${eventView.project}`)?.slug;
const p95 = totalValues?.['p95()'] ?? 0;
- const defaultQuery = new MutableSearch('');
- defaultQuery.addFilterValue('is_transaction', '1');
- defaultQuery.addFilterValue('transaction', transactionName);
+ const searchQuery = decodeScalar(location.query.query, '');
- const countQuery = new MutableSearch('');
- countQuery.addFilterValue('is_transaction', '1');
- countQuery.addFilterValue('transaction', transactionName);
+ const defaultQuery = new MutableSearch(searchQuery);
+ defaultQuery.setFilterValues('is_transaction', ['true']);
+ defaultQuery.setFilterValues('transaction', [transactionName]);
+
+ const countQuery = new MutableSearch(searchQuery);
+ countQuery.setFilterValues('is_transaction', ['true']);
+ countQuery.setFilterValues('transaction', [transactionName]);
const {data: numEvents, error: numEventsError} = useSpans(
{
diff --git a/static/app/views/performance/eap/segmentSpansTable.spec.tsx b/static/app/views/performance/eap/segmentSpansTable.spec.tsx
index ead4086ab88d38..6df55a4d7c6c01 100644
--- a/static/app/views/performance/eap/segmentSpansTable.spec.tsx
+++ b/static/app/views/performance/eap/segmentSpansTable.spec.tsx
@@ -6,7 +6,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';
import {PageFiltersStore} from 'sentry/components/pageFilters/store';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SegmentSpansTable} from 'sentry/views/performance/eap/segmentSpansTable';
describe('SegmentSpansTable', () => {
diff --git a/static/app/views/performance/eap/segmentSpansTable.tsx b/static/app/views/performance/eap/segmentSpansTable.tsx
index 96c6889cdc7a35..2afc650f11e11e 100644
--- a/static/app/views/performance/eap/segmentSpansTable.tsx
+++ b/static/app/views/performance/eap/segmentSpansTable.tsx
@@ -13,8 +13,7 @@ import {GridEditable} from 'sentry/components/tables/gridEditable';
import {IconPlay, IconProfiling} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {EventsMetaType} from 'sentry/utils/discover/eventView';
+import type {EventsMetaType, EventView} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {decodeScalar} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
diff --git a/static/app/views/performance/landing/chart/histogramChart.tsx b/static/app/views/performance/landing/chart/histogramChart.tsx
index f77bc9d00760e7..99a99901b22635 100644
--- a/static/app/views/performance/landing/chart/histogramChart.tsx
+++ b/static/app/views/performance/landing/chart/histogramChart.tsx
@@ -11,7 +11,7 @@ import {Placeholder} from 'sentry/components/placeholder';
import {t} from 'sentry/locale';
import type {Series} from 'sentry/types/echarts';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
import {getDynamicText} from 'sentry/utils/getDynamicText';
import type {HistogramData} from 'sentry/utils/performance/histogram/types';
diff --git a/static/app/views/performance/landing/metricsDataSwitcher.tsx b/static/app/views/performance/landing/metricsDataSwitcher.tsx
index b28b4a5839dcb6..39246425893897 100644
--- a/static/app/views/performance/landing/metricsDataSwitcher.tsx
+++ b/static/app/views/performance/landing/metricsDataSwitcher.tsx
@@ -5,7 +5,7 @@ import {Flex} from '@sentry/scraps/layout';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import type {Organization} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {MetricDataSwitcherOutcome} from 'sentry/utils/performance/contexts/metricsCardinality';
import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
import {
diff --git a/static/app/views/performance/landing/metricsDataSwitcherAlert.tsx b/static/app/views/performance/landing/metricsDataSwitcherAlert.tsx
index 32ab2306229356..ee9d53d512e9e2 100644
--- a/static/app/views/performance/landing/metricsDataSwitcherAlert.tsx
+++ b/static/app/views/performance/landing/metricsDataSwitcherAlert.tsx
@@ -11,7 +11,7 @@ import {
} from 'sentry/stores/onboardingDrawerStore';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {MetricDataSwitcherOutcome} from 'sentry/utils/performance/contexts/metricsCardinality';
import {useLocation} from 'sentry/utils/useLocation';
import {useRouter} from 'sentry/utils/useRouter';
diff --git a/static/app/views/performance/landing/utils.spec.tsx b/static/app/views/performance/landing/utils.spec.tsx
index f8e4bc84ea7d9f..13ea68a7193190 100644
--- a/static/app/views/performance/landing/utils.spec.tsx
+++ b/static/app/views/performance/landing/utils.spec.tsx
@@ -5,7 +5,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import type {Project} from 'sentry/types/project';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getCurrentLandingDisplay} from 'sentry/views/performance/landing/utils';
function initializeData(projects: Project[], query: any = {}) {
diff --git a/static/app/views/performance/landing/utils.tsx b/static/app/views/performance/landing/utils.tsx
index 1c4b7a49f06edb..fc5a9c64ae71e8 100644
--- a/static/app/views/performance/landing/utils.tsx
+++ b/static/app/views/performance/landing/utils.tsx
@@ -3,7 +3,7 @@ import type {Location} from 'history';
import {t} from 'sentry/locale';
import type {Project} from 'sentry/types/project';
import {browserHistory} from 'sentry/utils/browserHistory';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {
diff --git a/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx b/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx
index 0df26d6b1ad4bf..a555fc2759fedf 100644
--- a/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx
+++ b/static/app/views/performance/landing/widgets/components/widgetChartRow.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import type {Location} from 'history';
import {PerformanceLayoutBodyRow} from 'sentry/components/performance/layouts';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {usePerformanceDisplayType} from 'sentry/utils/performance/contexts/performanceDisplayContext';
import {getChartSetting} from 'sentry/views/performance/landing/widgets/utils';
import type {PerformanceWidgetSetting} from 'sentry/views/performance/landing/widgets/widgetDefinitions';
diff --git a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx
index 79482cf245eefa..3accb7e1196017 100644
--- a/static/app/views/performance/landing/widgets/components/widgetContainer.tsx
+++ b/static/app/views/performance/landing/widgets/components/widgetContainer.tsx
@@ -13,7 +13,7 @@ import {IconEllipsis} from 'sentry/icons/iconEllipsis';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {encodeSort} from 'sentry/utils/discover/eventView';
import type {Field} from 'sentry/utils/discover/fields';
import {DisplayModes, SavedQueryDatasets} from 'sentry/utils/discover/types';
diff --git a/static/app/views/performance/landing/widgets/types.tsx b/static/app/views/performance/landing/widgets/types.tsx
index 23cd15f229d66a..996dd09ef20fa6 100644
--- a/static/app/views/performance/landing/widgets/types.tsx
+++ b/static/app/views/performance/landing/widgets/types.tsx
@@ -4,7 +4,7 @@ import type {Client} from 'sentry/api';
import type {BaseChart} from 'sentry/components/charts/baseChart';
import type {DateString} from 'sentry/types/core';
import type {Organization, OrganizationSummary} from 'sentry/types/organization';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {PerformanceWidgetContainerTypes} from './components/performanceWidgetContainer';
import type {ChartDefinition, PerformanceWidgetSetting} from './widgetDefinitions';
diff --git a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx
index a7464a040c41d1..95fb7eca24f1f5 100644
--- a/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx
@@ -7,14 +7,14 @@ import {LinkButton} from '@sentry/scraps/button';
import {Link} from '@sentry/scraps/link';
import {Tooltip} from '@sentry/scraps/tooltip';
-import _EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest as _EventsRequest} from 'sentry/components/charts/eventsRequest';
import {getInterval} from 'sentry/components/charts/utils';
import {Count} from 'sentry/components/count';
import {TextOverflow} from 'sentry/components/textOverflow';
import {Truncate} from 'sentry/components/truncate';
import {t, tct} from 'sentry/locale';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {formatPercentage} from 'sentry/utils/number/formatPercentage';
import {
diff --git a/static/app/views/performance/landing/widgets/widgets/mobileReleaseComparisonListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/mobileReleaseComparisonListWidget.tsx
index cd69c0021d49cc..1af97bb755047d 100644
--- a/static/app/views/performance/landing/widgets/widgets/mobileReleaseComparisonListWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/mobileReleaseComparisonListWidget.tsx
@@ -6,7 +6,7 @@ import pick from 'lodash/pick';
import {LinkButton} from '@sentry/scraps/button';
import type {RenderProps} from 'sentry/components/charts/eventsRequest';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {getInterval} from 'sentry/components/charts/utils';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
diff --git a/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx
index 306f01bf2aca95..483ed9d3107f29 100644
--- a/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/performanceScoreListWidget.tsx
@@ -7,7 +7,7 @@ import {LinkButton} from '@sentry/scraps/button';
import {ExternalLink} from '@sentry/scraps/link';
import {Tooltip} from '@sentry/scraps/tooltip';
-import _EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest as _EventsRequest} from 'sentry/components/charts/eventsRequest';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {Truncate} from 'sentry/components/truncate';
import {t} from 'sentry/locale';
diff --git a/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx b/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx
index 393d4f9defc0e0..351759edf353f6 100644
--- a/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/singleFieldAreaWidget.tsx
@@ -2,7 +2,7 @@ import {Fragment, useMemo} from 'react';
import styled from '@emotion/styled';
import pick from 'lodash/pick';
-import _EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest as _EventsRequest} from 'sentry/components/charts/eventsRequest';
import {getInterval, getPreviousSeriesName} from 'sentry/components/charts/utils';
import {t} from 'sentry/locale';
import {axisLabelFormatter} from 'sentry/utils/discover/charts';
diff --git a/static/app/views/performance/landing/widgets/widgets/stackedAreaChartListWidget.tsx b/static/app/views/performance/landing/widgets/widgets/stackedAreaChartListWidget.tsx
index cc58270f38d7bc..42aaee3ec2dae3 100644
--- a/static/app/views/performance/landing/widgets/widgets/stackedAreaChartListWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/stackedAreaChartListWidget.tsx
@@ -2,7 +2,7 @@ import {Fragment, useMemo, useState} from 'react';
import {useTheme} from '@emotion/react';
import pick from 'lodash/pick';
-import _EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest as _EventsRequest} from 'sentry/components/charts/eventsRequest';
import {StackedAreaChart} from 'sentry/components/charts/stackedAreaChart';
import {getInterval} from 'sentry/components/charts/utils';
import {Count} from 'sentry/components/count';
diff --git a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx
index 0e99415d510b3d..d8550ade42ca4c 100644
--- a/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx
+++ b/static/app/views/performance/landing/widgets/widgets/trendsWidget.tsx
@@ -64,10 +64,7 @@ export function TrendsWidget(props: PerformanceWidgetProps) {
InteractiveTitle,
} = props;
- const withBreakpoint =
- organization.features.includes('performance-new-trends') &&
- !isCardinalityCheckLoading &&
- !outcome?.forceTransactionsOnly;
+ const withBreakpoint = !isCardinalityCheckLoading && !outcome?.forceTransactionsOnly;
const trendChangeType =
props.chartSetting === PerformanceWidgetSetting.MOST_IMPROVED
diff --git a/static/app/views/performance/newTraceDetails/traceApi/useReplayTraceMeta.tsx b/static/app/views/performance/newTraceDetails/traceApi/useReplayTraceMeta.tsx
index 9f70e9f6815849..81fe5a5f3eb9a5 100644
--- a/static/app/views/performance/newTraceDetails/traceApi/useReplayTraceMeta.tsx
+++ b/static/app/views/performance/newTraceDetails/traceApi/useReplayTraceMeta.tsx
@@ -4,7 +4,7 @@ import type {Location} from 'history';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {getTimeStampFromTableDateField, getUtcDateString} from 'sentry/utils/dates';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useApiQuery} from 'sentry/utils/queryClient';
import {useOrganization} from 'sentry/utils/useOrganization';
import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';
diff --git a/static/app/views/performance/newTraceDetails/traceApi/useTraceAverageTransactionDuration.tsx b/static/app/views/performance/newTraceDetails/traceApi/useTraceAverageTransactionDuration.tsx
index d63beaba7c4096..c893e81ed003c7 100644
--- a/static/app/views/performance/newTraceDetails/traceApi/useTraceAverageTransactionDuration.tsx
+++ b/static/app/views/performance/newTraceDetails/traceApi/useTraceAverageTransactionDuration.tsx
@@ -2,7 +2,7 @@ import type {Location} from 'history';
import type {Organization} from 'sentry/types/organization';
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import type {TransactionNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode/transactionNode';
diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/issues/issueSummary.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/issues/issueSummary.tsx
index 2de4c973f09c86..a9a7e84e51a26a 100644
--- a/static/app/views/performance/newTraceDetails/traceDrawer/details/issues/issueSummary.tsx
+++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/issues/issueSummary.tsx
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import {Link} from '@sentry/scraps/link';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventTitleError} from 'sentry/components/eventTitleError';
import {GroupTitle} from 'sentry/components/groupTitle';
import {extractSelectionParameters} from 'sentry/components/pageFilters/parse';
diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/spanSummaryLink.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/spanSummaryLink.tsx
index a5bb6a97d28214..56c3803405862e 100644
--- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/spanSummaryLink.tsx
+++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/components/spanSummaryLink.tsx
@@ -6,10 +6,15 @@ import {IconGraph} from 'sentry/icons/iconGraph';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
+import {FieldKind} from 'sentry/utils/fields';
import {useLocation} from 'sentry/utils/useLocation';
+import {WidgetType} from 'sentry/views/dashboards/types';
+import {PrebuiltDashboardId} from 'sentry/views/dashboards/utils/prebuiltConfigs';
+import {usePrebuiltDashboardUrl} from 'sentry/views/dashboards/utils/usePrebuiltDashboardUrl';
import {resolveSpanModule} from 'sentry/views/insights/common/utils/resolveSpanModule';
+import {hasPlatformizedInsights} from 'sentry/views/insights/common/utils/useHasPlatformizedInsights';
import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
-import {ModuleName} from 'sentry/views/insights/types';
+import {ModuleName, SpanFields} from 'sentry/views/insights/types';
import {
querySummaryRouteWithQuery,
resourceSummaryRouteWithQuery,
@@ -28,6 +33,31 @@ export function SpanSummaryLink(props: Props) {
const resourceBaseUrl = useModuleURL(ModuleName.RESOURCE);
const queryBaseUrl = useModuleURL(ModuleName.DB);
+ const isPlatformized = hasPlatformizedInsights(props.organization);
+ const spanGroupFilter = props.group
+ ? {
+ globalFilter: [
+ {
+ dataset: WidgetType.SPANS,
+ tag: {
+ key: SpanFields.SPAN_GROUP,
+ name: SpanFields.SPAN_GROUP,
+ kind: FieldKind.TAG,
+ },
+ value: `${SpanFields.SPAN_GROUP}:[${props.group}]`,
+ },
+ ],
+ }
+ : undefined;
+ const platformizedResourceUrl = usePrebuiltDashboardUrl(
+ PrebuiltDashboardId.FRONTEND_ASSETS_SUMMARY,
+ {filters: spanGroupFilter}
+ );
+ const platformizedQueryUrl = usePrebuiltDashboardUrl(
+ PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY,
+ {filters: spanGroupFilter}
+ );
+
if (!props.group) {
return null;
}
@@ -38,14 +68,22 @@ export function SpanSummaryLink(props: Props) {
props.organization.features.includes('insight-modules') &&
resolvedModule === ModuleName.DB
) {
- return (
- {
trackAnalytics('trace.trace_layout.view_in_insight_module', {
organization: props.organization,
@@ -64,14 +102,22 @@ export function SpanSummaryLink(props: Props) {
resolvedModule === ModuleName.RESOURCE &&
resourceSummaryAvailable(props.op)
) {
- return (
- {
trackAnalytics('trace.trace_layout.view_in_insight_module', {
organization: props.organization,
diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx
index 6a22e2afec8314..23583128173c41 100644
--- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx
+++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx
@@ -14,7 +14,7 @@ import type {NewQuery, Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
import {useProjects} from 'sentry/utils/useProjects';
diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/entries.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/entries.tsx
index 71b05d3808785a..93d0b5c5c62e5a 100644
--- a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/entries.tsx
+++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/entries.tsx
@@ -1,6 +1,6 @@
import {Fragment} from 'react';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {Csp} from 'sentry/components/events/interfaces/csp';
import {Exception} from 'sentry/components/events/interfaces/exception';
import {Generic} from 'sentry/components/events/interfaces/generic';
diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/request.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/request.tsx
index 0f9a666d0e83dc..74c0701af7ed74 100644
--- a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/request.tsx
+++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/request.tsx
@@ -5,7 +5,7 @@ import {CodeBlock} from '@sentry/scraps/code';
import {ExternalLink} from '@sentry/scraps/link';
import {SegmentedControl} from '@sentry/scraps/segmentedControl';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventDataSection} from 'sentry/components/events/eventDataSection';
import {getTransformedData} from 'sentry/components/events/interfaces/request/getTransformedData';
import {GraphQlRequestBody} from 'sentry/components/events/interfaces/request/graphQlRequestBody';
diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx
index 2aa5e8d8524dbe..d3b8bec5015c82 100644
--- a/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx
+++ b/static/app/views/performance/newTraceDetails/traceDrawer/traceDrawer.tsx
@@ -6,7 +6,7 @@ import {Button} from '@sentry/scraps/button';
import {IconCircleFill, IconClose, IconPin} from 'sentry/icons';
import {t} from 'sentry/locale';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {
cancelAnimationTimeout,
requestAnimationTimeout,
diff --git a/static/app/views/performance/newTraceDetails/traceHeader/breadcrumbs.tsx b/static/app/views/performance/newTraceDetails/traceHeader/breadcrumbs.tsx
index d55e18128d5354..5926de65e5ce4e 100644
--- a/static/app/views/performance/newTraceDetails/traceHeader/breadcrumbs.tsx
+++ b/static/app/views/performance/newTraceDetails/traceHeader/breadcrumbs.tsx
@@ -23,7 +23,7 @@ import {
import {DOMAIN_VIEW_TITLES} from 'sentry/views/insights/pages/types';
import type {DomainView} from 'sentry/views/insights/pages/useFilters';
import {ModuleName} from 'sentry/views/insights/types';
-import Tab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab} from 'sentry/views/performance/transactionSummary/tabs';
import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils';
import {getPerformanceBaseUrl} from 'sentry/views/performance/utils';
import {makeTracesPathname} from 'sentry/views/traces/pathnames';
diff --git a/static/app/views/performance/newTraceDetails/traceHeader/index.spec.tsx b/static/app/views/performance/newTraceDetails/traceHeader/index.spec.tsx
index 5fdc28982fd4b7..de60f6a53b2eb3 100644
--- a/static/app/views/performance/newTraceDetails/traceHeader/index.spec.tsx
+++ b/static/app/views/performance/newTraceDetails/traceHeader/index.spec.tsx
@@ -5,7 +5,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import {
TraceMetaDataHeader,
diff --git a/static/app/views/performance/newTraceDetails/traceHeader/index.tsx b/static/app/views/performance/newTraceDetails/traceHeader/index.tsx
index 1f9ec543b8d03f..5839228c410902 100644
--- a/static/app/views/performance/newTraceDetails/traceHeader/index.tsx
+++ b/static/app/views/performance/newTraceDetails/traceHeader/index.tsx
@@ -5,7 +5,7 @@ import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import {useProjects} from 'sentry/utils/useProjects';
import {
diff --git a/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx b/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx
index f46034e3d46c8a..129628f4d361e6 100644
--- a/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx
+++ b/static/app/views/performance/newTraceDetails/traceOpenInExploreButton.tsx
@@ -2,7 +2,7 @@ import {LinkButton} from '@sentry/scraps/button';
import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch';
import {t} from 'sentry/locale';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
import {Mode} from 'sentry/views/explore/queryParams/mode';
diff --git a/static/app/views/performance/newTraceDetails/traceWaterfall.tsx b/static/app/views/performance/newTraceDetails/traceWaterfall.tsx
index 1654174aac9d32..557e42c20ff831 100644
--- a/static/app/views/performance/newTraceDetails/traceWaterfall.tsx
+++ b/static/app/views/performance/newTraceDetails/traceWaterfall.tsx
@@ -22,7 +22,7 @@ import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {
cancelAnimationTimeout,
requestAnimationTimeout,
diff --git a/static/app/views/performance/newTraceDetails/useTraceEventView.tsx b/static/app/views/performance/newTraceDetails/useTraceEventView.tsx
index c22d9fb4d81b18..b3aa87fba8005a 100644
--- a/static/app/views/performance/newTraceDetails/useTraceEventView.tsx
+++ b/static/app/views/performance/newTraceDetails/useTraceEventView.tsx
@@ -2,7 +2,7 @@ import {useMemo} from 'react';
import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants';
import type {NewQuery} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {TraceViewQueryParams} from './useTraceQueryParams';
diff --git a/static/app/views/performance/onboarding.tsx b/static/app/views/performance/onboarding.tsx
index 256c4ffdc5d7f7..e9e7810849e17b 100644
--- a/static/app/views/performance/onboarding.tsx
+++ b/static/app/views/performance/onboarding.tsx
@@ -23,7 +23,8 @@ import {UnsupportedAlert} from 'sentry/components/alerts/unsupportedAlert';
import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import type {TourStep} from 'sentry/components/modals/featureTourModal';
-import FeatureTourModal, {
+import {
+ FeatureTourModal,
TourImage,
TourText,
} from 'sentry/components/modals/featureTourModal';
diff --git a/static/app/views/performance/table.spec.tsx b/static/app/views/performance/table.spec.tsx
index ef6ac3b8bb7ede..ee9f72832bebbd 100644
--- a/static/app/views/performance/table.spec.tsx
+++ b/static/app/views/performance/table.spec.tsx
@@ -5,7 +5,7 @@ import {initializeData as _initializeData} from 'sentry-test/performance/initial
import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {OrganizationContext} from 'sentry/views/organizationContext';
diff --git a/static/app/views/performance/table.tsx b/static/app/views/performance/table.tsx
index 352241bb71559f..b1f6cca904900c 100644
--- a/static/app/views/performance/table.tsx
+++ b/static/app/views/performance/table.tsx
@@ -22,8 +22,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {browserHistory} from 'sentry/utils/browserHistory';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
-import type {MetaType} from 'sentry/utils/discover/eventView';
+import type {EventView, MetaType} from 'sentry/utils/discover/eventView';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {fieldAlignment, getAggregateAlias} from 'sentry/utils/discover/fields';
diff --git a/static/app/views/performance/transactionEvents.spec.tsx b/static/app/views/performance/transactionEvents.spec.tsx
index 76c58b9b45a457..68f5451d553da7 100644
--- a/static/app/views/performance/transactionEvents.spec.tsx
+++ b/static/app/views/performance/transactionEvents.spec.tsx
@@ -6,7 +6,7 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {WebVital} from 'sentry/utils/fields';
import TransactionSummaryLayout from 'sentry/views/performance/transactionSummary/layout';
-import TransactionSummaryTab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab as TransactionSummaryTab} from 'sentry/views/performance/transactionSummary/tabs';
import TransactionEvents from 'sentry/views/performance/transactionSummary/transactionEvents';
// XXX(epurkhiser): This appears to also be tested by ./transactionSummary/transactionEvents/index.spec.tsx
diff --git a/static/app/views/performance/transactionSummary/header.spec.tsx b/static/app/views/performance/transactionSummary/header.spec.tsx
index 0ec49294401935..c52d8ffd21ccda 100644
--- a/static/app/views/performance/transactionSummary/header.spec.tsx
+++ b/static/app/views/performance/transactionSummary/header.spec.tsx
@@ -5,9 +5,9 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen} from 'sentry-test/reactTestingLibrary';
import type {PlatformKey} from 'sentry/types/project';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {TransactionHeader} from 'sentry/views/performance/transactionSummary/header';
-import Tab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab} from 'sentry/views/performance/transactionSummary/tabs';
type InitialOpts = {
features?: string[];
diff --git a/static/app/views/performance/transactionSummary/header.tsx b/static/app/views/performance/transactionSummary/header.tsx
index c3971f5a9b193d..1584db784ebce7 100644
--- a/static/app/views/performance/transactionSummary/header.tsx
+++ b/static/app/views/performance/transactionSummary/header.tsx
@@ -17,7 +17,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {MetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
import {isProfilingSupportedOrProjectHasProfiles} from 'sentry/utils/profiling/platforms';
import {useReplayCountForTransactions} from 'sentry/utils/replayCount/useReplayCountForTransactions';
@@ -42,7 +42,7 @@ import {tagsRouteWithQuery} from 'sentry/views/performance/transactionSummary/tr
import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
import {getSelectedProjectPlatforms} from 'sentry/views/performance/utils';
-import Tab from './tabs';
+import {Tab} from './tabs';
import TeamKeyTransactionButton from './teamKeyTransactionButton';
import TransactionThresholdButton from './transactionThresholdButton';
import type {TransactionThresholdMetric} from './transactionThresholdModal';
diff --git a/static/app/views/performance/transactionSummary/layout.tsx b/static/app/views/performance/transactionSummary/layout.tsx
index 260ee7fb5df5cb..9a2263edefc4c3 100644
--- a/static/app/views/performance/transactionSummary/layout.tsx
+++ b/static/app/views/performance/transactionSummary/layout.tsx
@@ -3,12 +3,12 @@ import * as Sentry from '@sentry/react';
import type {Location} from 'history';
import {t} from 'sentry/locale';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useLocation} from 'sentry/utils/useLocation';
import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';
import {PageLayout} from 'sentry/views/performance/transactionSummary/pageLayout';
-import Tab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab} from 'sentry/views/performance/transactionSummary/tabs';
import {generateTransactionEventsEventView} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
import {generateTransactionOverviewEventView} from 'sentry/views/performance/transactionSummary/transactionOverview/utils';
import {generateTransactionReplaysEventView} from 'sentry/views/performance/transactionSummary/transactionReplays/utils';
diff --git a/static/app/views/performance/transactionSummary/pageLayout.tsx b/static/app/views/performance/transactionSummary/pageLayout.tsx
index 8926e2284b03dd..98b10b4c75a23c 100644
--- a/static/app/views/performance/transactionSummary/pageLayout.tsx
+++ b/static/app/views/performance/transactionSummary/pageLayout.tsx
@@ -24,7 +24,7 @@ import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {browserHistory} from 'sentry/utils/browserHistory';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {
MetricsCardinalityProvider,
useMetricsCardinalityContext,
@@ -47,7 +47,7 @@ import {profilesRouteWithQuery} from './transactionProfiles/utils';
import {replaysRouteWithQuery} from './transactionReplays/utils';
import {tagsRouteWithQuery} from './transactionTags/utils';
import {TransactionHeader} from './header';
-import Tab from './tabs';
+import {Tab} from './tabs';
import type {TransactionThresholdMetric} from './transactionThresholdModal';
import {generateTransactionSummaryRoute, transactionSummaryRouteWithQuery} from './utils';
diff --git a/static/app/views/performance/transactionSummary/tabs.tsx b/static/app/views/performance/transactionSummary/tabs.tsx
index eb68a5a1bba570..7038b1677cb0fb 100644
--- a/static/app/views/performance/transactionSummary/tabs.tsx
+++ b/static/app/views/performance/transactionSummary/tabs.tsx
@@ -1,9 +1,7 @@
-enum Tab {
+export enum Tab {
TRANSACTION_SUMMARY = 'summary',
TAGS = 'tags',
EVENTS = 'events',
REPLAYS = 'replays',
PROFILING = 'profiling',
}
-
-export default Tab;
diff --git a/static/app/views/performance/transactionSummary/teamKeyTransactionButton.spec.tsx b/static/app/views/performance/transactionSummary/teamKeyTransactionButton.spec.tsx
index 13a5f4552360a3..89d73afe89917e 100644
--- a/static/app/views/performance/transactionSummary/teamKeyTransactionButton.spec.tsx
+++ b/static/app/views/performance/transactionSummary/teamKeyTransactionButton.spec.tsx
@@ -7,7 +7,7 @@ import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingL
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {TeamStore} from 'sentry/stores/teamStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MAX_TEAM_KEY_TRANSACTIONS} from 'sentry/utils/performance/constants';
import TeamKeyTransactionButton from 'sentry/views/performance/transactionSummary/teamKeyTransactionButton';
diff --git a/static/app/views/performance/transactionSummary/teamKeyTransactionButton.tsx b/static/app/views/performance/transactionSummary/teamKeyTransactionButton.tsx
index a2f27967ea1169..d0d468eed97ea6 100644
--- a/static/app/views/performance/transactionSummary/teamKeyTransactionButton.tsx
+++ b/static/app/views/performance/transactionSummary/teamKeyTransactionButton.tsx
@@ -9,7 +9,7 @@ import {t, tn} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {useTeams} from 'sentry/utils/useTeams';
import {withProjects} from 'sentry/utils/withProjects';
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/content.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/content.spec.tsx
index 411bbd18657f07..84b47ccc2384f1 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/content.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/content.spec.tsx
@@ -6,7 +6,7 @@ import {act, render, screen} from 'sentry-test/reactTestingLibrary';
import {textWithMarkupMatcher} from 'sentry-test/utils';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {
SPAN_OP_BREAKDOWN_FIELDS,
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/content.tsx b/static/app/views/performance/transactionSummary/transactionEvents/content.tsx
index c0aef722cc4b8d..1b37eec9c5b167 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/content.tsx
@@ -13,13 +13,14 @@ import {DatePageFilter} from 'sentry/components/pageFilters/date/datePageFilter'
import {EnvironmentPageFilter} from 'sentry/components/pageFilters/environment/environmentPageFilter';
import {PageFilterBar} from 'sentry/components/pageFilters/pageFilterBar';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
+import {useSpanSearchQueryBuilderProps} from 'sentry/components/performance/spanSearchQueryBuilder';
import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
import {t} from 'sentry/locale';
import {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import type {WebVital} from 'sentry/utils/fields';
import {decodeScalar} from 'sentry/utils/queryString';
@@ -29,6 +30,7 @@ import {useMaxPickableDays} from 'sentry/utils/useMaxPickableDays';
import {useNavigate} from 'sentry/utils/useNavigate';
import {useRoutes} from 'sentry/utils/useRoutes';
import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
+import {TraceItemSearchQueryBuilder} from 'sentry/views/explore/components/traceItemSearchQueryBuilder';
import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
import {OverviewSpansTable} from 'sentry/views/performance/eap/overviewSpansTable';
import {useTransactionSummaryEAP} from 'sentry/views/performance/eap/useTransactionSummaryEAP';
@@ -48,6 +50,27 @@ import {EventsTable} from './eventsTable';
import type {EventsDisplayFilterName} from './utils';
import {getEventsFilterOptions} from './utils';
+function EAPSearchBar({
+ projects,
+ initialQuery,
+ onSearch,
+}: {
+ initialQuery: string;
+ onSearch: (query: string) => void;
+ projects: number[];
+}) {
+ const {spanSearchQueryBuilderProps} = useSpanSearchQueryBuilderProps({
+ projects,
+ initialQuery,
+ onSearch,
+ searchSource: 'transaction_events',
+ });
+
+ return (
+
+ );
+}
+
type Props = {
eventView: EventView;
eventsDisplayFilterName: EventsDisplayFilterName;
@@ -90,8 +113,13 @@ export function EventsContent(props: Props) {
const routes = useRoutes();
const theme = useTheme();
const domainViewFilters = useDomainViewFilters();
+ const shouldUseEAP = useTransactionSummaryEAP();
const {eventView, titles} = useMemo(() => {
+ if (shouldUseEAP) {
+ return {eventView: originalEventView, titles: []};
+ }
+
const eventViewClone = originalEventView.clone();
const transactionsListTitles = TRANSACTIONS_LIST_TITLES.slice();
const project = projects.find(p => p.id === projectId);
@@ -164,6 +192,7 @@ export function EventsContent(props: Props) {
titles: transactionsListTitles,
};
}, [
+ shouldUseEAP,
originalEventView,
location,
organization,
@@ -173,8 +202,6 @@ export function EventsContent(props: Props) {
webVital,
]);
- const shouldUseEAP = useTransactionSummaryEAP();
-
const table = shouldUseEAP ? (
-
-
- (
-
- )}
- value={eventsDisplayFilterName}
- onChange={opt => onChangeEventsDisplayFilter(opt.value)}
- options={Object.entries(eventsFilterOptions).map(([name, filter]) => ({
- value: name as EventsDisplayFilterName,
- label: filter.label,
- }))}
- />
-
+ ) : (
+
)}
- onClick={handleDiscoverButtonClick}
- >
- {t('Open in Discover')}
-
+
+ {!shouldUseEAP && (
+ (
+
+ )}
+ value={eventsDisplayFilterName}
+ onChange={opt => onChangeEventsDisplayFilter(opt.value)}
+ options={Object.entries(eventsFilterOptions).map(([name, filter]) => ({
+ value: name as EventsDisplayFilterName,
+ label: filter.label,
+ }))}
+ />
+ )}
+ {!shouldUseEAP && (
+
+ {t('Open in Discover')}
+
+ )}
);
}
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx
index 4098a5e210201c..dc4a689cb69894 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.spec.tsx
@@ -5,7 +5,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {SPAN_OP_RELATIVE_BREAKDOWN_FIELD} from 'sentry/utils/discover/fields';
import {EventsTable} from 'sentry/views/performance/transactionSummary/transactionEvents/eventsTable';
import {
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx
index 9c20943b2bd8ca..a0d08c2dee3e47 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx
@@ -23,7 +23,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {toArray} from 'sentry/utils/array/toArray';
import type {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import {
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx
index 34d162990980e2..8b6fb566261c93 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/index.spec.tsx
@@ -5,7 +5,7 @@ import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import TransactionSummaryLayout from 'sentry/views/performance/transactionSummary/layout';
-import TransactionSummaryTab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab as TransactionSummaryTab} from 'sentry/views/performance/transactionSummary/tabs';
import TransactionEvents from 'sentry/views/performance/transactionSummary/transactionEvents';
import {
EVENTS_TABLE_RESPONSE_FIELDS,
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/index.tsx b/static/app/views/performance/transactionSummary/transactionEvents/index.tsx
index 513784ba399835..2b92827dc30ba0 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/index.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/index.tsx
@@ -8,6 +8,7 @@ import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
+import {useTransactionSummaryEAP} from 'sentry/views/performance/eap/useTransactionSummaryEAP';
import {
decodeFilterFromLocation,
filterToLocationQuery,
@@ -38,6 +39,7 @@ function TransactionEvents() {
const navigate = useNavigate();
const location = useLocation();
+ const shouldUseEAP = useTransactionSummaryEAP();
const eventsDisplayFilterName = decodeEventsDisplayFilterFromLocation(location);
const spanOperationBreakdownFilter = decodeFilterFromLocation(location);
const webVital = getWebVital(location);
@@ -121,6 +123,26 @@ function TransactionEvents() {
});
};
+ if (shouldUseEAP) {
+ return (
+
+ );
+ }
+
return (
>;
diff --git a/static/app/views/performance/transactionSummary/transactionEvents/utils.tsx b/static/app/views/performance/transactionSummary/transactionEvents/utils.tsx
index 1732eb0ffdfc3d..44b8f8e54fc65f 100644
--- a/static/app/views/performance/transactionSummary/transactionEvents/utils.tsx
+++ b/static/app/views/performance/transactionSummary/transactionEvents/utils.tsx
@@ -3,13 +3,14 @@ import type {Location, Query} from 'history';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {
isAggregateField,
SPAN_OP_BREAKDOWN_FIELDS,
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
type QueryFieldValue,
} from 'sentry/utils/discover/fields';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {WebVital} from 'sentry/utils/fields';
import {decodeScalar} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
@@ -212,6 +213,7 @@ export function getPercentilesEventView(eventView: EventView): EventView {
export function generateTransactionEventsEventView({
location,
transactionName,
+ shouldUseEAP,
}: {
location: Location;
organization: Organization;
@@ -221,7 +223,11 @@ export function generateTransactionEventsEventView({
const query = decodeScalar(location.query.query, '');
const conditions = new MutableSearch(query);
- conditions.setFilterValues('event.type', ['transaction']);
+ if (shouldUseEAP) {
+ conditions.setFilterValues('is_transaction', ['true']);
+ } else {
+ conditions.setFilterValues('event.type', ['transaction']);
+ }
conditions.setFilterValues('transaction', [transactionName]);
Object.keys(conditions.filters).forEach(field => {
@@ -230,7 +236,29 @@ export function generateTransactionEventsEventView({
}
});
- const orderby = decodeScalar(location.query.sort, '-timestamp');
+ let orderby = decodeScalar(location.query.sort, '-timestamp');
+
+ if (shouldUseEAP) {
+ orderby = orderby.replace('transaction.duration', 'span.duration');
+
+ return EventView.fromNewQueryWithLocation(
+ {
+ id: undefined,
+ version: 2,
+ name: transactionName,
+ // TODO(mjq): `fields` is never actually read - the relevant query comes
+ // from `useSegmentSpansQuery` instead. Confusingly, other fields of
+ // this EventView _are_ used in various places. Untangling this is a job
+ // for after the non-EAP branches are removed.
+ fields: [],
+ query: conditions.formatString(),
+ projects: [],
+ orderby,
+ dataset: DiscoverDatasets.SPANS,
+ },
+ location
+ );
+ }
// Default fields for relative span view
const fields = [
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/charts.tsx b/static/app/views/performance/transactionSummary/transactionOverview/charts.tsx
index a153ed17712e65..2cf9e3b580e3d0 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/charts.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/charts.tsx
@@ -13,7 +13,7 @@ import type {SelectValue} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
@@ -236,7 +236,7 @@ export function TransactionSummaryCharts({
end={eventView.end}
statsPeriod={eventView.statsPeriod}
projects={project ? [project] : []}
- withBreakpoint={organization.features.includes('performance-new-trends')}
+ withBreakpoint
/>
)}
{display === DisplayModes.VITALS && (
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx
index 8af9c0d836ae2e..eaf472bdf644a6 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/content.spec.tsx
@@ -5,7 +5,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen} from 'sentry-test/reactTestingLibrary';
import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
import SummaryContent from 'sentry/views/performance/transactionSummary/transactionOverview/content';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/content.tsx
index eb4fa735f49365..f1753b556c16d6 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/content.tsx
@@ -23,7 +23,7 @@ import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {generateQueryWithTag} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {
formatTagKey,
isRelativeSpanOperationBreakdownField,
@@ -100,27 +100,6 @@ type Props = {
export const SEGMENT_SPANS_CURSOR_NAME = 'segmentSpansCursor';
-function EAPSearchQueryBuilder({
- projects,
- initialQuery,
- onSearch,
-}: {
- initialQuery: string;
- onSearch: (query: string) => void;
- projects: number[];
-}) {
- const {spanSearchQueryBuilderProps} = useSpanSearchQueryBuilderProps({
- projects,
- initialQuery,
- onSearch,
- searchSource: 'transaction_summary',
- });
-
- return (
-
- );
-}
-
function EAPSummaryContentInner({
eventView,
location,
@@ -258,15 +237,12 @@ function EAPSummaryContentInner({
});
const datePageFilterProps = useDatePageFilterProps(maxPickableDays);
- function renderSearchBar() {
- return (
-
- );
- }
+ const {spanSearchQueryBuilderProps} = useSpanSearchQueryBuilderProps({
+ projects: projectIds,
+ initialQuery: query,
+ onSearch: handleSearch,
+ searchSource: 'transaction_summary',
+ });
return (
@@ -277,7 +253,12 @@ function EAPSummaryContentInner({
- {renderSearchBar()}
+
+
+
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/durationChart/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/durationChart/content.tsx
index 0f46f2665178fc..f4a11774c7b8dc 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/durationChart/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/durationChart/content.tsx
@@ -7,7 +7,7 @@ import ChartZoom from 'sentry/components/charts/chartZoom';
import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import type {LineChartProps} from 'sentry/components/charts/lineChart';
import ReleaseSeries from 'sentry/components/charts/releaseSeries';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {Placeholder} from 'sentry/components/placeholder';
import {IconWarning} from 'sentry/icons';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/durationChart/index.tsx b/static/app/views/performance/transactionSummary/transactionOverview/durationChart/index.tsx
index d99ddb81fb751f..b2bb4270f5aeae 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/durationChart/index.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/durationChart/index.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import {useTheme} from '@emotion/react';
import type {Query} from 'history';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
import {getInterval, getSeriesSelection} from 'sentry/components/charts/utils';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/durationPercentileChart/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/durationPercentileChart/content.tsx
index c48e3f0fcaa081..4f850815e84d59 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/durationPercentileChart/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/durationPercentileChart/content.tsx
@@ -7,7 +7,7 @@ import {IconWarning} from 'sentry/icons';
import type {OrganizationSummary} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useApiQuery} from 'sentry/utils/queryClient';
import {
filterToColor,
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx
index 8c6bb8a8da1a9f..76e4acedf64b27 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/index.spec.tsx
@@ -19,7 +19,7 @@ import type {Project} from 'sentry/types/project';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
import TransactionSummaryLayout from 'sentry/views/performance/transactionSummary/layout';
-import TransactionSummaryTab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab as TransactionSummaryTab} from 'sentry/views/performance/transactionSummary/tabs';
import TransactionSummary from 'sentry/views/performance/transactionSummary/transactionOverview';
const teams = [
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/index.tsx b/static/app/views/performance/transactionSummary/transactionOverview/index.tsx
index 34bb7ed5b606e5..876956bd55dacc 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/index.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/index.tsx
@@ -8,7 +8,7 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import type {Column, QueryFieldValue} from 'sentry/utils/discover/fields';
import type {WebVital} from 'sentry/utils/fields';
import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/latencyChart/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/latencyChart/content.tsx
index 2ec1639db7db48..0a2f8e6b0eac37 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/latencyChart/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/latencyChart/content.tsx
@@ -10,7 +10,7 @@ import {IconWarning} from 'sentry/icons';
import {t} from 'sentry/locale';
import type {OrganizationSummary} from 'sentry/types/organization';
import {toArray} from 'sentry/utils/array/toArray';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {formatPercentage} from 'sentry/utils/number/formatPercentage';
import {Histogram} from 'sentry/utils/performance/histogram';
import {HistogramQuery} from 'sentry/utils/performance/histogram/histogramQuery';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/sidebarCharts.tsx b/static/app/views/performance/transactionSummary/transactionOverview/sidebarCharts.tsx
index a0edb6a12e8a21..bc3dca4274e843 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/sidebarCharts.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/sidebarCharts.tsx
@@ -3,11 +3,11 @@ import styled from '@emotion/styled';
import ChartZoom from 'sentry/components/charts/chartZoom';
import {ErrorPanel} from 'sentry/components/charts/errorPanel';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import type {LineChartProps} from 'sentry/components/charts/lineChart';
import {LineChart} from 'sentry/components/charts/lineChart';
import {SectionHeading} from 'sentry/components/charts/styles';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {getInterval} from 'sentry/components/charts/utils';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
@@ -18,7 +18,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {getUtcToLocalDateObject} from 'sentry/utils/dates';
import {tooltipFormatter} from 'sentry/utils/discover/charts';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {aggregateOutputType} from 'sentry/utils/discover/fields';
import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
import {getDynamicText} from 'sentry/utils/getDynamicText';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/statusBreakdown.tsx b/static/app/views/performance/transactionSummary/transactionOverview/statusBreakdown.tsx
index b93f15e88e028e..bf59b768619c58 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/statusBreakdown.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/statusBreakdown.tsx
@@ -13,7 +13,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
import {DiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useNavigate} from 'sentry/utils/useNavigate';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx b/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx
index cce3578031b522..0762bc730ea84a 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.spec.tsx
@@ -5,7 +5,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
import {ProjectsStore} from 'sentry/stores/projectsStore';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {OrganizationContext} from 'sentry/views/organizationContext';
import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.tsx b/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.tsx
index 027c6f81fd095d..44a548080526fa 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/tagExplorer.tsx
@@ -17,7 +17,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {isFieldSortable} from 'sentry/utils/discover/eventView';
import {fieldAlignment} from 'sentry/utils/discover/fields';
import {formatPercentage} from 'sentry/utils/number/formatPercentage';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/trendChart/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/trendChart/content.tsx
index d61c730c9d2621..1afe06ed6b16ef 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/trendChart/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/trendChart/content.tsx
@@ -6,7 +6,7 @@ import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import type {LineChartProps} from 'sentry/components/charts/lineChart';
import {LineChart} from 'sentry/components/charts/lineChart';
import ReleaseSeries from 'sentry/components/charts/releaseSeries';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {Placeholder} from 'sentry/components/placeholder';
import {IconWarning} from 'sentry/icons';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/trendChart/index.tsx b/static/app/views/performance/transactionSummary/transactionOverview/trendChart/index.tsx
index ce4d3bf6aec509..a9df39d2278e1e 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/trendChart/index.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/trendChart/index.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import {useTheme} from '@emotion/react';
import type {Query} from 'history';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
import {getInterval, getSeriesSelection} from 'sentry/components/charts/utils';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
@@ -16,7 +16,7 @@ import type {
} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {getUtcToLocalDateObject} from 'sentry/utils/dates';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers';
import {getAggregateAlias} from 'sentry/utils/discover/fields';
import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/userMiseryChart/index.tsx b/static/app/views/performance/transactionSummary/transactionOverview/userMiseryChart/index.tsx
index 7836bdff4750be..d0a5dae3a348ef 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/userMiseryChart/index.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/userMiseryChart/index.tsx
@@ -1,7 +1,7 @@
import {Fragment} from 'react';
import type {Query} from 'history';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
import {getInterval} from 'sentry/components/charts/utils';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/userStats.tsx b/static/app/views/performance/transactionSummary/transactionOverview/userStats.tsx
index 18448b0b64f123..2d93f27b771463 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/userStats.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/userStats.tsx
@@ -10,7 +10,7 @@ import {UserMisery} from 'sentry/components/userMisery';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
import {WebVital} from 'sentry/utils/fields';
import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/utils.tsx b/static/app/views/performance/transactionSummary/transactionOverview/utils.tsx
index b820d4b88287dd..d3bc30eaf3a229 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/utils.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/utils.tsx
@@ -1,7 +1,7 @@
import type {Location} from 'history';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {isAggregateField} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import type {MetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/content.tsx b/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/content.tsx
index df91bca14ed8e5..3d3e6e6cd2b210 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/content.tsx
@@ -6,7 +6,7 @@ import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import type {LineChartProps} from 'sentry/components/charts/lineChart';
import {LineChart} from 'sentry/components/charts/lineChart';
import ReleaseSeries from 'sentry/components/charts/releaseSeries';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {Placeholder} from 'sentry/components/placeholder';
import {IconWarning} from 'sentry/icons';
diff --git a/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx b/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx
index dd805097e08641..5036e6a8bee43a 100644
--- a/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx
+++ b/static/app/views/performance/transactionSummary/transactionOverview/vitalsChart/index.tsx
@@ -2,7 +2,7 @@ import {Fragment} from 'react';
import {useTheme} from '@emotion/react';
import type {Query} from 'history';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
import {getInterval, getSeriesSelection} from 'sentry/components/charts/utils';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
diff --git a/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx
index c59e03c0a959b1..7c71ab35ad39b0 100644
--- a/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionReplays/index.spec.tsx
@@ -13,7 +13,7 @@ import {
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
} from 'sentry/utils/discover/fields';
import TransactionSummaryLayout from 'sentry/views/performance/transactionSummary/layout';
-import TransactionSummaryTab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab as TransactionSummaryTab} from 'sentry/views/performance/transactionSummary/tabs';
import TransactionReplays from 'sentry/views/performance/transactionSummary/transactionReplays';
type InitializeOrgProps = {
diff --git a/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx b/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx
index 2a93a4931a1e78..c9fc699db34bbd 100644
--- a/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx
+++ b/static/app/views/performance/transactionSummary/transactionReplays/transactionReplays.tsx
@@ -20,7 +20,7 @@ import {
} from 'sentry/components/replays/table/replayTableColumns';
import {usePlaylistQuery} from 'sentry/components/replays/usePlaylistQuery';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {useReplayList} from 'sentry/utils/replays/hooks/useReplayList';
import {useLocation} from 'sentry/utils/useLocation';
import {useMedia} from 'sentry/utils/useMedia';
diff --git a/static/app/views/performance/transactionSummary/transactionReplays/useReplaysFromTransaction.tsx b/static/app/views/performance/transactionSummary/transactionReplays/useReplaysFromTransaction.tsx
index 7f60ca002dc61b..aec423f35ac60f 100644
--- a/static/app/views/performance/transactionSummary/transactionReplays/useReplaysFromTransaction.tsx
+++ b/static/app/views/performance/transactionSummary/transactionReplays/useReplaysFromTransaction.tsx
@@ -4,7 +4,7 @@ import type {Location} from 'history';
import {DEFAULT_REPLAY_LIST_SORT} from 'sentry/components/replays/table/useReplayTableSort';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
import {decodeScalar} from 'sentry/utils/queryString';
import {useApi} from 'sentry/utils/useApi';
diff --git a/static/app/views/performance/transactionSummary/transactionReplays/utils.ts b/static/app/views/performance/transactionSummary/transactionReplays/utils.ts
index 3de026ebefd4a3..4579d357714c7d 100644
--- a/static/app/views/performance/transactionSummary/transactionReplays/utils.ts
+++ b/static/app/views/performance/transactionSummary/transactionReplays/utils.ts
@@ -1,7 +1,7 @@
import type {Location, Query} from 'history';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {
SPAN_OP_BREAKDOWN_FIELDS,
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
diff --git a/static/app/views/performance/transactionSummary/transactionSummaryContext.tsx b/static/app/views/performance/transactionSummary/transactionSummaryContext.tsx
index ac369185c55c72..1ba032e99717b5 100644
--- a/static/app/views/performance/transactionSummary/transactionSummaryContext.tsx
+++ b/static/app/views/performance/transactionSummary/transactionSummaryContext.tsx
@@ -2,7 +2,7 @@ import {createContext, useContext} from 'react';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {TransactionThresholdMetric} from 'sentry/views/performance/transactionSummary/transactionThresholdModal';
export type TransactionSummaryContext = {
diff --git a/static/app/views/performance/transactionSummary/transactionTags/content.tsx b/static/app/views/performance/transactionSummary/transactionTags/content.tsx
index fd8bb15cfa4639..7c6e86788a12de 100644
--- a/static/app/views/performance/transactionSummary/transactionTags/content.tsx
+++ b/static/app/views/performance/transactionSummary/transactionTags/content.tsx
@@ -20,7 +20,7 @@ import {DataCategory} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {TableData} from 'sentry/utils/performance/segmentExplorer/segmentExplorerQuery';
import {SegmentExplorerQuery} from 'sentry/utils/performance/segmentExplorer/segmentExplorerQuery';
import {decodeScalar} from 'sentry/utils/queryString';
diff --git a/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx b/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx
index 76d303616b5331..0e342424166b87 100644
--- a/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionTags/index.spec.tsx
@@ -6,7 +6,7 @@ import {selectEvent} from 'sentry-test/selectEvent';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import TransactionSummaryLayout from 'sentry/views/performance/transactionSummary/layout';
-import TransactionSummaryTab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab as TransactionSummaryTab} from 'sentry/views/performance/transactionSummary/tabs';
import TransactionTags from 'sentry/views/performance/transactionSummary/transactionTags';
const TEST_RELEASE_NAME = 'test-project@1.0.0';
diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagValueTable.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagValueTable.tsx
index ec44b1fecdf214..8ac22871da800b 100644
--- a/static/app/views/performance/transactionSummary/transactionTags/tagValueTable.tsx
+++ b/static/app/views/performance/transactionSummary/transactionTags/tagValueTable.tsx
@@ -15,7 +15,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {fieldAlignment} from 'sentry/utils/discover/fields';
import {formatPercentage} from 'sentry/utils/number/formatPercentage';
import type {
diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagsDisplay.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagsDisplay.tsx
index b74fe375f4a20a..e813adab31edda 100644
--- a/static/app/views/performance/transactionSummary/transactionTags/tagsDisplay.tsx
+++ b/static/app/views/performance/transactionSummary/transactionTags/tagsDisplay.tsx
@@ -5,7 +5,7 @@ import type {CursorHandler} from 'sentry/components/pagination';
import type {GridColumnOrder} from 'sentry/components/tables/gridEditable';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {SegmentExplorerQuery} from 'sentry/utils/performance/segmentExplorer/segmentExplorerQuery';
import {TagKeyHistogramQuery} from 'sentry/utils/performance/segmentExplorer/tagKeyHistogramQuery';
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx
index a6c2ff17989dc7..ae7ef1f8ab7e56 100644
--- a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx
+++ b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx
@@ -14,7 +14,7 @@ import {Flex} from '@sentry/scraps/layout';
import {HeatMapChart} from 'sentry/components/charts/heatMapChart';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {MenuItem} from 'sentry/components/menuItem';
@@ -29,7 +29,7 @@ import type {ReactEchartsRef, Series} from 'sentry/types/echarts';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {axisLabelFormatter} from 'sentry/utils/discover/charts';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
import {getDynamicText} from 'sentry/utils/getDynamicText';
@@ -41,7 +41,7 @@ import {TagTransactionsQuery} from 'sentry/utils/performance/segmentExplorer/tag
import {decodeScalar} from 'sentry/utils/queryString';
import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
-import Tab from 'sentry/views/performance/transactionSummary/tabs';
+import {Tab} from 'sentry/views/performance/transactionSummary/tabs';
import {eventsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
import {getPerformanceDuration} from 'sentry/views/performance/utils/getPerformanceDuration';
diff --git a/static/app/views/performance/transactionSummary/transactionTags/utils.tsx b/static/app/views/performance/transactionSummary/transactionTags/utils.tsx
index c024e72e94cd9f..1f15e878ef1979 100644
--- a/static/app/views/performance/transactionSummary/transactionTags/utils.tsx
+++ b/static/app/views/performance/transactionSummary/transactionTags/utils.tsx
@@ -2,7 +2,7 @@ import type {Location, Query} from 'history';
import type {Organization} from 'sentry/types/organization';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import type {DomainView} from 'sentry/views/insights/pages/useFilters';
diff --git a/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx b/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx
index 9ef6cca3616427..97b20acd6c52d6 100644
--- a/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionThresholdButton.spec.tsx
@@ -11,7 +11,7 @@ import {
import {ProjectsStore} from 'sentry/stores/projectsStore';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import TransactionThresholdButton from 'sentry/views/performance/transactionSummary/transactionThresholdButton';
function renderComponent(
diff --git a/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx b/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx
index 84a85c28193f93..6718cb0bb18346 100644
--- a/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx
+++ b/static/app/views/performance/transactionSummary/transactionThresholdButton.tsx
@@ -10,7 +10,7 @@ import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {withApi} from 'sentry/utils/withApi';
import {withProjects} from 'sentry/utils/withProjects';
diff --git a/static/app/views/performance/transactionSummary/transactionThresholdModal.spec.tsx b/static/app/views/performance/transactionSummary/transactionThresholdModal.spec.tsx
index afb4dea1de0c03..7f08d9753220c9 100644
--- a/static/app/views/performance/transactionSummary/transactionThresholdModal.spec.tsx
+++ b/static/app/views/performance/transactionSummary/transactionThresholdModal.spec.tsx
@@ -12,7 +12,7 @@ import {
} from 'sentry/components/globalModal/components';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import type {Organization} from 'sentry/types/organization';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import TransactionThresholdModal, {
TransactionThresholdMetric,
} from 'sentry/views/performance/transactionSummary/transactionThresholdModal';
diff --git a/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx b/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx
index 5f0e0ec95caee8..3a4051edcbcdfe 100644
--- a/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx
+++ b/static/app/views/performance/transactionSummary/transactionThresholdModal.tsx
@@ -16,7 +16,7 @@ import {t, tct} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {defined} from 'sentry/utils';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {withApi} from 'sentry/utils/withApi';
import {withProjects} from 'sentry/utils/withProjects';
diff --git a/static/app/views/performance/transactionSummary/useEventViewProject.ts b/static/app/views/performance/transactionSummary/useEventViewProject.ts
index 2dbf8b2ca49a7c..faef468a8afcfc 100644
--- a/static/app/views/performance/transactionSummary/useEventViewProject.ts
+++ b/static/app/views/performance/transactionSummary/useEventViewProject.ts
@@ -1,7 +1,7 @@
import {useMemo} from 'react';
import type {Project} from 'sentry/types/project';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
export function useEventViewProject(
projects: Project[],
diff --git a/static/app/views/performance/trends/chart.tsx b/static/app/views/performance/trends/chart.tsx
index ec5da68d5b72d2..b5011fef3a5c24 100644
--- a/static/app/views/performance/trends/chart.tsx
+++ b/static/app/views/performance/trends/chart.tsx
@@ -4,10 +4,9 @@ import type {LegendComponentOption, LineSeriesOption} from 'echarts';
import ChartZoom from 'sentry/components/charts/chartZoom';
import type {LineChartProps} from 'sentry/components/charts/lineChart';
import {LineChart} from 'sentry/components/charts/lineChart';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
-import type {OrganizationSummary} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {getUtcToLocalDateObject} from 'sentry/utils/dates';
import {
@@ -41,7 +40,6 @@ import {
type Props = ViewProps & {
isLoading: boolean;
- organization: OrganizationSummary;
projects: Project[];
statsData: TrendsStats;
trendChangeType: TrendChangeType;
@@ -93,7 +91,6 @@ export function Chart({
height,
projects,
project,
- organization,
additionalSeries,
applyRegressionFormatToInterval = false,
}: Props) {
@@ -119,9 +116,7 @@ export function Chart({
navigate(to);
};
- const derivedTrendChangeType = organization.features.includes('performance-new-trends')
- ? transaction?.change
- : trendChangeType;
+ const derivedTrendChangeType = transaction?.change ?? trendChangeType;
const trendToColor = makeTrendToColorMapping(theme);
const lineColor =
diff --git a/static/app/views/performance/trends/types.tsx b/static/app/views/performance/trends/types.tsx
index 439e22d85401df..764b368dbfb580 100644
--- a/static/app/views/performance/trends/types.tsx
+++ b/static/app/views/performance/trends/types.tsx
@@ -3,8 +3,7 @@ import type moment from 'moment-timezone';
import type {EventQuery} from 'sentry/actionCreators/events';
import type {EventsStatsData} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
-import type {LocationQuery} from 'sentry/utils/discover/eventView';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView, LocationQuery} from 'sentry/utils/discover/eventView';
export type TrendView = EventView & {
middle?: string;
diff --git a/static/app/views/performance/trends/utils/index.tsx b/static/app/views/performance/trends/utils/index.tsx
index 161b9c5ade6cc6..3082e03ff5d89a 100644
--- a/static/app/views/performance/trends/utils/index.tsx
+++ b/static/app/views/performance/trends/utils/index.tsx
@@ -29,8 +29,6 @@ import {
ProjectPerformanceType,
} from 'sentry/views/performance/utils';
-export const DEFAULT_MAX_DURATION = '15min';
-
export const TRENDS_FUNCTIONS: TrendFunction[] = [
{
label: 'p99',
diff --git a/static/app/views/performance/types.tsx b/static/app/views/performance/types.tsx
index e08b3b7cde191c..cce31cd684b333 100644
--- a/static/app/views/performance/types.tsx
+++ b/static/app/views/performance/types.tsx
@@ -1,4 +1,4 @@
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {QUERY_KEYS} from './utils';
diff --git a/static/app/views/performance/utils/index.tsx b/static/app/views/performance/utils/index.tsx
index 70b0ede7f2bf61..39c5877b50528a 100644
--- a/static/app/views/performance/utils/index.tsx
+++ b/static/app/views/performance/utils/index.tsx
@@ -14,7 +14,7 @@ import type {ReleaseProject} from 'sentry/types/release';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {toArray} from 'sentry/utils/array/toArray';
import type {EventData} from 'sentry/utils/discover/eventView';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {TRACING_FIELDS} from 'sentry/utils/discover/fields';
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {statsPeriodToDays} from 'sentry/utils/duration/statsPeriodToDays';
@@ -28,7 +28,6 @@ import {useProjects} from 'sentry/utils/useProjects';
import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
import {DOMAIN_VIEW_BASE_URL} from 'sentry/views/insights/pages/settings';
import type {DomainView} from 'sentry/views/insights/pages/useFilters';
-import {DEFAULT_MAX_DURATION} from 'sentry/views/performance/trends/utils';
export const QUERY_KEYS = [
'environment',
@@ -226,32 +225,7 @@ export function trendsTargetRoute({
...additionalQuery,
};
- const query = decodeScalar(location.query.query, '');
- const conditions = new MutableSearch(query);
-
const modifiedConditions = initialConditions ?? new MutableSearch([]);
-
- // Trends on metrics don't need these conditions
- if (!organization.features.includes('performance-new-trends')) {
- // No need to carry over tpm filters to transaction summary
- if (conditions.hasFilter('tpm()')) {
- modifiedConditions.setFilterValues('tpm()', conditions.getFilterValues('tpm()'));
- } else {
- modifiedConditions.setFilterValues('tpm()', ['>0.01']);
- }
-
- if (conditions.hasFilter('transaction.duration')) {
- modifiedConditions.setFilterValues(
- 'transaction.duration',
- conditions.getFilterValues('transaction.duration')
- );
- } else {
- modifiedConditions.setFilterValues('transaction.duration', [
- '>0',
- `<${DEFAULT_MAX_DURATION}`,
- ]);
- }
- }
newQuery.query = modifiedConditions.formatString();
return {pathname: getPerformanceTrendsUrl(organization, view), query: {...newQuery}};
diff --git a/static/app/views/performance/vitalDetail/vitalInfo.tsx b/static/app/views/performance/vitalDetail/vitalInfo.tsx
index 5e97507043ffe6..2368c5a9d2af94 100644
--- a/static/app/views/performance/vitalDetail/vitalInfo.tsx
+++ b/static/app/views/performance/vitalDetail/vitalInfo.tsx
@@ -2,7 +2,7 @@ import type {Location} from 'history';
import type {Organization} from 'sentry/types/organization';
import {toArray} from 'sentry/utils/array/toArray';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import type {WebVital} from 'sentry/utils/fields';
import {VitalsCardsDiscoverQuery as VitalsCardDiscoverQuery} from 'sentry/utils/performance/vitals/vitalsCardsDiscoverQuery';
import {VitalBar} from 'sentry/views/performance/landing/vitalsCards';
diff --git a/static/app/views/preprod/components/preprodQuotaAlert.tsx b/static/app/views/preprod/components/preprodQuotaAlert.tsx
index 882cbc7e7f1127..9e1f6e6a545faa 100644
--- a/static/app/views/preprod/components/preprodQuotaAlert.tsx
+++ b/static/app/views/preprod/components/preprodQuotaAlert.tsx
@@ -1,6 +1,6 @@
import {Alert} from '@sentry/scraps/alert';
-import ExternalLink from 'sentry/components/links/externalLink';
+import {ExternalLink} from 'sentry/components/links/externalLink';
import {t, tct} from 'sentry/locale';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {useApiQuery} from 'sentry/utils/queryClient';
diff --git a/static/app/views/profiling/profileSummary/index.tsx b/static/app/views/profiling/profileSummary/index.tsx
index ba3c1557534642..1e56955c2e3f31 100644
--- a/static/app/views/profiling/profileSummary/index.tsx
+++ b/static/app/views/profiling/profileSummary/index.tsx
@@ -11,7 +11,7 @@ import {TabList, Tabs} from '@sentry/scraps/tabs';
import {Count} from 'sentry/components/count';
import {DateTime} from 'sentry/components/dateTime';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
import {IdBadge} from 'sentry/components/idBadge';
import * as Layout from 'sentry/components/layouts/thirds';
@@ -37,7 +37,7 @@ import {DataCategory} from 'sentry/types/core';
import type {Project} from 'sentry/types/project';
import type {DeepPartial} from 'sentry/types/utils';
import {defined} from 'sentry/utils';
-import type EventView from 'sentry/utils/discover/eventView';
+import type {EventView} from 'sentry/utils/discover/eventView';
import {isAggregateField} from 'sentry/utils/discover/fields';
import type {CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
import {
diff --git a/static/app/views/projectDetail/charts/projectBaseEventsChart.tsx b/static/app/views/projectDetail/charts/projectBaseEventsChart.tsx
index a1f7f08c1dd0c0..81f065d4183b4c 100644
--- a/static/app/views/projectDetail/charts/projectBaseEventsChart.tsx
+++ b/static/app/views/projectDetail/charts/projectBaseEventsChart.tsx
@@ -4,7 +4,7 @@ import isEqual from 'lodash/isEqual';
import {fetchTotalCount} from 'sentry/actionCreators/events';
import type {EventsChartProps} from 'sentry/components/charts/eventsChart';
-import EventsChart from 'sentry/components/charts/eventsChart';
+import {EventsChart} from 'sentry/components/charts/eventsChart';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
diff --git a/static/app/views/projectDetail/charts/projectBaseSessionsChart.tsx b/static/app/views/projectDetail/charts/projectBaseSessionsChart.tsx
index 4ecc1e1806caa6..e553722aa39fff 100644
--- a/static/app/views/projectDetail/charts/projectBaseSessionsChart.tsx
+++ b/static/app/views/projectDetail/charts/projectBaseSessionsChart.tsx
@@ -14,7 +14,7 @@ import {LineChart} from 'sentry/components/charts/lineChart';
import ReleaseSeries from 'sentry/components/charts/releaseSeries';
import {StackedAreaChart} from 'sentry/components/charts/stackedAreaChart';
import {HeaderTitleLegend} from 'sentry/components/charts/styles';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {RELEASE_LINES_THRESHOLD} from 'sentry/components/charts/utils';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
diff --git a/static/app/views/projectDetail/missingFeatureButtons/missingPerformanceButtons.tsx b/static/app/views/projectDetail/missingFeatureButtons/missingPerformanceButtons.tsx
index 7981780856582f..a671fa5185dd32 100644
--- a/static/app/views/projectDetail/missingFeatureButtons/missingPerformanceButtons.tsx
+++ b/static/app/views/projectDetail/missingFeatureButtons/missingPerformanceButtons.tsx
@@ -3,7 +3,7 @@ import {Grid} from '@sentry/scraps/layout';
import {navigateTo} from 'sentry/actionCreators/navigation';
import Feature from 'sentry/components/acl/feature';
-import FeatureTourModal from 'sentry/components/modals/featureTourModal';
+import {FeatureTourModal} from 'sentry/components/modals/featureTourModal';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
diff --git a/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx b/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx
index d56e59d4b5f739..d8054004af47f7 100644
--- a/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx
+++ b/static/app/views/projectDetail/missingFeatureButtons/missingReleasesButtons.tsx
@@ -1,7 +1,7 @@
import {Button, LinkButton} from '@sentry/scraps/button';
import {Grid} from '@sentry/scraps/layout';
-import FeatureTourModal from 'sentry/components/modals/featureTourModal';
+import {FeatureTourModal} from 'sentry/components/modals/featureTourModal';
import {releaseHealth} from 'sentry/data/platformCategories';
import {t} from 'sentry/locale';
import {ConfigStore} from 'sentry/stores/configStore';
diff --git a/static/app/views/projectDetail/projectDetail.tsx b/static/app/views/projectDetail/projectDetail.tsx
index 71e8c2331608cc..d1bb5ef51b36b7 100644
--- a/static/app/views/projectDetail/projectDetail.tsx
+++ b/static/app/views/projectDetail/projectDetail.tsx
@@ -11,7 +11,7 @@ import {fetchTagValues} from 'sentry/actionCreators/tags';
import Feature from 'sentry/components/acl/feature';
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
import {CreateAlertButton} from 'sentry/components/createAlertButton';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
import {IdBadge} from 'sentry/components/idBadge';
import * as Layout from 'sentry/components/layouts/thirds';
diff --git a/static/app/views/projectDetail/projectIssues.tsx b/static/app/views/projectDetail/projectIssues.tsx
index fa57a756506815..557e4246bef3e8 100644
--- a/static/app/views/projectDetail/projectIssues.tsx
+++ b/static/app/views/projectDetail/projectIssues.tsx
@@ -26,7 +26,7 @@ import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {decodeScalar} from 'sentry/utils/queryString';
import {appendQueryDatasetParam} from 'sentry/views/dashboards/utils';
import {makeDiscoverPathname} from 'sentry/views/discover/pathnames';
-import NoGroupsHandler from 'sentry/views/issueList/noGroupsHandler';
+import {NoGroupsHandler} from 'sentry/views/issueList/noGroupsHandler';
enum IssuesType {
NEW = 'new',
diff --git a/static/app/views/releases/detail/overview/index.tsx b/static/app/views/releases/detail/overview/index.tsx
index 627f240146ae7d..93a351917936b3 100644
--- a/static/app/views/releases/detail/overview/index.tsx
+++ b/static/app/views/releases/detail/overview/index.tsx
@@ -28,7 +28,7 @@ import {SessionFieldWithOperation} from 'sentry/types/organization';
import {getUtcDateString} from 'sentry/utils/dates';
import {DemoTourElement, DemoTourStep} from 'sentry/utils/demoMode/demoTours';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {decodeScalar} from 'sentry/utils/queryString';
import {useApi} from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
diff --git a/static/app/views/releases/detail/overview/releaseComparisonChart/index.tsx b/static/app/views/releases/detail/overview/releaseComparisonChart/index.tsx
index c3e5eba4f3aa3f..ee0f8caf5b21a5 100644
--- a/static/app/views/releases/detail/overview/releaseComparisonChart/index.tsx
+++ b/static/app/views/releases/detail/overview/releaseComparisonChart/index.tsx
@@ -12,7 +12,7 @@ import type {Client} from 'sentry/api';
import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import {ChartContainer} from 'sentry/components/charts/styles';
import {Count} from 'sentry/components/count';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {NotAvailable} from 'sentry/components/notAvailable';
import {extractSelectionParameters} from 'sentry/components/pageFilters/parse';
import {Panel} from 'sentry/components/panels/panel';
diff --git a/static/app/views/releases/detail/overview/releaseComparisonChart/releaseEventsChart.tsx b/static/app/views/releases/detail/overview/releaseComparisonChart/releaseEventsChart.tsx
index b63945bc59c102..f2c90bdf2751ba 100644
--- a/static/app/views/releases/detail/overview/releaseComparisonChart/releaseEventsChart.tsx
+++ b/static/app/views/releases/detail/overview/releaseComparisonChart/releaseEventsChart.tsx
@@ -3,8 +3,8 @@ import {useTheme} from '@emotion/react';
import type {ToolboxComponentOption} from 'echarts';
import {Client} from 'sentry/api';
-import EventsChart from 'sentry/components/charts/eventsChart';
-import EventsRequest from 'sentry/components/charts/eventsRequest';
+import {EventsChart} from 'sentry/components/charts/eventsChart';
+import {EventsRequest} from 'sentry/components/charts/eventsRequest';
import {HeaderTitleLegend, HeaderValue} from 'sentry/components/charts/styles';
import {getInterval} from 'sentry/components/charts/utils';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
@@ -13,7 +13,7 @@ import type {Organization} from 'sentry/types/organization';
import type {ReleaseProject, ReleaseWithHealth} from 'sentry/types/release';
import {ReleaseComparisonChartType} from 'sentry/types/release';
import {tooltipFormatter} from 'sentry/utils/discover/charts';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {aggregateOutputType} from 'sentry/utils/discover/fields';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
diff --git a/static/app/views/releases/detail/overview/releaseComparisonChart/releaseSessionsChart.tsx b/static/app/views/releases/detail/overview/releaseComparisonChart/releaseSessionsChart.tsx
index ce7750bad2d1a4..518456c0be6e7b 100644
--- a/static/app/views/releases/detail/overview/releaseComparisonChart/releaseSessionsChart.tsx
+++ b/static/app/views/releases/detail/overview/releaseComparisonChart/releaseSessionsChart.tsx
@@ -9,7 +9,7 @@ import {AreaChart} from 'sentry/components/charts/areaChart';
import ChartZoom from 'sentry/components/charts/chartZoom';
import {StackedAreaChart} from 'sentry/components/charts/stackedAreaChart';
import {HeaderTitleLegend, HeaderValue} from 'sentry/components/charts/styles';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
import {t} from 'sentry/locale';
diff --git a/static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx b/static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx
index d578eecee416fa..8d703817ce644c 100644
--- a/static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx
+++ b/static/app/views/releases/detail/overview/sidebar/releaseAdoption.tsx
@@ -9,9 +9,9 @@ import ChartZoom from 'sentry/components/charts/chartZoom';
import {ErrorPanel} from 'sentry/components/charts/errorPanel';
import type {LineChartProps} from 'sentry/components/charts/lineChart';
import {LineChart} from 'sentry/components/charts/lineChart';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {NotAvailable} from 'sentry/components/notAvailable';
import {QuestionTooltip} from 'sentry/components/questionTooltip';
import * as SidebarSection from 'sentry/components/sidebarSection';
diff --git a/static/app/views/releases/drawer/releasesDrawerDetails.tsx b/static/app/views/releases/drawer/releasesDrawerDetails.tsx
index 10e9b72efb12b1..ad259c16837646 100644
--- a/static/app/views/releases/drawer/releasesDrawerDetails.tsx
+++ b/static/app/views/releases/drawer/releasesDrawerDetails.tsx
@@ -7,7 +7,7 @@ import {Flex, Stack} from '@sentry/scraps/layout';
import {Link} from '@sentry/scraps/link';
import {Select} from '@sentry/scraps/select';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
EventDrawerBody,
EventDrawerContainer,
diff --git a/static/app/views/releases/list/mobileBuildsChart.tsx b/static/app/views/releases/list/mobileBuildsChart.tsx
index bbd11a2e932eb4..3fb7e2619f5a25 100644
--- a/static/app/views/releases/list/mobileBuildsChart.tsx
+++ b/static/app/views/releases/list/mobileBuildsChart.tsx
@@ -8,7 +8,7 @@ import {Container} from '@sentry/scraps/layout';
import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';
import {LineChart} from 'sentry/components/charts/lineChart';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {Panel} from 'sentry/components/panels/panel';
import {PanelBody} from 'sentry/components/panels/panelBody';
import {Placeholder} from 'sentry/components/placeholder';
diff --git a/static/app/views/releases/list/releasesAdoptionChart.tsx b/static/app/views/releases/list/releasesAdoptionChart.tsx
index 9c46882558dad9..8802e978db757d 100644
--- a/static/app/views/releases/list/releasesAdoptionChart.tsx
+++ b/static/app/views/releases/list/releasesAdoptionChart.tsx
@@ -11,14 +11,14 @@ import {Flex} from '@sentry/scraps/layout';
import ChartZoom from 'sentry/components/charts/chartZoom';
import {LineChart} from 'sentry/components/charts/lineChart';
-import SessionsRequest from 'sentry/components/charts/sessionsRequest';
+import {SessionsRequest} from 'sentry/components/charts/sessionsRequest';
import {
HeaderTitleLegend,
InlineContainer,
SectionHeading,
SectionValue,
} from 'sentry/components/charts/styles';
-import TransitionChart from 'sentry/components/charts/transitionChart';
+import {TransitionChart} from 'sentry/components/charts/transitionChart';
import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask';
import {
getDiffInMinutes,
diff --git a/static/app/views/replays/detail/console/consoleLogRow.tsx b/static/app/views/replays/detail/console/consoleLogRow.tsx
index 8b387e483c8099..90133f26ba1c41 100644
--- a/static/app/views/replays/detail/console/consoleLogRow.tsx
+++ b/static/app/views/replays/detail/console/consoleLogRow.tsx
@@ -4,7 +4,7 @@ import classNames from 'classnames';
import {Tooltip} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {IconClose, IconInfo, IconWarning} from 'sentry/icons';
import {BreadcrumbLevelType} from 'sentry/types/breadcrumbs';
import type {useCrumbHandlers} from 'sentry/utils/replays/hooks/useCrumbHandlers';
diff --git a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx
index 032203baf1b1b6..9071adaaf542db 100644
--- a/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx
+++ b/static/app/views/replays/detail/header/replayDetailsPageBreadcrumbs.tsx
@@ -15,7 +15,7 @@ import {IconChevron, IconCopy, IconRefresh} from 'sentry/icons';
import {t} from 'sentry/locale';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getShortEventId} from 'sentry/utils/events';
import type {useLoadReplayReader} from 'sentry/utils/replays/hooks/useLoadReplayReader';
import {useReplayPlaylist} from 'sentry/utils/replays/playback/providers/replayPlaylistProvider';
diff --git a/static/app/views/replays/detail/layout/replayLayout.tsx b/static/app/views/replays/detail/layout/replayLayout.tsx
index 7644ad8a2ef185..f525068e9df268 100644
--- a/static/app/views/replays/detail/layout/replayLayout.tsx
+++ b/static/app/views/replays/detail/layout/replayLayout.tsx
@@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import {Stack} from '@sentry/scraps/layout';
import {TooltipContext} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {Placeholder} from 'sentry/components/placeholder';
import {ReplayController} from 'sentry/components/replays/replayController';
import {ReplayView} from 'sentry/components/replays/replayView';
diff --git a/static/app/views/replays/detail/trace/useReplayTraces.tsx b/static/app/views/replays/detail/trace/useReplayTraces.tsx
index 64a79e753a18ef..f20bd793c35a95 100644
--- a/static/app/views/replays/detail/trace/useReplayTraces.tsx
+++ b/static/app/views/replays/detail/trace/useReplayTraces.tsx
@@ -3,7 +3,7 @@ import type {Location} from 'history';
import {getTimeStampFromTableDateField, getUtcDateString} from 'sentry/utils/dates';
import type {TableData} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
import type {ParsedHeader} from 'sentry/utils/parseLinkHeader';
import {parseLinkHeader} from 'sentry/utils/parseLinkHeader';
diff --git a/static/app/views/replays/selectors/exampleReplaysList.tsx b/static/app/views/replays/selectors/exampleReplaysList.tsx
index c701b321e36e23..b49af9304dab59 100644
--- a/static/app/views/replays/selectors/exampleReplaysList.tsx
+++ b/static/app/views/replays/selectors/exampleReplaysList.tsx
@@ -12,7 +12,7 @@ import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {ReplayBadge} from 'sentry/components/replays/replayBadge';
import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
-import EventView from 'sentry/utils/discover/eventView';
+import {EventView} from 'sentry/utils/discover/eventView';
import {getRouteStringFromRoutes} from 'sentry/utils/getRouteStringFromRoutes';
import {useReplayList} from 'sentry/utils/replays/hooks/useReplayList';
import {useOrganization} from 'sentry/utils/useOrganization';
diff --git a/static/app/views/seerExplorer/explorerPanel.tsx b/static/app/views/seerExplorer/explorerPanel.tsx
index 47ac8ca4511bee..ca521c04342cde 100644
--- a/static/app/views/seerExplorer/explorerPanel.tsx
+++ b/static/app/views/seerExplorer/explorerPanel.tsx
@@ -1,8 +1,17 @@
import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {useTheme} from '@emotion/react';
+import {AnimatePresence, motion} from 'framer-motion';
+
+import {Button} from '@sentry/scraps/button';
+import {Container} from '@sentry/scraps/layout';
+import {Flex} from '@sentry/scraps/layout';
+import {Text} from '@sentry/scraps/text';
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
+import {HotkeysLabel} from 'sentry/components/hotkeysLabel';
+import {IconSeer} from 'sentry/icons';
+import {t} from 'sentry/locale';
import type {User} from 'sentry/types/user';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
@@ -12,6 +21,7 @@ import {useOrganization} from 'sentry/utils/useOrganization';
import {useProjects} from 'sentry/utils/useProjects';
import {useUser} from 'sentry/utils/useUser';
import {getConversationsUrl} from 'sentry/views/insights/pages/conversations/utils/urlParams';
+import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature';
import {AskUserQuestionBlock} from 'sentry/views/seerExplorer/askUserQuestionBlock';
import {BlockComponent} from 'sentry/views/seerExplorer/blockComponents';
import {EmptyState} from 'sentry/views/seerExplorer/emptyState';
@@ -28,7 +38,6 @@ import {
PanelContainers,
} from 'sentry/views/seerExplorer/panelContainers';
import {usePRWidgetData} from 'sentry/views/seerExplorer/prWidget';
-import {SeerFab} from 'sentry/views/seerExplorer/seerFab';
import {TopBar} from 'sentry/views/seerExplorer/topBar';
import type {Block} from 'sentry/views/seerExplorer/types';
import {useExplorerPanel} from 'sentry/views/seerExplorer/useExplorerPanel';
@@ -66,6 +75,8 @@ export function ExplorerPanel() {
const sessionHistoryButtonRef = useRef(null);
const prWidgetButtonRef = useRef(null);
+ const hasPageFrame = useHasPageFrameFeature();
+
const {panelSize, handleMaxSize, handleMedSize} = usePanelSizing();
// Default to max size when Seer drawer is open
@@ -772,11 +783,62 @@ export function ExplorerPanel() {
return null;
}
+ if (hasPageFrame) {
+ // When an organization has page frame enabled, Seer is a button inside the top bar.
+ return null;
+ }
+
return createPortal(
{panelContent}
-
+
,
document.body
);
}
+
+const MotionButton = motion.create(Button);
+
+interface SeerFloatingActionButtonProps extends React.ComponentProps<
+ typeof MotionButton
+> {
+ visible: boolean;
+}
+
+function SeerFloatingActionButton(props: SeerFloatingActionButtonProps) {
+ const {visible, ...rest} = props;
+ const theme = useTheme();
+
+ return (
+
+ {visible && (
+
+
+
+
+ {t('Ask Seer')}
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/static/app/views/seerExplorer/seerFab.tsx b/static/app/views/seerExplorer/seerFab.tsx
deleted file mode 100644
index c02608f4a47dba..00000000000000
--- a/static/app/views/seerExplorer/seerFab.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import styled from '@emotion/styled';
-import {AnimatePresence, motion} from 'framer-motion';
-
-import {Flex} from '@sentry/scraps/layout';
-import {Text} from '@sentry/scraps/text';
-
-import {HotkeysLabel} from 'sentry/components/hotkeysLabel';
-import {IconSeer} from 'sentry/icons';
-import {t} from 'sentry/locale';
-
-interface AskSeerFabProps {
- hide: boolean;
- onOpen: () => void;
-}
-
-export function SeerFab({hide, onOpen}: AskSeerFabProps) {
- return (
-
- {!hide && (
-
-
-
- {t('Ask Seer')}
-
-
-
-
-
- )}
-
- );
-}
-
-const FloatingActionButton = styled(motion.button)`
- position: fixed;
- bottom: ${p => p.theme.space.lg};
- right: ${p => p.theme.space.lg};
- z-index: 9999;
- padding: ${p => p.theme.space.sm} ${p => p.theme.space.md};
- background: ${p => p.theme.tokens.background.primary};
- border: 1px solid ${p => p.theme.tokens.border.primary};
- border-radius: ${p => p.theme.radius.md};
- box-shadow: ${p => p.theme.dropShadowHeavy};
- cursor: pointer;
-
- &:hover {
- background: ${p => p.theme.tokens.background.secondary};
- }
-
- &:active {
- transform: scale(0.98);
- }
-`;
diff --git a/static/app/views/settings/account/accountDetails.tsx b/static/app/views/settings/account/accountDetails.tsx
index 0224c911406c8a..16e067d19f8666 100644
--- a/static/app/views/settings/account/accountDetails.tsx
+++ b/static/app/views/settings/account/accountDetails.tsx
@@ -9,7 +9,7 @@ import {AvatarChooser} from 'sentry/components/avatarChooser';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
-import languages from 'sentry/data/languages';
+import {languages} from 'sentry/data/languages';
import {timezoneOptions} from 'sentry/data/timezones';
import {t} from 'sentry/locale';
import {StacktraceOrder, type User} from 'sentry/types/user';
diff --git a/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx b/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx
index 6dec567b6438cb..86ac906cee94b7 100644
--- a/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx
+++ b/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx
@@ -45,7 +45,7 @@ import {
import {AttributeField} from './form/attributeField';
import {EventIdField} from './form/eventIdField';
-import SourceField from './form/sourceField';
+import {SourceField} from './form/sourceField';
import {ErrorType, handleError} from './handleError';
import {hasCaptureGroups, useSourceGroupData} from './utils';
diff --git a/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.spec.tsx b/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.spec.tsx
index c55ab2f009fe87..cb913076a734e4 100644
--- a/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.spec.tsx
+++ b/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.spec.tsx
@@ -1,6 +1,6 @@
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-import SourceField from 'sentry/views/settings/components/dataScrubbing/modals/form/sourceField';
+import {SourceField} from 'sentry/views/settings/components/dataScrubbing/modals/form/sourceField';
import {
binarySuggestions,
unarySuggestions,
diff --git a/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.tsx b/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.tsx
index ad26caf7e25a35..f7a557d0b208df 100644
--- a/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.tsx
+++ b/static/app/views/settings/components/dataScrubbing/modals/form/sourceField.tsx
@@ -42,7 +42,7 @@ type State = {
suggestions: SourceSuggestion[];
};
-class SourceField extends Component {
+export class SourceField extends Component {
state: State = {
suggestions: [],
fieldValues: [],
@@ -451,8 +451,6 @@ class SourceField extends Component {
}
}
-export default SourceField;
-
const Wrapper = styled('div')<{hideCaret?: boolean}>`
position: relative;
width: 100%;
diff --git a/static/app/views/settings/organizationIntegrations/integrationAlertContainer.tsx b/static/app/views/settings/organizationIntegrations/integrationAlertContainer.tsx
index 9fd894e2853b5f..894c3444d24179 100644
--- a/static/app/views/settings/organizationIntegrations/integrationAlertContainer.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationAlertContainer.tsx
@@ -1,5 +1,5 @@
import styled from '@emotion/styled';
-export default styled('div')`
+export const AlertContainer = styled('div')`
padding: 0px ${p => p.theme.space['2xl']} 0px 68px;
`;
diff --git a/static/app/views/settings/organizationIntegrations/integrationRow.tsx b/static/app/views/settings/organizationIntegrations/integrationRow.tsx
index 552337d18f0603..3121f9b77c6a7a 100644
--- a/static/app/views/settings/organizationIntegrations/integrationRow.tsx
+++ b/static/app/views/settings/organizationIntegrations/integrationRow.tsx
@@ -22,7 +22,7 @@ import {
trackIntegrationAnalytics,
} from 'sentry/utils/integrationUtil';
-import AlertContainer from './integrationAlertContainer';
+import {AlertContainer} from './integrationAlertContainer';
import {IntegrationStatus} from './integrationStatus';
import {PluginDeprecationAlert} from './pluginDeprecationAlert';
diff --git a/static/app/views/settings/organizationRelay/modals/add/index.tsx b/static/app/views/settings/organizationRelay/modals/add/index.tsx
index d5041303cd53a5..997b5632e02a09 100644
--- a/static/app/views/settings/organizationRelay/modals/add/index.tsx
+++ b/static/app/views/settings/organizationRelay/modals/add/index.tsx
@@ -4,12 +4,12 @@ import {ExternalLink} from '@sentry/scraps/link';
import {List} from 'sentry/components/list';
import {t, tct} from 'sentry/locale';
-import ModalManager from 'sentry/views/settings/organizationRelay/modals/modalManager';
+import {ModalManager} from 'sentry/views/settings/organizationRelay/modals/modalManager';
import {Item} from './item';
import {Terminal} from './terminal';
-class Add extends ModalManager {
+export class Add extends ModalManager {
getTitle() {
return t('Register Key');
}
@@ -63,8 +63,6 @@ class Add extends ModalManager {
}
}
-export default Add;
-
const StyledList = styled(List)`
display: grid;
gap: ${p => p.theme.space['2xl']};
diff --git a/static/app/views/settings/organizationRelay/modals/edit.tsx b/static/app/views/settings/organizationRelay/modals/edit.tsx
index 43480582c905a2..5d3d2620f33646 100644
--- a/static/app/views/settings/organizationRelay/modals/edit.tsx
+++ b/static/app/views/settings/organizationRelay/modals/edit.tsx
@@ -1,7 +1,7 @@
import {t} from 'sentry/locale';
import type {Relay} from 'sentry/types/relay';
-import ModalManager from './modalManager';
+import {ModalManager} from './modalManager';
type Props = {
relay: Relay;
@@ -9,7 +9,7 @@ type Props = {
type State = ModalManager['state'];
-class Edit extends ModalManager {
+export class Edit extends ModalManager {
getDefaultState() {
return {
...super.getDefaultState(),
@@ -40,5 +40,3 @@ class Edit extends ModalManager {
return {trustedRelays};
}
}
-
-export default Edit;
diff --git a/static/app/views/settings/organizationRelay/modals/modalManager.tsx b/static/app/views/settings/organizationRelay/modals/modalManager.tsx
index b86693f70075cc..78dc32cdda2799 100644
--- a/static/app/views/settings/organizationRelay/modals/modalManager.tsx
+++ b/static/app/views/settings/organizationRelay/modals/modalManager.tsx
@@ -32,10 +32,10 @@ type State = {
values: Values;
};
-class DialogManager extends Component<
- P,
- S
-> {
+export class ModalManager<
+ P extends Props = Props,
+ S extends State = State,
+> extends Component
{
state = this.getDefaultState();
componentDidMount() {
@@ -236,5 +236,3 @@ class DialogManager
extends Co
);
}
}
-
-export default DialogManager;
diff --git a/static/app/views/settings/organizationRelay/relayWrapper.tsx b/static/app/views/settings/organizationRelay/relayWrapper.tsx
index f0ba87c1037a3d..2cfd23c1755c31 100644
--- a/static/app/views/settings/organizationRelay/relayWrapper.tsx
+++ b/static/app/views/settings/organizationRelay/relayWrapper.tsx
@@ -24,8 +24,8 @@ import {SettingsPageHeader} from 'sentry/views/settings/components/settingsPageH
import {TextBlock} from 'sentry/views/settings/components/text/textBlock';
import {OrganizationPermissionAlert} from 'sentry/views/settings/organization/organizationPermissionAlert';
-import Add from './modals/add';
-import Edit from './modals/edit';
+import {Add} from './modals/add';
+import {Edit} from './modals/edit';
import {EmptyState} from './emptyState';
import {List} from './list';
diff --git a/static/app/views/settings/project/projectFilters/groupTombstones.tsx b/static/app/views/settings/project/projectFilters/groupTombstones.tsx
index 0beba4d2b04762..c63689589d9d73 100644
--- a/static/app/views/settings/project/projectFilters/groupTombstones.tsx
+++ b/static/app/views/settings/project/projectFilters/groupTombstones.tsx
@@ -9,7 +9,7 @@ import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicato
import {Access} from 'sentry/components/acl/access';
import {Confirm} from 'sentry/components/confirm';
import {Count} from 'sentry/components/count';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {EventMessage} from 'sentry/components/events/eventMessage';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/views/settings/project/projectOwnership/index.tsx b/static/app/views/settings/project/projectOwnership/index.tsx
index 95cb45d06f0908..235db6a1221f69 100644
--- a/static/app/views/settings/project/projectOwnership/index.tsx
+++ b/static/app/views/settings/project/projectOwnership/index.tsx
@@ -9,7 +9,7 @@ import {ExternalLink} from '@sentry/scraps/link';
import {closeModal, openEditOwnershipRules, openModal} from 'sentry/actionCreators/modal';
import {Access, hasEveryAccess} from 'sentry/components/acl/access';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
import {IconEdit} from 'sentry/icons';
diff --git a/static/app/views/settings/project/projectServiceHookDetails.tsx b/static/app/views/settings/project/projectServiceHookDetails.tsx
index 112e74bc61380c..73398b2c1efad1 100644
--- a/static/app/views/settings/project/projectServiceHookDetails.tsx
+++ b/static/app/views/settings/project/projectServiceHookDetails.tsx
@@ -9,7 +9,7 @@ import {
} from 'sentry/actionCreators/indicator';
import {MiniBarChart} from 'sentry/components/charts/miniBarChart';
import {EmptyMessage} from 'sentry/components/emptyMessage';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {FieldGroup} from 'sentry/components/forms/fieldGroup';
import {LoadingError} from 'sentry/components/loadingError';
import {LoadingIndicator} from 'sentry/components/loadingIndicator';
diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx
index f9f0820f6a7f1a..f7a686fd914152 100644
--- a/static/app/views/settings/projectSeer/index.tsx
+++ b/static/app/views/settings/projectSeer/index.tsx
@@ -25,7 +25,7 @@ import {Form} from 'sentry/components/forms/form';
import JsonForm from 'sentry/components/forms/jsonForm';
import type {FieldObject, JsonFormObject} from 'sentry/components/forms/types';
import {HookOrDefault} from 'sentry/components/hookOrDefault';
-import ExternalLink from 'sentry/components/links/externalLink';
+import {ExternalLink} from 'sentry/components/links/externalLink';
import {NoAccess} from 'sentry/components/noAccess';
import {Placeholder} from 'sentry/components/placeholder';
import {SentryDocumentTitle} from 'sentry/components/sentryDocumentTitle';
diff --git a/static/eslint/eslintPluginSentry/no-default-exports.spec.ts b/static/eslint/eslintPluginSentry/no-default-exports.spec.ts
index 46d17072ecfc34..b381e9d798a859 100644
--- a/static/eslint/eslintPluginSentry/no-default-exports.spec.ts
+++ b/static/eslint/eslintPluginSentry/no-default-exports.spec.ts
@@ -34,37 +34,248 @@ export default wrap(MyComponentInner);
code: `export const util = () => null;`,
filename: 'valid.tsx',
},
+ {
+ code: `
+ export const a = 1;
+ export const b = 2;
+ `,
+ filename: 'valid.tsx',
+ },
+ {
+ code: `export class MyClass {}`,
+ filename: 'valid.tsx',
+ },
+ {
+ code: `const x = 1;`,
+ filename: 'valid.tsx',
+ },
+ {
+ code: `export default withConfig(MyComponent);`,
+ filename: 'valid.tsx',
+ },
+ {
+ code: `export default styled(MyComponent)\`color: red;\`;`,
+ filename: 'valid.tsx',
+ },
+ {
+ code: `export default withConfig(MyComponent) as React.FC;`,
+ filename: 'valid.tsx',
+ },
],
invalid: [
{
- code: 'function example() {}\nexport default example;',
- output: 'export function example() {}\n',
+ code: `
+ function example() {}
+ export default example;
+ `,
+ output: `
+ export function example() {}
+ `,
errors: [{messageId: 'forbidden'}],
filename: 'invalid.tsx',
},
{
code: `
-export function alsoExported() {}
-export default function defaultExported() {}
-`,
+ export function alsoExported() {}
+ export default function defaultExported() {}
+ `,
output: `
-export function alsoExported() {}
-export function defaultExported() {}
-`,
+ export function alsoExported() {}
+ export function defaultExported() {}
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ function MyComponent() { return
; }
+ export default MyComponent;
+ `,
+ output: `
+ export function MyComponent() { return ; }
+ `,
errors: [{messageId: 'forbidden'}],
filename: 'invalid.tsx',
},
{
- code: 'function MyComponent() { return ; }\nexport default MyComponent;',
- output: 'export function MyComponent() { return ; }\n',
+ code: `
+ const Panel = styled('div')\`padding: 0;\`;
+ export default Panel;
+ `,
+ output: `
+ export const Panel = styled('div')\`padding: 0;\`;
+ `,
errors: [{messageId: 'forbidden'}],
filename: 'invalid.tsx',
},
{
- code: `const Panel = styled('div')\`padding: 0;\`;
-export default Panel;`,
- output: `export const Panel = styled('div')\`padding: 0;\`;
-`,
+ code: `
+ const MyComponent = () => ;
+ export default MyComponent;
+ `,
+ output: `
+ export const MyComponent = () => ;
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ class MyComponent extends React.Component {};
+ export default MyComponent;
+ `,
+ output: `
+ export class MyComponent extends React.Component {};
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ enum MyShape {};
+ export default MyShape;
+ `,
+ output: `
+ export enum MyShape {};
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ interface MyShape {};
+ export default MyShape;
+ `,
+ output: `
+ export interface MyShape {};
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ type MyShape = {};
+ export default MyShape;
+ `,
+ output: `
+ export type MyShape = {};
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ let count = 0;
+ export default count;
+ `,
+ output: `
+ export let count = 0;
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default function myFunction() { return 1; }`,
+ output: `export function myFunction() { return 1; }`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default class MyClass {}`,
+ output: `export class MyClass {}`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default function() { return 1; }`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default class {}`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ const x = 1;
+ export default x as number;
+ `,
+ output: `
+ export const x = 1;
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ function myFn() {}
+ export default myFn as unknown as () => void;
+ `,
+ output: `
+ export function myFn() {}
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default { key: "value" };`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default [1, 2, 3];`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default "hello";`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default 42;`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `export default () => null;`,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ const a = 1, b = 2;
+ export default b;
+ `,
+ output: `
+ export const a = 1, b = 2;
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ export const a = 1;
+ export default function foo() {}
+ `,
+ output: `
+ export const a = 1;
+ export function foo() {}
+ `,
+ errors: [{messageId: 'forbidden'}],
+ filename: 'invalid.tsx',
+ },
+ {
+ code: `
+ export const a = 1;
+ function bar() {}
+ export default bar;
+ `,
+ output: `
+ export const a = 1;
+ export function bar() {}
+ `,
errors: [{messageId: 'forbidden'}],
filename: 'invalid.tsx',
},
diff --git a/static/eslint/eslintPluginSentry/no-default-exports.ts b/static/eslint/eslintPluginSentry/no-default-exports.ts
index 6a6f13c82944b6..f7120117792cce 100644
--- a/static/eslint/eslintPluginSentry/no-default-exports.ts
+++ b/static/eslint/eslintPluginSentry/no-default-exports.ts
@@ -63,27 +63,24 @@ function collectResolvedImportFiles(program: ts.Program) {
return allowedFiles;
}
-function findTopLevelFunctionDeclaration(
- body: TSESTree.ProgramStatement[],
- name: string
-) {
- return body.find(
- statement =>
- statement.type === AST_NODE_TYPES.FunctionDeclaration && statement.id?.name === name
- );
-}
-
-function findTopLevelVariableDeclaration(
- body: TSESTree.ProgramStatement[],
- name: string
-): TSESTree.VariableDeclaration | undefined {
- return body.find((statement): statement is TSESTree.VariableDeclaration => {
- if (statement.type !== AST_NODE_TYPES.VariableDeclaration) {
- return false;
+function findTopLevelDeclaration(body: TSESTree.ProgramStatement[], name: string) {
+ return body.find(statement => {
+ switch (statement.type) {
+ case AST_NODE_TYPES.FunctionDeclaration:
+ case AST_NODE_TYPES.ClassDeclaration:
+ case AST_NODE_TYPES.TSEnumDeclaration:
+ case AST_NODE_TYPES.TSInterfaceDeclaration:
+ case AST_NODE_TYPES.TSTypeAliasDeclaration:
+ return statement.id?.name === name;
+ case AST_NODE_TYPES.VariableDeclaration:
+ return statement.declarations.some(
+ declaration =>
+ declaration.id.type === AST_NODE_TYPES.Identifier &&
+ declaration.id.name === name
+ );
+ default:
+ return false;
}
- return statement.declarations.some(
- decl => decl.id.type === AST_NODE_TYPES.Identifier && decl.id.name === name
- );
});
}
@@ -112,55 +109,80 @@ export const noDefaultExports = ESLintUtils.RuleCreator.withoutDocs({
return {};
}
- return {
- ExportDefaultDeclaration(node) {
- if (
- node.declaration.type === AST_NODE_TYPES.ClassDeclaration ||
- node.declaration.type === AST_NODE_TYPES.FunctionDeclaration
- ) {
- if (!node.declaration.id) {
- return;
- }
-
+ function visitDeclaration(
+ exported: TSESTree.Node,
+ declaration: TSESTree.ExportDefaultDeclaration
+ ) {
+ switch (exported.type) {
+ case AST_NODE_TYPES.ClassDeclaration:
+ case AST_NODE_TYPES.FunctionDeclaration: {
context.report({
- node,
+ node: declaration,
messageId: 'forbidden',
- fix: fixer => [
- fixer.replaceTextRange(
- [node.range[0], node.declaration.range[0]],
- 'export '
- ),
- ],
+ fix: exported.id
+ ? fixer => [
+ fixer.replaceTextRange(
+ [declaration.range[0], exported.range[0]],
+ 'export '
+ ),
+ ]
+ : undefined,
});
return;
}
- if (node.declaration.type === AST_NODE_TYPES.Identifier) {
- const exportedName = node.declaration.name;
- const functionDeclaration = findTopLevelFunctionDeclaration(
- node.parent.body,
- exportedName
+ case AST_NODE_TYPES.Identifier: {
+ const declarationToExport = findTopLevelDeclaration(
+ declaration.parent.body,
+ exported.name
);
- const variableDeclaration = findTopLevelVariableDeclaration(
- node.parent.body,
- exportedName
- );
-
- const declarationToExport = functionDeclaration ?? variableDeclaration;
- if (!declarationToExport) {
- return;
- }
context.report({
- node,
+ node: declaration,
messageId: 'forbidden',
- fix: fixer => [
- fixer.insertTextBefore(declarationToExport, 'export '),
- fixer.remove(node),
- ],
+ fix: declarationToExport
+ ? fixer => {
+ const text = context.sourceCode.getText();
+ let removeStart = declaration.range[0];
+ while (removeStart > 0 && ' \t'.includes(text[removeStart - 1]!)) {
+ removeStart--;
+ }
+ if (removeStart > 0 && text[removeStart - 1] === '\n') {
+ removeStart--;
+ }
+ return [
+ fixer.insertTextBefore(declarationToExport, 'export '),
+ fixer.removeRange([removeStart, declaration.range[1]]),
+ ];
+ }
+ : undefined,
});
return;
}
+
+ case AST_NODE_TYPES.TSAsExpression:
+ visitDeclaration(exported.expression, declaration);
+ return;
+
+ // Calls like HoCs often result in differences between internal and exported names:
+ // export default withConfig(MyComponent);
+ // export default styled(MyComponent)``;
+ case AST_NODE_TYPES.CallExpression:
+ case AST_NODE_TYPES.TaggedTemplateExpression: {
+ return;
+ }
+
+ default:
+ context.report({
+ node: declaration,
+ messageId: 'forbidden',
+ });
+ }
+ }
+
+ return {
+ ExportDefaultDeclaration(node) {
+ visitDeclaration(node.declaration, node);
},
};
},
diff --git a/static/gsAdmin/components/detailsPage.tsx b/static/gsAdmin/components/detailsPage.tsx
index 88c6f4f6a41aeb..70a4efc04a05ab 100644
--- a/static/gsAdmin/components/detailsPage.tsx
+++ b/static/gsAdmin/components/detailsPage.tsx
@@ -5,7 +5,7 @@ import {Tag, type TagProps} from '@sentry/scraps/badge';
import {Flex} from '@sentry/scraps/layout';
import {Tooltip} from '@sentry/scraps/tooltip';
-import ErrorBoundary from 'sentry/components/errorBoundary';
+import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {Panel} from 'sentry/components/panels/panel';
import {PanelHeader} from 'sentry/components/panels/panelHeader';
diff --git a/static/gsAdmin/components/forkCustomer.tsx b/static/gsAdmin/components/forkCustomer.tsx
index 8d9fbbfa13d0a8..0b87a82ef0c523 100644
--- a/static/gsAdmin/components/forkCustomer.tsx
+++ b/static/gsAdmin/components/forkCustomer.tsx
@@ -26,7 +26,7 @@ type State = {
/**
* Rendered as part of a openAdminConfirmModal call
*/
-class ForkCustomerAction extends Component {
+export class ForkCustomerAction extends Component {
state: State = {
regionUrl: '',
};
@@ -81,5 +81,3 @@ class ForkCustomerAction extends Component {
);
}
}
-
-export default ForkCustomerAction;
diff --git a/static/gsAdmin/components/provisionSubscriptionAction.tsx b/static/gsAdmin/components/provisionSubscriptionAction.tsx
index 14329a85e81a95..be2f0b71434a23 100644
--- a/static/gsAdmin/components/provisionSubscriptionAction.tsx
+++ b/static/gsAdmin/components/provisionSubscriptionAction.tsx
@@ -8,8 +8,8 @@ import {openModal} from 'sentry/actionCreators/modal';
import type {Client} from 'sentry/api';
import BooleanField from 'sentry/components/deprecatedforms/booleanField';
import {DateTimeField} from 'sentry/components/deprecatedforms/dateTimeField';
-import Form from 'sentry/components/deprecatedforms/form';
-import InputField from 'sentry/components/deprecatedforms/inputField';
+import {Form} from 'sentry/components/deprecatedforms/form';
+import {InputField} from 'sentry/components/deprecatedforms/inputField';
import NumberField, {
NumberField as NumberFieldNoContext,
} from 'sentry/components/deprecatedforms/numberField';
diff --git a/static/gsAdmin/components/sponsorshipAction.tsx b/static/gsAdmin/components/sponsorshipAction.tsx
index e2c8c892bd04a1..ceca1c08e87799 100644
--- a/static/gsAdmin/components/sponsorshipAction.tsx
+++ b/static/gsAdmin/components/sponsorshipAction.tsx
@@ -21,7 +21,7 @@ type State = {
/**
* Rendered as part of a openAdminConfirmModal call
*/
-class SponsorshipAction extends Component {
+export class SponsorshipAction extends Component {
state: State = {
sponsoredType: undefined,
};
@@ -80,5 +80,3 @@ class SponsorshipAction extends Component {
);
}
}
-
-export default SponsorshipAction;
diff --git a/static/gsAdmin/components/suspendAccountAction.tsx b/static/gsAdmin/components/suspendAccountAction.tsx
index f8d706978bd484..36096840919e86 100644
--- a/static/gsAdmin/components/suspendAccountAction.tsx
+++ b/static/gsAdmin/components/suspendAccountAction.tsx
@@ -21,7 +21,7 @@ type State = {
/**
* Rendered as part of a openAdminConfirmModal call
*/
-class SuspendAccountAction extends Component {
+export class SuspendAccountAction extends Component {
state: State = {
suspensionReason: null,
};
@@ -62,5 +62,3 @@ class SuspendAccountAction extends Component {
));
}
}
-
-export default SuspendAccountAction;
diff --git a/static/gsAdmin/components/trialSubscriptionAction.spec.tsx b/static/gsAdmin/components/trialSubscriptionAction.spec.tsx
index 6454636d3cc589..210a57636ac0c7 100644
--- a/static/gsAdmin/components/trialSubscriptionAction.spec.tsx
+++ b/static/gsAdmin/components/trialSubscriptionAction.spec.tsx
@@ -10,7 +10,7 @@ import {
} from 'sentry-test/reactTestingLibrary';
import {openAdminConfirmModal} from 'admin/components/adminConfirmationModal';
-import TrialSubscriptionAction from 'admin/components/trialSubscriptionAction';
+import {TrialSubscriptionAction} from 'admin/components/trialSubscriptionAction';
import {PlanTier} from 'getsentry/types';
describe('TrialSubscriptionAction', () => {
diff --git a/static/gsAdmin/components/trialSubscriptionAction.tsx b/static/gsAdmin/components/trialSubscriptionAction.tsx
index 9a699a897397f5..5ece6e33900a11 100644
--- a/static/gsAdmin/components/trialSubscriptionAction.tsx
+++ b/static/gsAdmin/components/trialSubscriptionAction.tsx
@@ -27,7 +27,7 @@ type State = {
/**
* Rendered as part of a openAdminConfirmModal call
*/
-class TrialSubscriptionAction extends Component {
+export class TrialSubscriptionAction extends Component {
state: State = {
trialDays:
this.props.subscription.isEnterpriseTrial || this.props.startEnterpriseTrial
@@ -143,5 +143,3 @@ class TrialSubscriptionAction extends Component