commit 9fcc8e7f0b1624433e4228228e4ae911456e5ab0
parent b71bcebac0dfa1a23a2b0c6a032ee4170bf79360
Author: Alex Franchuk <afranchuk@mozilla.com>
Date: Tue, 4 Nov 2025 16:43:46 +0000
Bug 1989410 - Include Glean information in CrashAnnotations.yaml r=gsvelto,geckoview-reviewers,nalexander
Also cache the interpreted crash annotations so multiple consumers need
not repeat preprocessing.
Differential Revision: https://phabricator.services.mozilla.com/D265513
Diffstat:
5 files changed, 461 insertions(+), 63 deletions(-)
diff --git a/mobile/android/moz.build b/mobile/android/moz.build
@@ -60,11 +60,13 @@ GeneratedFile(
GeneratedFile(
"geckoview/src/main/java/org/mozilla/geckoview/CrashReport.java",
script="/toolkit/crashreporter/annotations/generate.py",
+ inputs=["!/toolkit/crashreporter/annotations/validated.yaml"],
)
GeneratedFile(
"android-components/components/lib/crash/src/main/java/mozilla/components/lib/crash/service/CrashReport.kt",
script="/toolkit/crashreporter/annotations/generate.py",
+ inputs=["!/toolkit/crashreporter/annotations/validated.yaml"],
)
CONFIGURE_SUBST_FILES += ["installer/Makefile"]
diff --git a/toolkit/crashreporter/CrashAnnotations.yaml b/toolkit/crashreporter/CrashAnnotations.yaml
@@ -9,16 +9,56 @@
# by the crash reporting machinery; these values can't be accessed by the
# C++/JS APIs.
#
-# Additionally a field can have the following optional fields:
+# Additionally an entry can have the following optional fields:
# - altname: A string that will be used when writing out the annotation to the
# .extra file instead of the annotation name
# - scope: A string that indicates the scope of the annotation. Valid values
# include:
# - "client": annotations which are never sent remotely (the default if unspecified),
-# - "report": annotations which are sent in reports, and
-# - "ping": annotations which are sent in pings and reports.
+# - "report": annotations which are sent in reports,
+# - "ping": annotations which are sent in pings and reports, and
+# - "ping-only": annotations which are sent only in pings.
# - skip_if: A string that will cause the annotation not to be included in the
# crash report if the contents match it.
+# - bugs: An array of bug numbers on bugzilla that relate to the annotation.
+# - glean: How the annotation is surfaced as a Glean metric. This may only be
+# present if scope is "ping" or "ping-only".
+#
+# This should be a map with the following (optional) properties:
+# - metric: Either the full name of the Glean metric (e.g. crash.foobar) or
+# the top-level Glean metric category to use (in which case the metric name
+# will be derived as the snake_case version of the annotation name).
+# Defaults to `crash` (as a category).
+# - type: The Glean metric type. Defaults based on the annotation type:
+# - `string` -> `string`
+# - `boolean` -> `boolean`
+# Other types have no default and will require an entry.
+# Valid type descriptions:
+# - `boolean`
+# - `datetime`
+# - requires an adjacent `time_unit` field
+# - `timespan`
+# - requires an adjacent `time_unit` field
+# - `string`
+# - `string_list`
+# - requires an adjacent `delimiter` field
+# - `quantity`
+# - requires an adjacent `unit` field
+# - `object`
+# - requires an adjacent `structure` field (matching the glean schema)
+# - custom_convert: If set to `true` or a string, a custom conversion function
+# will be called to convert to the metric value. When set to `true`, the
+# function will be based on the metric name. When set to a string, the
+# function will be based on the provided string.
+#
+# It is only necessary to set this to override the default conversion
+# functions or to share a conversion function -- other conversions will
+# always use a custom conversion function. Default conversion functions
+# exist for:
+# - `boolean` -> `boolean`
+# - `string` -> `string`
+# - `u32`/`u64`/`usize` -> `quantity`
+# - `string` -> `string_list`
AbortMessage:
description: >
@@ -193,7 +233,9 @@ ApplicationBuildID:
description: >
Product application's build ID.
type: string
- scope: report
+ scope: ping
+ glean:
+ metric: crash.app_build
ApplicationServicesVersion:
description: The version of Application Services libraries.
@@ -207,6 +249,19 @@ AsyncShutdownTimeout:
condition that caused the hang is contained in the annotation.
type: string
scope: ping
+ glean:
+ type: object
+ structure:
+ type: object
+ properties:
+ phase:
+ type: string
+ conditions:
+ type: string
+ broken_add_blockers:
+ type: array
+ items:
+ type: string
AvailablePageFile:
description: >
@@ -219,6 +274,10 @@ AvailablePageFile:
- Not available on other platforms.
type: usize
scope: ping
+ glean:
+ metric: memory.available_commit
+ type: quantity
+ unit: bytes
AvailablePhysicalMemory:
description: >
@@ -230,6 +289,10 @@ AvailablePhysicalMemory:
- Not available on other platforms.
type: usize
scope: ping
+ glean:
+ metric: memory.available_physical
+ type: quantity
+ unit: bytes
AvailableSwapMemory:
description: >
@@ -240,6 +303,10 @@ AvailableSwapMemory:
- Not available on other platforms.
type: usize
scope: ping
+ glean:
+ metric: memory.available_swap
+ type: quantity
+ unit: bytes
AvailableVirtualMemory:
description: >
@@ -250,6 +317,10 @@ AvailableVirtualMemory:
- For macOS, see AvailableSwapMemory, AvailablePhysicalMemory and PurgeablePhysicalMemory.
type: usize
scope: ping
+ glean:
+ metric: memory.available_virtual
+ type: quantity
+ unit: bytes
BackgroundTaskMode:
description: >
@@ -265,9 +336,13 @@ BackgroundTaskName:
BlockedDllList:
description: >
- Comma-separated list of blocked DLLS, Windows-only
+ Semicolon-separated list of blocked DLLS, Windows-only
type: string
scope: ping
+ glean:
+ metric: dll_blocklist.list
+ type: string_list
+ delimiter: ";"
BlocklistInitFailed:
description: >
@@ -275,6 +350,8 @@ BlocklistInitFailed:
type: boolean
scope: ping
skip_if: "0"
+ glean:
+ metric: dll_blocklist.init_failed
Breadcrumbs:
description: >
@@ -292,7 +369,7 @@ BuildID:
CrashType:
description: The type of crash that occurred (Android-only).
type: string
- scope: report
+ scope: ping
ContentSandboxCapabilities:
description: >
@@ -347,6 +424,10 @@ CrashTime:
Crash time in seconds since the Epoch.
type: string
scope: ping
+ glean:
+ metric: crash.time
+ type: datetime
+ time_unit: second
CycleCollector:
description: >
@@ -405,6 +486,9 @@ EventLoopNestingLevel:
type: u32
scope: ping
skip_if: "0"
+ glean:
+ type: quantity
+ unit: levels
FontName:
description: >
@@ -439,6 +523,10 @@ GPUProcessLaunchCount:
Number of times the GPU process was launched.
type: u32
scope: ping
+ glean:
+ metric: crash.gpu_process_launch
+ type: quantity
+ unit: events
GPUProcessStatus:
description: >
@@ -489,6 +577,8 @@ HeadlessMode:
True if the app was invoked in headless mode via `--headless ...` or `--backgroundtask ...`, false otherwise.
type: boolean
scope: ping
+ glean:
+ metric: environment
PHCKind:
description: >
@@ -544,7 +634,7 @@ InstallTime:
ipc_channel_error:
description: >
- Set before a content process crashes because of an IPC channel error, holds
+ Set before a content process crashes because of an IPC channel error. Holds
a description of the error.
type: string
scope: ping
@@ -651,7 +741,63 @@ JavaException:
JSON structured Java stack trace, only present on Firefox for Android if we encounter an
uncaught Java exception.
type: string
- scope: report
+ scope: ping
+ glean:
+ type: object
+ # version history (typescript for brevity)
+ # # 0
+ # {
+ # messages: string[],
+ # stack: { file: string, line: number, class_name: string, method_name: string, is_native: boolean }[]
+ # }
+ #
+ # # 1
+ # {
+ # throwables: {
+ # message: string,
+ # type_name: string,
+ # stack: { file: string, line: number, class_name: string, method_name: string, is_native: boolean }[]
+ # }[]
+ # }
+ version: 1
+ structure:
+ type: object
+ properties:
+ # The Throwable and any cause Throwables.
+ # They are always in cause order (e.g., [`e`, `e.cause`, `e.cause.cause`, ...]).
+ throwables:
+ type: array
+ items:
+ type: object
+ properties:
+ # The message of the Throwable.
+ message:
+ type: string
+ # The java fully qualified type name of the Throwable.
+ type_name:
+ type: string
+ # The stack trace, from nearest to farthest execution point, of the Throwable.
+ # This may be omitted on `cause` Throwables.
+ stack:
+ type: array
+ items:
+ type: object
+ properties:
+ # The source file containing this stack trace element.
+ file:
+ type: string
+ # The line number of the source file containing this stack trace element.
+ line:
+ type: number
+ # The name of the class containing this stack trace element.
+ class_name:
+ type: string
+ # The name of the method containing this stack trace element.
+ method_name:
+ type: string
+ # Whether the method is native.
+ is_native:
+ type: boolean
JavaStackTrace:
description: >
@@ -680,6 +826,8 @@ JSLargeAllocationFailure:
description for the possible values of this annotation.
type: string
scope: ping
+ glean:
+ metric: memory
JSModuleLoadError:
description: >
@@ -697,6 +845,8 @@ JSOutOfMemory:
to satisfy the allocation.
type: string
scope: ping
+ glean:
+ metric: memory
LastInteractionDuration:
@@ -705,6 +855,10 @@ LastInteractionDuration:
at crash. The value is not set if the user state was active.
type: u64
scope: ping
+ glean:
+ type: timespan
+ time_unit: second
+ custom_convert: duration_seconds
LastStartupWasCrash:
description: >
@@ -801,6 +955,10 @@ LowPhysicalMemoryEvents:
type: u32
scope: ping
skip_if: "0"
+ glean:
+ metric: memory.low_physical
+ type: quantity
+ unit: events
MainThreadRunnableName:
description: >
@@ -808,6 +966,12 @@ MainThreadRunnableName:
type: string
scope: ping
+MinidumpSha256Hash:
+ description: >
+ The sha256 hash of the minidump file, if available.
+ type: string
+ scope: ping
+
ModuleSignatureInfo:
description: >
A string holding a JSON object describing which entity signed the modules
@@ -829,6 +993,10 @@ NimbusEnrollments:
rollouts, as well as their branches.
type: string
scope: ping
+ glean:
+ metric: environment
+ type: string_list
+ delimiter: ","
Notes:
description: >
@@ -842,6 +1010,10 @@ OOMAllocationSize:
type: usize
scope: ping
skip_if: "0"
+ glean:
+ metric: memory
+ type: quantity
+ unit: bytes
PluginFilename:
description: >
@@ -868,7 +1040,7 @@ ProcessType:
Type of the process that crashed, the possible values are defined in
GeckoProcessTypes.h.
type: string
- scope: report
+ scope: ping
ProductName:
description: >
@@ -901,6 +1073,10 @@ PurgeablePhysicalMemory:
vm_statistics64_data_t::purgeable_count * vm_page_size.
type: usize
scope: ping
+ glean:
+ metric: memory.purgeable_physical
+ type: quantity
+ unit: bytes
QuotaManagerShutdownTimeout:
description: >
@@ -932,6 +1108,12 @@ QuotaManagerShutdownTimeout:
started. These include the time difference and the type of object.
type: string
scope: ping
+ glean:
+ type: object
+ structure:
+ type: array
+ items:
+ type: string
QuotaManagerStorageIsNetworkResource:
description: >
@@ -951,6 +1133,8 @@ ReleaseChannel:
Application release channel (e.g. default, beta, ...)
type: string
scope: ping
+ glean:
+ metric: crash.app_channel
RemoteAgent:
description: >
@@ -975,6 +1159,11 @@ SecondsSinceLastCrash:
Time in seconds since the last crash occurred.
type: u64
scope: ping
+ glean:
+ metric: crash.time_since_last_crash
+ type: timespan
+ time_unit: second
+ custom_convert: duration_seconds
ServerURL:
description: >
@@ -1002,6 +1191,70 @@ StackTraces:
sent in pings, however they are extracted and transformed into a different
format. The field in the extra file is meant only for client use.
type: object
+ scope: ping-only
+ glean:
+ type: object
+ # All addresses are hex strings rather than numbers.
+ structure:
+ type: object
+ properties:
+ # An error from the stack trace analysis, omitted if no errors occurred.
+ error:
+ type: string
+ # The type of crash (SIGSEGV, assertion, etc)
+ crash_type:
+ type: string
+ crash_address:
+ type: string
+ # Index of the crash thread
+ crash_thread:
+ type: number
+ # Index of the main executable module.
+ main_module:
+ type: number
+ # Modules ordered by base memory address.
+ modules:
+ type: array
+ items:
+ type: object
+ properties:
+ base_address:
+ type: string
+ end_address:
+ type: string
+ # Unique ID of the module
+ code_id:
+ type: string
+ debug_file:
+ type: string
+ debug_id:
+ type: string
+ filename:
+ type: string
+ version:
+ type: string
+ # Stack traces for each thread.
+ threads:
+ type: array
+ items:
+ type: object
+ properties:
+ # Frames in the thread stack trace
+ frames:
+ type: array
+ items:
+ type: object
+ properties:
+ # Index of the module that contains the frame
+ module_index:
+ type: number
+ # The instruction pointer of the frame
+ ip:
+ type: string
+ # Trust of the frame, one of "context", "prewalked", "cfi",
+ # "frame_pointer", "cfi_scan", "scan", or "none"
+ trust:
+ type: string
StartupCacheValid:
description: >
@@ -1015,6 +1268,8 @@ StartupCrash:
If set to 1 then this crash occurred during startup.
type: boolean
scope: ping
+ glean:
+ metric: crash.startup
StartupTime:
description: >
@@ -1049,6 +1304,10 @@ SystemMemoryUsePercentage:
field.
type: u32
scope: ping
+ glean:
+ metric: memory.system_use_percentage
+ type: quantity
+ unit: percent
TelemetryClientId:
description: >
@@ -1107,6 +1366,10 @@ TextureUsage:
type: usize
scope: ping
skip_if: "0"
+ glean:
+ metric: memory.texture
+ type: quantity
+ unit: bytes
Throttleable:
description: >
@@ -1127,6 +1390,10 @@ TotalPageFile:
- Not available on other systems.
type: usize
scope: ping
+ glean:
+ metric: memory
+ type: quantity
+ unit: bytes
TotalPhysicalMemory:
description: >
@@ -1138,6 +1405,10 @@ TotalPhysicalMemory:
- Not available on other systems.
type: usize
scope: ping
+ glean:
+ metric: memory.total_physical
+ type: quantity
+ unit: bytes
TotalVirtualMemory:
description: >
@@ -1147,6 +1418,10 @@ TotalVirtualMemory:
- Not available on other platforms.
type: usize
scope: ping
+ glean:
+ metric: memory.total_virtual
+ type: quantity
+ unit: bytes
UnknownNetAddrSocketFamily:
description: >
@@ -1161,6 +1436,10 @@ UptimeTS:
because it has a fractional component.
type: string # This is a floating-point number but we treat it as a string
scope: ping
+ glean:
+ metric: environment.uptime
+ type: timespan
+ time_unit: millisecond
URL:
description: >
@@ -1180,6 +1459,8 @@ User32BeforeBlocklist:
type: boolean
scope: ping
skip_if: "0"
+ glean:
+ metric: dll_blocklist.user32_loaded_before
useragent_locale:
description: >
@@ -1205,6 +1486,9 @@ UtilityActorsName:
Comma-separated list of IPC actors name running on this Utility process instance
type: string
scope: ping
+ glean:
+ type: string_list
+ delimiter: ","
Vendor:
description: >
@@ -1217,6 +1501,8 @@ Version:
Product version.
type: string
scope: ping
+ glean:
+ metric: crash.app_display_version
VRProcessStatus:
description: >
@@ -1238,6 +1524,9 @@ WindowsFileDialogErrorCode:
for the value.
type: u32 # This is an HRESULT which is defined as signed, but we don't want to print it as a signed integer
scope: ping
+ glean:
+ metric: crash.windows.file_dialog_error_code
+ type: string
WindowsPackageFamilyName:
description: >
@@ -1248,6 +1537,8 @@ WindowsPackageFamilyName:
starts "Mozilla." or "MozillaCorporation.".
type: string
scope: ping
+ glean:
+ metric: windows.package_family_name
WindowsErrorReporting:
description: >
@@ -1255,6 +1546,8 @@ WindowsErrorReporting:
runtime exception module.
type: boolean
scope: ping
+ glean:
+ metric: crash.windows.error_reporting
Winsock_LSP:
description: >
diff --git a/toolkit/crashreporter/annotations/generate.py b/toolkit/crashreporter/annotations/generate.py
@@ -12,63 +12,12 @@ import yaml
# Language-agnostic functionality #
###############################################################################
-annotations_filename = path.join(path.dirname(__file__), "..", "CrashAnnotations.yaml")
-
template_header = (
"/* This file was autogenerated by "
"toolkit/crashreporter/annotations/generate.py. DO NOT EDIT */\n\n"
)
-def sort_annotations(annotations):
- """Return annotations in ascending alphabetical order ignoring case"""
-
- return sorted(annotations.items(), key=lambda annotation: str.lower(annotation[0]))
-
-
-def validate_annotations(annotations):
- """Ensure that the annotations have all the required fields"""
-
- for name, data in annotations:
- if "description" not in data:
- sys.exit("Annotation " + name + " does not have a description\n")
- if "type" not in data:
- sys.exit("Annotation " + name + " does not have a type\n")
-
- annotation_type = data.get("type")
- valid_types = ["string", "boolean", "u32", "u64", "usize", "object"]
- if annotation_type not in valid_types:
- sys.exit(
- "Annotation " + name + " has an unknown type: " + annotation_type + "\n"
- )
-
- annotation_scope = data.get("scope", "client")
- valid_scopes = ["client", "report", "ping", "ping-only"]
- if annotation_scope not in valid_scopes:
- sys.exit(
- "Annotation "
- + name
- + " has an unknown scope: "
- + annotation_scope
- + "\n"
- )
-
-
-def read_annotations():
- """Read the annotations from a YAML file.
- If an error is encountered quit the program."""
-
- try:
- with open(annotations_filename) as annotations_file:
- annotations = sort_annotations(yaml.safe_load(annotations_file))
- except (OSError, ValueError) as e:
- sys.exit("Error parsing " + annotations_filename + ":\n" + str(e) + "\n")
-
- validate_annotations(annotations)
-
- return annotations
-
-
def read_template(template_filename):
"""Read the contents of the template.
If an error is encountered quit the program."""
@@ -257,8 +206,9 @@ def emit_java(annotations, output_name):
# Main script entrypoint for GeneratedFile
-def main(output):
- annotations = read_annotations()
+def main(output, annotations_path):
+ with open(annotations_path) as annotations_file:
+ annotations = yaml.safe_load(annotations_file)
suffix = output.name.rpartition(".")[2]
generator = generators.get(suffix)
@@ -278,4 +228,4 @@ def main(output):
except OSError as ex:
sys.exit("Error while writing out the generated file:\n" + str(ex) + "\n")
- return {annotations_filename, template_path}
+ return {template_path}
diff --git a/toolkit/crashreporter/annotations/load.py b/toolkit/crashreporter/annotations/load.py
@@ -0,0 +1,150 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+from os import path
+
+import yaml
+
+__all__ = ["annotations_filename", "read_annotations"]
+
+annotations_filename = path.normpath(
+ path.join(path.dirname(__file__), "..", "CrashAnnotations.yaml")
+)
+
+
+def sort_annotations(annotations):
+ """Return annotations in ascending alphabetical order ignoring case"""
+
+ return sorted(annotations.items(), key=lambda annotation: str.lower(annotation[0]))
+
+
+# Convert CamelCase to snake_case. Also supports CAPCamelCase.
+def camel_to_snake(s):
+ if s.islower():
+ return s
+ lowers = [c.islower() for c in s] + [False]
+ words = []
+ last = 0
+ for i in range(1, len(s)):
+ if not lowers[i] and (lowers[i - 1] or lowers[i + 1]):
+ words.append(s[last:i])
+ last = i
+ words.append(s[last:])
+ return "_".join(words).lower()
+
+
+class AnnotationValidator:
+ def __init__(self, name):
+ self._name = name
+ self.passed = True
+
+ def validate(self, data):
+ """
+ Ensure that the annotation has all the required fields, and elaborate
+ default values.
+ """
+ if "description" not in data:
+ self._error("does not have a description")
+ annotation_type = data.get("type")
+ if annotation_type is None:
+ self._error("does not have a type")
+
+ valid_types = ["string", "boolean", "u32", "u64", "usize", "object"]
+ if annotation_type and annotation_type not in valid_types:
+ self._error(f"has an unknown type: {annotation_type}")
+ annotation_type = None
+
+ annotation_scope = data.setdefault("scope", "client")
+ valid_scopes = ["client", "report", "ping", "ping-only"]
+ if annotation_scope not in valid_scopes:
+ self._error(f"has an unknown scope: {annotation_scope}")
+ annotation_scope = None
+
+ is_ping = annotation_scope and annotation_scope in ["ping", "ping-only"]
+
+ if annotation_scope and "glean" in data and not is_ping:
+ self._error("has a glean metric specification but does not have ping scope")
+
+ if annotation_type and is_ping:
+ self._glean(annotation_type, data.setdefault("glean", {}))
+
+ def _error(self, message):
+ print(
+ f"{annotations_filename}: Annotation {self._name} {message}.",
+ file=sys.stderr,
+ )
+ self.passed = False
+
+ def _glean(self, annotation_type, glean):
+ if not isinstance(glean, dict):
+ self._error("has invalid glean metric specification (expected a map)")
+ return
+
+ glean_metric_name = glean.setdefault("metric", "crash")
+ # If only a category is given, derive the metric name from the annotation name.
+ if "." not in glean_metric_name:
+ glean_metric_name = glean["metric"] = (
+ f"{glean_metric_name}.{camel_to_snake(self._name)}"
+ )
+
+ glean_default_type = (
+ annotation_type if annotation_type in ["string", "boolean"] else None
+ )
+ glean_type = glean.setdefault("type", glean_default_type)
+ if glean_type is None:
+ self._error("must have a glean metric type specified")
+
+ glean_types = [
+ "boolean",
+ "datetime",
+ "timespan",
+ "string",
+ "string_list",
+ "quantity",
+ "object",
+ ]
+ if glean_type and glean_type not in glean_types:
+ self._error(f"has an invalid glean metric type ({glean_type})")
+ glean_type = None
+
+ metric_required_fields = {
+ "datetime": ["time_unit"],
+ "timespan": ["time_unit"],
+ "quantity": ["unit"],
+ "string_list": ["delimiter"],
+ "object": ["structure"],
+ }
+
+ required_fields = metric_required_fields.get(glean_type, [])
+ for field in required_fields:
+ if field not in glean:
+ self._error(f"requires a `{field}` field for glean {glean_type} metric")
+
+
+def read_annotations():
+ """Read the annotations from the YAML file.
+ If an error is encountered quit the program."""
+
+ try:
+ with open(annotations_filename) as annotations_file:
+ annotations = sort_annotations(yaml.safe_load(annotations_file))
+ except (OSError, ValueError) as e:
+ sys.exit("Error parsing " + annotations_filename + ":\n" + str(e) + "\n")
+
+ valid = True
+ for name, data in annotations:
+ validator = AnnotationValidator(name)
+ validator.validate(data)
+ valid &= validator.passed
+
+ if not valid:
+ sys.exit(1)
+
+ return annotations
+
+
+def main(output):
+ yaml.safe_dump(read_annotations(), stream=output)
+ return {annotations_filename}
diff --git a/toolkit/crashreporter/annotations/moz.build b/toolkit/crashreporter/annotations/moz.build
@@ -14,8 +14,11 @@ UNIFIED_SOURCES = [
FINAL_LIBRARY = "xul"
+GeneratedFile("validated.yaml", script="load.py")
+
# Generate CrashAnnotations.h
GeneratedFile(
"CrashAnnotations.h",
script="generate.py",
+ inputs=["!validated.yaml"],
)