Closed Bug 1675560 Opened 2 years ago Closed 1 year ago

support JavaException annotation for Java crashes

Categories

(Socorro :: Webapp, enhancement, P2)

enhancement

Tracking

(Not tracked)

RESOLVED FIXED

People

(Reporter: willkg, Assigned: willkg)

References

(Blocks 1 open bug)

Details

Attachments

(1 file)

android-components is adding a JavaException annotation which is a structured form of the Java stack trace and exception information.

It's in the same form as what Sentry sends which is nice.

This bug covers:

  1. processing the JavaException field like we do with JavaStackTrace where we have a raw form and a sanitized form that doesn't have the exception message which can contain PII (bug #1496599)
  2. making the sanitized form viewable in the report view to everyone

Bug #1541120 covers improving signature generation using this information. We'll need to implement the processor bits before working on that, so I'm adding a blocker.

Blocks: 1541120

The JavaException annotation is a JSON-encoded string that decodes to a dict. Here's an example:

{
    "exception": {
        "values": [
            {
                "stacktrace": {
                    "frames": [
                        {"module":"java.lang.AbstractStringBuilder","function":"enlargeBuffer","in_app":true,"lineno":95,"filename":"AbstractStringBuilder.java"},         
                        {"module":"java.lang.AbstractStringBuilder","function":"append0","in_app":true,"lineno":163,"filename":"AbstractStringBuilder.java"},
                        {"module":"java.lang.StringBuilder","function":"append","in_app":true,"lineno":311,"filename":"StringBuilder.java"},
                        {"module":"org.json.JSONTokener","function":"nextString","in_app":true,"lineno":212,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"nextValue","in_app":true,"lineno":107,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"readObject","in_app":true,"lineno":385,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"nextValue","in_app":true,"lineno":100,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"readArray","in_app":true,"lineno":430,"filename":"JSONTokener.java"},                    
                        {"module":"org.json.JSONTokener","function":"nextValue","in_app":true,"lineno":103,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"readObject","in_app":true,"lineno":385,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"nextValue","in_app":true,"lineno":100,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"readObject","in_app":true,"lineno":385,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONTokener","function":"nextValue","in_app":true,"lineno":100,"filename":"JSONTokener.java"},
                        {"module":"org.json.JSONObject","function":"<init>","in_app":true,"lineno":156,"filename":"JSONObject.java"},
                        {"module":"org.json.JSONObject","function":"<init>","in_app":true,"lineno":173,"filename":"JSONObject.java"},
                        {"module":"org.mozilla.geckoview.GeckoSession$SessionState","function":"fromString","in_app":true,"lineno":1,"filename":"GeckoSession.java"},
                        {"module":"mozilla.components.browser.engine.gecko.GeckoEngine","function":"createSessionState","in_app":true,"lineno":4,"filename":"GeckoEngine.kt"},
                        {"module":"mozilla.components.browser.session.storage.SnapshotSerializer","function":"itemFromJSON","in_app":true,"lineno":20,"filename":"SnapshotSerializer.kt"},
                        {"module":"mozilla.components.browser.session.storage.SnapshotSerializer","function":"fromJSON","in_app":true,"lineno":7,"filename":"SnapshotSerializer.kt"},
                        {"module":"androidx.core.app.AppOpsManagerCompat","function":"readSnapshot","in_app":true,"lineno":3,"filename":"AppOpsManagerCompat.java"},
                        {"module":"mozilla.components.browser.session.storage.SessionStorage","function":"restore","in_app":true,"lineno":3,"filename":"SessionStorage.kt"},
                        {"module":"org.mozilla.fenix.components.Core$sessionManager$2$$special$$inlined$also$lambda$1$1","function":"invokeSuspend","in_app":true,"lineno":2,"filename":"Core.kt"},
                        {"module":"kotlin.coroutines.jvm.internal.BaseContinuationImpl","function":"resumeWith","in_app":true,"lineno":3,"filename":"ContinuationImpl.kt"},
                        {"module":"kotlinx.coroutines.DispatchedTask","function":"run","in_app":true,"lineno":15,"filename":"DispatchedTask.kt"},
                        {"module":"kotlinx.coroutines.scheduling.CoroutineScheduler","function":"runSafely","in_app":true,"lineno":1,"filename":"CoroutineScheduler.kt"},
                        {"module":"kotlinx.coroutines.scheduling.CoroutineScheduler$Worker","function":"run","in_app":true,"lineno":11,"filename":"CoroutineScheduler.kt"}                    
                    ],
                    "type":"OutOfMemoryError",
                    "module":"java.lang",
                    "value":"Failed to allocate a 34516 byte allocation with 13138 free bytes and 12KB until OOM"
                }
            }
        ]
    }
}

The "exception message" is the ["stacktrace"]["value"] thing. In this case, it's "Failed to allocate a 34516 byte allocation with 13138 free bytes and 12KB until OOM". That can contain protected data.

So if someone has protected data access, they can see the whole thing and if they don't, they see everything except that part.

We do something similar with "json_dump", but it's hard to reason about whether someone accessing data is getting protected data they shouldn't be getting if it's deep in the structure. I think I want to do what we do with java_stack_trace where we have a "raw" form that has the whole structure and a "redacted form" that has everything that's fine, except the part that isn't.

Then we have:

  • raw_crash.JavaException -- protected data
  • processed_crash.java_exception_raw -- protected data
  • processed_crash.java_exception -- public data

I'm not thrilled about the naming scheme, but switching it takes 6 months and it's probably better to accrue a bunch of bad naming decisions and then fix them all at once.

The Sentry docs for the exception part of the payload are here:

https://develop.sentry.dev/sdk/event-payloads/exception/

Here's an example of a crash report with a single exception:

https://crash-stats.mozilla.org/report/index/0aeae7f7-04ef-4d43-bf31-da6c30210115

We should make sure we support cascading exceptions. I bet we have an example in our data. I'll look for one later.

Assignee: nobody → willkg
Status: NEW → ASSIGNED

Some interesting bits about the data:

  1. The frame may not have a "filename". For example, this frame has no filename:
     {'function': 'emitAllImpl$FlowKt__ChannelsKt',
      'in_app': True,
      'lineno': 13,
      'module': 'kotlinx.coroutines.flow.FlowKt'}
    
  2. The stacktrace may not have a "value". I've only seen that with NullPointerExceptions so far, but I bet there are other exceptions that don't have values.

Need to keep that in mind when displaying things.

This was deployed in bug #1690378. Marking as FIXED.

There are some follow-up changes we could make, but we'll do those in future bugs as we figure out the details.

Status: ASSIGNED → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.