Repository: getsentry/sentry
Pull request #89859: feat(auto_source): Support correcting code mappings
Author: login: armenzg
Labels: name: Scope: Backend
description: Automatically applied to PRs that change backend components
name: Trigger: Revert
description: Add to a merged PR to revert it (skips CI)
Created: 2025-04-17T14:18:57Z | Merged: 2025-04-17T17:53:56Z

Support correcting automatically generated code mappings because the code base has changed, or the automation created it incorrectly.

Manually defined code mappings do not get modified.

It requires an option to enable saving it.


Diff:
diff --git a/src/sentry/issues/auto_source_code_config/code_mapping.py b/src/sentry/issues/auto_source_code_config/code_mapping.py
index b1c71ca9acbbd2..d9ee95cb425aad 100644
--- a/src/sentry/issues/auto_source_code_config/code_mapping.py
+++ b/src/sentry/issues/auto_source_code_config/code_mapping.py
@@ -293,7 +293,18 @@ def _find_code_mapping(self, frame_filename: FrameInfo) -> CodeMapping | None:
             logger.warning("More than one repo matched %s", frame_filename.raw_path)
             return None
 
-        return code_mappings[0]
+        cm = code_mappings[0]
+        logger.info(
+            "Found code mapping.",
+            extra={
+                "stack_path": frame_filename.raw_path,
+                "stack_root": cm.stacktrace_root,
+                "source_path": cm.source_path,
+                "repo_name": cm.repo.name,
+                "repo_branch": cm.repo.branch,
+            },
+        )
+        return cm
 
     def _generate_code_mapping_from_tree(
         self,
diff --git a/src/sentry/issues/auto_source_code_config/task.py b/src/sentry/issues/auto_source_code_config/task.py
index ba29be00d865be..3447cd4b3f2b9d 100644
--- a/src/sentry/issues/auto_source_code_config/task.py
+++ b/src/sentry/issues/auto_source_code_config/task.py
@@ -1,13 +1,12 @@
 from __future__ import annotations
 
 import logging
-from collections.abc import Mapping
 from enum import StrEnum
 from typing import Any
 
 from sentry_sdk import set_tag, set_user
 
-from sentry import eventstore
+from sentry import eventstore, options
 from sentry.integrations.base import IntegrationInstallation
 from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig
 from sentry.integrations.services.integration.model import RpcOrganizationIntegration
@@ -187,11 +186,11 @@ def create_configurations(
 
     dry_run = platform_config.is_dry_run_platform(project.organization)
     platform = platform_config.platform
-    tags: Mapping[str, str | bool] = {"platform": platform, "dry_run": dry_run}
+    tags: dict[str, str | bool] = {"platform": platform, "dry_run": dry_run}
     with metrics.timer(f"{METRIC_PREFIX}.create_configurations.duration", tags=tags):
         for code_mapping in code_mappings:
             repository = create_repository(code_mapping.repo.name, org_integration, tags)
-            create_code_mapping(code_mapping, repository, project, org_integration, tags)
+            create_or_update_code_mapping(code_mapping, repository, project, org_integration, tags)
 
     in_app_stack_trace_rules: list[str] = []
     if platform_config.creates_in_app_stack_trace_rules():
@@ -204,27 +203,79 @@ def create_configurations(
     return code_mappings, in_app_stack_trace_rules
 
 
-def create_code_mapping(
+def create_or_update_code_mapping(
     code_mapping: CodeMapping,
     repository: Repository | None,
     project: Project,
     org_integration: RpcOrganizationIntegration,
-    tags: Mapping[str, str | bool],
+    tags: dict[str, str | bool],
 ) -> None:
     created = False
     if not tags["dry_run"] and repository is not None:
-        _, created = RepositoryProjectPathConfig.objects.get_or_create(
-            project=project,
-            stack_root=code_mapping.stacktrace_root,
-            defaults={
-                "repository": repository,
-                "organization_integration_id": org_integration.id,
-                "integration_id": org_integration.integration_id,
-                "organization_id": org_integration.organization_id,
-                "source_root": code_mapping.source_path,
-                "default_branch": code_mapping.repo.branch,
-                "automatically_generated": True,
-            },
-        )
+        try:
+            code_mapping_config = RepositoryProjectPathConfig.objects.get(
+                project=project, stack_root=code_mapping.stacktrace_root
+            )
+            if code_mapping_config.automatically_generated:
+                update_code_mapping_if_needed(code_mapping_config, code_mapping, tags)
+
+        except RepositoryProjectPathConfig.DoesNotExist:
+            _, created = RepositoryProjectPathConfig.objects.update_or_create(
+                project=project,
+                stack_root=code_mapping.stacktrace_root,
+                # The values in defaults are the ones not needed for uniqueness
+                defaults={
+                    "repository": repository,
+                    "organization_integration_id": org_integration.id,
+                    "integration_id": org_integration.integration_id,
+                    "organization_id": org_integration.organization_id,
+                    "default_branch": code_mapping.repo.branch,
+                    "source_root": code_mapping.source_path,
+                    "automatically_generated": True,
+                },
+            )
+
     if created or tags["dry_run"]:
         metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.created", tags=tags, sample_rate=1.0)
+
+
+def update_code_mapping_if_needed(
+    code_mapping_config: RepositoryProjectPathConfig,
+    code_mapping: CodeMapping,
+    tags: dict[str, str | bool],
+) -> None:
+    updated = False
+    update_reasons: list[str] = []
+    # The code has been moved to a new location or the automation
+    # may have created the code mapping with the wrong source path
+    if code_mapping_config.source_root != code_mapping.source_path:
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_source_root": code_mapping_config.source_root,
+                "new_source_root": code_mapping.source_path,
+            },
+        )
+        code_mapping_config.source_root = code_mapping.source_path
+
+        update_reasons.append("source_root_changed")
+        updated = True
+
+    if code_mapping_config.default_branch != code_mapping.repo.branch:
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_default_branch": code_mapping_config.default_branch,
+                "new_default_branch": code_mapping.repo.branch,
+            },
+        )
+        code_mapping_config.default_branch = code_mapping.repo.branch
+        update_reasons.append("default_branch_changed")
+        updated = True
+
+    if updated:
+        if options.get("issues.auto_source_code_config.update_code_mapping_if_needed"):
+            code_mapping_config.save()
+        for reason in update_reasons:
+            tags["reason"] = reason
+            metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.updated", tags=tags, sample_rate=1.0)
diff --git a/src/sentry/options/defaults.py b/src/sentry/options/defaults.py
index 2cc47a0f5a6a54..c23d3bb6152df5 100644
--- a/src/sentry/options/defaults.py
+++ b/src/sentry/options/defaults.py
@@ -2453,6 +2453,12 @@
 
 # END: SDK Crash Detection
 
+register(
+    "issues.auto_source_code_config.update_code_mapping_if_needed",
+    default=False,
+    flags=FLAG_AUTOMATOR_MODIFIABLE,
+)
+
 register(
     # Lists the shared resource ids we want to account usage for.
     "shared_resources_accounting_enabled",
diff --git a/tests/sentry/issues/auto_source_code_config/test_process_event.py b/tests/sentry/issues/auto_source_code_config/test_process_event.py
index 75b58309c7809d..d1cdab6bbe6ab7 100644
--- a/tests/sentry/issues/auto_source_code_config/test_process_event.py
+++ b/tests/sentry/issues/auto_source_code_config/test_process_event.py
@@ -18,6 +18,7 @@
 from sentry.shared_integrations.exceptions import ApiError
 from sentry.testutils.asserts import assert_failure_metric, assert_halt_metric
 from sentry.testutils.cases import TestCase
+from sentry.testutils.helpers import override_options
 from sentry.testutils.silo import assume_test_silo_mode_of
 from sentry.testutils.skips import requires_snuba
 from sentry.utils.locking import UnableToAcquireLock
@@ -82,7 +83,12 @@ def create_event(self, frames: Sequence[Mapping[str, str | bool]], platform: str
         return cast(GroupEvent, self.store_event(data=test_data, project_id=self.project.id))
 
     def create_repo_and_code_mapping(
-        self, repo_name: str, stack_root: str, source_root: str
+        self,
+        repo_name: str,
+        stack_root: str,
+        source_root: str,
+        automatically_generated: bool = False,
+        default_branch: str = "master",
     ) -> None:
         with assume_test_silo_mode_of(OrganizationIntegration):
             organization_integration = OrganizationIntegration.objects.get(
@@ -98,10 +104,12 @@ def create_repo_and_code_mapping(
             project_id=self.project.id,
             stack_root=stack_root,
             source_root=source_root,
+            default_branch=default_branch,
             repository=repository,
             organization_integration_id=organization_integration.id,
             integration_id=organization_integration.integration_id,
             organization_id=organization_integration.organization_id,
+            automatically_generated=automatically_generated,
         )
 
     def _process_and_assert_configuration_changes(
@@ -112,6 +120,8 @@ def _process_and_assert_configuration_changes(
         platform: str,
         expected_new_code_mappings: Sequence[ExpectedCodeMapping] | None = None,
         expected_new_in_app_stack_trace_rules: list[str] | None = None,
+        updated_code_mapping: ExpectedCodeMapping | None = None,
+        updated_reasons: list[str] | None = None,
     ) -> GroupEvent:
         platform_config = PlatformConfig(platform)
         dry_run = platform_config.is_dry_run_platform(self.organization)
@@ -194,6 +204,19 @@ def _process_and_assert_configuration_changes(
                 else:
                     assert current_enhancements == starting_enhancements
 
+            if updated_code_mapping:
+                assert current_code_mappings.filter(
+                    stack_root=updated_code_mapping["stack_root"],
+                    source_root=updated_code_mapping["source_root"],
+                ).exists()
+                assert updated_reasons is not None
+                for reason in updated_reasons:
+                    mock_incr.assert_any_call(
+                        key=f"{METRIC_PREFIX}.code_mapping.updated",
+                        tags={**tags, "reason": reason},
+                        sample_rate=1.0,
+                    )
+
             if (current_repositories.count() > starting_repositories_count) or dry_run:
                 mock_incr.assert_any_call(
                     key=f"{METRIC_PREFIX}.repository.created", tags=tags, sample_rate=1.0
@@ -218,7 +241,6 @@ def frame_from_module(
         self,
         module: str,
         abs_path: str,
-        category: str | None = None,
         in_app: bool = False,
     ) -> dict[str, str | bool | Any]:
         frame: dict[str, str | bool | Any] = {}
@@ -228,8 +250,6 @@ def frame_from_module(
             frame["abs_path"] = abs_path
         if in_app and in_app is not None:
             frame["in_app"] = in_app
-        if category:
-            frame["data"] = {"category": category}
         return frame
 
     def code_mapping(
@@ -885,3 +905,31 @@ def test_categorized_frames_are_not_processed(self) -> None:
                 expected_new_code_mappings=[self.code_mapping("android/app/", "src/android/app/")],
                 expected_new_in_app_stack_trace_rules=["stack.module:android.app.** +app"],
             )
+
+    @override_options({"issues.auto_source_code_config.update_code_mapping_if_needed": True})
+    def test_automatic_bad_code_mapping_is_updated(self) -> None:
+        # The automation may have created a code mapping with the wrong source path
+        # or the code has been moved to a new location and need to correct it
+        self.create_repo_and_code_mapping(
+            REPO1, "foo/bar/", "bad/source/path", automatically_generated=True
+        )
+        # We expect the code mapping to be updated
+        self._process_and_assert_configuration_changes(
+            repo_trees={REPO1: ["src/foo/bar/Baz.java"]},
+            frames=[self.frame_from_module("foo.bar.Baz", "Baz.java")],
+            platform=self.platform,
+            updated_code_mapping=self.code_mapping("foo/bar/", "src/foo/bar/"),
+            updated_reasons=["source_root_changed"],
+            expected_new_in_app_stack_trace_rules=["stack.module:foo.bar.** +app"],
+        )
+
+    def test_manual_bad_code_mapping_is_not_updated(self) -> None:
+        # The developer creates a bad code mapping
+        self.create_repo_and_code_mapping(REPO1, "foo/bar/", "bad/source/path")
+        # We do not expect code mappings since it matches the existing code mapping
+        self._process_and_assert_configuration_changes(
+            repo_trees={REPO1: ["src/foo/bar/Baz.java"]},
+            frames=[self.frame_from_module("foo.bar.Baz", "Baz.java")],
+            platform=self.platform,
+            expected_new_in_app_stack_trace_rules=["stack.module:foo.bar.** +app"],
+        )


Reviews:
user: login: armenzg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T14:25:59Z
user: login: yuvmen
body: Looks good, some minor comments 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-17T15:49:50Z
user: login: armenzg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T17:53:09Z

Review comments:
diff_hunk: @@ -293,7 +293,18 @@ def _find_code_mapping(self, frame_filename: FrameInfo) -> CodeMapping | None:
             logger.warning("More than one repo matched %s", frame_filename.raw_path)
             return None
 
-        return code_mappings[0]
+        cm = code_mappings[0]
+        logger.info(
+            "Found code mapping.",
path: src/sentry/issues/auto_source_code_config/code_mapping.py
user: login: armenzg
body: This can be used for finding how a code mapping was generated for a customer.
created_at: 2025-04-17T14:21:49Z
author_association: MEMBER
diff_hunk: @@ -218,7 +241,6 @@ def frame_from_module(
         self,
         module: str,
         abs_path: str,
-        category: str | None = None,
path: tests/sentry/issues/auto_source_code_config/test_process_event.py
user: login: armenzg
body: Drive-by change. We were not using this.
created_at: 2025-04-17T14:23:17Z
author_association: MEMBER
diff_hunk: @@ -204,27 +203,78 @@ def create_configurations(
     return code_mappings, in_app_stack_trace_rules
 
 
-def create_code_mapping(
+def create_or_update_code_mapping(
     code_mapping: CodeMapping,
     repository: Repository | None,
     project: Project,
     org_integration: RpcOrganizationIntegration,
-    tags: Mapping[str, str | bool],
+    tags: dict[str, str | bool],
 ) -> None:
     created = False
     if not tags["dry_run"] and repository is not None:
-        _, created = RepositoryProjectPathConfig.objects.get_or_create(
-            project=project,
-            stack_root=code_mapping.stacktrace_root,
-            defaults={
-                "repository": repository,
-                "organization_integration_id": org_integration.id,
-                "integration_id": org_integration.integration_id,
-                "organization_id": org_integration.organization_id,
-                "source_root": code_mapping.source_path,
-                "default_branch": code_mapping.repo.branch,
-                "automatically_generated": True,
-            },
-        )
+        try:
+            code_mapping_config = RepositoryProjectPathConfig.objects.get(
+                project=project, stack_root=code_mapping.stacktrace_root
+            )
+            if code_mapping_config.automatically_generated:
+                update_code_mapping_if_needed(code_mapping_config, code_mapping, tags)
+
+        except RepositoryProjectPathConfig.DoesNotExist:
+            _, created = RepositoryProjectPathConfig.objects.update_or_create(
+                project=project,
+                stack_root=code_mapping.stacktrace_root,
+                # The values in defaults are the ones not needed for uniqueness
+                defaults={
+                    "repository": repository,
+                    "organization_integration_id": org_integration.id,
+                    "integration_id": org_integration.integration_id,
+                    "organization_id": org_integration.organization_id,
+                    "default_branch": code_mapping.repo.branch,
+                    "source_root": code_mapping.source_path,
+                    "automatically_generated": True,
+                },
+            )
+
     if created or tags["dry_run"]:
         metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.created", tags=tags, sample_rate=1.0)
+
+
+def update_code_mapping_if_needed(
+    code_mapping_config: RepositoryProjectPathConfig,
+    code_mapping: CodeMapping,
+    tags: dict[str, str | bool],
+) -> None:
+    updated = False
+    update_reasons: list[str] = []
+    # The code has been moved to a new location or the automation
+    # may have created the code mapping with the wrong source path
+    if code_mapping_config.source_root != code_mapping.source_path:
+        code_mapping_config.source_root = code_mapping.source_path
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_source_root": code_mapping_config.source_root,
path: src/sentry/issues/auto_source_code_config/task.py
user: login: yuvmen
body: Wont this already have the new value, and you will basically be printing the same value in both old and new?
created_at: 2025-04-17T15:39:13Z
author_association: MEMBER
diff_hunk: @@ -204,27 +203,78 @@ def create_configurations(
     return code_mappings, in_app_stack_trace_rules
 
 
-def create_code_mapping(
+def create_or_update_code_mapping(
     code_mapping: CodeMapping,
     repository: Repository | None,
     project: Project,
     org_integration: RpcOrganizationIntegration,
-    tags: Mapping[str, str | bool],
+    tags: dict[str, str | bool],
 ) -> None:
     created = False
     if not tags["dry_run"] and repository is not None:
-        _, created = RepositoryProjectPathConfig.objects.get_or_create(
-            project=project,
-            stack_root=code_mapping.stacktrace_root,
-            defaults={
-                "repository": repository,
-                "organization_integration_id": org_integration.id,
-                "integration_id": org_integration.integration_id,
-                "organization_id": org_integration.organization_id,
-                "source_root": code_mapping.source_path,
-                "default_branch": code_mapping.repo.branch,
-                "automatically_generated": True,
-            },
-        )
+        try:
+            code_mapping_config = RepositoryProjectPathConfig.objects.get(
+                project=project, stack_root=code_mapping.stacktrace_root
+            )
+            if code_mapping_config.automatically_generated:
+                update_code_mapping_if_needed(code_mapping_config, code_mapping, tags)
+
+        except RepositoryProjectPathConfig.DoesNotExist:
+            _, created = RepositoryProjectPathConfig.objects.update_or_create(
+                project=project,
+                stack_root=code_mapping.stacktrace_root,
+                # The values in defaults are the ones not needed for uniqueness
+                defaults={
+                    "repository": repository,
+                    "organization_integration_id": org_integration.id,
+                    "integration_id": org_integration.integration_id,
+                    "organization_id": org_integration.organization_id,
+                    "default_branch": code_mapping.repo.branch,
+                    "source_root": code_mapping.source_path,
+                    "automatically_generated": True,
+                },
+            )
+
     if created or tags["dry_run"]:
         metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.created", tags=tags, sample_rate=1.0)
+
+
+def update_code_mapping_if_needed(
+    code_mapping_config: RepositoryProjectPathConfig,
+    code_mapping: CodeMapping,
+    tags: dict[str, str | bool],
+) -> None:
+    updated = False
+    update_reasons: list[str] = []
+    # The code has been moved to a new location or the automation
+    # may have created the code mapping with the wrong source path
+    if code_mapping_config.source_root != code_mapping.source_path:
+        code_mapping_config.source_root = code_mapping.source_path
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_source_root": code_mapping_config.source_root,
+                "new_source_root": code_mapping.source_path,
+            },
+        )
+        update_reasons.append("source_root_changed")
+        updated = True
+
+    if code_mapping_config.default_branch != code_mapping.repo.branch:
+        code_mapping_config.default_branch = code_mapping.repo.branch
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_default_branch": code_mapping_config.default_branch,
path: src/sentry/issues/auto_source_code_config/task.py
user: login: yuvmen
body: same problem?
created_at: 2025-04-17T15:40:14Z
author_association: MEMBER
diff_hunk: @@ -204,27 +203,78 @@ def create_configurations(
     return code_mappings, in_app_stack_trace_rules
 
 
-def create_code_mapping(
+def create_or_update_code_mapping(
     code_mapping: CodeMapping,
     repository: Repository | None,
     project: Project,
     org_integration: RpcOrganizationIntegration,
-    tags: Mapping[str, str | bool],
+    tags: dict[str, str | bool],
 ) -> None:
     created = False
     if not tags["dry_run"] and repository is not None:
-        _, created = RepositoryProjectPathConfig.objects.get_or_create(
-            project=project,
-            stack_root=code_mapping.stacktrace_root,
-            defaults={
-                "repository": repository,
-                "organization_integration_id": org_integration.id,
-                "integration_id": org_integration.integration_id,
-                "organization_id": org_integration.organization_id,
-                "source_root": code_mapping.source_path,
-                "default_branch": code_mapping.repo.branch,
-                "automatically_generated": True,
-            },
-        )
+        try:
+            code_mapping_config = RepositoryProjectPathConfig.objects.get(
+                project=project, stack_root=code_mapping.stacktrace_root
+            )
+            if code_mapping_config.automatically_generated:
+                update_code_mapping_if_needed(code_mapping_config, code_mapping, tags)
+
+        except RepositoryProjectPathConfig.DoesNotExist:
+            _, created = RepositoryProjectPathConfig.objects.update_or_create(
+                project=project,
+                stack_root=code_mapping.stacktrace_root,
+                # The values in defaults are the ones not needed for uniqueness
+                defaults={
+                    "repository": repository,
+                    "organization_integration_id": org_integration.id,
+                    "integration_id": org_integration.integration_id,
+                    "organization_id": org_integration.organization_id,
+                    "default_branch": code_mapping.repo.branch,
+                    "source_root": code_mapping.source_path,
+                    "automatically_generated": True,
+                },
+            )
+
     if created or tags["dry_run"]:
         metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.created", tags=tags, sample_rate=1.0)
+
+
+def update_code_mapping_if_needed(
+    code_mapping_config: RepositoryProjectPathConfig,
+    code_mapping: CodeMapping,
+    tags: dict[str, str | bool],
+) -> None:
+    updated = False
+    update_reasons: list[str] = []
+    # The code has been moved to a new location or the automation
+    # may have created the code mapping with the wrong source path
+    if code_mapping_config.source_root != code_mapping.source_path:
+        code_mapping_config.source_root = code_mapping.source_path
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_source_root": code_mapping_config.source_root,
+                "new_source_root": code_mapping.source_path,
+            },
+        )
+        update_reasons.append("source_root_changed")
+        updated = True
+
+    if code_mapping_config.default_branch != code_mapping.repo.branch:
+        code_mapping_config.default_branch = code_mapping.repo.branch
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_default_branch": code_mapping_config.default_branch,
+                "new_default_branch": code_mapping.repo.branch,
+            },
+        )
+        update_reasons.append("default_branch_changed")
+        updated = True
+
+    if updated or tags["dry_run"]:
path: src/sentry/issues/auto_source_code_config/task.py
user: login: yuvmen
body: just making sure, if its a "dry_run" it is still ok to actually call `save()`?
created_at: 2025-04-17T15:42:03Z
author_association: MEMBER
diff_hunk: @@ -885,3 +905,31 @@ def test_categorized_frames_are_not_processed(self) -> None:
                 expected_new_code_mappings=[self.code_mapping("android/app/", "src/android/app/")],
                 expected_new_in_app_stack_trace_rules=["stack.module:android.app.** +app"],
             )
+
+    @override_options({"issues.auto_source_code_config.update_code_mapping_if_needed": True})
+    def test_automatic_bad_code_mapping_is_updated(self) -> None:
+        # The automation may have created a code mapping with the wrong source path
+        # or the code has been moved to a new location and need to correct it
+        self.create_repo_and_code_mapping(
+            REPO1, "foo/bar/", "bad/source/path", automatically_generated=True
+        )
+        # We expect the code mapping to be updated
+        self._process_and_assert_configuration_changes(
+            repo_trees={REPO1: ["src/foo/bar/Baz.java"]},
+            frames=[self.frame_from_module("foo.bar.Baz", "Baz.java")],
+            platform=self.platform,
+            updated_code_mapping=self.code_mapping("foo/bar/", "src/foo/bar/"),
+            updated_reasons=["source_root_changed"],
+            expected_new_in_app_stack_trace_rules=["stack.module:foo.bar.** +app"],
+        )
+
+    def test_manual_bad_code_mapping_is_not_updated(self) -> None:
+        # The developer creates a bad code mapping
+        self.create_repo_and_code_mapping(REPO1, "foo/bar/", "bad/source/path")
+        # We do not expect code mappings sincee it matches the existing code mapping
path: tests/sentry/issues/auto_source_code_config/test_process_event.py
user: login: yuvmen
body: minor typo - sincee
created_at: 2025-04-17T15:48:22Z
author_association: MEMBER
diff_hunk: @@ -204,27 +203,78 @@ def create_configurations(
     return code_mappings, in_app_stack_trace_rules
 
 
-def create_code_mapping(
+def create_or_update_code_mapping(
     code_mapping: CodeMapping,
     repository: Repository | None,
     project: Project,
     org_integration: RpcOrganizationIntegration,
-    tags: Mapping[str, str | bool],
+    tags: dict[str, str | bool],
 ) -> None:
     created = False
     if not tags["dry_run"] and repository is not None:
-        _, created = RepositoryProjectPathConfig.objects.get_or_create(
-            project=project,
-            stack_root=code_mapping.stacktrace_root,
-            defaults={
-                "repository": repository,
-                "organization_integration_id": org_integration.id,
-                "integration_id": org_integration.integration_id,
-                "organization_id": org_integration.organization_id,
-                "source_root": code_mapping.source_path,
-                "default_branch": code_mapping.repo.branch,
-                "automatically_generated": True,
-            },
-        )
+        try:
+            code_mapping_config = RepositoryProjectPathConfig.objects.get(
+                project=project, stack_root=code_mapping.stacktrace_root
+            )
+            if code_mapping_config.automatically_generated:
+                update_code_mapping_if_needed(code_mapping_config, code_mapping, tags)
+
+        except RepositoryProjectPathConfig.DoesNotExist:
+            _, created = RepositoryProjectPathConfig.objects.update_or_create(
+                project=project,
+                stack_root=code_mapping.stacktrace_root,
+                # The values in defaults are the ones not needed for uniqueness
+                defaults={
+                    "repository": repository,
+                    "organization_integration_id": org_integration.id,
+                    "integration_id": org_integration.integration_id,
+                    "organization_id": org_integration.organization_id,
+                    "default_branch": code_mapping.repo.branch,
+                    "source_root": code_mapping.source_path,
+                    "automatically_generated": True,
+                },
+            )
+
     if created or tags["dry_run"]:
         metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.created", tags=tags, sample_rate=1.0)
+
+
+def update_code_mapping_if_needed(
+    code_mapping_config: RepositoryProjectPathConfig,
+    code_mapping: CodeMapping,
+    tags: dict[str, str | bool],
+) -> None:
+    updated = False
+    update_reasons: list[str] = []
+    # The code has been moved to a new location or the automation
+    # may have created the code mapping with the wrong source path
+    if code_mapping_config.source_root != code_mapping.source_path:
+        code_mapping_config.source_root = code_mapping.source_path
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_source_root": code_mapping_config.source_root,
path: src/sentry/issues/auto_source_code_config/task.py
user: login: armenzg
body: Good catch!
created_at: 2025-04-17T16:07:16Z
author_association: MEMBER
diff_hunk: @@ -204,27 +203,78 @@ def create_configurations(
     return code_mappings, in_app_stack_trace_rules
 
 
-def create_code_mapping(
+def create_or_update_code_mapping(
     code_mapping: CodeMapping,
     repository: Repository | None,
     project: Project,
     org_integration: RpcOrganizationIntegration,
-    tags: Mapping[str, str | bool],
+    tags: dict[str, str | bool],
 ) -> None:
     created = False
     if not tags["dry_run"] and repository is not None:
-        _, created = RepositoryProjectPathConfig.objects.get_or_create(
-            project=project,
-            stack_root=code_mapping.stacktrace_root,
-            defaults={
-                "repository": repository,
-                "organization_integration_id": org_integration.id,
-                "integration_id": org_integration.integration_id,
-                "organization_id": org_integration.organization_id,
-                "source_root": code_mapping.source_path,
-                "default_branch": code_mapping.repo.branch,
-                "automatically_generated": True,
-            },
-        )
+        try:
+            code_mapping_config = RepositoryProjectPathConfig.objects.get(
+                project=project, stack_root=code_mapping.stacktrace_root
+            )
+            if code_mapping_config.automatically_generated:
+                update_code_mapping_if_needed(code_mapping_config, code_mapping, tags)
+
+        except RepositoryProjectPathConfig.DoesNotExist:
+            _, created = RepositoryProjectPathConfig.objects.update_or_create(
+                project=project,
+                stack_root=code_mapping.stacktrace_root,
+                # The values in defaults are the ones not needed for uniqueness
+                defaults={
+                    "repository": repository,
+                    "organization_integration_id": org_integration.id,
+                    "integration_id": org_integration.integration_id,
+                    "organization_id": org_integration.organization_id,
+                    "default_branch": code_mapping.repo.branch,
+                    "source_root": code_mapping.source_path,
+                    "automatically_generated": True,
+                },
+            )
+
     if created or tags["dry_run"]:
         metrics.incr(key=f"{METRIC_PREFIX}.code_mapping.created", tags=tags, sample_rate=1.0)
+
+
+def update_code_mapping_if_needed(
+    code_mapping_config: RepositoryProjectPathConfig,
+    code_mapping: CodeMapping,
+    tags: dict[str, str | bool],
+) -> None:
+    updated = False
+    update_reasons: list[str] = []
+    # The code has been moved to a new location or the automation
+    # may have created the code mapping with the wrong source path
+    if code_mapping_config.source_root != code_mapping.source_path:
+        code_mapping_config.source_root = code_mapping.source_path
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_source_root": code_mapping_config.source_root,
+                "new_source_root": code_mapping.source_path,
+            },
+        )
+        update_reasons.append("source_root_changed")
+        updated = True
+
+    if code_mapping_config.default_branch != code_mapping.repo.branch:
+        code_mapping_config.default_branch = code_mapping.repo.branch
+        logger.info(
+            "Updating code mapping.",
+            extra={
+                "old_default_branch": code_mapping_config.default_branch,
+                "new_default_branch": code_mapping.repo.branch,
+            },
+        )
+        update_reasons.append("default_branch_changed")
+        updated = True
+
+    if updated or tags["dry_run"]:
path: src/sentry/issues/auto_source_code_config/task.py
user: login: armenzg
body: That makes sense in the other functions where we return what would have been created.
In this case, it doesn't. I will remove it.
created_at: 2025-04-17T16:08:21Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-17T14:54:12Z
body: ## [Codecov](https://app.codecov.io/gh/getsentry/sentry/pull/89859?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
Attention: Patch coverage is `91.30435%` with `4 lines` in your changes missing coverage. Please review.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/getsentry/sentry/pull/89859?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Patch % | Lines |
|---|---|---|
| [src/sentry/issues/auto\_source\_code\_config/task.py](https://app.codecov.io/gh/getsentry/sentry/pull/89859?src=pr&el=tree&filepath=src%2Fsentry%2Fissues%2Fauto_source_code_config%2Ftask.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-c3JjL3NlbnRyeS9pc3N1ZXMvYXV0b19zb3VyY2VfY29kZV9jb25maWcvdGFzay5weQ==) | 86.20% | [4 Missing :warning: ](https://app.codecov.io/gh/getsentry/sentry/pull/89859?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           master   #89859    +/-   ##
========================================
  Coverage   87.71%   87.71%            
========================================
  Files       10206    10204     -2     
  Lines      575530   575654   +124     
  Branches    22663    22663            
========================================
+ Hits       504801   504935   +134     
+ Misses      70295    70285    -10     
  Partials      434      434            
```

</details>
author_association: NONE
user: login: getsentry-bot
created_at: 2025-04-25T12:37:52Z
body: PR reverted: 8f87d22b46b47eac58f89eace66db6050d346f46
author_association: CONTRIBUTOR
user: login: armenzg
created_at: 2025-04-25T12:43:38Z
body: I reverted this since we won't need it after all.
author_association: MEMBER