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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/sentry/backup/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from sentry.backup.helpers import Filter, ImportFlags, Printer
from sentry.backup.scopes import ImportScope
from sentry.backup.services.import_export.impl import fixup_array_fields
from sentry.backup.services.import_export.model import (
RpcFilter,
RpcImportError,
Expand Down Expand Up @@ -148,9 +149,7 @@ def _import(
# wasteful - in the future, we should explore chunking strategies to enable a smaller memory
# footprint when processing super large (>100MB) exports.
content: bytes | str = (
decrypt_encrypted_tarball(src, decryptor)
if decryptor is not None
else src.read().decode("utf-8")
decrypt_encrypted_tarball(src, decryptor) if decryptor is not None else src.read()
)

if len(DELETED_MODELS) > 0 or len(DELETED_FIELDS) > 0:
Expand All @@ -170,6 +169,8 @@ def _import(
# Return the content to byte form, as that is what the Django deserializer expects.
content = orjson.dumps(content_as_json)

content = fixup_array_fields(content)

filters = []
if filter_by is not None:
filters.append(filter_by)
Expand Down
29 changes: 29 additions & 0 deletions src/sentry/backup/services/import_export/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
# in modules such as this one where hybrid cloud data models or service classes are
# defined, because we want to reflect on type annotations and avoid forward references.

import ast
import logging
import traceback

import sentry_sdk
from django.apps import apps
from django.contrib.postgres.fields.array import ArrayField
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.serializers import deserialize, serialize
from django.core.serializers.base import DeserializationError
Expand Down Expand Up @@ -51,6 +54,7 @@
from sentry.users.models.user import User
from sentry.users.models.userpermission import UserPermission
from sentry.users.models.userrole import UserRoleUser
from sentry.utils import json

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -88,6 +92,28 @@ def get_existing_import_chunk(
)


def fixup_array_fields[T: (str, str | bytes)](json_data: T) -> T:
# preserve for 3 versions as per https://docs.sentry.io/concepts/migration/#version-support-window
# so probably 2025.09 this can go away?
Comment on lines +96 to +97
Copy link
Member

Choose a reason for hiding this comment

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

We'll probably need to keep this around much longer, as sales often works with customers on much older sentry versions 😢

Copy link
Contributor Author

Choose a reason for hiding this comment

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

should we.... update the policy to reflect that then?

try:
contents = json.loads(json_data)
except Exception: # let the actual import/export produce a better message
return json_data

for dct in contents:
model = apps.get_model(dct["model"])
for k, v in dct["fields"].items():
if isinstance(model._meta.get_field(k), ArrayField) and isinstance(v, str):
try:
json.loads(v)
except Exception:
# old ArrayField: value was not properly encoded as json
dct["fields"][k] = json.dumps(ast.literal_eval(v))
else:
pass
return json.dumps(contents)


class UniversalImportExportService(ImportExportService):
"""
This implementation is universal regardless of which mode (CONTROL, REGION, or MONOLITH) it is
Expand Down Expand Up @@ -208,6 +234,9 @@ def import_by_model(
min_inserted_pk: int | None = None
max_inserted_pk: int | None = None
last_seen_ordinal = min_ordinal - 1

json_data = fixup_array_fields(json_data)

for deserialized_object in deserialize(
"json", json_data, use_natural_keys=False, ignorenonexistent=True
):
Expand Down
8 changes: 7 additions & 1 deletion tests/sentry/backup/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.backup.dependencies import NormalizedModelName, get_model_name
from sentry.backup.helpers import ImportFlags
from sentry.backup.services.import_export import import_export_service
from sentry.backup.services.import_export.impl import get_existing_import_chunk
from sentry.backup.services.import_export.impl import fixup_array_fields, get_existing_import_chunk
from sentry.backup.services.import_export.model import (
RpcExportError,
RpcExportErrorKind,
Expand Down Expand Up @@ -501,3 +501,9 @@ def test_bad_unspecified_scope(self):

assert isinstance(result, RpcExportError)
assert result.get_kind() == RpcExportErrorKind.UnspecifiedScope


def test_fixup_array_fields() -> None:
before = '[{"model":"sentry.dashboardwidgetquery","fields":{"aggregates":"[\'a\',\'b\']"}}]'
expect = '[{"model":"sentry.dashboardwidgetquery","fields":{"aggregates":"[\\"a\\",\\"b\\"]"}}]'
assert fixup_array_fields(before) == expect
Loading