Browse Source

ref(backup): Parse model names into NormalizedModelName (#56669)

Previously, the stringified versions of model names used at various
points in the import export process were haphazard and all over the
place: a `UserEmail` model could variously be stringified to
`UserEmail`, `useremail`, `sentry.UserEmail`, or `sentry.useremail`,
depending on when and where the stringification was performed. This was
error prone, as later on, asking the question "what model am I holding"
was difficult and edge-casey. Basically, this change takes the advice in
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
and... well, parses instead of validating.

Some refactors included here:
- Renamed some functions
- Converted `ComparatingFinding` and `InstanceID` from NamedTuples into
dataclasses for inheritance and future RPC-serialization purposes, and
made a separate `Finding` base class for the former to inherit from.
- Use less `from __future__ import annotations`, and more `from typing
...`, as this will play nice with Pydantic when we try to send some of
these models over the wire.

Issue: getsentry/team-ospo#196
Alex Zaslavsky 1 year ago
parent
commit
1b68429d47

+ 1 - 1
bin/generate-model-dependency-fixtures

@@ -28,7 +28,7 @@ def main():
 
     # Do all of the calculation before any file writes, so that we don't end up with partial
     # overwrites.
-    detailed = dependencies()
+    detailed = {str(k): v for k, v in dependencies().items()}
     flat = {k: v.flatten() for k, v in detailed.items()}
     sorted = sorted_dependencies()
 

File diff suppressed because it is too large
+ 458 - 458
fixtures/backup/model_dependencies/detailed.json


+ 761 - 761
fixtures/backup/model_dependencies/flat.json

@@ -1,794 +1,794 @@
 {
-  "feedback.Feedback": [],
-  "nodestore.Node": [],
-  "replays.ReplayRecordingSegment": [],
-  "sentry.Activity": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.Actor": [
-    "sentry.User"
+  "feedback.feedback": [],
+  "nodestore.node": [],
+  "replays.replayrecordingsegment": [],
+  "sentry.activity": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.actor": [
+    "sentry.user"
   ],
-  "sentry.AlertRule": [
-    "sentry.Actor",
-    "sentry.Organization",
-    "sentry.SnubaQuery"
+  "sentry.alertrule": [
+    "sentry.actor",
+    "sentry.organization",
+    "sentry.snubaquery"
   ],
-  "sentry.AlertRuleActivity": [
-    "sentry.AlertRule",
-    "sentry.User"
-  ],
-  "sentry.AlertRuleExcludedProjects": [
-    "sentry.AlertRule",
-    "sentry.Project"
-  ],
-  "sentry.AlertRuleTrigger": [
-    "sentry.AlertRule"
+  "sentry.alertruleactivity": [
+    "sentry.alertrule",
+    "sentry.user"
+  ],
+  "sentry.alertruleexcludedprojects": [
+    "sentry.alertrule",
+    "sentry.project"
+  ],
+  "sentry.alertruletrigger": [
+    "sentry.alertrule"
   ],
-  "sentry.AlertRuleTriggerAction": [
-    "sentry.AlertRuleTrigger",
-    "sentry.Integration",
-    "sentry.SentryApp"
-  ],
-  "sentry.AlertRuleTriggerExclusion": [
-    "sentry.AlertRuleTrigger",
-    "sentry.QuerySubscription"
-  ],
-  "sentry.ApiApplication": [
-    "sentry.User"
+  "sentry.alertruletriggeraction": [
+    "sentry.alertruletrigger",
+    "sentry.integration",
+    "sentry.sentryapp"
+  ],
+  "sentry.alertruletriggerexclusion": [
+    "sentry.alertruletrigger",
+    "sentry.querysubscription"
+  ],
+  "sentry.apiapplication": [
+    "sentry.user"
   ],
-  "sentry.ApiAuthorization": [
-    "sentry.ApiApplication",
-    "sentry.User"
+  "sentry.apiauthorization": [
+    "sentry.apiapplication",
+    "sentry.user"
   ],
-  "sentry.ApiGrant": [
-    "sentry.ApiApplication",
-    "sentry.User"
+  "sentry.apigrant": [
+    "sentry.apiapplication",
+    "sentry.user"
   ],
-  "sentry.ApiKey": [
-    "sentry.Organization"
+  "sentry.apikey": [
+    "sentry.organization"
   ],
-  "sentry.ApiToken": [
-    "sentry.ApiApplication",
-    "sentry.User"
+  "sentry.apitoken": [
+    "sentry.apiapplication",
+    "sentry.user"
   ],
-  "sentry.AppConnectBuild": [
-    "sentry.Project"
+  "sentry.appconnectbuild": [
+    "sentry.project"
   ],
-  "sentry.ArtifactBundle": [
-    "sentry.File",
-    "sentry.Organization"
+  "sentry.artifactbundle": [
+    "sentry.file",
+    "sentry.organization"
   ],
-  "sentry.ArtifactBundleFlatFileIndex": [
-    "sentry.Project"
+  "sentry.artifactbundleflatfileindex": [
+    "sentry.project"
   ],
-  "sentry.ArtifactBundleIndex": [
-    "sentry.ArtifactBundle",
-    "sentry.Organization"
+  "sentry.artifactbundleindex": [
+    "sentry.artifactbundle",
+    "sentry.organization"
   ],
-  "sentry.AssistantActivity": [
-    "sentry.User"
+  "sentry.assistantactivity": [
+    "sentry.user"
   ],
-  "sentry.AuditLogEntry": [
-    "sentry.ApiKey",
-    "sentry.Organization",
-    "sentry.User"
+  "sentry.auditlogentry": [
+    "sentry.apikey",
+    "sentry.organization",
+    "sentry.user"
   ],
-  "sentry.AuthIdentity": [
-    "sentry.AuthProvider",
-    "sentry.User"
+  "sentry.authenticator": [
+    "sentry.user"
   ],
-  "sentry.AuthIdentityReplica": [
-    "sentry.AuthIdentity",
-    "sentry.AuthProvider",
-    "sentry.User"
+  "sentry.authidentity": [
+    "sentry.authprovider",
+    "sentry.user"
   ],
-  "sentry.AuthProvider": [
-    "sentry.Organization"
-  ],
-  "sentry.AuthProviderDefaultTeams": [
-    "sentry.AuthProvider",
-    "sentry.Team"
-  ],
-  "sentry.AuthProviderReplica": [
-    "sentry.AuthProvider",
-    "sentry.Organization"
-  ],
-  "sentry.Authenticator": [
-    "sentry.User"
+  "sentry.authidentityreplica": [
+    "sentry.authidentity",
+    "sentry.authprovider",
+    "sentry.user"
+  ],
+  "sentry.authprovider": [
+    "sentry.organization"
   ],
-  "sentry.Broadcast": [],
-  "sentry.BroadcastSeen": [
-    "sentry.Broadcast",
-    "sentry.User"
+  "sentry.authproviderdefaultteams": [
+    "sentry.authprovider",
+    "sentry.team"
   ],
-  "sentry.Commit": [
-    "sentry.CommitAuthor",
-    "sentry.Organization",
-    "sentry.Repository"
+  "sentry.authproviderreplica": [
+    "sentry.authprovider",
+    "sentry.organization"
   ],
-  "sentry.CommitAuthor": [
-    "sentry.Organization"
+  "sentry.broadcast": [],
+  "sentry.broadcastseen": [
+    "sentry.broadcast",
+    "sentry.user"
   ],
-  "sentry.CommitFileChange": [
-    "sentry.Commit",
-    "sentry.Organization"
+  "sentry.commit": [
+    "sentry.commitauthor",
+    "sentry.organization",
+    "sentry.repository"
   ],
-  "sentry.ControlFile": [],
-  "sentry.ControlFileBlob": [],
-  "sentry.ControlFileBlobIndex": [
-    "sentry.ControlFile",
-    "sentry.ControlFileBlob"
+  "sentry.commitauthor": [
+    "sentry.organization"
   ],
-  "sentry.ControlFileBlobOwner": [
-    "sentry.ControlFileBlob",
-    "sentry.Organization"
+  "sentry.commitfilechange": [
+    "sentry.commit",
+    "sentry.organization"
   ],
-  "sentry.ControlOption": [],
-  "sentry.ControlOutbox": [],
-  "sentry.ControlTombstone": [],
-  "sentry.Counter": [
-    "sentry.Project"
+  "sentry.controlfile": [],
+  "sentry.controlfileblob": [],
+  "sentry.controlfileblobindex": [
+    "sentry.controlfile",
+    "sentry.controlfileblob"
   ],
-  "sentry.CustomDynamicSamplingRule": [
-    "sentry.Organization"
+  "sentry.controlfileblobowner": [
+    "sentry.controlfileblob",
+    "sentry.organization"
   ],
-  "sentry.CustomDynamicSamplingRuleProject": [
-    "sentry.CustomDynamicSamplingRule",
-    "sentry.Project"
+  "sentry.controloption": [],
+  "sentry.controloutbox": [],
+  "sentry.controltombstone": [],
+  "sentry.counter": [
+    "sentry.project"
   ],
-  "sentry.Dashboard": [
-    "sentry.Organization",
-    "sentry.User"
+  "sentry.customdynamicsamplingrule": [
+    "sentry.organization"
   ],
-  "sentry.DashboardProject": [
-    "sentry.Dashboard",
-    "sentry.Project"
+  "sentry.customdynamicsamplingruleproject": [
+    "sentry.customdynamicsamplingrule",
+    "sentry.project"
   ],
-  "sentry.DashboardTombstone": [
-    "sentry.Organization"
+  "sentry.dashboard": [
+    "sentry.organization",
+    "sentry.user"
   ],
-  "sentry.DashboardWidget": [
-    "sentry.Dashboard"
+  "sentry.dashboardproject": [
+    "sentry.dashboard",
+    "sentry.project"
   ],
-  "sentry.DashboardWidgetQuery": [
-    "sentry.DashboardWidget"
-  ],
-  "sentry.DebugIdArtifactBundle": [
-    "sentry.ArtifactBundle",
-    "sentry.Organization"
-  ],
-  "sentry.DeletedOrganization": [],
-  "sentry.DeletedProject": [
-    "sentry.Organization"
-  ],
-  "sentry.DeletedTeam": [
-    "sentry.Organization"
-  ],
-  "sentry.Deploy": [
-    "sentry.Environment",
-    "sentry.Organization",
-    "sentry.Release"
-  ],
-  "sentry.DiscoverSavedQuery": [
-    "sentry.Organization",
-    "sentry.User"
-  ],
-  "sentry.DiscoverSavedQueryProject": [
-    "sentry.DiscoverSavedQuery",
-    "sentry.Project"
-  ],
-  "sentry.Distribution": [
-    "sentry.Organization",
-    "sentry.Release"
-  ],
-  "sentry.DocIntegration": [],
-  "sentry.DocIntegrationAvatar": [
-    "sentry.ControlFile",
-    "sentry.DocIntegration",
-    "sentry.File"
-  ],
-  "sentry.Email": [],
-  "sentry.Environment": [
-    "sentry.Organization"
-  ],
-  "sentry.EnvironmentProject": [
-    "sentry.Environment",
-    "sentry.Project"
-  ],
-  "sentry.EventAttachment": [
-    "sentry.File",
-    "sentry.Group",
-    "sentry.Project"
-  ],
-  "sentry.EventProcessingIssue": [
-    "sentry.ProcessingIssue",
-    "sentry.RawEvent"
-  ],
-  "sentry.EventUser": [
-    "sentry.Project"
-  ],
-  "sentry.ExportedData": [
-    "sentry.File",
-    "sentry.Organization",
-    "sentry.User"
-  ],
-  "sentry.ExportedDataBlob": [
-    "sentry.ExportedData"
-  ],
-  "sentry.ExternalActor": [
-    "sentry.Integration",
-    "sentry.Organization",
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.ExternalIssue": [
-    "sentry.Integration",
-    "sentry.Organization"
-  ],
-  "sentry.FeatureAdoption": [
-    "sentry.Organization"
-  ],
-  "sentry.File": [
-    "sentry.FileBlob"
-  ],
-  "sentry.FileBlob": [],
-  "sentry.FileBlobIndex": [
-    "sentry.File",
-    "sentry.FileBlob"
-  ],
-  "sentry.FileBlobOwner": [
-    "sentry.FileBlob",
-    "sentry.Organization"
-  ],
-  "sentry.FlatFileIndexState": [
-    "sentry.ArtifactBundle",
-    "sentry.ArtifactBundleFlatFileIndex"
-  ],
-  "sentry.Group": [
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.GroupAssignee": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.GroupBookmark": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.GroupCommitResolution": [
-    "sentry.Commit",
-    "sentry.Group"
-  ],
-  "sentry.GroupEmailThread": [
-    "sentry.Group",
-    "sentry.Project"
-  ],
-  "sentry.GroupEnvironment": [
-    "sentry.Environment",
-    "sentry.Group",
-    "sentry.Release"
-  ],
-  "sentry.GroupHash": [
-    "sentry.Group",
-    "sentry.GroupTombstone",
-    "sentry.Project"
-  ],
-  "sentry.GroupHistory": [
-    "sentry.Actor",
-    "sentry.Group",
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.GroupInbox": [
-    "sentry.Group",
-    "sentry.Organization",
-    "sentry.Project"
-  ],
-  "sentry.GroupLink": [
-    "sentry.Group",
-    "sentry.Project"
-  ],
-  "sentry.GroupMeta": [
-    "sentry.Group"
-  ],
-  "sentry.GroupOwner": [
-    "sentry.Group",
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.GroupRedirect": [
-    "sentry.Group",
-    "sentry.Organization"
-  ],
-  "sentry.GroupRelease": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.GroupResolution": [
-    "sentry.Group",
-    "sentry.Release"
-  ],
-  "sentry.GroupRuleStatus": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.Rule"
-  ],
-  "sentry.GroupSeen": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.GroupShare": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.GroupSnooze": [
-    "sentry.Group"
-  ],
-  "sentry.GroupSubscription": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.GroupTombstone": [
-    "sentry.Project"
-  ],
-  "sentry.Identity": [
-    "sentry.IdentityProvider",
-    "sentry.User"
-  ],
-  "sentry.IdentityProvider": [],
-  "sentry.Incident": [
-    "sentry.AlertRule",
-    "sentry.Organization"
-  ],
-  "sentry.IncidentActivity": [
-    "sentry.Incident",
-    "sentry.User"
-  ],
-  "sentry.IncidentProject": [
-    "sentry.Incident",
-    "sentry.Project"
-  ],
-  "sentry.IncidentSeen": [
-    "sentry.Incident",
-    "sentry.User"
-  ],
-  "sentry.IncidentSnapshot": [
-    "sentry.Incident",
-    "sentry.TimeSeriesSnapshot"
-  ],
-  "sentry.IncidentSubscription": [
-    "sentry.Incident",
-    "sentry.User"
-  ],
-  "sentry.IncidentTrigger": [
-    "sentry.AlertRuleTrigger",
-    "sentry.Incident"
-  ],
-  "sentry.Integration": [],
-  "sentry.IntegrationExternalProject": [
-    "sentry.OrganizationIntegration"
-  ],
-  "sentry.IntegrationFeature": [],
-  "sentry.LatestAppConnectBuildsCheck": [
-    "sentry.Project"
-  ],
-  "sentry.LatestRepoReleaseEnvironment": [
-    "sentry.Commit",
-    "sentry.Deploy",
-    "sentry.Environment",
-    "sentry.Release",
-    "sentry.Repository"
-  ],
-  "sentry.LostPasswordHash": [
-    "sentry.User"
-  ],
-  "sentry.MetricsKeyIndexer": [],
-  "sentry.Monitor": [
-    "sentry.Organization",
-    "sentry.Project"
-  ],
-  "sentry.MonitorCheckIn": [
-    "sentry.Monitor",
-    "sentry.MonitorEnvironment",
-    "sentry.MonitorLocation",
-    "sentry.Project"
-  ],
-  "sentry.MonitorEnvironment": [
-    "sentry.Environment",
-    "sentry.Monitor"
-  ],
-  "sentry.MonitorIncident": [
-    "sentry.Monitor",
-    "sentry.MonitorCheckIn",
-    "sentry.MonitorEnvironment"
-  ],
-  "sentry.MonitorLocation": [],
-  "sentry.NeglectedRule": [
-    "sentry.Organization",
-    "sentry.Rule"
-  ],
-  "sentry.NotificationAction": [
-    "sentry.Integration",
-    "sentry.Organization",
-    "sentry.SentryApp"
-  ],
-  "sentry.NotificationActionProject": [
-    "sentry.NotificationAction",
-    "sentry.Project"
-  ],
-  "sentry.NotificationSetting": [
-    "sentry.Actor",
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.NotificationSettingOption": [
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.NotificationSettingProvider": [
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.Option": [],
-  "sentry.OrgAuthToken": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.Organization": [],
-  "sentry.OrganizationAccessRequest": [
-    "sentry.OrganizationMember",
-    "sentry.Team",
-    "sentry.User"
-  ],
-  "sentry.OrganizationAvatar": [
-    "sentry.File",
-    "sentry.Organization"
-  ],
-  "sentry.OrganizationIntegration": [
-    "sentry.Integration",
-    "sentry.Organization"
-  ],
-  "sentry.OrganizationMapping": [
-    "sentry.Organization"
-  ],
-  "sentry.OrganizationMember": [
-    "sentry.Organization",
-    "sentry.User"
-  ],
-  "sentry.OrganizationMemberMapping": [
-    "sentry.Organization",
-    "sentry.OrganizationMember",
-    "sentry.User"
-  ],
-  "sentry.OrganizationMemberTeam": [
-    "sentry.OrganizationMember",
-    "sentry.Team"
-  ],
-  "sentry.OrganizationMemberTeamReplica": [
-    "sentry.Organization",
-    "sentry.OrganizationMember",
-    "sentry.OrganizationMemberTeam",
-    "sentry.Team"
-  ],
-  "sentry.OrganizationOnboardingTask": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.OrganizationOption": [
-    "sentry.Organization"
-  ],
-  "sentry.PendingIncidentSnapshot": [
-    "sentry.Incident"
-  ],
-  "sentry.PerfStringIndexer": [
-    "sentry.Organization"
-  ],
-  "sentry.PlatformExternalIssue": [
-    "sentry.Group",
-    "sentry.Project"
-  ],
-  "sentry.ProcessingIssue": [
-    "sentry.Project"
-  ],
-  "sentry.ProguardArtifactRelease": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.ProjectDebugFile"
-  ],
-  "sentry.Project": [
-    "sentry.Organization"
-  ],
-  "sentry.ProjectArtifactBundle": [
-    "sentry.ArtifactBundle",
-    "sentry.Organization",
-    "sentry.Project"
-  ],
-  "sentry.ProjectAvatar": [
-    "sentry.File",
-    "sentry.Project"
-  ],
-  "sentry.ProjectBookmark": [
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.ProjectCodeOwners": [
-    "sentry.Project",
-    "sentry.RepositoryProjectPathConfig"
-  ],
-  "sentry.ProjectDebugFile": [
-    "sentry.File",
-    "sentry.Project"
-  ],
-  "sentry.ProjectIntegration": [
-    "sentry.Integration",
-    "sentry.Project"
-  ],
-  "sentry.ProjectKey": [
-    "sentry.Project"
-  ],
-  "sentry.ProjectOption": [
-    "sentry.Project"
-  ],
-  "sentry.ProjectOwnership": [
-    "sentry.Project"
-  ],
-  "sentry.ProjectPlatform": [
-    "sentry.Project"
-  ],
-  "sentry.ProjectRedirect": [
-    "sentry.Organization",
-    "sentry.Project"
-  ],
-  "sentry.ProjectTeam": [
-    "sentry.Project",
-    "sentry.Team"
-  ],
-  "sentry.ProjectTransactionThreshold": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.ProjectTransactionThresholdOverride": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.PromptsActivity": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.PullRequest": [
-    "sentry.CommitAuthor",
-    "sentry.Organization",
-    "sentry.Repository"
-  ],
-  "sentry.PullRequestComment": [
-    "sentry.PullRequest"
-  ],
-  "sentry.PullRequestCommit": [
-    "sentry.Commit",
-    "sentry.PullRequest"
-  ],
-  "sentry.QuerySubscription": [
-    "sentry.Project",
-    "sentry.SnubaQuery"
-  ],
-  "sentry.RawEvent": [
-    "sentry.Project"
-  ],
-  "sentry.RecentSearch": [
-    "sentry.Organization",
-    "sentry.User"
-  ],
-  "sentry.RegionOutbox": [],
-  "sentry.RegionScheduledDeletion": [],
-  "sentry.RegionTombstone": [],
-  "sentry.Relay": [],
-  "sentry.RelayUsage": [],
-  "sentry.Release": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.ReleaseActivity": [
-    "sentry.Release"
-  ],
-  "sentry.ReleaseArtifactBundle": [
-    "sentry.ArtifactBundle",
-    "sentry.Organization"
-  ],
-  "sentry.ReleaseCommit": [
-    "sentry.Commit",
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.ReleaseEnvironment": [
-    "sentry.Environment",
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.ReleaseFile": [
-    "sentry.File",
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.ReleaseHeadCommit": [
-    "sentry.Commit",
-    "sentry.Organization",
-    "sentry.Release",
-    "sentry.Repository"
-  ],
-  "sentry.ReleaseProject": [
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.ReleaseProjectEnvironment": [
-    "sentry.Environment",
-    "sentry.Project",
-    "sentry.Release"
-  ],
-  "sentry.ReleaseThreshold": [
-    "sentry.Environment",
-    "sentry.Project"
-  ],
-  "sentry.Repository": [
-    "sentry.Integration",
-    "sentry.Organization"
-  ],
-  "sentry.RepositoryProjectPathConfig": [
-    "sentry.Integration",
-    "sentry.Organization",
-    "sentry.OrganizationIntegration",
-    "sentry.Project",
-    "sentry.Repository"
-  ],
-  "sentry.ReprocessingReport": [
-    "sentry.Project"
-  ],
-  "sentry.Rule": [
-    "sentry.Actor",
-    "sentry.Environment",
-    "sentry.Project"
-  ],
-  "sentry.RuleActivity": [
-    "sentry.Rule",
-    "sentry.User"
-  ],
-  "sentry.RuleFireHistory": [
-    "sentry.Group",
-    "sentry.Project",
-    "sentry.Rule"
-  ],
-  "sentry.RuleSnooze": [
-    "sentry.AlertRule",
-    "sentry.Rule",
-    "sentry.User"
-  ],
-  "sentry.SavedSearch": [
-    "sentry.Organization",
-    "sentry.User"
-  ],
-  "sentry.ScheduledDeletion": [],
-  "sentry.SentryApp": [
-    "sentry.ApiApplication",
-    "sentry.Organization",
-    "sentry.User"
-  ],
-  "sentry.SentryAppAvatar": [
-    "sentry.ControlFile",
-    "sentry.File",
-    "sentry.SentryApp"
-  ],
-  "sentry.SentryAppComponent": [
-    "sentry.SentryApp"
-  ],
-  "sentry.SentryAppInstallation": [
-    "sentry.ApiGrant",
-    "sentry.ApiToken",
-    "sentry.Organization",
-    "sentry.SentryApp"
-  ],
-  "sentry.SentryAppInstallationForProvider": [
-    "sentry.Organization",
-    "sentry.SentryAppInstallation"
-  ],
-  "sentry.SentryAppInstallationToken": [
-    "sentry.ApiToken",
-    "sentry.SentryAppInstallation"
-  ],
-  "sentry.SentryFunction": [
-    "sentry.Organization"
-  ],
-  "sentry.ServiceHook": [
-    "sentry.ApiApplication",
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.SentryAppInstallation"
-  ],
-  "sentry.ServiceHookProject": [
-    "sentry.Project",
-    "sentry.ServiceHook"
-  ],
-  "sentry.SnubaQuery": [
-    "sentry.Environment"
-  ],
-  "sentry.SnubaQueryEventType": [
-    "sentry.SnubaQuery"
-  ],
-  "sentry.StringIndexer": [
-    "sentry.Organization"
-  ],
-  "sentry.Team": [
-    "sentry.Actor",
-    "sentry.Organization"
-  ],
-  "sentry.TeamAvatar": [
-    "sentry.File",
-    "sentry.Team"
-  ],
-  "sentry.TeamKeyTransaction": [
-    "sentry.Organization",
-    "sentry.ProjectTeam"
-  ],
-  "sentry.TeamReplica": [
-    "sentry.Organization",
-    "sentry.Team"
-  ],
-  "sentry.TimeSeriesSnapshot": [],
-  "sentry.User": [],
-  "sentry.UserAvatar": [
-    "sentry.ControlFile",
-    "sentry.File",
-    "sentry.User"
-  ],
-  "sentry.UserEmail": [
-    "sentry.User"
-  ],
-  "sentry.UserIP": [
-    "sentry.User"
-  ],
-  "sentry.UserOption": [
-    "sentry.Organization",
-    "sentry.Project",
-    "sentry.User"
-  ],
-  "sentry.UserPermission": [
-    "sentry.User"
-  ],
-  "sentry.UserReport": [
-    "sentry.Environment",
-    "sentry.EventUser",
-    "sentry.Group",
-    "sentry.Project"
-  ],
-  "sentry.UserRole": [],
-  "sentry.UserRoleUser": [
-    "sentry.User",
-    "sentry.UserRole"
-  ],
-  "sessions.Session": [],
-  "sites.Site": [],
-  "social_auth.UserSocialAuth": [
-    "sentry.User"
+  "sentry.dashboardtombstone": [
+    "sentry.organization"
+  ],
+  "sentry.dashboardwidget": [
+    "sentry.dashboard"
+  ],
+  "sentry.dashboardwidgetquery": [
+    "sentry.dashboardwidget"
+  ],
+  "sentry.debugidartifactbundle": [
+    "sentry.artifactbundle",
+    "sentry.organization"
+  ],
+  "sentry.deletedorganization": [],
+  "sentry.deletedproject": [
+    "sentry.organization"
+  ],
+  "sentry.deletedteam": [
+    "sentry.organization"
+  ],
+  "sentry.deploy": [
+    "sentry.environment",
+    "sentry.organization",
+    "sentry.release"
+  ],
+  "sentry.discoversavedquery": [
+    "sentry.organization",
+    "sentry.user"
+  ],
+  "sentry.discoversavedqueryproject": [
+    "sentry.discoversavedquery",
+    "sentry.project"
+  ],
+  "sentry.distribution": [
+    "sentry.organization",
+    "sentry.release"
+  ],
+  "sentry.docintegration": [],
+  "sentry.docintegrationavatar": [
+    "sentry.controlfile",
+    "sentry.docintegration",
+    "sentry.file"
+  ],
+  "sentry.email": [],
+  "sentry.environment": [
+    "sentry.organization"
+  ],
+  "sentry.environmentproject": [
+    "sentry.environment",
+    "sentry.project"
+  ],
+  "sentry.eventattachment": [
+    "sentry.file",
+    "sentry.group",
+    "sentry.project"
+  ],
+  "sentry.eventprocessingissue": [
+    "sentry.processingissue",
+    "sentry.rawevent"
+  ],
+  "sentry.eventuser": [
+    "sentry.project"
+  ],
+  "sentry.exporteddata": [
+    "sentry.file",
+    "sentry.organization",
+    "sentry.user"
+  ],
+  "sentry.exporteddatablob": [
+    "sentry.exporteddata"
+  ],
+  "sentry.externalactor": [
+    "sentry.integration",
+    "sentry.organization",
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.externalissue": [
+    "sentry.integration",
+    "sentry.organization"
+  ],
+  "sentry.featureadoption": [
+    "sentry.organization"
+  ],
+  "sentry.file": [
+    "sentry.fileblob"
+  ],
+  "sentry.fileblob": [],
+  "sentry.fileblobindex": [
+    "sentry.file",
+    "sentry.fileblob"
+  ],
+  "sentry.fileblobowner": [
+    "sentry.fileblob",
+    "sentry.organization"
+  ],
+  "sentry.flatfileindexstate": [
+    "sentry.artifactbundle",
+    "sentry.artifactbundleflatfileindex"
+  ],
+  "sentry.group": [
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.groupassignee": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.groupbookmark": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.groupcommitresolution": [
+    "sentry.commit",
+    "sentry.group"
+  ],
+  "sentry.groupemailthread": [
+    "sentry.group",
+    "sentry.project"
+  ],
+  "sentry.groupenvironment": [
+    "sentry.environment",
+    "sentry.group",
+    "sentry.release"
+  ],
+  "sentry.grouphash": [
+    "sentry.group",
+    "sentry.grouptombstone",
+    "sentry.project"
+  ],
+  "sentry.grouphistory": [
+    "sentry.actor",
+    "sentry.group",
+    "sentry.organization",
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.groupinbox": [
+    "sentry.group",
+    "sentry.organization",
+    "sentry.project"
+  ],
+  "sentry.grouplink": [
+    "sentry.group",
+    "sentry.project"
+  ],
+  "sentry.groupmeta": [
+    "sentry.group"
+  ],
+  "sentry.groupowner": [
+    "sentry.group",
+    "sentry.organization",
+    "sentry.project",
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.groupredirect": [
+    "sentry.group",
+    "sentry.organization"
+  ],
+  "sentry.grouprelease": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.groupresolution": [
+    "sentry.group",
+    "sentry.release"
+  ],
+  "sentry.grouprulestatus": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.rule"
+  ],
+  "sentry.groupseen": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.groupshare": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.groupsnooze": [
+    "sentry.group"
+  ],
+  "sentry.groupsubscription": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.grouptombstone": [
+    "sentry.project"
+  ],
+  "sentry.identity": [
+    "sentry.identityprovider",
+    "sentry.user"
+  ],
+  "sentry.identityprovider": [],
+  "sentry.incident": [
+    "sentry.alertrule",
+    "sentry.organization"
+  ],
+  "sentry.incidentactivity": [
+    "sentry.incident",
+    "sentry.user"
+  ],
+  "sentry.incidentproject": [
+    "sentry.incident",
+    "sentry.project"
+  ],
+  "sentry.incidentseen": [
+    "sentry.incident",
+    "sentry.user"
+  ],
+  "sentry.incidentsnapshot": [
+    "sentry.incident",
+    "sentry.timeseriessnapshot"
+  ],
+  "sentry.incidentsubscription": [
+    "sentry.incident",
+    "sentry.user"
+  ],
+  "sentry.incidenttrigger": [
+    "sentry.alertruletrigger",
+    "sentry.incident"
+  ],
+  "sentry.integration": [],
+  "sentry.integrationexternalproject": [
+    "sentry.organizationintegration"
+  ],
+  "sentry.integrationfeature": [],
+  "sentry.latestappconnectbuildscheck": [
+    "sentry.project"
+  ],
+  "sentry.latestreporeleaseenvironment": [
+    "sentry.commit",
+    "sentry.deploy",
+    "sentry.environment",
+    "sentry.release",
+    "sentry.repository"
+  ],
+  "sentry.lostpasswordhash": [
+    "sentry.user"
+  ],
+  "sentry.metricskeyindexer": [],
+  "sentry.monitor": [
+    "sentry.organization",
+    "sentry.project"
+  ],
+  "sentry.monitorcheckin": [
+    "sentry.monitor",
+    "sentry.monitorenvironment",
+    "sentry.monitorlocation",
+    "sentry.project"
+  ],
+  "sentry.monitorenvironment": [
+    "sentry.environment",
+    "sentry.monitor"
+  ],
+  "sentry.monitorincident": [
+    "sentry.monitor",
+    "sentry.monitorcheckin",
+    "sentry.monitorenvironment"
+  ],
+  "sentry.monitorlocation": [],
+  "sentry.neglectedrule": [
+    "sentry.organization",
+    "sentry.rule"
+  ],
+  "sentry.notificationaction": [
+    "sentry.integration",
+    "sentry.organization",
+    "sentry.sentryapp"
+  ],
+  "sentry.notificationactionproject": [
+    "sentry.notificationaction",
+    "sentry.project"
+  ],
+  "sentry.notificationsetting": [
+    "sentry.actor",
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.notificationsettingoption": [
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.notificationsettingprovider": [
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.option": [],
+  "sentry.organization": [],
+  "sentry.organizationaccessrequest": [
+    "sentry.organizationmember",
+    "sentry.team",
+    "sentry.user"
+  ],
+  "sentry.organizationavatar": [
+    "sentry.file",
+    "sentry.organization"
+  ],
+  "sentry.organizationintegration": [
+    "sentry.integration",
+    "sentry.organization"
+  ],
+  "sentry.organizationmapping": [
+    "sentry.organization"
+  ],
+  "sentry.organizationmember": [
+    "sentry.organization",
+    "sentry.user"
+  ],
+  "sentry.organizationmembermapping": [
+    "sentry.organization",
+    "sentry.organizationmember",
+    "sentry.user"
+  ],
+  "sentry.organizationmemberteam": [
+    "sentry.organizationmember",
+    "sentry.team"
+  ],
+  "sentry.organizationmemberteamreplica": [
+    "sentry.organization",
+    "sentry.organizationmember",
+    "sentry.organizationmemberteam",
+    "sentry.team"
+  ],
+  "sentry.organizationonboardingtask": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.organizationoption": [
+    "sentry.organization"
+  ],
+  "sentry.orgauthtoken": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.pendingincidentsnapshot": [
+    "sentry.incident"
+  ],
+  "sentry.perfstringindexer": [
+    "sentry.organization"
+  ],
+  "sentry.platformexternalissue": [
+    "sentry.group",
+    "sentry.project"
+  ],
+  "sentry.processingissue": [
+    "sentry.project"
+  ],
+  "sentry.proguardartifactrelease": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.projectdebugfile"
+  ],
+  "sentry.project": [
+    "sentry.organization"
+  ],
+  "sentry.projectartifactbundle": [
+    "sentry.artifactbundle",
+    "sentry.organization",
+    "sentry.project"
+  ],
+  "sentry.projectavatar": [
+    "sentry.file",
+    "sentry.project"
+  ],
+  "sentry.projectbookmark": [
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.projectcodeowners": [
+    "sentry.project",
+    "sentry.repositoryprojectpathconfig"
+  ],
+  "sentry.projectdebugfile": [
+    "sentry.file",
+    "sentry.project"
+  ],
+  "sentry.projectintegration": [
+    "sentry.integration",
+    "sentry.project"
+  ],
+  "sentry.projectkey": [
+    "sentry.project"
+  ],
+  "sentry.projectoption": [
+    "sentry.project"
+  ],
+  "sentry.projectownership": [
+    "sentry.project"
+  ],
+  "sentry.projectplatform": [
+    "sentry.project"
+  ],
+  "sentry.projectredirect": [
+    "sentry.organization",
+    "sentry.project"
+  ],
+  "sentry.projectteam": [
+    "sentry.project",
+    "sentry.team"
+  ],
+  "sentry.projecttransactionthreshold": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.projecttransactionthresholdoverride": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.promptsactivity": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.pullrequest": [
+    "sentry.commitauthor",
+    "sentry.organization",
+    "sentry.repository"
+  ],
+  "sentry.pullrequestcomment": [
+    "sentry.pullrequest"
+  ],
+  "sentry.pullrequestcommit": [
+    "sentry.commit",
+    "sentry.pullrequest"
+  ],
+  "sentry.querysubscription": [
+    "sentry.project",
+    "sentry.snubaquery"
+  ],
+  "sentry.rawevent": [
+    "sentry.project"
+  ],
+  "sentry.recentsearch": [
+    "sentry.organization",
+    "sentry.user"
+  ],
+  "sentry.regionoutbox": [],
+  "sentry.regionscheduleddeletion": [],
+  "sentry.regiontombstone": [],
+  "sentry.relay": [],
+  "sentry.relayusage": [],
+  "sentry.release": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.releaseactivity": [
+    "sentry.release"
+  ],
+  "sentry.releaseartifactbundle": [
+    "sentry.artifactbundle",
+    "sentry.organization"
+  ],
+  "sentry.releasecommit": [
+    "sentry.commit",
+    "sentry.organization",
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.releaseenvironment": [
+    "sentry.environment",
+    "sentry.organization",
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.releasefile": [
+    "sentry.file",
+    "sentry.organization",
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.releaseheadcommit": [
+    "sentry.commit",
+    "sentry.organization",
+    "sentry.release",
+    "sentry.repository"
+  ],
+  "sentry.releaseproject": [
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.releaseprojectenvironment": [
+    "sentry.environment",
+    "sentry.project",
+    "sentry.release"
+  ],
+  "sentry.releasethreshold": [
+    "sentry.environment",
+    "sentry.project"
+  ],
+  "sentry.repository": [
+    "sentry.integration",
+    "sentry.organization"
+  ],
+  "sentry.repositoryprojectpathconfig": [
+    "sentry.integration",
+    "sentry.organization",
+    "sentry.organizationintegration",
+    "sentry.project",
+    "sentry.repository"
+  ],
+  "sentry.reprocessingreport": [
+    "sentry.project"
+  ],
+  "sentry.rule": [
+    "sentry.actor",
+    "sentry.environment",
+    "sentry.project"
+  ],
+  "sentry.ruleactivity": [
+    "sentry.rule",
+    "sentry.user"
+  ],
+  "sentry.rulefirehistory": [
+    "sentry.group",
+    "sentry.project",
+    "sentry.rule"
+  ],
+  "sentry.rulesnooze": [
+    "sentry.alertrule",
+    "sentry.rule",
+    "sentry.user"
+  ],
+  "sentry.savedsearch": [
+    "sentry.organization",
+    "sentry.user"
+  ],
+  "sentry.scheduleddeletion": [],
+  "sentry.sentryapp": [
+    "sentry.apiapplication",
+    "sentry.organization",
+    "sentry.user"
+  ],
+  "sentry.sentryappavatar": [
+    "sentry.controlfile",
+    "sentry.file",
+    "sentry.sentryapp"
+  ],
+  "sentry.sentryappcomponent": [
+    "sentry.sentryapp"
+  ],
+  "sentry.sentryappinstallation": [
+    "sentry.apigrant",
+    "sentry.apitoken",
+    "sentry.organization",
+    "sentry.sentryapp"
+  ],
+  "sentry.sentryappinstallationforprovider": [
+    "sentry.organization",
+    "sentry.sentryappinstallation"
+  ],
+  "sentry.sentryappinstallationtoken": [
+    "sentry.apitoken",
+    "sentry.sentryappinstallation"
+  ],
+  "sentry.sentryfunction": [
+    "sentry.organization"
+  ],
+  "sentry.servicehook": [
+    "sentry.apiapplication",
+    "sentry.organization",
+    "sentry.project",
+    "sentry.sentryappinstallation"
+  ],
+  "sentry.servicehookproject": [
+    "sentry.project",
+    "sentry.servicehook"
+  ],
+  "sentry.snubaquery": [
+    "sentry.environment"
+  ],
+  "sentry.snubaqueryeventtype": [
+    "sentry.snubaquery"
+  ],
+  "sentry.stringindexer": [
+    "sentry.organization"
+  ],
+  "sentry.team": [
+    "sentry.actor",
+    "sentry.organization"
+  ],
+  "sentry.teamavatar": [
+    "sentry.file",
+    "sentry.team"
+  ],
+  "sentry.teamkeytransaction": [
+    "sentry.organization",
+    "sentry.projectteam"
+  ],
+  "sentry.teamreplica": [
+    "sentry.organization",
+    "sentry.team"
+  ],
+  "sentry.timeseriessnapshot": [],
+  "sentry.user": [],
+  "sentry.useravatar": [
+    "sentry.controlfile",
+    "sentry.file",
+    "sentry.user"
+  ],
+  "sentry.useremail": [
+    "sentry.user"
+  ],
+  "sentry.userip": [
+    "sentry.user"
+  ],
+  "sentry.useroption": [
+    "sentry.organization",
+    "sentry.project",
+    "sentry.user"
+  ],
+  "sentry.userpermission": [
+    "sentry.user"
+  ],
+  "sentry.userreport": [
+    "sentry.environment",
+    "sentry.eventuser",
+    "sentry.group",
+    "sentry.project"
+  ],
+  "sentry.userrole": [],
+  "sentry.userroleuser": [
+    "sentry.user",
+    "sentry.userrole"
+  ],
+  "sessions.session": [],
+  "sites.site": [],
+  "social_auth.usersocialauth": [
+    "sentry.user"
   ]
 }

+ 215 - 215
fixtures/backup/model_dependencies/sorted.json

@@ -1,217 +1,217 @@
 [
-  "sessions.Session",
-  "sites.Site",
-  "sentry.Option",
-  "sentry.ControlOption",
-  "sentry.RegionOutbox",
-  "sentry.ControlOutbox",
-  "sentry.ScheduledDeletion",
-  "sentry.RegionScheduledDeletion",
-  "sentry.ControlFileBlob",
-  "sentry.ControlFile",
-  "sentry.FileBlob",
-  "sentry.File",
-  "sentry.Broadcast",
-  "sentry.DeletedOrganization",
-  "sentry.Email",
-  "sentry.Organization",
-  "sentry.OrganizationMapping",
-  "sentry.IdentityProvider",
-  "sentry.DocIntegration",
-  "sentry.Integration",
-  "sentry.IntegrationFeature",
-  "sentry.User",
-  "sentry.Project",
-  "sentry.ProjectBookmark",
-  "sentry.ProjectKey",
-  "sentry.ProjectOwnership",
-  "sentry.ProjectPlatform",
-  "sentry.ProjectRedirect",
-  "sentry.PromptsActivity",
-  "sentry.RawEvent",
-  "sentry.RecentSearch",
-  "sentry.RelayUsage",
-  "sentry.Relay",
-  "sentry.Repository",
-  "sentry.ReprocessingReport",
-  "sentry.SavedSearch",
-  "sentry.SentryFunction",
-  "sentry.RegionTombstone",
-  "sentry.ControlTombstone",
-  "sentry.ProjectTransactionThresholdOverride",
-  "sentry.ProjectTransactionThreshold",
-  "sentry.UserEmail",
-  "sentry.UserIP",
-  "sentry.UserPermission",
-  "sentry.UserRole",
-  "sentry.UserRoleUser",
-  "sentry.TimeSeriesSnapshot",
-  "sentry.DiscoverSavedQuery",
-  "sentry.Monitor",
-  "sentry.MonitorLocation",
-  "sentry.MetricsKeyIndexer",
-  "sentry.StringIndexer",
-  "sentry.PerfStringIndexer",
-  "sentry.ExportedData",
-  "sentry.ExportedDataBlob",
-  "nodestore.Node",
-  "replays.ReplayRecordingSegment",
-  "social_auth.UserSocialAuth",
-  "feedback.Feedback",
-  "sentry.DiscoverSavedQueryProject",
-  "sentry.ProcessingIssue",
-  "sentry.OrgAuthToken",
-  "sentry.OrganizationOnboardingTask",
-  "sentry.LostPasswordHash",
-  "sentry.LatestAppConnectBuildsCheck",
-  "sentry.ProjectIntegration",
-  "sentry.OrganizationIntegration",
-  "sentry.ExternalIssue",
-  "sentry.Identity",
-  "sentry.GroupTombstone",
-  "sentry.Release",
-  "sentry.ReleaseProject",
-  "sentry.Group",
-  "sentry.OrganizationMember",
-  "sentry.FeatureAdoption",
-  "sentry.EventUser",
-  "sentry.EventAttachment",
-  "sentry.Environment",
-  "sentry.EnvironmentProject",
-  "sentry.CustomDynamicSamplingRule",
-  "sentry.CustomDynamicSamplingRuleProject",
-  "sentry.Distribution",
-  "sentry.Deploy",
-  "sentry.DeletedTeam",
-  "sentry.DeletedProject",
-  "sentry.ProjectDebugFile",
-  "sentry.DashboardTombstone",
-  "sentry.Dashboard",
-  "sentry.DashboardProject",
-  "sentry.Counter",
-  "sentry.CommitAuthor",
-  "sentry.Commit",
-  "sentry.BroadcastSeen",
-  "sentry.UserAvatar",
-  "sentry.ProjectAvatar",
-  "sentry.OrganizationAvatar",
-  "sentry.DocIntegrationAvatar",
-  "sentry.FileBlobIndex",
-  "sentry.FileBlobOwner",
-  "sentry.ControlFileBlobIndex",
-  "sentry.ControlFileBlobOwner",
-  "sentry.AuthProvider",
-  "sentry.AuthIdentity",
-  "sentry.Authenticator",
-  "sentry.AssistantActivity",
-  "sentry.ArtifactBundleFlatFileIndex",
-  "sentry.ArtifactBundle",
-  "sentry.AppConnectBuild",
-  "sentry.ApiKey",
-  "sentry.ApiApplication",
-  "sentry.Actor",
-  "sentry.UserOption",
-  "sentry.ProjectOption",
-  "sentry.OrganizationOption",
-  "sentry.Activity",
-  "sentry.ApiAuthorization",
-  "sentry.ApiGrant",
-  "sentry.ApiToken",
-  "sentry.FlatFileIndexState",
-  "sentry.ArtifactBundleIndex",
-  "sentry.ReleaseArtifactBundle",
-  "sentry.DebugIdArtifactBundle",
-  "sentry.ProjectArtifactBundle",
-  "sentry.AuditLogEntry",
-  "sentry.AuthIdentityReplica",
-  "sentry.AuthProviderReplica",
-  "sentry.CommitFileChange",
-  "sentry.DashboardWidget",
-  "sentry.ProguardArtifactRelease",
-  "sentry.GroupHistory",
-  "sentry.Team",
-  "sentry.GroupOwner",
-  "sentry.GroupAssignee",
-  "sentry.GroupBookmark",
-  "sentry.GroupCommitResolution",
-  "sentry.GroupEmailThread",
-  "sentry.GroupEnvironment",
-  "sentry.GroupHash",
-  "sentry.GroupInbox",
-  "sentry.GroupLink",
-  "sentry.GroupMeta",
-  "sentry.GroupRedirect",
-  "sentry.GroupRelease",
-  "sentry.GroupResolution",
-  "sentry.GroupSeen",
-  "sentry.GroupShare",
-  "sentry.GroupSnooze",
-  "sentry.OrganizationMemberMapping",
-  "sentry.GroupSubscription",
-  "sentry.ExternalActor",
-  "sentry.IntegrationExternalProject",
-  "sentry.SentryApp",
-  "sentry.RepositoryProjectPathConfig",
-  "sentry.SentryAppComponent",
-  "sentry.SentryAppInstallation",
-  "sentry.SentryAppInstallationForProvider",
-  "sentry.SentryAppInstallationToken",
-  "sentry.LatestRepoReleaseEnvironment",
-  "sentry.NotificationSettingOption",
-  "sentry.NotificationSettingProvider",
-  "sentry.NotificationSetting",
-  "sentry.OrganizationAccessRequest",
-  "sentry.PlatformExternalIssue",
-  "sentry.EventProcessingIssue",
-  "sentry.SnubaQuery",
-  "sentry.SnubaQueryEventType",
-  "sentry.QuerySubscription",
-  "sentry.ProjectTeam",
-  "sentry.ProjectCodeOwners",
-  "sentry.PullRequest",
-  "sentry.PullRequestCommit",
-  "sentry.PullRequestComment",
-  "sentry.ReleaseActivity",
-  "sentry.ReleaseCommit",
-  "sentry.ReleaseEnvironment",
-  "sentry.ReleaseFile",
-  "sentry.ReleaseHeadCommit",
-  "sentry.ReleaseProjectEnvironment",
-  "sentry.Rule",
-  "sentry.RuleActivity",
-  "sentry.NeglectedRule",
-  "sentry.RuleFireHistory",
-  "sentry.ServiceHook",
-  "sentry.TeamReplica",
-  "sentry.UserReport",
-  "sentry.NotificationAction",
-  "sentry.AlertRule",
-  "sentry.AlertRuleTrigger",
-  "sentry.AlertRuleTriggerExclusion",
-  "sentry.AlertRuleTriggerAction",
-  "sentry.AlertRuleActivity",
-  "sentry.TeamKeyTransaction",
-  "sentry.MonitorEnvironment",
-  "sentry.ReleaseThreshold",
-  "sentry.MonitorCheckIn",
-  "sentry.AlertRuleExcludedProjects",
-  "sentry.Incident",
-  "sentry.IncidentSeen",
-  "sentry.IncidentProject",
-  "sentry.NotificationActionProject",
-  "sentry.ServiceHookProject",
-  "sentry.RuleSnooze",
-  "sentry.GroupRuleStatus",
-  "sentry.OrganizationMemberTeam",
-  "sentry.DashboardWidgetQuery",
-  "sentry.TeamAvatar",
-  "sentry.SentryAppAvatar",
-  "sentry.AuthProviderDefaultTeams",
-  "sentry.OrganizationMemberTeamReplica",
-  "sentry.PendingIncidentSnapshot",
-  "sentry.IncidentSnapshot",
-  "sentry.IncidentActivity",
-  "sentry.IncidentSubscription",
-  "sentry.IncidentTrigger",
-  "sentry.MonitorIncident"
+  "sessions.session",
+  "sites.site",
+  "sentry.option",
+  "sentry.controloption",
+  "sentry.regionoutbox",
+  "sentry.controloutbox",
+  "sentry.scheduleddeletion",
+  "sentry.regionscheduleddeletion",
+  "sentry.controlfileblob",
+  "sentry.controlfile",
+  "sentry.fileblob",
+  "sentry.file",
+  "sentry.broadcast",
+  "sentry.deletedorganization",
+  "sentry.email",
+  "sentry.organization",
+  "sentry.organizationmapping",
+  "sentry.identityprovider",
+  "sentry.docintegration",
+  "sentry.integration",
+  "sentry.integrationfeature",
+  "sentry.user",
+  "sentry.project",
+  "sentry.projectbookmark",
+  "sentry.projectkey",
+  "sentry.projectownership",
+  "sentry.projectplatform",
+  "sentry.projectredirect",
+  "sentry.promptsactivity",
+  "sentry.rawevent",
+  "sentry.recentsearch",
+  "sentry.relayusage",
+  "sentry.relay",
+  "sentry.repository",
+  "sentry.reprocessingreport",
+  "sentry.savedsearch",
+  "sentry.sentryfunction",
+  "sentry.regiontombstone",
+  "sentry.controltombstone",
+  "sentry.projecttransactionthresholdoverride",
+  "sentry.projecttransactionthreshold",
+  "sentry.useremail",
+  "sentry.userip",
+  "sentry.userpermission",
+  "sentry.userrole",
+  "sentry.userroleuser",
+  "sentry.timeseriessnapshot",
+  "sentry.discoversavedquery",
+  "sentry.monitor",
+  "sentry.monitorlocation",
+  "sentry.metricskeyindexer",
+  "sentry.stringindexer",
+  "sentry.perfstringindexer",
+  "sentry.exporteddata",
+  "sentry.exporteddatablob",
+  "nodestore.node",
+  "replays.replayrecordingsegment",
+  "social_auth.usersocialauth",
+  "feedback.feedback",
+  "sentry.discoversavedqueryproject",
+  "sentry.processingissue",
+  "sentry.orgauthtoken",
+  "sentry.organizationonboardingtask",
+  "sentry.lostpasswordhash",
+  "sentry.latestappconnectbuildscheck",
+  "sentry.projectintegration",
+  "sentry.organizationintegration",
+  "sentry.externalissue",
+  "sentry.identity",
+  "sentry.grouptombstone",
+  "sentry.release",
+  "sentry.releaseproject",
+  "sentry.group",
+  "sentry.organizationmember",
+  "sentry.featureadoption",
+  "sentry.eventuser",
+  "sentry.eventattachment",
+  "sentry.environment",
+  "sentry.environmentproject",
+  "sentry.customdynamicsamplingrule",
+  "sentry.customdynamicsamplingruleproject",
+  "sentry.distribution",
+  "sentry.deploy",
+  "sentry.deletedteam",
+  "sentry.deletedproject",
+  "sentry.projectdebugfile",
+  "sentry.dashboardtombstone",
+  "sentry.dashboard",
+  "sentry.dashboardproject",
+  "sentry.counter",
+  "sentry.commitauthor",
+  "sentry.commit",
+  "sentry.broadcastseen",
+  "sentry.useravatar",
+  "sentry.projectavatar",
+  "sentry.organizationavatar",
+  "sentry.docintegrationavatar",
+  "sentry.fileblobindex",
+  "sentry.fileblobowner",
+  "sentry.controlfileblobindex",
+  "sentry.controlfileblobowner",
+  "sentry.authprovider",
+  "sentry.authidentity",
+  "sentry.authenticator",
+  "sentry.assistantactivity",
+  "sentry.artifactbundleflatfileindex",
+  "sentry.artifactbundle",
+  "sentry.appconnectbuild",
+  "sentry.apikey",
+  "sentry.apiapplication",
+  "sentry.actor",
+  "sentry.useroption",
+  "sentry.projectoption",
+  "sentry.organizationoption",
+  "sentry.activity",
+  "sentry.apiauthorization",
+  "sentry.apigrant",
+  "sentry.apitoken",
+  "sentry.flatfileindexstate",
+  "sentry.artifactbundleindex",
+  "sentry.releaseartifactbundle",
+  "sentry.debugidartifactbundle",
+  "sentry.projectartifactbundle",
+  "sentry.auditlogentry",
+  "sentry.authidentityreplica",
+  "sentry.authproviderreplica",
+  "sentry.commitfilechange",
+  "sentry.dashboardwidget",
+  "sentry.proguardartifactrelease",
+  "sentry.grouphistory",
+  "sentry.team",
+  "sentry.groupowner",
+  "sentry.groupassignee",
+  "sentry.groupbookmark",
+  "sentry.groupcommitresolution",
+  "sentry.groupemailthread",
+  "sentry.groupenvironment",
+  "sentry.grouphash",
+  "sentry.groupinbox",
+  "sentry.grouplink",
+  "sentry.groupmeta",
+  "sentry.groupredirect",
+  "sentry.grouprelease",
+  "sentry.groupresolution",
+  "sentry.groupseen",
+  "sentry.groupshare",
+  "sentry.groupsnooze",
+  "sentry.organizationmembermapping",
+  "sentry.groupsubscription",
+  "sentry.externalactor",
+  "sentry.integrationexternalproject",
+  "sentry.sentryapp",
+  "sentry.repositoryprojectpathconfig",
+  "sentry.sentryappcomponent",
+  "sentry.sentryappinstallation",
+  "sentry.sentryappinstallationforprovider",
+  "sentry.sentryappinstallationtoken",
+  "sentry.latestreporeleaseenvironment",
+  "sentry.notificationsettingoption",
+  "sentry.notificationsettingprovider",
+  "sentry.notificationsetting",
+  "sentry.organizationaccessrequest",
+  "sentry.platformexternalissue",
+  "sentry.eventprocessingissue",
+  "sentry.snubaquery",
+  "sentry.snubaqueryeventtype",
+  "sentry.querysubscription",
+  "sentry.projectteam",
+  "sentry.projectcodeowners",
+  "sentry.pullrequest",
+  "sentry.pullrequestcommit",
+  "sentry.pullrequestcomment",
+  "sentry.releaseactivity",
+  "sentry.releasecommit",
+  "sentry.releaseenvironment",
+  "sentry.releasefile",
+  "sentry.releaseheadcommit",
+  "sentry.releaseprojectenvironment",
+  "sentry.rule",
+  "sentry.ruleactivity",
+  "sentry.neglectedrule",
+  "sentry.rulefirehistory",
+  "sentry.servicehook",
+  "sentry.teamreplica",
+  "sentry.userreport",
+  "sentry.notificationaction",
+  "sentry.alertrule",
+  "sentry.alertruletrigger",
+  "sentry.alertruletriggerexclusion",
+  "sentry.alertruletriggeraction",
+  "sentry.alertruleactivity",
+  "sentry.teamkeytransaction",
+  "sentry.monitorenvironment",
+  "sentry.releasethreshold",
+  "sentry.monitorcheckin",
+  "sentry.alertruleexcludedprojects",
+  "sentry.incident",
+  "sentry.incidentseen",
+  "sentry.incidentproject",
+  "sentry.notificationactionproject",
+  "sentry.servicehookproject",
+  "sentry.rulesnooze",
+  "sentry.grouprulestatus",
+  "sentry.organizationmemberteam",
+  "sentry.dashboardwidgetquery",
+  "sentry.teamavatar",
+  "sentry.sentryappavatar",
+  "sentry.authproviderdefaultteams",
+  "sentry.organizationmemberteamreplica",
+  "sentry.pendingincidentsnapshot",
+  "sentry.incidentsnapshot",
+  "sentry.incidentactivity",
+  "sentry.incidentsubscription",
+  "sentry.incidenttrigger",
+  "sentry.monitorincident"
 ]

+ 7 - 7
src/sentry/backup/comparators.py

@@ -10,7 +10,7 @@ from typing import Callable, Dict, List, Type
 from dateutil import parser
 from django.db import models
 
-from sentry.backup.dependencies import PrimaryKeyMap, dependencies
+from sentry.backup.dependencies import PrimaryKeyMap, dependencies, get_model_name
 from sentry.backup.findings import ComparatorFinding, ComparatorFindingKind, InstanceID
 from sentry.backup.helpers import Side, get_exportable_sentry_models
 from sentry.models.team import Team
@@ -262,8 +262,7 @@ class ForeignKeyComparator(JSONScrubbingComparator):
         findings = []
         fields = sorted(self.fields)
         for f in fields:
-            obj_name = self.foreign_fields[f]._meta.object_name.lower()  # type: ignore[union-attr]
-            field_model_name = "sentry." + obj_name
+            field_model_name = get_model_name(self.foreign_fields[f])
             if left["fields"].get(f) is None and right["fields"].get(f) is None:
                 continue
 
@@ -597,7 +596,7 @@ def auto_assign_datetime_equality_comparators(comps: ComparatorMap) -> None:
 
     exportable = get_exportable_sentry_models()
     for e in exportable:
-        name = "sentry." + e.__name__.lower()
+        name = str(get_model_name(e))
         fields = e._meta.get_fields()
         assign = set()
         for f in fields:
@@ -623,7 +622,7 @@ def auto_assign_email_obfuscating_comparators(comps: ComparatorMap) -> None:
 
     exportable = get_exportable_sentry_models()
     for e in exportable:
-        name = "sentry." + e.__name__.lower()
+        name = str(get_model_name(e))
         fields = e._meta.get_fields()
         assign = set()
         for f in fields:
@@ -632,7 +631,8 @@ def auto_assign_email_obfuscating_comparators(comps: ComparatorMap) -> None:
 
         if len(assign):
             found = next(
-                filter(lambda e: isinstance(e, EmailObfuscatingComparator), comps[name]), None
+                filter(lambda e: isinstance(e, EmailObfuscatingComparator), comps[name]),
+                None,
             )
             if found:
                 found.fields.update(assign)
@@ -645,7 +645,7 @@ def auto_assign_foreign_key_comparators(comps: ComparatorMap) -> None:
     dependencies.py for more on what "appropriate" means in this context)."""
 
     for model_name, rels in dependencies().items():
-        comps[model_name.lower()].append(
+        comps[str(model_name)].append(
             ForeignKeyComparator({k: v.model for k, v in rels.foreign_keys.items()})
         )
 

+ 69 - 30
src/sentry/backup/dependencies.py

@@ -3,7 +3,7 @@ from __future__ import annotations
 from collections import defaultdict
 from enum import Enum, auto, unique
 from functools import lru_cache
-from typing import NamedTuple, Optional, Tuple, Type
+from typing import Dict, NamedTuple, Optional, Tuple, Type
 
 from django.db import models
 from django.db.models.fields.related import ForeignKey, OneToOneField
@@ -60,16 +60,51 @@ class ModelRelations(NamedTuple):
         return {ff.model for ff in self.foreign_keys.values()}
 
 
-def normalize_model_name(model: Type[models.base.Model]):
-    return f"{model._meta.app_label}.{model._meta.object_name}"
+class NormalizedModelName:
+    """
+    A wrapper type that ensures that the contained model name has been properly normalized. A "normalized" model name is one that is identical to the name as it appears in an exported JSON backup, so a string of the form `{app_label.lower()}.{model_name.lower()}`.
+    """
+
+    __model_name: str
+
+    def __init__(self, model_name: str):
+        if "." not in model_name:
+            raise TypeError("cannot create NormalizedModelName from invalid input string")
+        self.__model_name = model_name.lower()
+
+    def __hash__(self):
+        return hash(self.__model_name)
+
+    def __eq__(self, other) -> bool:
+        if other is None:
+            return False
+        if not isinstance(other, self.__class__):
+            raise TypeError(
+                "NormalizedModelName can only be compared with other NormalizedModelName"
+            )
+        return self.__model_name == other.__model_name
+
+    def __lt__(self, other) -> bool:
+        if not isinstance(other, self.__class__):
+            raise TypeError(
+                "NormalizedModelName can only be compared with other NormalizedModelName"
+            )
+        return self.__model_name < other.__model_name
+
+    def __str__(self) -> str:
+        return self.__model_name
+
+
+def get_model_name(model: Type[models.base.Model]) -> NormalizedModelName:
+    return NormalizedModelName(f"{model._meta.app_label}.{model._meta.object_name}")
 
 
-def get_model(model_name: str) -> Optional[Type[models.base.Model]]:
+def get_model(model_name: NormalizedModelName) -> Optional[Type[models.base.Model]]:
     """
     Given a standardized model name string, retrieve the matching Sentry model.
     """
     for model in sorted_dependencies():
-        if f"sentry.{str(model.__name__).lower()}" == model_name.lower():
+        if get_model_name(model) == model_name:
             return model
     return None
 
@@ -80,7 +115,7 @@ class DependenciesJSONEncoder(json.JSONEncoder):
 
     def default(self, obj):
         if meta := getattr(obj, "_meta", None):
-            return f"{meta.app_label}.{meta.object_name}"
+            return f"{meta.app_label}.{meta.object_name}".lower()
         if isinstance(obj, ForeignFieldKind):
             return obj.name
         if isinstance(obj, RelocationScope):
@@ -91,7 +126,7 @@ class DependenciesJSONEncoder(json.JSONEncoder):
         if isinstance(obj, SiloMode):
             return obj.name.lower().capitalize()
         if isinstance(obj, set):
-            return sorted(list(obj), key=lambda obj: normalize_model_name(obj))
+            return sorted(list(obj), key=lambda obj: get_model_name(obj))
         return super().default(obj)
 
 
@@ -122,15 +157,18 @@ class PrimaryKeyMap:
     keys are not supported!
     """
 
-    mapping: dict[str, dict[int, Tuple[int, ImportKind]]]
+    # Pydantic duplicates global default models on a per-instance basis, so using `{}` here is safe.
+    mapping: Dict[str, Dict[int, Tuple[int, ImportKind]]]
 
     def __init__(self):
         self.mapping = defaultdict(dict)
 
-    def get_pk(self, model: str, old: int) -> int | None:
-        """Get the new, post-mapping primary key from an old primary key."""
+    def get_pk(self, model_name: NormalizedModelName, old: int) -> Optional[int]:
+        """
+        Get the new, post-mapping primary key from an old primary key.
+        """
 
-        pk_map = self.mapping.get(model)
+        pk_map = self.mapping.get(str(model_name))
         if pk_map is None:
             return None
 
@@ -140,10 +178,12 @@ class PrimaryKeyMap:
 
         return entry[0]
 
-    def get_kind(self, model: str, old: int) -> ImportKind | None:
-        """Is the mapped entry a newly inserted model, or an already existing one that has been merged in?"""
+    def get_kind(self, model_name: NormalizedModelName, old: int) -> Optional[ImportKind]:
+        """
+        Is the mapped entry a newly inserted model, or an already existing one that has been merged in?
+        """
 
-        pk_map = self.mapping.get(model)
+        pk_map = self.mapping.get(str(model_name))
         if pk_map is None:
             return None
 
@@ -153,15 +193,17 @@ class PrimaryKeyMap:
 
         return entry[1]
 
-    def insert(self, model: str, old: int, new: int, kind: ImportKind):
-        """Create a new OLD_PK -> NEW_PK mapping for the given model."""
+    def insert(self, model_name: NormalizedModelName, old: int, new: int, kind: ImportKind) -> None:
+        """
+        Create a new OLD_PK -> NEW_PK mapping for the given model.
+        """
 
-        self.mapping[model][old] = (new, kind)
+        self.mapping[str(model_name)][old] = (new, kind)
 
 
 # No arguments, so we lazily cache the result after the first calculation.
 @lru_cache(maxsize=1)
-def dependencies() -> dict[str, ModelRelations]:
+def dependencies() -> dict[NormalizedModelName, ModelRelations]:
     """Produce a dictionary mapping model type definitions to a `ModelDeps` describing their dependencies."""
 
     from django.apps import apps
@@ -179,20 +221,17 @@ def dependencies() -> dict[str, ModelRelations]:
     from sentry.models.team import Team
 
     # Process the list of models, and get the list of dependencies
-    model_dependencies_list: dict[str, ModelRelations] = {}
+    model_dependencies_list: Dict[NormalizedModelName, ModelRelations] = {}
     app_configs = apps.get_app_configs()
     for app_config in app_configs:
         if app_config.label in EXCLUDED_APPS:
             continue
 
-        models_from_names = {
-            model._meta.object_name.lower(): model  # type: ignore[union-attr]
-            for model in app_config.get_models()
-        }
+        models_from_names = {get_model_name(model): model for model in app_config.get_models()}
         model_iterator = app_config.get_models()
 
         for model in model_iterator:
-            foreign_keys: dict[str, ForeignField] = dict()
+            foreign_keys: Dict[str, ForeignField] = dict()
 
             # Now add a dependency for any FK relation visible to Django.
             for field in model._meta.get_fields():
@@ -214,7 +253,7 @@ def dependencies() -> dict[str, ModelRelations]:
                             kind=ForeignFieldKind.DefaultForeignKey,
                         )
                 elif isinstance(field, HybridCloudForeignKey):
-                    rel_model = models_from_names[field.foreign_model_name[7:].lower()]
+                    rel_model = models_from_names[NormalizedModelName(field.foreign_model_name)]
                     foreign_keys[field.name] = ForeignField(
                         model=rel_model,
                         kind=ForeignFieldKind.HybridCloudForeignKey,
@@ -255,14 +294,14 @@ def dependencies() -> dict[str, ModelRelations]:
                 # `Actor` model. Because of this, we avoid assuming that it is a dependency into
                 # `Actor` and just ignore it.
                 if field.name.endswith("_id") and field.name != "actor_id":
-                    candidate = field.name[:-3].replace("_", "")
+                    candidate = NormalizedModelName("sentry." + field.name[:-3].replace("_", ""))
                     if candidate and candidate in models_from_names:
                         foreign_keys[field.name] = ForeignField(
                             model=models_from_names[candidate],
                             kind=ForeignFieldKind.ImplicitForeignKey,
                         )
 
-            model_dependencies_list[normalize_model_name(model)] = ModelRelations(
+            model_dependencies_list[get_model_name(model)] = ModelRelations(
                 model=model,
                 foreign_keys=foreign_keys,
                 relocation_scope=getattr(model, "__relocation_scope__", RelocationScope.Excluded),
@@ -275,7 +314,7 @@ def dependencies() -> dict[str, ModelRelations]:
 
 # No arguments, so we lazily cache the result after the first calculation.
 @lru_cache(maxsize=1)
-def sorted_dependencies():
+def sorted_dependencies() -> list[Type[models.base.Model]]:
     """Produce a list of model definitions such that, for every item in the list, all of the other models it mentions in its fields and/or natural key (ie, its "dependencies") have already appeared in the list.
 
     Similar to Django's algorithm except that we discard the importance of natural keys
@@ -318,8 +357,8 @@ def sorted_dependencies():
             raise RuntimeError(
                 "Can't resolve dependencies for %s in serialized app list."
                 % ", ".join(
-                    normalize_model_name(m.model)
-                    for m in sorted(skipped, key=lambda mr: normalize_model_name(mr.model))
+                    str(get_model_name(m.model))
+                    for m in sorted(skipped, key=lambda mr: get_model_name(mr.model))
                 )
             )
         model_dependencies_list = skipped

+ 11 - 8
src/sentry/backup/exports.py

@@ -10,7 +10,7 @@ from sentry.backup.dependencies import (
     ImportKind,
     PrimaryKeyMap,
     dependencies,
-    normalize_model_name,
+    get_model_name,
     sorted_dependencies,
 )
 from sentry.backup.helpers import Filter
@@ -94,7 +94,7 @@ def _export(
                 continue
 
             model = type(item)
-            model_name = normalize_model_name(model)
+            model_name = get_model_name(model)
 
             # Make sure this model is not explicitly being filtered.
             for f in filters:
@@ -103,7 +103,7 @@ def _export(
             else:
                 # Now make sure its not transitively filtered either.
                 for field, foreign_field in deps[model_name].foreign_keys.items():
-                    dependency_model_name = normalize_model_name(foreign_field.model)
+                    dependency_model_name = get_model_name(foreign_field.model)
                     field_id = field if field.endswith("_id") else f"{field}_id"
                     fk = getattr(item, field_id, None)
                     if fk is None:
@@ -118,16 +118,19 @@ def _export(
                     yield item
 
     def yield_objects():
+        from sentry.db.models.base import BaseModel
+
         # Collate the objects to be serialized.
         for model in sorted_dependencies():
-            includable = (
-                hasattr(model, "__relocation_scope__")
-                and model.get_possible_relocation_scopes() & allowed_relocation_scopes
-            )
+            if not issubclass(model, BaseModel):
+                continue
+
+            possible_relocation_scopes = model.get_possible_relocation_scopes()
+            includable = possible_relocation_scopes & allowed_relocation_scopes  # type: ignore
             if not includable or model._meta.proxy:
                 continue
 
-            queryset = model._base_manager.order_by(model._meta.pk.name)
+            queryset = model._base_manager.order_by(model._meta.pk.name)  # type: ignore
             yield from filter_objects(queryset.iterator())
 
     serialize(

+ 34 - 5
src/sentry/backup/findings.py

@@ -1,10 +1,14 @@
 from __future__ import annotations
 
+from dataclasses import dataclass
 from enum import IntEnum, auto, unique
-from typing import NamedTuple
+from typing import Optional
 
+from sentry.backup.dependencies import NormalizedModelName
 
-class InstanceID(NamedTuple):
+
+@dataclass
+class InstanceID:
     """Every entry in the generated backup JSON file should have a unique model+ordinal combination,
     which serves as its identifier."""
 
@@ -15,6 +19,13 @@ class InstanceID(NamedTuple):
     # can use the ordinal as a unique identifier.
     ordinal: int | None = None
 
+    def __init__(self, model: NormalizedModelName, ordinal: Optional[int] = None):
+        self.model = str(model)
+        self.ordinal = ordinal
+
+    def __hash__(self):
+        return hash((self.model, self.ordinal))
+
     def pretty(self) -> str:
         out = f"InstanceID(model: {self.model!r}"
         if self.ordinal:
@@ -24,6 +35,8 @@ class InstanceID(NamedTuple):
 
 @unique
 class ComparatorFindingKind(IntEnum):
+    Unknown = auto()
+
     # The instances of a particular model did not maintain total ordering of pks (that is, pks did not appear in ascending order, or appear multiple times).
     UnorderedInput = auto()
 
@@ -112,15 +125,31 @@ class ComparatorFindingKind(IntEnum):
     UserPasswordObfuscatingComparatorExistenceCheck = auto()
 
 
-class ComparatorFinding(NamedTuple):
-    """Store all information about a single failed matching between expected and actual output."""
+@dataclass(frozen=True)
+class Finding:
+    """
+    A JSON serializable and user-reportable finding for an import/export operation.
+    """
 
-    kind: ComparatorFindingKind
     on: InstanceID
+
+    # The original `pk` of the model in question, if one is specified in the `InstanceID`.
     left_pk: int | None = None
+
+    # The post-import `pk` of the model in question, if one is specified in the `InstanceID`.
     right_pk: int | None = None
+
     reason: str = ""
 
+
+@dataclass(frozen=True)
+class ComparatorFinding(Finding):
+    """
+    Store all information about a single failed matching between expected and actual output.
+    """
+
+    kind: ComparatorFindingKind = ComparatorFindingKind.Unknown
+
     def pretty(self) -> str:
         out = f"Finding(\n\tkind: {self.kind.name},\n\ton: {self.on.pretty()}"
         if self.left_pk:

+ 17 - 12
src/sentry/backup/imports.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Iterator, Tuple, Type
+from typing import Iterator, Optional, Tuple, Type
 
 import click
 from django.conf import settings
@@ -10,7 +10,7 @@ from django.db import IntegrityError, connections, router, transaction
 from django.db.models.base import Model
 from rest_framework.serializers import ValidationError as DjangoRestFrameworkValidationError
 
-from sentry.backup.dependencies import PrimaryKeyMap, get_model, normalize_model_name
+from sentry.backup.dependencies import NormalizedModelName, PrimaryKeyMap, get_model, get_model_name
 from sentry.backup.helpers import EXCLUDED_APPS, Filter, ImportFlags
 from sentry.backup.scopes import ImportScope
 from sentry.silo import unguarded_write
@@ -41,9 +41,14 @@ def _import(
     # Import here to prevent circular module resolutions.
     from sentry.models.email import Email
     from sentry.models.organization import Organization
+    from sentry.models.organizationmember import OrganizationMember
     from sentry.models.user import User
 
     flags = flags if flags is not None else ImportFlags()
+    user_model_name = get_model_name(User)
+    org_model_name = get_model_name(Organization)
+    org_member_model_name = get_model_name(OrganizationMember)
+
     start = src.tell()
     filters = []
     if filter_by is not None:
@@ -70,18 +75,18 @@ def _import(
             # importing library like ijson for this.
             for obj in serializers.deserialize("json", src, stream=True):
                 o = obj.object
-                model_name = normalize_model_name(o)
-                if model_name == "sentry.User":
+                model_name = get_model_name(o)
+                if model_name == user_model_name:
                     username = getattr(o, "username", None)
                     email = getattr(o, "email", None)
                     if username is not None and email is not None:
                         user_to_email[username] = email
-                elif model_name == "sentry.Organization":
+                elif model_name == org_model_name:
                     pk = getattr(o, "pk", None)
                     slug = getattr(o, "slug", None)
                     if pk is not None and slug in filter_by.values:
                         filtered_org_pks.add(pk)
-                elif model_name == "sentry.OrganizationMember":
+                elif model_name == org_member_model_name:
                     seen_first_org_member_model = True
                     user = getattr(o, "user_id", None)
                     org = getattr(o, "organization_id", None)
@@ -95,8 +100,8 @@ def _import(
             seen_first_user_model = False
             for obj in serializers.deserialize("json", src, stream=True):
                 o = obj.object
-                model_name = normalize_model_name(o)
-                if model_name == "sentry.User":
+                model_name = get_model_name(o)
+                if model_name == user_model_name:
                     seen_first_user_model = False
                     username = getattr(o, "username", None)
                     email = getattr(o, "email", None)
@@ -120,13 +125,13 @@ def _import(
 
     # The input JSON blob should already be ordered by model kind. We simply break up 1 JSON blob
     # with N model kinds into N json blobs with 1 model kind each.
-    def yield_json_models(src) -> Iterator[Tuple[str, str]]:
+    def yield_json_models(src) -> Iterator[Tuple[NormalizedModelName, str]]:
         # TODO(getsentry#team-ospo/190): Better error handling for unparsable JSON.
         models = json.load(src)
-        last_seen_model_name: str | None = None
+        last_seen_model_name: Optional[NormalizedModelName] = None
         batch: list[Type[Model]] = []
         for model in models:
-            model_name = model["model"]
+            model_name = NormalizedModelName(model["model"])
             if last_seen_model_name != model_name:
                 if last_seen_model_name is not None and len(batch) > 0:
                     yield (last_seen_model_name, json.dumps(batch))
@@ -158,7 +163,7 @@ def _import(
                     if o._meta.app_label not in EXCLUDED_APPS or o:
                         if o.get_possible_relocation_scopes() & allowed_relocation_scopes:
                             o = obj.object
-                            model_name = normalize_model_name(o)
+                            model_name = get_model_name(o)
                             for f in filters:
                                 if f.model == type(o) and getattr(o, f.field, None) not in f.values:
                                     break

+ 11 - 7
src/sentry/backup/validate.py

@@ -6,7 +6,7 @@ from difflib import unified_diff
 from typing import Dict, Tuple
 
 from sentry.backup.comparators import ComparatorMap, ForeignKeyComparator, get_default_comparators
-from sentry.backup.dependencies import ImportKind, PrimaryKeyMap
+from sentry.backup.dependencies import ImportKind, NormalizedModelName, PrimaryKeyMap
 from sentry.backup.findings import (
     ComparatorFinding,
     ComparatorFindingKind,
@@ -44,7 +44,7 @@ def validate(
             """Assigns the next available ordinal to the supplied `obj` model."""
 
             pk = obj["pk"]
-            model = obj["model"]
+            model_name = NormalizedModelName(obj["model"])
             findings = []
             if pk > self.max_seen_pk:
                 self.max_seen_pk = pk
@@ -52,7 +52,7 @@ def validate(
                 findings.append(
                     ComparatorFinding(
                         kind=ComparatorFindingKind.UnorderedInput,
-                        on=InstanceID(model, self.next_ordinal),
+                        on=InstanceID(model_name, self.next_ordinal),
                         left_pk=pk if side == Side.left else None,
                         right_pk=pk if side == Side.right else None,
                         reason=f"""instances not listed in ascending `pk` order; `pk` {pk} is less than or equal to {self.max_seen_pk} which precedes it""",
@@ -63,7 +63,7 @@ def validate(
             self.next_ordinal += 1
             return (obj["ordinal"], findings if findings else [])
 
-    OrdinalCounters = Dict[str, OrdinalCounter]
+    OrdinalCounters = Dict[NormalizedModelName, OrdinalCounter]
     ModelMap = Dict[InstanceID, JSONData]
 
     def build_model_map(
@@ -74,7 +74,7 @@ def validate(
         model_map: ModelMap = {}
         ordinal_counters: OrdinalCounters = defaultdict(OrdinalCounter)
         for model in models:
-            model_name = model["model"]
+            model_name = NormalizedModelName(model["model"])
             counter = ordinal_counters[model_name]
             ordinal, found = counter.assign(model, side)
             findings.extend(found)
@@ -139,8 +139,12 @@ def validate(
             raise RuntimeError("all InstanceIDs used for comparisons must have their ordinal set")
 
         left = left_models[id]
-        left_pk_map.insert(id.model, left_models[id]["pk"], id.ordinal, ImportKind.Inserted)
-        right_pk_map.insert(id.model, right["pk"], id.ordinal, ImportKind.Inserted)
+        left_pk_map.insert(
+            NormalizedModelName(id.model), left_models[id]["pk"], id.ordinal, ImportKind.Inserted
+        )
+        right_pk_map.insert(
+            NormalizedModelName(id.model), right["pk"], id.ordinal, ImportKind.Inserted
+        )
 
     # We only perform custom comparisons and JSON diffs on non-duplicate entries that exist in both
     # outputs.

Some files were not shown because too many files changed in this diff