Skip to content

Commit e89220b

Browse files
authored
feat(symbolication): Make frame order explicit (#103638)
This adds an enum `FrameOrder` with variants `callee_first` and `caller_first` to distinguish two common ways stack frames are stored, processed, and displayed (innermost frame at the top and at the bottom, respectively). This enum is now passed to all stacktrace-processing methods Symbolicator methods. Since Symbolicator does not (yet) read the parameter, there are no functional changes. The intention of this is to clarify in which order frames are sent to Symbolicator, which currently differs between platforms because we never properly documented or enforced this. Once Symbolicator becomes able to process the new parameter, there may also be opportunities for simplification. As a somewhat related change, this also adjusts some JS tests so they sort the "scraping_attempts" returned by Symbolicator before checking them. This is sound because there is no intended order of scraping attempts anyway, and makes sure tests pass once Symbolicator is allowed to process frames in a different order. Fixes INGEST-640.
1 parent c93fde5 commit e89220b

File tree

6 files changed

+109
-16
lines changed

6 files changed

+109
-16
lines changed

src/sentry/lang/java/processing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sentry.lang.java.utils import JAVA_PLATFORMS, get_jvm_images, get_proguard_images
77
from sentry.lang.java.view_hierarchies import ViewHierarchies
88
from sentry.lang.native.error import SymbolicationFailed, write_error
9-
from sentry.lang.native.symbolicator import Symbolicator
9+
from sentry.lang.native.symbolicator import FrameOrder, Symbolicator
1010
from sentry.models.eventerror import EventError
1111
from sentry.models.project import Project
1212
from sentry.models.release import Release
@@ -220,6 +220,9 @@ def process_jvm_stacktraces(symbolicator: Symbolicator, data: Any) -> Any:
220220
modules=modules,
221221
release_package=release_package,
222222
classes=window_class_names + exception_class_names,
223+
# We are sending frames in the same order in which
224+
# they were stored in the event, so this has to be "caller_first".
225+
frame_order=FrameOrder.caller_first,
223226
)
224227

225228
if not _handle_response_status(data, response):

src/sentry/lang/javascript/processing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sentry.debug_files.artifact_bundles import maybe_renew_artifact_bundles_from_processing
66
from sentry.lang.javascript.utils import JAVASCRIPT_PLATFORMS
77
from sentry.lang.native.error import SymbolicationFailed, write_error
8-
from sentry.lang.native.symbolicator import Symbolicator
8+
from sentry.lang.native.symbolicator import FrameOrder, Symbolicator
99
from sentry.models.eventerror import EventError
1010
from sentry.stacktraces.processing import find_stacktraces_in_data
1111
from sentry.utils import metrics
@@ -252,6 +252,9 @@ def process_js_stacktraces(symbolicator: Symbolicator, data: Any) -> Any:
252252
modules=modules,
253253
release=data.get("release"),
254254
dist=data.get("dist"),
255+
# We are sending frames in the same order in which
256+
# they were stored in the event, so this has to be "caller_first".
257+
frame_order=FrameOrder.caller_first,
255258
)
256259

257260
if not _handle_response_status(data, response):

src/sentry/lang/native/processing.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from sentry import options
1313
from sentry.lang.native.error import SymbolicationFailed, write_error
14-
from sentry.lang.native.symbolicator import Symbolicator
14+
from sentry.lang.native.symbolicator import FrameOrder, Symbolicator
1515
from sentry.lang.native.utils import (
1616
get_event_attachment,
1717
get_os_from_event,
@@ -471,7 +471,13 @@ def process_native_stacktraces(symbolicator: Symbolicator, data: Any) -> Any:
471471

472472
metrics.incr("process.native.symbolicate.request")
473473
response = symbolicator.process_payload(
474-
platform=data.get("platform"), stacktraces=stacktraces, modules=modules, signal=signal
474+
platform=data.get("platform"),
475+
stacktraces=stacktraces,
476+
modules=modules,
477+
# Frames were reversed in `get_frames_for_symbolication`,
478+
# so this has to be "callee_first".
479+
frame_order=FrameOrder.callee_first,
480+
signal=signal,
475481
)
476482

477483
if not _handle_response_status(data, response):

src/sentry/lang/native/symbolicator.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ class SymbolicatorPlatform(Enum):
4747
native = "native"
4848

4949

50+
class FrameOrder(Enum):
51+
"""The order in which stack frames are sent to
52+
and returned from Symbolicator."""
53+
54+
# Caller frames come before callee frames. This is the
55+
# order in which stack frames are stored in events.
56+
caller_first = "caller_first"
57+
# Callee frames come before caller frames. This is the
58+
# order in which stack frames are usually displayed.
59+
callee_first = "callee_first"
60+
61+
5062
@dataclass(frozen=True)
5163
class SymbolicatorTaskKind:
5264
"""Bundles information about a symbolication task:
@@ -261,8 +273,20 @@ def process_applecrashreport(self, platform: str, report: CachedAttachment):
261273
return process_response(res)
262274

263275
def process_payload(
264-
self, platform, stacktraces, modules, signal=None, apply_source_context=True
276+
self, platform, stacktraces, modules, frame_order, signal=None, apply_source_context=True
265277
):
278+
"""
279+
Process a native event by symbolicating its frames.
280+
281+
:param platform: The event's platform. This should be either unset or one of "objc", "cocoa", "swift", "native", "c", "csharp".
282+
:param stacktraces: The event's stacktraces. Frames must contain an `instruction_address`.
283+
Frames are expected to be ordered according to the frame_order (see below).
284+
:param modules: ProGuard modules and source bundles. They must contain a `uuid` and have a
285+
`type` of either "proguard" or "source".
286+
:param frame_order: The order of frames within stacktraces. See the documentation of `FrameOrder`.
287+
:param signal: A numeric crash signal value. This is optional.
288+
:param apply_source_context: Whether to add source context to frames.
289+
"""
266290
(sources, process_response) = sources_for_symbolication(self.project)
267291
scraping_config = get_scraping_config(self.project)
268292
json = {
@@ -271,6 +295,7 @@ def process_payload(
271295
"options": {
272296
"dif_candidates": True,
273297
"apply_source_context": apply_source_context,
298+
"frame_order": frame_order.value,
274299
},
275300
"stacktraces": stacktraces,
276301
"modules": modules,
@@ -283,7 +308,22 @@ def process_payload(
283308
res = self._process("symbolicate_stacktraces", "symbolicate", json=json)
284309
return process_response(res)
285310

286-
def process_js(self, platform, stacktraces, modules, release, dist, apply_source_context=True):
311+
def process_js(
312+
self, platform, stacktraces, modules, release, dist, frame_order, apply_source_context=True
313+
):
314+
"""
315+
Process a JS event by remapping its frames with sourcemaps.
316+
317+
:param platform: The event's platform. This should be unset, "javascript", or "node".
318+
:param stacktraces: The event's stacktraces. Frames must contain a `function` and a `module`.
319+
Frames are expected to be ordered according to the frame_order (see below).
320+
:param modules: Minified source files/sourcemaps Thy must contain a `type` field with value "sourcemap",
321+
a `code_file`, and a `debug_id`.
322+
:param release: The event's release.
323+
:param dist: The event's dist.
324+
:param frame_order: The order of frames within stacktraces. See the documentation of `FrameOrder`.
325+
:param apply_source_context: Whether to add source context to frames.
326+
"""
287327
source = get_internal_artifact_lookup_source(self.project)
288328
scraping_config = get_scraping_config(self.project)
289329

@@ -292,7 +332,10 @@ def process_js(self, platform, stacktraces, modules, release, dist, apply_source
292332
"source": source,
293333
"stacktraces": stacktraces,
294334
"modules": modules,
295-
"options": {"apply_source_context": apply_source_context},
335+
"options": {
336+
"apply_source_context": apply_source_context,
337+
"frame_order": frame_order.value,
338+
},
296339
"scraping": scraping_config,
297340
}
298341

@@ -311,6 +354,7 @@ def process_jvm(
311354
modules,
312355
release_package,
313356
classes,
357+
frame_order,
314358
apply_source_context=True,
315359
):
316360
"""
@@ -320,10 +364,12 @@ def process_jvm(
320364
:param platform: The event's platform. This should be either unset or "java".
321365
:param exceptions: The event's exceptions. These must contain a `type` and a `module`.
322366
:param stacktraces: The event's stacktraces. Frames must contain a `function` and a `module`.
367+
Frames are expected to be ordered according to the frame_order (see below).
323368
:param modules: ProGuard modules and source bundles. They must contain a `uuid` and have a
324369
`type` of either "proguard" or "source".
325370
:param release_package: The name of the release's package. This is optional.
326371
Used for determining whether frames are in-app.
372+
:param frame_order: The order of frames within stacktraces. See the documentation of `FrameOrder`.
327373
:param apply_source_context: Whether to add source context to frames.
328374
"""
329375
source = get_internal_source(self.project)
@@ -335,7 +381,10 @@ def process_jvm(
335381
"stacktraces": stacktraces,
336382
"modules": modules,
337383
"classes": classes,
338-
"options": {"apply_source_context": apply_source_context},
384+
"options": {
385+
"apply_source_context": apply_source_context,
386+
"frame_order": frame_order.value,
387+
},
339388
}
340389

341390
if release_package is not None:

src/sentry/profiles/task.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@
2929
from sentry.constants import DataCategory
3030
from sentry.lang.javascript.processing import _handles_frame as is_valid_javascript_frame
3131
from sentry.lang.native.processing import _merge_image
32-
from sentry.lang.native.symbolicator import Symbolicator, SymbolicatorPlatform, SymbolicatorTaskKind
32+
from sentry.lang.native.symbolicator import (
33+
FrameOrder,
34+
Symbolicator,
35+
SymbolicatorPlatform,
36+
SymbolicatorTaskKind,
37+
)
3338
from sentry.lang.native.utils import native_images_from_data
3439
from sentry.models.eventerror import EventError
3540
from sentry.models.files.utils import get_profiles_storage
@@ -424,6 +429,9 @@ def _symbolicate_profile(profile: Profile, project: Project) -> bool:
424429
profile=profile,
425430
modules=raw_modules,
426431
stacktraces=raw_stacktraces,
432+
# Frames in a profile aren't inherently ordered,
433+
# but returned inlinees should be ordered callee first.
434+
frame_order=FrameOrder.callee_first,
427435
platform=platform,
428436
)
429437

@@ -599,6 +607,7 @@ def symbolicate(
599607
profile: Profile,
600608
modules: list[Any],
601609
stacktraces: list[Any],
610+
frame_order: FrameOrder,
602611
platform: str,
603612
) -> Any:
604613
if platform in SHOULD_SYMBOLICATE_JS:
@@ -608,6 +617,7 @@ def symbolicate(
608617
modules=modules,
609618
release=profile.get("release"),
610619
dist=profile.get("dist"),
620+
frame_order=frame_order,
611621
apply_source_context=False,
612622
)
613623
elif platform == "android":
@@ -617,13 +627,15 @@ def symbolicate(
617627
stacktraces=stacktraces,
618628
modules=modules,
619629
release_package=profile.get("transaction_metadata", {}).get("app.identifier"),
630+
frame_order=frame_order,
620631
apply_source_context=False,
621632
classes=[],
622633
)
623634
return symbolicator.process_payload(
624635
platform=platform,
625636
stacktraces=stacktraces,
626637
modules=modules,
638+
frame_order=frame_order,
627639
apply_source_context=False,
628640
)
629641

@@ -638,6 +650,7 @@ def run_symbolicate(
638650
profile: Profile,
639651
modules: list[Any],
640652
stacktraces: list[Any],
653+
frame_order: FrameOrder,
641654
platform: str,
642655
) -> tuple[list[Any], list[Any], bool]:
643656
symbolication_start_time = time()
@@ -665,6 +678,7 @@ def on_symbolicator_request() -> None:
665678
profile=profile,
666679
stacktraces=stacktraces,
667680
modules=modules,
681+
frame_order=frame_order,
668682
platform=platform,
669683
)
670684

@@ -943,6 +957,9 @@ def on_symbolicator_request() -> None:
943957
)
944958
},
945959
],
960+
# Methods in a profile aren't inherently ordered, but the order of returned
961+
# inlinees should be caller first.
962+
frame_order=FrameOrder.caller_first,
946963
platform=profile["platform"],
947964
)
948965
if response:

tests/relay_integration/lang/javascript/test_plugin.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,10 @@ def test_sourcemap_source_expansion(self) -> None:
354354
}
355355
]
356356

357-
assert event.data["scraping_attempts"] == [
357+
scraping_attempts = sorted(
358+
event.data["scraping_attempts"], key=lambda attempt: attempt["url"]
359+
)
360+
assert scraping_attempts == [
358361
{"status": "not_attempted", "url": "http://example.com/file.min.js"},
359362
{"status": "not_attempted", "url": "http://example.com/file.sourcemap.js"},
360363
{"status": "not_attempted", "url": "http://example.com/file1.js"},
@@ -461,7 +464,10 @@ def test_sourcemap_webpack(self) -> None:
461464
exception = event.interfaces["exception"]
462465
frame_list = exception.values[0].stacktrace.frames
463466

464-
assert event.data["scraping_attempts"] == [
467+
scraping_attempts = sorted(
468+
event.data["scraping_attempts"], key=lambda attempt: attempt["url"]
469+
)
470+
assert scraping_attempts == [
465471
{"url": "http://example.com/webpack1.min.js", "status": "not_attempted"},
466472
{"url": "http://example.com/webpack1.min.js.map", "status": "not_attempted"},
467473
{"url": "http://example.com/webpack2.min.js", "status": "not_attempted"},
@@ -564,7 +570,10 @@ def test_sourcemap_embedded_source_expansion(self) -> None:
564570
}
565571
]
566572

567-
assert event.data["scraping_attempts"] == [
573+
scraping_attempts = sorted(
574+
event.data["scraping_attempts"], key=lambda attempt: attempt["url"]
575+
)
576+
assert scraping_attempts == [
568577
{"status": "not_attempted", "url": "http://example.com/embedded.js"},
569578
{"status": "not_attempted", "url": "http://example.com/embedded.js.map"},
570579
]
@@ -634,7 +643,10 @@ def test_sourcemap_nofiles_source_expansion(self) -> None:
634643

635644
assert "errors" not in event.data
636645

637-
assert event.data["scraping_attempts"] == [
646+
scraping_attempts = sorted(
647+
event.data["scraping_attempts"], key=lambda attempt: attempt["url"]
648+
)
649+
assert scraping_attempts == [
638650
{"url": "app:///nofiles.js", "status": "not_attempted"},
639651
{"url": "app:///nofiles.js.map", "status": "not_attempted"},
640652
]
@@ -710,11 +722,14 @@ def test_indexed_sourcemap_source_expansion(self) -> None:
710722

711723
assert "errors" not in event.data
712724

713-
assert event.data["scraping_attempts"] == [
714-
{"status": "not_attempted", "url": "http://example.com/indexed.min.js"},
715-
{"status": "not_attempted", "url": "http://example.com/indexed.sourcemap.js"},
725+
scraping_attempts = sorted(
726+
event.data["scraping_attempts"], key=lambda attempt: attempt["url"]
727+
)
728+
assert scraping_attempts == [
716729
{"status": "not_attempted", "url": "http://example.com/file1.js"},
717730
{"status": "not_attempted", "url": "http://example.com/file2.js"},
731+
{"status": "not_attempted", "url": "http://example.com/indexed.min.js"},
732+
{"status": "not_attempted", "url": "http://example.com/indexed.sourcemap.js"},
718733
]
719734

720735
exception = event.interfaces["exception"]

0 commit comments

Comments
 (0)