Skip to content

ref(errors): Use enum for stacktrace order #96719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 30, 2025
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
16 changes: 14 additions & 2 deletions src/sentry/interfaces/stacktrace.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math
from enum import Enum

from django.utils.translation import gettext as _

Expand All @@ -11,6 +12,12 @@
__all__ = ("Stacktrace",)


class StacktraceOrder(str, Enum):
DEFAULT = "-1" # Equivalent to `MOST_RECENT_FIRST`
MOST_RECENT_LAST = "1"
MOST_RECENT_FIRST = "2"


def max_addr(cur, addr):
if addr is None:
return cur
Expand Down Expand Up @@ -86,16 +93,21 @@ def get_context(lineno, context_line, pre_context=None, post_context=None):


def is_newest_frame_first(event):
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this function could return a StacktraceOrder directly, there's no need to turn the enum into a boolean. Similarly, the newest_first parameter of get_stacktrace could then be a StacktraceOrder.

Copy link
Member Author

@lobsterkatie lobsterkatie Jul 30, 2025

Choose a reason for hiding this comment

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

I think those are both valid points, but my preference would be to address them in a follow-up PR, so that this one can remain solely about switching to using the enum. (Happy to add a TODO about it here, though, since it would make sense to address your points alongside addressing the Python-special-casing question.)

# TODO: Investigate if we should keep this special-casing for python, since we don't special
# case it anywhere else we check stacktrace order. If we do remove it, we might consider
# ditching this helper altogether, and just checking and using the option value directly in the
# one spot this helper is used.
# (See https://github.com/getsentry/sentry/pull/96719 for more context.)
newest_first = event.platform not in ("python", None)

if env.request and env.request.user.is_authenticated:
options = user_option_service.get_many(
filter=dict(user_ids=[env.request.user.id], keys=["stacktrace_order"])
)
display = get_option_from_list(options, default=None)
if display == "1":
if display == StacktraceOrder.MOST_RECENT_LAST:
newest_first = False
elif display == "2":
elif display == StacktraceOrder.MOST_RECENT_FIRST:
newest_first = True

return newest_first
Expand Down
19 changes: 17 additions & 2 deletions src/sentry/users/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from collections import defaultdict
from collections.abc import Mapping, MutableMapping, Sequence
from datetime import datetime
from enum import Enum
from typing import Any, DefaultDict, TypedDict, cast

from django.conf import settings
Expand All @@ -15,6 +16,7 @@
from sentry.app import env
from sentry.auth.elevated_mode import has_elevated_mode
from sentry.hybridcloud.services.organization_mapping import organization_mapping_service
from sentry.interfaces.stacktrace import StacktraceOrder
from sentry.models.authidentity import AuthIdentity
from sentry.models.organization import OrganizationStatus
from sentry.models.organizationmapping import OrganizationMapping
Expand Down Expand Up @@ -57,10 +59,16 @@ class _Identity(TypedDict):
dateSynced: datetime


class _SerializedStacktraceOrder(int, Enum):
DEFAULT = int(StacktraceOrder.DEFAULT) # Equivalent to `MOST_RECENT_FIRST`
MOST_RECENT_LAST = int(StacktraceOrder.MOST_RECENT_LAST)
MOST_RECENT_FIRST = int(StacktraceOrder.MOST_RECENT_FIRST)


class _UserOptions(TypedDict):
theme: str # TODO: enum/literal for theme options
language: str
stacktraceOrder: int # TODO: enum/literal
stacktraceOrder: _SerializedStacktraceOrder
defaultIssueEvent: str
timezone: str
clock24Hours: bool
Expand Down Expand Up @@ -191,7 +199,14 @@ def serialize(
for o in UserOption.objects.filter(user_id=user.id, project_id__isnull=True)
if o.value is not None
}
stacktrace_order = int(options.get("stacktrace_order", -1) or -1)

stacktrace_order = _SerializedStacktraceOrder(
int(
options.get("stacktrace_order", StacktraceOrder.DEFAULT)
# TODO: This second `or` won't be necessary once we remove empty strings from the DB
or StacktraceOrder.DEFAULT
)
)

d["options"] = {
"theme": options.get("theme") or "light",
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/users/models/user_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class UserOption(Model):
- self_notifications
- "Notify Me About My Own Activity"
- stacktrace_order
- default, most recent first, most recent last
- default, most recent first, most recent last (see `StacktraceOrder` enum)
- subscribe_by_default
- "Only On Issues I Subscribe To", "Only On Deploys With My Commits"
- subscribe_notes
Expand Down
10 changes: 7 additions & 3 deletions tests/sentry/users/api/endpoints/test_user_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pytest import fixture

from sentry.deletions.tasks.hybrid_cloud import schedule_hybrid_cloud_foreign_key_jobs
from sentry.interfaces.stacktrace import StacktraceOrder
from sentry.models.deletedorganization import DeletedOrganization
from sentry.models.organization import Organization, OrganizationStatus
from sentry.models.organizationmember import OrganizationMember
Expand Down Expand Up @@ -43,7 +44,7 @@ def test_lookup_self(self) -> None:
assert resp.data["options"]["defaultIssueEvent"] == "recommended"
assert resp.data["options"]["timezone"] == "UTC"
assert resp.data["options"]["language"] == "en"
assert resp.data["options"]["stacktraceOrder"] == -1
assert resp.data["options"]["stacktraceOrder"] == int(StacktraceOrder.DEFAULT)
assert not resp.data["options"]["clock24Hours"]
assert not resp.data["options"]["prefersIssueDetailsStreamlinedUI"]
assert not resp.data["options"]["prefersStackedNavigation"]
Expand Down Expand Up @@ -113,7 +114,7 @@ def test_simple(self) -> None:
"theme": "system",
"defaultIssueEvent": "latest",
"timezone": "UTC",
"stacktraceOrder": "2",
"stacktraceOrder": StacktraceOrder.MOST_RECENT_FIRST,
"language": "fr",
"clock24Hours": True,
"extra": True,
Expand All @@ -135,7 +136,10 @@ def test_simple(self) -> None:
assert UserOption.objects.get_value(user=self.user, key="theme") == "system"
assert UserOption.objects.get_value(user=self.user, key="default_issue_event") == "latest"
assert UserOption.objects.get_value(user=self.user, key="timezone") == "UTC"
assert UserOption.objects.get_value(user=self.user, key="stacktrace_order") == "2"
assert (
UserOption.objects.get_value(user=self.user, key="stacktrace_order")
== StacktraceOrder.MOST_RECENT_FIRST
)
assert UserOption.objects.get_value(user=self.user, key="language") == "fr"
assert UserOption.objects.get_value(user=self.user, key="clock_24_hours")
assert UserOption.objects.get_value(
Expand Down
Loading