Native Filter Scope Management#
Overview#
Native filter scope determines which dashboard charts a filter applies to. Each native filter carries a scope object with two fields:
rootPath— array of layout component IDs (tabs, the dashboard root) that are in scopeexcluded— array of chart IDs explicitly excluded from the filter
The scope object is stored inside the filter's entry in native_filter_configuration within the dashboard's json_metadata column . At runtime, it is hydrated into the Redux nativeFilters.filters map, keyed by filter ID .
Key Files#
| File | Role |
|---|---|
useFilterScope.ts | React hook that converts a filter's scope into human-readable tab/chart labels for the filter card UI |
getRelatedCharts.ts | Resolves which chart IDs a filter actually targets, handling native, cross-filter, and global-scope cases |
getFormDataWithExtraFilters.ts | Merges active filter data into chart formData; checks scope.includes(chart.id) before applying |
activeDashboardFilters.js | Legacy utility; builds the activeFilters map with { values, scope } per filter key |
nativeFilters.ts (reducer) | Maintains NativeFiltersState.filters in Redux; rebuilt on SET_NATIVE_FILTERS_CONFIG_COMPLETE and HYDRATE_DASHBOARD |
How Scope Is Resolved#
UI display (useFilterScope)#
useFilterScope reads filter.scope.rootPath and filter.scope.excluded from the Redux filter object and derives one of four display states:
- Empty rootPath → filter is scoped to nothing, returns
undefined - No exclusions + root/all tabs in path →
{ all: ['All charts'] } - No exclusions + partial tabs →
{ tabs: [...] } - With exclusions →
{ tabs: [...], charts: [...] }listing individually scoped items
Filter application (getRelatedCharts)#
getRelatedCharts normalizes scope before applying: it checks Array.isArray(filter.scope) first (applied filter form), then falls back to filter.chartsInScope (native filter definition form) . isGlobalScope returns true when scope length equals total slice count, short-circuiting the per-chart set lookup.
getFormDataWithExtraFilters then gates filter injection with scope.includes(chart.id) — if scope is undefined, this line throws.
Crash Patterns and Fixes#
1. Missing scope on grain filters — PR #41746#
Symptom: TypeError: Cannot read properties of undefined (reading 'scope') when applying a grain native filter to a dashboard containing Time Series Table charts.
Root cause: Some filter entries existed in Redux without an initialized scope array. The scope.includes(chart.id) check in getFormDataWithExtraFilters and activeDashboardFilters dereferenced undefined.
Fix: Added optional chaining (activeFilter?.scope?.includes(chart.id)) at the point of scope access so an undefined scope is treated as non-matching rather than a crash.
2. Native filter map wiped on chart customization save — PR #42032#
Symptom: After saving display controls, the next filter interaction crashed with Cannot read properties of undefined (reading 'scope').
Root cause: handleFilterChangesComplete rebuilt the filter map from scratch using only the current save's payload. Saving a chart customization (display control) sent a customization-only payload, wiping every native filter from state.filters. Filters still rendered (driven by dashboard metadata) but Redux had no entry for them, so filter.scope was undefined on the next access.
Fix: Changed the reducer to merge incoming filters into the existing map instead of replacing it, with an explicit deletedIds field for intentional removals. Added a guard in getRelatedCharts to return [] on an undefined filter , so transient missing entries never crash the dashboard.
3. Dynamic Group By not applying to scoped charts — PRs #39356 / #39416#
Symptom: Selecting a column in a Dynamic Group By display control either had no effect or applied incorrectly to scoped charts.
Root cause (PR #39356): Selected display-control values were injected as row filters (status IN ("status")) rather than as chart dimensions (groupby). Legacy chart customization metadata was also not normalized before scope calculation, causing targeted charts to be skipped.
Fix (PR #39356): Applied Dynamic Group By selections to formData as dimensions. Normalized legacy single-select customization values before scope resolution.
Root cause (PR #39416): The fix in #39356 removed existing groupby columns from buildExistingColumnsSet(), causing Dynamic Group By customizations to replace the chart's original groupby instead of merging with it. Separately, null entries in chart_customization_config caused TypeErrors at 8 consumer sites.
Fix (PR #39416): Restored existingGroupBy in buildExistingColumnsSet using a new extractColumnNames() helper that handles both string and AdhocColumn formats. Added a customization != null type guard and .filter(Boolean) at all 8 consumer sites.
Design Notes#
- Scope is always resolved to a flat list of chart IDs before reaching filter-application code.
rootPath/excludedare layout-level constructs resolved to chart IDs bygetChartIdsInFilterScope(legacy) orchartsInScope(native) beforegetFormDataWithExtraFiltersruns. - Legacy and native scope coexist.
activeDashboardFilters.jshandles legacy dashboard filters;activeAllDashboardFilters.tshandles native + cross-filter scopes. Both converge ongetFormDataWithExtraFilters. - Redux state integrity is the most common crash source. Any action that replaces
nativeFilters.filterswholesale — rather than merging — risks producingundefinedfilter entries that crash downstreamscopedereferences. - Display Control Filter, Grain, and Dynamic Group By all mutate
formDataat query time via the same scope resolution path, making scope correctness a shared prerequisite across all three features.