Skip to content

fix(dashboard-import): dashboard import fails with keyerror when referencing deleted charts in expanded_slices metadata#37042

Closed
ramiroaquinoromero wants to merge 2 commits into
apache:masterfrom
ramiroaquinoromero:ramiro.aquino.romero/sc-93629/dashboard-import-fails-with-keyerror-when
Closed

fix(dashboard-import): dashboard import fails with keyerror when referencing deleted charts in expanded_slices metadata#37042
ramiroaquinoromero wants to merge 2 commits into
apache:masterfrom
ramiroaquinoromero:ramiro.aquino.romero/sc-93629/dashboard-import-fails-with-keyerror-when

Conversation

@ramiroaquinoromero
Copy link
Copy Markdown
Contributor

SUMMARY

This PR fixes a critical bug where dashboard imports fail with a KeyError when the exported dashboard contains references to deleted charts in its metadata.

Problem:
When a chart is deleted from Superset, the dashboard metadata (specifically expanded_slices, timed_refresh_immune_slices, and other metadata fields) retains references to the deleted chart IDs. When these dashboards are exported and then imported into another workspace, the import process crashes with a KeyError and displays a generic error message: "Import dashboard failed for an unknown reason."

Root Cause:
The update_id_refs() function in superset/commands/dashboard/importers/v1/utils.py attempted to map all chart IDs from the metadata without checking if the charts exist in the import, causing a KeyError when encountering deleted chart references.

Solution:

  1. Modified update_id_refs() to gracefully skip missing chart references in expanded_slices and timed_refresh_immune_slices metadata fields, consistent with how other metadata fields (filter_scopes, default_filters, native_filter_configuration) already handle missing charts.

  2. Added _cleanup_dashboard_metadata() method to DeleteChartCommand that automatically cleans up all dashboard metadata when a chart is deleted, preventing orphaned references from being created in the first place.

Changes:

  • Modified superset/commands/dashboard/importers/v1/utils.py:

    • Added if int(old_id) in id_map check to expanded_slices processing (line 105)
    • Added if old_id in id_map check to timed_refresh_immune_slices processing (line 80)
  • Modified superset/commands/chart/delete.py:

    • Added _cleanup_dashboard_metadata() method to remove chart references from dashboard metadata on deletion
    • Cleans up: expanded_slices, timed_refresh_immune_slices, filter_scopes, default_filters, native_filter_configuration, chart_configuration, and global_chart_configuration
  • Added comprehensive unit tests in tests/unit_tests/dashboards/commands/importers/v1/utils_test.py:

    • test_update_id_refs_expanded_slices_with_missing_chart()
    • test_update_id_refs_timed_refresh_immune_slices_with_missing_chart()
    • test_update_id_refs_multiple_missing_chart_references()

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

BEFORE:

AFTER:

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@codeant-ai-for-open-source
Copy link
Copy Markdown
Contributor

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Jan 12, 2026

Code Review Agent Run #0be174

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/commands/chart/delete.py - 1
    • Reduce method complexity and statement count · Line 77-77
      The `_cleanup_dashboard_metadata()` method has too many branches (20 > 12) and statements (53 > 50). Consider extracting cleanup logic into separate helper methods.
      Code suggestion
       @@ -77,6 +83,50 @@
            def _cleanup_dashboard_metadata(  # noqa: C901
                self, chart_id: int
            ) -> None:
      +        """Remove references to this chart from all dashboard metadata.
      +
      +        When a chart is deleted, dashboards may still contain references to the
      +        chart ID in various metadata fields (expanded_slices, filter_scopes, etc.).
      +        This method cleans up those references to prevent issues during dashboard
      +        export/import.
      +        """
      +        dashboards = (
      +            db.session.query(Dashboard)
      +            .filter(Dashboard.slices.any(id=chart_id))
      +            .all()
      +        )
      +
      +        for dashboard in dashboards:
      +            metadata = dashboard.params_dict
      +            modified = False
      +            modified |= self._cleanup_expanded_slices(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_timed_refresh_immune_slices(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_filter_scopes(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_default_filters(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_native_filter_configuration(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_chart_configuration(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_global_chart_configuration(metadata, chart_id, dashboard.id)
      +            if modified:
      +                dashboard.json_metadata = json.dumps(metadata)
      +                logger.info(
      +                    "Cleaned up metadata for dashboard %s after deleting chart %s",
      +                    dashboard.id,
      +                    chart_id,
      +                )
      +
      +    def _cleanup_expanded_slices(
      +        self, metadata: dict, chart_id: int, dashboard_id: int
      +    ) -> bool:
                """
      -        Remove references to this chart from all dashboard metadata.
      +        Remove chart from expanded_slices metadata.
       
      -        When a chart is deleted, dashboards may still contain references to the
      -        chart ID in various metadata fields (expanded_slices, filter_scopes, etc.).
      -        This method cleans up those references to prevent issues during dashboard
      -        export/import.
      +        Returns True if metadata was modified.
                """
      -        # Find all dashboards that contain this chart
      -        dashboards = (
      -            db.session.query(Dashboard)
      -            .filter(Dashboard.slices.any(id=chart_id))  # type: ignore[attr-defined]
      -            .all()
      -        )
      -
      -        for dashboard in dashboards:
      -            metadata = dashboard.params_dict
      -            modified = False
      -
      -            # Clean up expanded_slices
                    if "expanded_slices" in metadata:
                        chart_id_str = str(chart_id)
                        if chart_id_str in metadata["expanded_slices"]:
                            del metadata["expanded_slices"][chart_id_str]
      -                    modified = True
                            logger.info(
                                "Removed chart %s from expanded_slices in dashboard %s",
                                chart_id,
      -                        dashboard.id,
      +                        dashboard_id,
                            )
      +                    return True
      +        return False
Review Details
  • Files reviewed - 3 · Commit Range: efa5b45..efa5b45
    • superset/commands/chart/delete.py
    • superset/commands/dashboard/importers/v1/utils.py
    • tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@dosubot dosubot Bot added the dashboard:import Related to importing dashboards label Jan 12, 2026
@codeant-ai-for-open-source
Copy link
Copy Markdown
Contributor

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Type mismatch / incomplete removal
    The cleanup assumes dashboard metadata uses specific types (ints for lists,
    strings for dict keys, JSON strings for some fields). In practice the same
    ID may be stored as a string or int (or fields may already be dicts vs JSON
    strings). This can lead to orphan references not being removed (e.g.,
    'timed_refresh_immune_slices' containing string IDs, 'default_filters'
    already being a dict, or native filter 'excluded' lists containing strings).
    Consider normalizing types and guarding JSON parsing to avoid leaving stale
    references.

  • Robustness of JSON parsing
    The code calls json.loads on metadata['default_filters'] without guarding
    against already-parsed dicts or invalid JSON. This may raise exceptions
    and roll back the transaction during chart deletion. Add safer parsing
    (check types and handle exceptions) to avoid failing the delete flow.

  • Silent data loss
    The updated comprehensions drop entries that don't map to a known chart (e.g., when a referenced chart was deleted). This is intentional to avoid KeyError, but it may silently remove important user metadata (expanded slices, default filters, filter scopes). Review whether telemetry/logging or a warning should be emitted so users/admins are aware that metadata was pruned during import.

  • Type mismatch risk
    Membership checks against id_map are inconsistent: some checks use if old_id in id_map (assumes metadata values are ints) while others use if int(old_id) in id_map (assumes metadata keys are strings). If metadata sometimes stores IDs as strings and sometimes as ints, the current checks may silently skip valid references or raise ValueError when casting. Normalize or defensively handle both types to avoid missed mappings or exceptions.

  • Performance (N+1 queries)
    The current implementation queries dashboards per chart in a loop:
    for each chart it executes a query to find dashboards that contain it.
    Deleting many charts will yield N+1 queries and may be slow. It's better
    to fetch all affected dashboards once (for all chart IDs) and process
    them in-memory to remove references for all charts in a single pass.

Comment thread superset/commands/chart/delete.py
Comment thread superset/commands/chart/delete.py
Comment thread superset/commands/chart/delete.py
Comment thread superset/commands/chart/delete.py
Comment thread superset/commands/dashboard/importers/v1/utils.py
Comment thread tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
Comment thread tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
@codeant-ai-for-open-source
Copy link
Copy Markdown
Contributor

CodeAnt AI finished reviewing your PR.

@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 12, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 7ba0378
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6978454fcd9f800008eacce4
😎 Deploy Preview https://deploy-preview-37042--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@msyavuz msyavuz requested a review from EnxDev January 12, 2026 09:56
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 12, 2026

Codecov Report

❌ Patch coverage is 12.85714% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.52%. Comparing base (bd419d1) to head (7ba0378).
⚠️ Report is 44 commits behind head on master.

Files with missing lines Patch % Lines
superset/commands/chart/delete.py 12.85% 60 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #37042       +/-   ##
===========================================
+ Coverage        0   66.52%   +66.52%     
===========================================
  Files           0      643      +643     
  Lines           0    49107    +49107     
  Branches        0     5520     +5520     
===========================================
+ Hits            0    32669    +32669     
- Misses          0    15142    +15142     
- Partials        0     1296     +1296     
Flag Coverage Δ
hive 41.87% <7.14%> (?)
mysql 64.58% <12.85%> (?)
postgres 64.66% <12.85%> (?)
presto 41.89% <7.14%> (?)
python 66.49% <12.85%> (?)
sqlite 64.36% <12.85%> (?)
unit 100.00% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@EnxDev EnxDev added the 🎪 ⚡ showtime-trigger-start Create new ephemeral environment for this PR label Jan 12, 2026
@github-actions github-actions Bot added 🎪 efa5b45 🚦 building 🎪 ⌛ 48h Environment expires after 48 hours (default) and removed 🎪 ⚡ showtime-trigger-start Create new ephemeral environment for this PR labels Jan 12, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🎪 Showtime is building environment on GHA for efa5b45

@github-actions
Copy link
Copy Markdown
Contributor

🎪 Showtime deployed environment on GHA for efa5b45

Environment: http://35.167.230.94:8080 (admin/admin)
Lifetime: 48h auto-cleanup
Updates: New commits create fresh environments automatically

@ramiroaquinoromero ramiroaquinoromero force-pushed the ramiro.aquino.romero/sc-93629/dashboard-import-fails-with-keyerror-when branch from efa5b45 to 4aaf14b Compare January 16, 2026 05:49
@codeant-ai-for-open-source
Copy link
Copy Markdown
Contributor

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai-for-open-source
Copy link
Copy Markdown
Contributor

CodeAnt AI Incremental review completed.

@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Jan 16, 2026

Code Review Agent Run #381a5e

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/commands/chart/delete.py - 1
    • Reduce method complexity and statement count · Line 77-77
      The `_cleanup_dashboard_metadata()` method has too many branches (20 > 12) and statements (53 > 50). Consider extracting cleanup logic into separate helper methods.
      Code suggestion
       @@ -77,6 +83,50 @@
            def _cleanup_dashboard_metadata(  # noqa: C901
                self, chart_id: int
            ) -> None:
      +        """Remove references to this chart from all dashboard metadata.
      +
      +        When a chart is deleted, dashboards may still contain references to the
      +        chart ID in various metadata fields (expanded_slices, filter_scopes, etc.).
      +        This method cleans up those references to prevent issues during dashboard
      +        export/import.
      +        """
      +        dashboards = (
      +            db.session.query(Dashboard)
      +            .filter(Dashboard.slices.any(id=chart_id))
      +            .all()
      +        )
      +
      +        for dashboard in dashboards:
      +            metadata = dashboard.params_dict
      +            modified = False
      +            modified |= self._cleanup_expanded_slices(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_timed_refresh_immune_slices(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_filter_scopes(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_default_filters(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_native_filter_configuration(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_chart_configuration(metadata, chart_id, dashboard.id)
      +            modified |= self._cleanup_global_chart_configuration(metadata, chart_id, dashboard.id)
      +            if modified:
      +                dashboard.json_metadata = json.dumps(metadata)
      +                logger.info(
      +                    "Cleaned up metadata for dashboard %s after deleting chart %s",
      +                    dashboard.id,
      +                    chart_id,
      +                )
      +
      +    def _cleanup_expanded_slices(
      +        self, metadata: dict, chart_id: int, dashboard_id: int
      +    ) -> bool:
                """
      -        Remove references to this chart from all dashboard metadata.
      +        Remove chart from expanded_slices metadata.
       
      -        When a chart is deleted, dashboards may still contain references to the
      -        chart ID in various metadata fields (expanded_slices, filter_scopes, etc.).
      -        This method cleans up those references to prevent issues during dashboard
      -        export/import.
      +        Returns True if metadata was modified.
                """
      -        # Find all dashboards that contain this chart
      -        dashboards = (
      -            db.session.query(Dashboard)
      -            .filter(Dashboard.slices.any(id=chart_id))  # type: ignore[attr-defined]
      -            .all()
      -        )
      -
      -        for dashboard in dashboards:
      -            metadata = dashboard.params_dict
      -            modified = False
      -
      -            # Clean up expanded_slices
                    if "expanded_slices" in metadata:
                        chart_id_str = str(chart_id)
                        if chart_id_str in metadata["expanded_slices"]:
                            del metadata["expanded_slices"][chart_id_str]
      -                    modified = True
                            logger.info(
                                "Removed chart %s from expanded_slices in dashboard %s",
                                chart_id,
      -                        dashboard.id,
      +                        dashboard_id,
                            )
      +                    return True
      +        return False
Review Details
  • Files reviewed - 3 · Commit Range: 4aaf14b..4aaf14b
    • superset/commands/chart/delete.py
    • superset/commands/dashboard/importers/v1/utils.py
    • tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@EnxDev
Copy link
Copy Markdown
Contributor

EnxDev commented Jan 16, 2026

@ramiroaquinoromero would it be possible to add some before-and-after photos or videos?

@EnxDev EnxDev added the 🎪 ⚡ showtime-trigger-start Create new ephemeral environment for this PR label Jan 16, 2026
@ramiroaquinoromero
Copy link
Copy Markdown
Contributor Author

@EnxDev Good morning. Sure. I will add a video with this behavior. Thank you.

Copy link
Copy Markdown
Contributor

@reynoldmorel reynoldmorel left a comment

Choose a reason for hiding this comment

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

Nice test coverage!!!!

I have some concerns, that would be good to address.

Also probably the commit msg and title of the PR are not following the conventions.

Something like:

fix(api): import fails with keyerror when referencing deleted charts in expanded_slices metadata

might work.

Comment thread superset/commands/chart/delete.py Outdated
Comment on lines +89 to +93
dashboards = (
db.session.query(Dashboard)
.filter(Dashboard.slices.any(id=chart_id)) # type: ignore[attr-defined]
.all()
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Personal concern, since I don't have context]

How many records are we expecting to load here? I think we should investigate that, since it could break the server, also not sure if we should be safe by adding a limit.

Make sure we don't have a status column that marks records as deleted, because that could affect the number of records returned here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good morning @reynoldmorel, you're right. I've optimized this:
Performance: Changed from loading full Dashboard objects to just (id, json_metadata) tuples. Added a safety limit of 1000 dashboards with logging if exceeded.
Soft-delete: Confirmed there's no soft-delete column on Dashboard - only published boolean, so the query won't return deleted records.
The limit of 1000 is somewhat arbitrary - happy to adjust based on what makes sense for your production usage. The error log will make it clear if we ever hit that edge case.

Comment thread superset/commands/chart/delete.py Outdated
Comment on lines +100 to +109
if "expanded_slices" in metadata:
chart_id_str = str(chart_id)
if chart_id_str in metadata["expanded_slices"]:
del metadata["expanded_slices"][chart_id_str]
modified = True
logger.info(
"Removed chart %s from expanded_slices in dashboard %s",
chart_id,
dashboard.id,
)
Copy link
Copy Markdown
Contributor

@reynoldmorel reynoldmorel Jan 17, 2026

Choose a reason for hiding this comment

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

Looks like this pattern is repeated a lot, can't we just put it behind a function and do something like:

(modified, updated_expanded_slices_metadata) = delete_metadata_by_chart("expanded_slices", metadata, chart_id) or modified

(modified, updated_timed_refresh_immune_slices_metadata) = delete_metadata_by_chart("timed_refresh_immune_slices", updated_expanded_slices_metadata, chart_id) or modified

For different patterns you can write a condition to check whether the property needs a unique pattern or not to get its value where chart_id might be located

Also I don't think you should be updating metadata and dashboard properties by object reference, this could lead to unpredictable behavior if this function keeps growing and could become harder to maintain.

I would return a new updated metadata reference in general from delete_metadata_by_chart, you could use copy.deepcopy or just build a new reference of metadata once, before start calling delete_metadata_by_chart and then you could modify the props directly to the reference instead of touching the existing one

Comment on lines +51 to 54
# Clean up dashboard metadata before deleting charts
for chart in self._models:
self._cleanup_dashboard_metadata(chart.id)
ChartDAO.delete(self._models)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Personal concern, since I don't have context]

It doesn't feel really safe to do this since the I don't know how many models we could get here and also _cleanup_dashboard_metadata could be time consuming. I would suggest to investigate if this won't cause any performance issues, and if not, I would write a comment denoting why this solution works and what scenario will cover, so if at some point in the future it fails, devs would know what to consider to fix it.

Comment on lines +252 to +273
config = {
"position": {
"CHART1": {
"id": "CHART1",
"meta": {"chartId": 101, "uuid": "uuid1"},
"type": "CHART",
},
"CHART2": {
"id": "CHART2",
"meta": {"chartId": 102, "uuid": "uuid2"},
"type": "CHART",
},
},
"metadata": {
"timed_refresh_immune_slices": [
101, # This chart exists
102, # This chart exists
103, # This chart was deleted and doesn't exist
104, # Another deleted chart
],
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could add a general function that builds a new config for you, so you can modify as you wish for each test

chart_id,
dashboard.id,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see a lot of repeating logic for the if statements I think a util function will clean this up quite a bit

Comment on lines +180 to +182
scope_excluded = native_filter.get("scope", {}).get("excluded", [])
if chart_id in scope_excluded:
scope_excluded.remove(chart_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: The code compares and removes chart_id as an int from lists that may store IDs as strings (and vice versa), so removals can silently fail (logic bug). Normalize comparisons by checking and removing both the integer and string representations to ensure the chart ID is removed regardless of stored type. [logic error]

Severity Level: Critical 🚨
- ❌ Dashboard export/importers/v1 may fail on orphaned references.
- ❌ Native filter exclusion lists remain stale.
- ⚠️ Chart deletion metadata cleanup incomplete.
Suggested change
scope_excluded = native_filter.get("scope", {}).get("excluded", [])
if chart_id in scope_excluded:
scope_excluded.remove(chart_id)
scope = native_filter.get("scope", {})
scope_excluded = scope.get("excluded", [])
removed = False
# remove both int and string representations if present
if chart_id in scope_excluded:
scope_excluded.remove(chart_id)
removed = True
chart_id_str = str(chart_id)
if chart_id_str in scope_excluded:
scope_excluded.remove(chart_id_str)
removed = True
if removed:
Steps of Reproduction ✅
1. Create a dashboard whose json_metadata["native_filter_configuration"] contains an entry
where scope.excluded is a list of string IDs (e.g., ["123"]) rather than ints. The cleanup
loop is at superset/commands/chart/delete.py:177-183 in
DeleteChartCommand._cleanup_dashboard_metadata (function start at
superset/commands/chart/delete.py:78).

2. Invoke DeleteChartCommand.run with the chart id 123 present on that dashboard
(superset/commands/chart/delete.py:49). The run method calls _cleanup_dashboard_metadata
at superset/commands/chart/delete.py:52-54.

3. The current code checks `if chart_id in scope_excluded` at
superset/commands/chart/delete.py:180. Because scope_excluded contains string "123" and
chart_id is the integer 123, the test is False; no removal occurs and modified remains
False for that dashboard.

4. The string-ID exclusion remains in the dashboard metadata and is not cleaned. This
leaves an orphaned reference that can later trigger import-time KeyError in the importer
(see PR description for superset/commands/dashboard/importers/v1/utils.py). The behavior
is concrete and reproducible using string-stored IDs.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/commands/chart/delete.py
**Line:** 180:182
**Comment:**
	*Logic Error: The code compares and removes `chart_id` as an int from lists that may store IDs as strings (and vice versa), so removals can silently fail (logic bug). Normalize comparisons by checking and removing both the integer and string representations to ensure the chart ID is removed regardless of stored type.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@EnxDev
Copy link
Copy Markdown
Contributor

EnxDev commented Jan 22, 2026

@ramiroaquinoromero would it be possible to add some before-and-after photos or videos?

@ramiroaquinoromero If you have some time, would you be able to take care of this?

@ramiroaquinoromero
Copy link
Copy Markdown
Contributor Author

@ramiroaquinoromero would it be possible to add some before-and-after photos or videos?

@ramiroaquinoromero If you have some time, would you be able to take care of this?

Yup I am pushing some changes I will add some videos about this one.

@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Jan 22, 2026

Code Review Agent Run #0fe9a7

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: 4aaf14b..8097901
    • superset/commands/chart/delete.py
    • superset/commands/dashboard/importers/v1/utils.py
    • tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@ramiroaquinoromero ramiroaquinoromero force-pushed the ramiro.aquino.romero/sc-93629/dashboard-import-fails-with-keyerror-when branch from 2b988e1 to 7ba0378 Compare January 27, 2026 04:55
@ramiroaquinoromero
Copy link
Copy Markdown
Contributor Author

I cant reproduce this behavior again It was fixed on this PR #36551 . I am closing this ticket.

Copy link
Copy Markdown
Contributor

@bito-code-review bito-code-review Bot left a comment

Choose a reason for hiding this comment

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

Code Review Agent Run #eacd8a

Actionable Suggestions - 1
  • superset/commands/chart/delete.py - 1
Review Details
  • Files reviewed - 3 · Commit Range: 1a098fb..7ba0378
    • superset/commands/chart/delete.py
    • superset/commands/dashboard/importers/v1/utils.py
    • tests/unit_tests/dashboards/commands/importers/v1/utils_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +168 to +175
# Clean up default_filters
if "default_filters" in metadata:
default_filters = json.loads(metadata["default_filters"])
chart_id_str = str(chart_id)
if chart_id_str in default_filters:
del default_filters[chart_id_str]
metadata["default_filters"] = json.dumps(default_filters)
modified = True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Error Handling Gap

The json.loads call for metadata["default_filters"] lacks error handling, which could raise an exception if the value is malformed. It looks like this should be wrapped in a try-except block similar to the main metadata parsing, to log a warning and continue instead of failing the entire chart deletion.

Code suggestion
Check the AI-generated fix before applying
Suggested change
# Clean up default_filters
if "default_filters" in metadata:
default_filters = json.loads(metadata["default_filters"])
chart_id_str = str(chart_id)
if chart_id_str in default_filters:
del default_filters[chart_id_str]
metadata["default_filters"] = json.dumps(default_filters)
modified = True
# Clean up default_filters
if "default_filters" in metadata:
try:
default_filters = json.loads(metadata["default_filters"])
chart_id_str = str(chart_id)
if chart_id_str in default_filters:
del default_filters[chart_id_str]
metadata["default_filters"] = json.dumps(default_filters)
modified = True
except (json.JSONDecodeError, TypeError):
logger.warning("Could not parse default_filters for dashboard %s", dashboard_id)

Code Review Run #eacd8a


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dashboard:import Related to importing dashboards 🎪 🔒 showtime-blocked 🎪 ⚡ showtime-trigger-start Create new ephemeral environment for this PR size/L 🎪 ⌛ 48h Environment expires after 48 hours (default)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants