Skip to content

Commit c0420fb

Browse files
authored
ref(errors): Use enum for stacktrace order (#96719)
Users can choose to display their stacktraces on the issue details page in reverse order, using the `stacktrace_order` user option. With a few caveats (see below), under the hood we store the option values as the strings `"-1"` (default), `"1"` (chronological order, with newest frame last), and `"2"` (same as default, reverse chronological order, with newest frame first).* In the front end we use numbers instead of strings, but with corresponding values. Because the mapping is not obvious, this adds to the backend a `StacktraceOrder` enum (and a `_SerializedStacktraceOrder` enum to handle the conversion to numbers), and uses them rather than the corresponding strings/numbers. A similar change for the front end will happen in a follow-up PR. Note: The description above is not strictly correct, for two reasons (each of which now has a corresponding TODO): 1) There are a small number of users which don't have any of these and instead have the empty string as their option value. This is treated the same as not having the option set at all, so it's not breaking anything, but we should nonetheless probably just clean those up. 2) `"2"` is not always the same as the default, or rather, the default behavior is not always to have the newest frame first. There is one place, (indirectly) in the `Stacktrace` interface's `to_string` method[1], where for Python events (and events with no platform, though practically speaking Relay normalization means there's always a platform) the default is the opposite, newest last[2]. This doesn't match stacktrace ordering behavior anywhere else, so it might be worth investigating if we want to keep that special-casing in place. (It's also not immediately obvious where we (ultimately) use that method. Back when the special-casing was introduced[3] (in late 2012), it was controlling the result of `Stacktrace.to_html`, but now we obviously let react create the HTML, and in our current front end there's no such exception for Python.) [1] https://github.com/getsentry/sentry/blob/5778c7a72e21e9d8c83248e0ad6b7d906c3aa02b/src/sentry/interfaces/stacktrace.py#L519-L526 [2] https://github.com/getsentry/sentry/blob/5778c7a72e21e9d8c83248e0ad6b7d906c3aa02b/src/sentry/interfaces/stacktrace.py#L89 [3] 329085b
1 parent b9278a2 commit c0420fb

File tree

4 files changed

+39
-8
lines changed

4 files changed

+39
-8
lines changed

src/sentry/interfaces/stacktrace.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import math
2+
from enum import Enum
23

34
from django.utils.translation import gettext as _
45

@@ -11,6 +12,12 @@
1112
__all__ = ("Stacktrace",)
1213

1314

15+
class StacktraceOrder(str, Enum):
16+
DEFAULT = "-1" # Equivalent to `MOST_RECENT_FIRST`
17+
MOST_RECENT_LAST = "1"
18+
MOST_RECENT_FIRST = "2"
19+
20+
1421
def max_addr(cur, addr):
1522
if addr is None:
1623
return cur
@@ -86,16 +93,21 @@ def get_context(lineno, context_line, pre_context=None, post_context=None):
8693

8794

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

91103
if env.request and env.request.user.is_authenticated:
92104
options = user_option_service.get_many(
93105
filter=dict(user_ids=[env.request.user.id], keys=["stacktrace_order"])
94106
)
95107
display = get_option_from_list(options, default=None)
96-
if display == "1":
108+
if display == StacktraceOrder.MOST_RECENT_LAST:
97109
newest_first = False
98-
elif display == "2":
110+
elif display == StacktraceOrder.MOST_RECENT_FIRST:
99111
newest_first = True
100112

101113
return newest_first

src/sentry/users/api/serializers/user.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections import defaultdict
66
from collections.abc import Mapping, MutableMapping, Sequence
77
from datetime import datetime
8+
from enum import Enum
89
from typing import Any, DefaultDict, TypedDict, cast
910

1011
from django.conf import settings
@@ -15,6 +16,7 @@
1516
from sentry.app import env
1617
from sentry.auth.elevated_mode import has_elevated_mode
1718
from sentry.hybridcloud.services.organization_mapping import organization_mapping_service
19+
from sentry.interfaces.stacktrace import StacktraceOrder
1820
from sentry.models.authidentity import AuthIdentity
1921
from sentry.models.organization import OrganizationStatus
2022
from sentry.models.organizationmapping import OrganizationMapping
@@ -57,10 +59,16 @@ class _Identity(TypedDict):
5759
dateSynced: datetime
5860

5961

62+
class _SerializedStacktraceOrder(int, Enum):
63+
DEFAULT = int(StacktraceOrder.DEFAULT) # Equivalent to `MOST_RECENT_FIRST`
64+
MOST_RECENT_LAST = int(StacktraceOrder.MOST_RECENT_LAST)
65+
MOST_RECENT_FIRST = int(StacktraceOrder.MOST_RECENT_FIRST)
66+
67+
6068
class _UserOptions(TypedDict):
6169
theme: str # TODO: enum/literal for theme options
6270
language: str
63-
stacktraceOrder: int # TODO: enum/literal
71+
stacktraceOrder: _SerializedStacktraceOrder
6472
defaultIssueEvent: str
6573
timezone: str
6674
clock24Hours: bool
@@ -191,7 +199,14 @@ def serialize(
191199
for o in UserOption.objects.filter(user_id=user.id, project_id__isnull=True)
192200
if o.value is not None
193201
}
194-
stacktrace_order = int(options.get("stacktrace_order", -1) or -1)
202+
203+
stacktrace_order = _SerializedStacktraceOrder(
204+
int(
205+
options.get("stacktrace_order", StacktraceOrder.DEFAULT)
206+
# TODO: This second `or` won't be necessary once we remove empty strings from the DB
207+
or StacktraceOrder.DEFAULT
208+
)
209+
)
195210

196211
d["options"] = {
197212
"theme": options.get("theme") or "light",

src/sentry/users/models/user_option.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class UserOption(Model):
185185
- self_notifications
186186
- "Notify Me About My Own Activity"
187187
- stacktrace_order
188-
- default, most recent first, most recent last
188+
- default, most recent first, most recent last (see `StacktraceOrder` enum)
189189
- subscribe_by_default
190190
- "Only On Issues I Subscribe To", "Only On Deploys With My Commits"
191191
- subscribe_notes

tests/sentry/users/api/endpoints/test_user_details.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pytest import fixture
33

44
from sentry.deletions.tasks.hybrid_cloud import schedule_hybrid_cloud_foreign_key_jobs
5+
from sentry.interfaces.stacktrace import StacktraceOrder
56
from sentry.models.deletedorganization import DeletedOrganization
67
from sentry.models.organization import Organization, OrganizationStatus
78
from sentry.models.organizationmember import OrganizationMember
@@ -43,7 +44,7 @@ def test_lookup_self(self) -> None:
4344
assert resp.data["options"]["defaultIssueEvent"] == "recommended"
4445
assert resp.data["options"]["timezone"] == "UTC"
4546
assert resp.data["options"]["language"] == "en"
46-
assert resp.data["options"]["stacktraceOrder"] == -1
47+
assert resp.data["options"]["stacktraceOrder"] == int(StacktraceOrder.DEFAULT)
4748
assert not resp.data["options"]["clock24Hours"]
4849
assert not resp.data["options"]["prefersIssueDetailsStreamlinedUI"]
4950
assert not resp.data["options"]["prefersStackedNavigation"]
@@ -113,7 +114,7 @@ def test_simple(self) -> None:
113114
"theme": "system",
114115
"defaultIssueEvent": "latest",
115116
"timezone": "UTC",
116-
"stacktraceOrder": "2",
117+
"stacktraceOrder": StacktraceOrder.MOST_RECENT_FIRST,
117118
"language": "fr",
118119
"clock24Hours": True,
119120
"extra": True,
@@ -135,7 +136,10 @@ def test_simple(self) -> None:
135136
assert UserOption.objects.get_value(user=self.user, key="theme") == "system"
136137
assert UserOption.objects.get_value(user=self.user, key="default_issue_event") == "latest"
137138
assert UserOption.objects.get_value(user=self.user, key="timezone") == "UTC"
138-
assert UserOption.objects.get_value(user=self.user, key="stacktrace_order") == "2"
139+
assert (
140+
UserOption.objects.get_value(user=self.user, key="stacktrace_order")
141+
== StacktraceOrder.MOST_RECENT_FIRST
142+
)
139143
assert UserOption.objects.get_value(user=self.user, key="language") == "fr"
140144
assert UserOption.objects.get_value(user=self.user, key="clock_24_hours")
141145
assert UserOption.objects.get_value(

0 commit comments

Comments
 (0)