Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion static/app/views/dashboards/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,14 @@ class DashboardDetail extends Component<Props, State> {
)
: [''];

filterParams[DashboardFilterKeys.TEMPORARY_FILTERS] = activeFilters[
DashboardFilterKeys.TEMPORARY_FILTERS
]?.length
? activeFilters[DashboardFilterKeys.TEMPORARY_FILTERS].map(filter =>
JSON.stringify(filter)
)
: [''];

if (
!isEqualWith(activeFilters, dashboard.filters, (a, b) => {
// This is to handle the case where dashboard filters has release:[] and the new filter is release:""
Expand Down Expand Up @@ -1187,7 +1195,7 @@ class DashboardDetail extends Component<Props, State> {
isPreview={this.isPreview}
onDashboardFilterChange={this.handleChangeFilter}
shouldBusySaveButton={this.state.isSavingDashboardFilters}
isPrebuiltDashboard={defined(dashboard.prebuiltId)}
prebuiltDashboardId={dashboard.prebuiltId}
onCancel={() => {
resetPageFilters(dashboard, location);
trackAnalytics('dashboards2.filter.cancel', {
Expand Down
26 changes: 23 additions & 3 deletions static/app/views/dashboards/filtersBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilt
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {User} from 'sentry/types/user';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {ToggleOnDemand} from 'sentry/utils/performance/contexts/onDemandControl';
import {ReleasesProvider} from 'sentry/utils/releases/releasesProvider';
Expand All @@ -28,6 +29,10 @@ import {
getCombinedDashboardFilters,
getDashboardFiltersFromURL,
} from 'sentry/views/dashboards/utils';
import {
PREBUILT_DASHBOARDS,
type PrebuiltDashboardId,
} from 'sentry/views/dashboards/utils/prebuiltConfigs';

import {checkUserHasEditAccess} from './utils/checkUserHasEditAccess';
import ReleasesSelectControl from './releasesSelectControl';
Expand All @@ -44,9 +49,9 @@ type FiltersBarProps = {
onDashboardFilterChange: (activeFilters: DashboardFilters) => void;
dashboardCreator?: User;
dashboardPermissions?: DashboardPermissions;
isPrebuiltDashboard?: boolean;
onCancel?: () => void;
onSave?: () => Promise<void>;
prebuiltDashboardId?: PrebuiltDashboardId;
shouldBusySaveButton?: boolean;
};

Expand All @@ -63,14 +68,18 @@ export default function FiltersBar({
onDashboardFilterChange,
onSave,
shouldBusySaveButton,
isPrebuiltDashboard,
prebuiltDashboardId,
}: FiltersBarProps) {
const {selection} = usePageFilters();
const organization = useOrganization();
const currentUser = useUser();
const {teams: userTeams} = useUserTeams();
const getSearchBarData = useDatasetSearchBarData();
const hasDrillDownFlowsFeature = useHasDrillDownFlows();
const isPrebuiltDashboard = defined(prebuiltDashboardId);
const prebuiltDashboardFilters: GlobalFilter[] = prebuiltDashboardId
? (PREBUILT_DASHBOARDS[prebuiltDashboardId].filters.globalFilter ?? [])
: [];
Comment on lines +73 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing validation for prebuiltDashboardId as a key in PREBUILT_DASHBOARDS can lead to runtime crash when accessing .filters.
Severity: CRITICAL | Confidence: High

🔍 Detailed Analysis

The code at static/app/views/dashboards/filtersBar.tsx:73-75 accesses PREBUILT_DASHBOARDS[prebuiltDashboardId] without validating if prebuiltDashboardId is a valid key within PREBUILT_DASHBOARDS. This can lead to PREBUILT_DASHBOARDS[prebuiltDashboardId] evaluating to undefined if prebuiltDashboardId exists but is not a recognized key. Subsequently, attempting to access .filters on undefined will cause a runtime crash. This can occur due to incomplete deployments, API mismatches, or server/client version discrepancies.

💡 Suggested Fix

Before accessing PREBUILT_DASHBOARDS[prebuiltDashboardId], add a check to ensure prebuiltDashboardId in PREBUILT_DASHBOARDS to prevent accessing properties on undefined.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: static/app/views/dashboards/filtersBar.tsx#L73-L75

Potential issue: The code at `static/app/views/dashboards/filtersBar.tsx:73-75` accesses
`PREBUILT_DASHBOARDS[prebuiltDashboardId]` without validating if `prebuiltDashboardId`
is a valid key within `PREBUILT_DASHBOARDS`. This can lead to
`PREBUILT_DASHBOARDS[prebuiltDashboardId]` evaluating to `undefined` if
`prebuiltDashboardId` exists but is not a recognized key. Subsequently, attempting to
access `.filters` on `undefined` will cause a runtime crash. This can occur due to
incomplete deployments, API mismatches, or server/client version discrepancies.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 6235892

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typing wouldn't allow this


const hasEditAccess = checkUserHasEditAccess(
currentUser,
Expand Down Expand Up @@ -106,9 +115,12 @@ export default function FiltersBar({

const updateGlobalFilters = (newGlobalFilters: GlobalFilter[]) => {
setActiveGlobalFilters(newGlobalFilters);
const temporaryFilters = newGlobalFilters.filter(filter => filter.isTemporary);
const globalFilters = newGlobalFilters.filter(filter => !filter.isTemporary);
onDashboardFilterChange({
[DashboardFilterKeys.RELEASE]: selectedReleases,
[DashboardFilterKeys.GLOBAL_FILTER]: newGlobalFilters,
[DashboardFilterKeys.GLOBAL_FILTER]: globalFilters,
[DashboardFilterKeys.TEMPORARY_FILTERS]: temporaryFilters,
});
};

Expand Down Expand Up @@ -163,6 +175,14 @@ export default function FiltersBar({
<Fragment>
{activeGlobalFilters.map(filter => (
<GenericFilterSelector
disableRemoveFilter={
isPrebuiltDashboard &&
prebuiltDashboardFilters.some(
prebuiltFilter =>
prebuiltFilter.tag.key === filter.tag.key &&
prebuiltFilter.dataset === filter.dataset
)
}
key={filter.tag.key + filter.value}
globalFilter={filter}
searchBarData={getSearchBarData(filter.dataset)}
Expand Down
18 changes: 11 additions & 7 deletions static/app/views/dashboards/globalFilter/filterSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ type FilterSelectorProps = {
onRemoveFilter: (filter: GlobalFilter) => void;
onUpdateFilter: (filter: GlobalFilter) => void;
searchBarData: SearchBarData;
disableRemoveFilter?: boolean;
};

function FilterSelector({
globalFilter,
searchBarData,
onRemoveFilter,
onUpdateFilter,
disableRemoveFilter,
}: FilterSelectorProps) {
const {selection} = usePageFilters();

Expand Down Expand Up @@ -275,13 +277,15 @@ function FilterSelector({
{t('Clear')}
</StyledButton>
)}
<StyledButton
aria-label={t('Remove Filter')}
size="zero"
onClick={() => onRemoveFilter(globalFilter)}
>
{t('Remove Filter')}
</StyledButton>
{!disableRemoveFilter && (
<StyledButton
aria-label={t('Remove Filter')}
size="zero"
onClick={() => onRemoveFilter(globalFilter)}
>
{t('Remove Filter')}
</StyledButton>
)}
</Flex>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type GenericFilterSelectorProps = {
onRemoveFilter: (filter: GlobalFilter) => void;
onUpdateFilter: (filter: GlobalFilter) => void;
searchBarData: SearchBarData;
disableRemoveFilter?: boolean;
};

function getFilterSelector(
Expand Down
1 change: 1 addition & 0 deletions static/app/views/dashboards/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export type GlobalFilter = {
tag: Tag;
// The raw filter condition string (e.g. 'tagKey:[values,...]')
value: string;
isTemporary?: boolean;
};

/**
Expand Down
6 changes: 4 additions & 2 deletions static/app/views/dashboards/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ export function getDashboardFiltersFromURL(location: Location): DashboardFilters
dashboardFilters[key] = queryFilters
.map(filter => {
try {
return JSON.parse(filter);
return {...JSON.parse(filter), isTemporary: true};
} catch (error) {
return null;
}
Expand Down Expand Up @@ -633,16 +633,18 @@ export function getCombinedDashboardFilters(
): GlobalFilter[] {
const finalFilters = [...(globalFilters ?? [])];
const temporaryFiltersCopy = [...(temporaryFilters ?? [])];

finalFilters.forEach((filter, idx) => {
// if a temporary filter exists for the same dataset and key, override it and delete it from the temporary filters to avoid duplicates
const temporaryFilter = temporaryFiltersCopy.find(
tf => tf.dataset === filter.dataset && tf.tag.key === filter.tag.key
);
if (temporaryFilter) {
finalFilters[idx] = {...filter, value: temporaryFilter.value};
finalFilters[idx] = {...temporaryFilter};
temporaryFiltersCopy.splice(temporaryFiltersCopy.indexOf(temporaryFilter), 1);
}
});

return [...finalFilters, ...temporaryFiltersCopy];
}

Expand Down
Loading