diff --git a/static/app/views/insights/database/components/databaseSystemSelector.spec.tsx b/static/app/views/insights/database/components/databaseSystemSelector.spec.tsx
new file mode 100644
index 00000000000000..62a2c19d89a695
--- /dev/null
+++ b/static/app/views/insights/database/components/databaseSystemSelector.spec.tsx
@@ -0,0 +1,234 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
+import {DatabaseSystemSelector} from 'sentry/views/insights/database/components/databaseSystemSelector';
+import {SpanMetricsField} from 'sentry/views/insights/types';
+
+jest.mock('sentry/views/insights/common/queries/useDiscover', () => ({
+ useSpanMetrics: jest.fn(),
+}));
+
+jest.mock('sentry/utils/useLocalStorageState', () => ({
+ useLocalStorageState: jest.fn(),
+}));
+
+jest.mock('sentry/utils/useLocation', () => ({
+ useLocation: jest.fn(),
+}));
+
+jest.mock('sentry/utils/useNavigate', () => ({
+ useNavigate: jest.fn(),
+}));
+
+const mockUseLocalStorageState = jest.mocked(useLocalStorageState);
+const mockUseSpanMetrics = jest.mocked(useSpanMetrics);
+const mockUseLocation = jest.mocked(useLocation);
+const mockUseNavigate = jest.mocked(useNavigate);
+
+describe('DatabaseSystemSelector', function () {
+ const organization = OrganizationFixture();
+
+ afterAll(() => {
+ jest.clearAllMocks();
+ });
+
+ beforeEach(() => {
+ mockUseLocation.mockReturnValue({
+ query: {project: ['1']},
+ pathname: '',
+ search: '',
+ hash: '',
+ state: undefined,
+ action: 'POP',
+ key: '',
+ });
+ });
+
+ it('is disabled and does not select a system if there are none available', async function () {
+ const mockSetState = jest.fn();
+ mockUseLocalStorageState.mockReturnValue(['', mockSetState]);
+ mockUseSpanMetrics.mockReturnValue({
+ data: [],
+ isLoading: false,
+ isError: false,
+ } as any);
+
+ render(, {organization});
+
+ expect(mockSetState).not.toHaveBeenCalled();
+ const dropdownButton = await screen.findByRole('button');
+ expect(dropdownButton).toBeInTheDocument();
+ expect(dropdownButton).toHaveTextContent('DB SystemNone');
+ });
+
+ it('is disabled when only one database system is present and shows that system as selected', async function () {
+ const mockSetState = jest.fn();
+ mockUseLocalStorageState.mockReturnValue(['', mockSetState]);
+ mockUseSpanMetrics.mockReturnValue({
+ data: [
+ {
+ 'span.system': 'postgresql',
+ 'count()': 1000,
+ },
+ ],
+ isLoading: false,
+ isError: false,
+ } as any);
+
+ render(, {organization});
+
+ const dropdownSelector = await screen.findByRole('button');
+ expect(dropdownSelector).toBeDisabled();
+ expect(mockSetState).toHaveBeenCalledWith('postgresql');
+ });
+
+ it('renders all database system options correctly', async function () {
+ mockUseSpanMetrics.mockReturnValue({
+ data: [
+ {
+ 'span.system': 'postgresql',
+ 'count()': 1000,
+ },
+ {
+ 'span.system': 'mongodb',
+ 'count()': 500,
+ },
+ {
+ 'span.system': 'chungusdb',
+ 'count()': 200,
+ },
+ ],
+ isLoading: false,
+ isError: false,
+ } as any);
+
+ render(, {organization});
+
+ const dropdownSelector = await screen.findByRole('button');
+ expect(dropdownSelector).toBeEnabled();
+ expect(mockUseSpanMetrics).toHaveBeenCalled();
+
+ const dropdownButton = await screen.findByRole('button');
+ expect(dropdownButton).toBeInTheDocument();
+
+ await userEvent.click(dropdownButton);
+
+ const dropdownOptionLabels = await screen.findAllByTestId('menu-list-item-label');
+ expect(dropdownOptionLabels[0]).toHaveTextContent('PostgreSQL');
+ expect(dropdownOptionLabels[1]).toHaveTextContent('MongoDB');
+ // chungusdb does not exist, so we do not expect this option to have casing
+ expect(dropdownOptionLabels[2]).toHaveTextContent('chungusdb');
+ });
+
+ it('chooses the currently selected system from localStorage', async function () {
+ mockUseLocalStorageState.mockReturnValue(['mongodb', () => {}]);
+ mockUseSpanMetrics.mockReturnValue({
+ data: [
+ {
+ 'span.system': 'postgresql',
+ 'count()': 1000,
+ },
+ {
+ 'span.system': 'mongodb',
+ 'count()': 500,
+ },
+ {
+ 'span.system': 'chungusdb',
+ 'count()': 200,
+ },
+ ],
+ isLoading: false,
+ isError: false,
+ } as any);
+
+ render(, {organization});
+
+ expect(await screen.findByText('MongoDB')).toBeInTheDocument();
+ });
+
+ it('does not set the value from localStorage if the value is invalid', async function () {
+ const mockSetState = jest.fn();
+ mockUseLocalStorageState.mockReturnValue(['chungusdb', mockSetState]);
+ mockUseSpanMetrics.mockReturnValue({
+ data: [
+ {
+ 'span.system': 'postgresql',
+ 'count()': 1000,
+ },
+ ],
+ isLoading: false,
+ isError: false,
+ } as any);
+
+ render(, {organization});
+
+ const dropdownSelector = await screen.findByRole('button');
+ expect(dropdownSelector).toBeInTheDocument();
+ expect(mockSetState).not.toHaveBeenCalledWith('chungusdb');
+ });
+
+ it('prioritizes the system set in query parameters but does not replace localStorage value until an option is clicked', async function () {
+ const {SPAN_SYSTEM} = SpanMetricsField;
+ const mockNavigate = jest.fn();
+ mockUseNavigate.mockReturnValue(mockNavigate);
+
+ mockUseLocation.mockReturnValue({
+ query: {project: ['1'], [SPAN_SYSTEM]: 'mongodb'},
+ pathname: '',
+ search: '',
+ hash: '',
+ state: undefined,
+ action: 'POP',
+ key: '',
+ });
+
+ mockUseSpanMetrics.mockReturnValue({
+ data: [
+ {
+ 'span.system': 'postgresql',
+ 'count()': 1000,
+ },
+ {
+ 'span.system': 'mongodb',
+ 'count()': 500,
+ },
+ ],
+ isLoading: false,
+ isError: false,
+ } as any);
+
+ const mockSetState = jest.fn();
+ mockUseLocalStorageState.mockReturnValue(['postgresql', mockSetState]);
+
+ render(, {organization});
+
+ const dropdownSelector = await screen.findByRole('button');
+ expect(dropdownSelector).toHaveTextContent('DB SystemMongoDB');
+ expect(mockSetState).not.toHaveBeenCalledWith('mongodb');
+
+ // Now that it has been confirmed that following a URL does not reset localStorage state, confirm that
+ // clicking a different option will update both the state and the URL
+ await userEvent.click(dropdownSelector);
+ const dropdownOptionLabels = await screen.findAllByTestId('menu-list-item-label');
+ expect(dropdownOptionLabels[0]).toHaveTextContent('PostgreSQL');
+ expect(dropdownOptionLabels[1]).toHaveTextContent('MongoDB');
+
+ await userEvent.click(dropdownOptionLabels[0]);
+ expect(dropdownSelector).toHaveTextContent('DB SystemPostgreSQL');
+ expect(mockSetState).toHaveBeenCalledWith('postgresql');
+ expect(mockNavigate).toHaveBeenCalledWith({
+ action: 'POP',
+ hash: '',
+ key: '',
+ pathname: '',
+ query: {project: ['1'], 'span.system': 'postgresql'},
+ search: '',
+ state: undefined,
+ });
+ });
+});
diff --git a/static/app/views/insights/database/components/databaseSystemSelector.tsx b/static/app/views/insights/database/components/databaseSystemSelector.tsx
new file mode 100644
index 00000000000000..a5d0bc53d3d46b
--- /dev/null
+++ b/static/app/views/insights/database/components/databaseSystemSelector.tsx
@@ -0,0 +1,33 @@
+import {CompactSelect} from 'sentry/components/compactSelect';
+import {t} from 'sentry/locale';
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import {useSystemSelectorOptions} from 'sentry/views/insights/database/components/useSystemSelectorOptions';
+import {SpanMetricsField} from 'sentry/views/insights/types';
+
+const {SPAN_SYSTEM} = SpanMetricsField;
+
+export function DatabaseSystemSelector() {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ // If there is no query parameter for the system, retrieve the current value from the hook instead
+ const systemQueryParam = decodeScalar(location.query?.[SPAN_SYSTEM]);
+ const {selectedSystem, setSelectedSystem, options, isLoading, isError} =
+ useSystemSelectorOptions();
+
+ return (
+ {
+ setSelectedSystem(option.value);
+ navigate({...location, query: {...location.query, [SPAN_SYSTEM]: option.value}});
+ }}
+ options={options}
+ triggerProps={{prefix: t('DB System')}}
+ loading={isLoading}
+ disabled={isError || isLoading || options.length <= 1}
+ value={systemQueryParam ?? selectedSystem}
+ />
+ );
+}
diff --git a/static/app/views/insights/database/components/useSystemSelectorOptions.tsx b/static/app/views/insights/database/components/useSystemSelectorOptions.tsx
new file mode 100644
index 00000000000000..4d3dba9dc56e9b
--- /dev/null
+++ b/static/app/views/insights/database/components/useSystemSelectorOptions.tsx
@@ -0,0 +1,71 @@
+import type {SelectOption} from 'sentry/components/compactSelect';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
+import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
+import {SpanMetricsField} from 'sentry/views/insights/types';
+
+/**
+ * The supported relational database system values are based on what is
+ * set in the Sentry Python SDK. The only currently supported NoSQL DBMS is MongoDB.
+ *
+ * https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/sqlalchemy.py#L125
+ */
+enum SupportedDatabaseSystems {
+ // SQL
+ SQLITE = 'sqlite',
+ POSTGRESQL = 'postgresql',
+ MARIADB = 'mariadb',
+ MYSQL = 'mysql',
+ ORACLE = 'oracle',
+ // NoSQL
+ MONGODB = 'mongodb',
+}
+
+const DATABASE_SYSTEM_TO_LABEL: Record = {
+ [SupportedDatabaseSystems.SQLITE]: 'SQLite',
+ [SupportedDatabaseSystems.POSTGRESQL]: 'PostgreSQL',
+ [SupportedDatabaseSystems.MARIADB]: 'MariaDB',
+ [SupportedDatabaseSystems.MYSQL]: 'MySQL',
+ [SupportedDatabaseSystems.ORACLE]: 'Oracle',
+ [SupportedDatabaseSystems.MONGODB]: 'MongoDB',
+};
+
+export function useSystemSelectorOptions() {
+ const [selectedSystem, setSelectedSystem] = useLocalStorageState(
+ 'insights-db-system-selector',
+ ''
+ );
+
+ const {data, isLoading, isError} = useSpanMetrics(
+ {
+ search: MutableSearch.fromQueryObject({'span.op': 'db'}),
+
+ fields: [SpanMetricsField.SPAN_SYSTEM, 'count()'],
+ sorts: [{field: 'count()', kind: 'desc'}],
+ },
+ 'api.starfish.database-system-selector'
+ );
+
+ const options: SelectOption[] = [];
+ data.forEach(entry => {
+ const system = entry['span.system'];
+ if (system) {
+ const label: string =
+ system in DATABASE_SYSTEM_TO_LABEL ? DATABASE_SYSTEM_TO_LABEL[system] : system;
+
+ options.push({value: system, label, textValue: label});
+ }
+ });
+
+ // Edge case: Invalid DB system was retrieved from localStorage
+ if (!options.find(option => selectedSystem === option.value) && options.length > 0) {
+ setSelectedSystem(options[0].value);
+ }
+
+ // Edge case: No current system is saved in localStorage
+ if (!selectedSystem && options.length > 0) {
+ setSelectedSystem(options[0].value);
+ }
+
+ return {selectedSystem, setSelectedSystem, options, isLoading, isError};
+}
diff --git a/static/app/views/insights/database/views/databaseLandingPage.tsx b/static/app/views/insights/database/views/databaseLandingPage.tsx
index 86f63af637d088..d972da2ffb401b 100644
--- a/static/app/views/insights/database/views/databaseLandingPage.tsx
+++ b/static/app/views/insights/database/views/databaseLandingPage.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import styled from '@emotion/styled';
import Alert from 'sentry/components/alert';
import {Breadcrumbs} from 'sentry/components/breadcrumbs';
@@ -8,6 +9,7 @@ import * as Layout from 'sentry/components/layouts/thirds';
import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
import SearchBar from 'sentry/components/searchBar';
import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
import {trackAnalytics} from 'sentry/utils/analytics';
import {browserHistory} from 'sentry/utils/browserHistory';
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
@@ -30,6 +32,7 @@ import {ActionSelector} from 'sentry/views/insights/common/views/spans/selectors
import {DomainSelector} from 'sentry/views/insights/common/views/spans/selectors/domainSelector';
import {DurationChart} from 'sentry/views/insights/database/components/charts/durationChart';
import {ThroughputChart} from 'sentry/views/insights/database/components/charts/throughputChart';
+import {DatabaseSystemSelector} from 'sentry/views/insights/database/components/databaseSystemSelector';
import {NoDataMessage} from 'sentry/views/insights/database/components/noDataMessage';
import {
isAValidSort,
@@ -179,7 +182,12 @@ export function DatabaseLandingPage() {
)}
-
+
+
+ {organization.features.includes(
+ 'performance-queries-mongodb-extraction'
+ ) && }
+
@@ -251,4 +259,9 @@ function PageWithProviders() {
);
}
+const PageFilterWrapper = styled('div')`
+ display: flex;
+ gap: ${space(3)};
+`;
+
export default PageWithProviders;
diff --git a/static/app/views/insights/types.tsx b/static/app/views/insights/types.tsx
index 3387eb2fb682c8..114481fd919226 100644
--- a/static/app/views/insights/types.tsx
+++ b/static/app/views/insights/types.tsx
@@ -28,6 +28,7 @@ export enum SpanMetricsField {
SPAN_GROUP = 'span.group',
SPAN_DURATION = 'span.duration',
SPAN_SELF_TIME = 'span.self_time',
+ SPAN_SYSTEM = 'span.system',
PROJECT = 'project',
PROJECT_ID = 'project.id',
TRANSACTION = 'transaction',
@@ -78,6 +79,7 @@ export type SpanStringFields =
| 'span.action'
| 'span.group'
| 'span.category'
+ | 'span.system'
| 'transaction'
| 'transaction.method'
| 'release'