diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 02021f94..d0dd3f4c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -211,8 +211,9 @@ elseif(DD_CRASH_MODE STREQUAL "inprocess") message(FATAL_ERROR "In-process crash handler not supported on this platform") endif() - # In-process implementation needs to write crash report files + # In-process implementation needs to write crash report and context files target_sources(dd_native PRIVATE + datadog/impl/platform/crash_context_write.cpp datadog/impl/platform/crash_report_write.cpp datadog/impl/platform/crash_handler_buildid_cache.cpp ) diff --git a/src/datadog/impl/core/feature_types/rum.hpp b/src/datadog/impl/core/feature_types/rum.hpp index 142032f9..b8e530ce 100644 --- a/src/datadog/impl/core/feature_types/rum.hpp +++ b/src/datadog/impl/core/feature_types/rum.hpp @@ -126,6 +126,11 @@ struct RumFeatureContext { UUID session_id; // UUID::Zero if no active session UUID view_id; // UUID::Zero if no active view UUID action_id; // UUID::Zero if no active action + + bool operator==(const RumFeatureContext& other) const { + return application_id == other.application_id && session_id == other.session_id && + view_id == other.view_id && action_id == other.action_id; + } }; DATADOG_STRING_ENUM( diff --git a/src/datadog/impl/features/crash_reporting/crash_reporting.cpp b/src/datadog/impl/features/crash_reporting/crash_reporting.cpp index 9025860c..a98262c0 100644 --- a/src/datadog/impl/features/crash_reporting/crash_reporting.cpp +++ b/src/datadog/impl/features/crash_reporting/crash_reporting.cpp @@ -6,7 +6,10 @@ #include "datadog/impl/features/crash_reporting/crash_reporting.hpp" +#include + #include "datadog/impl/assert.hpp" +#include "datadog/impl/core/feature_message.hpp" #include "datadog/impl/core/writer.hpp" namespace datadog::impl { @@ -24,6 +27,27 @@ std::optional CrashReporting::UploadThread_PrepareReport( return std::nullopt; } +std::optional> +CrashReporting::MakeMessageHandler() { + const auto weak_self = weak_from_this(); + return [weak_self](const FeatureMessage& msg) { + const auto* context_changed = std::get_if(&msg); + if (!context_changed || !context_changed->context.rum) { + return; + } + auto self = std::static_pointer_cast(weak_self.lock()); + if (!self || !self->_crash_handler) { + return; + } + const auto& rum_ctx = *context_changed->context.rum; + if (self->_last_rum_ctx == rum_ctx) { + return; + } + self->_last_rum_ctx = rum_ctx; + self->_crash_handler->SetRumContext(rum_ctx); + }; +} + void CrashReporting::Start() { DATADOG_ASSERT(_scope, "CrashReporting::Start called without valid FeatureScope"); @@ -46,6 +70,7 @@ void CrashReporting::Start() { _scope->diagnostic_logger.Status("Crash handler initialized"); } else { _scope->diagnostic_logger.Error("Crash handler initialization failed"); + _crash_handler.reset(); } } } diff --git a/src/datadog/impl/features/crash_reporting/crash_reporting.hpp b/src/datadog/impl/features/crash_reporting/crash_reporting.hpp index 3f2dfd23..510f2aa9 100644 --- a/src/datadog/impl/features/crash_reporting/crash_reporting.hpp +++ b/src/datadog/impl/features/crash_reporting/crash_reporting.hpp @@ -12,6 +12,7 @@ #include #include "datadog/impl/core/feature.hpp" +#include "datadog/impl/core/feature_types/rum.hpp" #include "datadog/impl/platform/clock.hpp" #include "datadog/impl/platform/crash_handler.hpp" @@ -34,6 +35,9 @@ class CrashReporting final : public Feature { const HttpContext& context, BatchReader& reader ) override; + std::optional> + MakeMessageHandler() override; + protected: /** * Responds to SDK start by initializing the Crashpad crash handler process. @@ -53,6 +57,10 @@ class CrashReporting final : public Feature { // Platform-specific crash handler implementation std::unique_ptr _crash_handler; + + // Last RUM context forwarded to the crash handler; used to suppress redundant + // SetRumContext calls when the context hasn't actually changed + std::optional _last_rum_ctx; }; } // namespace datadog::impl diff --git a/src/datadog/impl/platform/crash_context.hpp b/src/datadog/impl/platform/crash_context.hpp new file mode 100644 index 00000000..7601d5e0 --- /dev/null +++ b/src/datadog/impl/platform/crash_context.hpp @@ -0,0 +1,30 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#pragma once + +#include + +/** + * Example layout: + * + * 0x0000: + * 0x0008: version + * 0x0010: rum_application_id + * 0x0020: rum_session_id + * 0x0030: rum_view_id + * 0x0040: rum_action_id + * 0x0050: + */ + +namespace datadog::platform { + +static const uint64_t CrashContextHeaderMagic = 0xdc01; +static const uint64_t CrashContextFileVersion = 1; + +static const uint64_t CrashContextFooterMagic = 0xdcff; + +} // namespace datadog::platform diff --git a/src/datadog/impl/platform/crash_context_write.cpp b/src/datadog/impl/platform/crash_context_write.cpp new file mode 100644 index 00000000..30821f95 --- /dev/null +++ b/src/datadog/impl/platform/crash_context_write.cpp @@ -0,0 +1,114 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#include "datadog/impl/platform/crash_context_write.hpp" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#else +#include +#include +#include +#endif + +#include +#include +#include + +#include "datadog/impl/core/feature_types/rum.hpp" +#include "datadog/impl/platform/crash_context.hpp" + +namespace datadog::platform { + +// NOLINTBEGIN(cppcoreguidelines-pro-bounds-array-to-pointer-decay) +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) + +#ifdef _WIN32 +using FileHandle = HANDLE; +static const FileHandle kInvalidHandle = INVALID_HANDLE_VALUE; +#else +using FileHandle = int; +static const FileHandle kInvalidHandle = -1; +#endif + +static void write_bytes(FileHandle fd, const void* data, size_t size) { +#ifdef _WIN32 + DWORD written = 0; + WriteFile(fd, data, static_cast(size), &written, nullptr); + (void)written; +#else + ssize_t result = write(fd, data, size); + (void)result; +#endif +} + +static void write_uint64(FileHandle fd, uint64_t value) { + write_bytes(fd, &value, sizeof(value)); +} + +void WriteCrashContext(const char* filename, const impl::RumFeatureContext& rum_ctx) { + // Write to a .tmp file first, then atomically rename to the final path. This + // ensures readers on the next launch never observe a partially-written file. + char tmp[512]; +#ifdef _WIN32 + _snprintf_s(tmp, sizeof(tmp), _TRUNCATE, "%s.tmp", filename); +#else + snprintf(tmp, sizeof(tmp), "%s.tmp", filename); +#endif + +#ifdef _WIN32 + FileHandle fd = CreateFileA( + tmp, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr + ); +#else + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) + FileHandle fd = open(tmp, O_CREAT | O_WRONLY | O_TRUNC, 0644); +#endif + + if (fd == kInvalidHandle) { + return; + } + + write_uint64(fd, CrashContextHeaderMagic); + write_uint64(fd, CrashContextFileVersion); + write_bytes(fd, rum_ctx.application_id.bytes.data(), 16); + write_bytes(fd, rum_ctx.session_id.bytes.data(), 16); + write_bytes(fd, rum_ctx.view_id.bytes.data(), 16); + write_bytes(fd, rum_ctx.action_id.bytes.data(), 16); + write_uint64(fd, CrashContextFooterMagic); + +#ifdef _WIN32 + CloseHandle(fd); + // MoveFileExA with MOVEFILE_REPLACE_EXISTING is the closest Windows equivalent + // to an atomic rename; it replaces the destination in a single operation + MoveFileExA(tmp, filename, MOVEFILE_REPLACE_EXISTING); +#else + close(fd); + // rename() is atomic on POSIX when src and dst are on the same filesystem, + // which is guaranteed here since both paths share the same .crashes/ directory + rename(tmp, filename); +#endif +} + +void DeleteCrashContext(const char* filename) { + char tmp[512]; +#ifdef _WIN32 + DeleteFileA(filename); + _snprintf_s(tmp, sizeof(tmp), _TRUNCATE, "%s.tmp", filename); + DeleteFileA(tmp); +#else + unlink(filename); + snprintf(tmp, sizeof(tmp), "%s.tmp", filename); + unlink(tmp); +#endif +} + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) +// NOLINTEND(cppcoreguidelines-pro-bounds-array-to-pointer-decay) + +} // namespace datadog::platform diff --git a/src/datadog/impl/platform/crash_context_write.hpp b/src/datadog/impl/platform/crash_context_write.hpp new file mode 100644 index 00000000..3ec46671 --- /dev/null +++ b/src/datadog/impl/platform/crash_context_write.hpp @@ -0,0 +1,30 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2025-Present Datadog, Inc. + +#pragma once + +namespace datadog::impl { +struct RumFeatureContext; +} + +namespace datadog::platform { + +/** + * Creates or overwrites a crash context file at `filename`, writing the current RUM + * session identifiers in binary format. + * + * The file persists across crashes so the next SDK launch can recover the session + * context and attach it to any crash report found on disk. Call `DeleteCrashContext` on + * clean shutdown to remove it. + */ +void WriteCrashContext(const char* filename, const impl::RumFeatureContext& rum_ctx); + +/** + * Deletes the crash context file at `filename`, if any such file exists. + */ +void DeleteCrashContext(const char* filename); + +} // namespace datadog::platform diff --git a/src/datadog/impl/platform/crash_handler.hpp b/src/datadog/impl/platform/crash_handler.hpp index f7d6fe90..5c561b88 100644 --- a/src/datadog/impl/platform/crash_handler.hpp +++ b/src/datadog/impl/platform/crash_handler.hpp @@ -11,6 +11,7 @@ namespace datadog::impl { class DiagnosticLogger; +struct RumFeatureContext; } // namespace datadog::impl namespace datadog::platform { @@ -86,6 +87,13 @@ class ICrashHandler { * like `virtual bool IsProcessGlobal()` or `IsReversible()` etc. */ virtual void Shutdown() = 0; + + /** + * Persists `rum_ctx` so that it is available if a crash occurs before the next + * normal SDK shutdown. Only called when the RUM context has actually changed since + * the last call. + */ + virtual void SetRumContext(const impl::RumFeatureContext& rum_ctx) = 0; }; namespace CrashHandler { diff --git a/src/datadog/impl/platform/crash_handler_crashpad.cpp b/src/datadog/impl/platform/crash_handler_crashpad.cpp index debadbda..03b404b6 100644 --- a/src/datadog/impl/platform/crash_handler_crashpad.cpp +++ b/src/datadog/impl/platform/crash_handler_crashpad.cpp @@ -14,6 +14,7 @@ #include "client/crashpad_client.h" #include "client/settings.h" +#include "datadog/impl/core/feature_types/rum.hpp" #include "datadog/impl/diagnostics.hpp" #include "datadog/impl/platform/crash_handler.hpp" @@ -31,8 +32,10 @@ // them safe to read during a crash. The Crashpad handler will automatically resolve // these values and include them as annotations when the crash dump is uploaded. // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) -static crashpad::StringAnnotation<64> s_test_annotation1("test_annotation1"); -static crashpad::StringAnnotation<64> s_test_annotation2("test_annotation2"); +static crashpad::StringAnnotation<37> s_rum_application_id("rum.application_id"); +static crashpad::StringAnnotation<37> s_rum_session_id("rum.session_id"); +static crashpad::StringAnnotation<37> s_rum_view_id("rum.view_id"); +static crashpad::StringAnnotation<37> s_rum_action_id("rum.action_id"); // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) /** @@ -159,9 +162,11 @@ class CrashpadCrashHandler final : public ICrashHandler { return false; } - // Set initial annotation values as a test - s_test_annotation1.Set("foo"); - s_test_annotation2.Set("bar"); + // Clear annotation values, if any were set previously + s_rum_application_id.Set(""); + s_rum_session_id.Set(""); + s_rum_view_id.Set(""); + s_rum_action_id.Set(""); // When the Crashpad client is first initialized, it populates the configured // database directory with configuration metadata and other state. By default, a @@ -225,9 +230,61 @@ class CrashpadCrashHandler final : public ICrashHandler { // and continue running after SDK shutdown. We don't forcibly terminate it here. } + void SetRumContext(const impl::RumFeatureContext& rum_ctx) override { + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-array-to-pointer-decay) + char buf[37] = {0}; + + if (rum_ctx.application_id != _rum_application_id) { + _rum_application_id = rum_ctx.application_id; + if (_rum_application_id == UUID::Zero) { + s_rum_application_id.Set(""); + } else { + _rum_application_id.ToBytes(buf, std::size(buf)); + s_rum_application_id.Set(buf); + } + } + + if (rum_ctx.session_id != _rum_session_id) { + _rum_session_id = rum_ctx.session_id; + if (_rum_session_id == UUID::Zero) { + s_rum_session_id.Set(""); + } else { + _rum_session_id.ToBytes(buf, std::size(buf)); + s_rum_session_id.Set(buf); + } + } + + if (rum_ctx.view_id != _rum_view_id) { + _rum_view_id = rum_ctx.view_id; + if (_rum_view_id == UUID::Zero) { + s_rum_view_id.Set(""); + } else { + _rum_view_id.ToBytes(buf, std::size(buf)); + s_rum_view_id.Set(buf); + } + } + + if (rum_ctx.action_id != _rum_action_id) { + _rum_action_id = rum_ctx.action_id; + if (_rum_action_id == UUID::Zero) { + s_rum_action_id.Set(""); + } else { + _rum_action_id.ToBytes(buf, std::size(buf)); + s_rum_action_id.Set(buf); + } + } + // NOLINTEND(cppcoreguidelines-pro-bounds-array-to-pointer-decay) + }; + private: impl::DiagnosticLogger _logger; std::string _handler_exe_path; + + // Values cached on last call to SetRumContext + UUID _rum_application_id; + UUID _rum_session_id; + UUID _rum_view_id; + UUID _rum_action_id; }; std::unique_ptr CrashHandler::Init( diff --git a/src/datadog/impl/platform/crash_handler_inprocess_posix.cpp b/src/datadog/impl/platform/crash_handler_inprocess_posix.cpp index 401d404b..02ea158e 100644 --- a/src/datadog/impl/platform/crash_handler_inprocess_posix.cpp +++ b/src/datadog/impl/platform/crash_handler_inprocess_posix.cpp @@ -39,7 +39,9 @@ static_assert( #endif #include "datadog/impl/assert.hpp" +#include "datadog/impl/core/feature_types/rum.hpp" #include "datadog/impl/diagnostics.hpp" +#include "datadog/impl/platform/crash_context_write.hpp" #include "datadog/impl/platform/crash_handler.hpp" #include "datadog/impl/platform/crash_handler_buildid_cache.hpp" #include "datadog/impl/platform/crash_report_write.hpp" @@ -69,7 +71,9 @@ namespace datadog::platform { // Pre-opened file descriptor for crash report file to be written static volatile sig_atomic_t s_crash_fd = -1; -static char s_crash_filename[256]; // Path to crash report file +static char s_crash_filename[256]; // Path to crash report file +static char s_crash_context_filename[260]; // Path to companion crash context file + // (.ctx suffix) // Atomically set to 1 once our signal handler has been called, and never reset. This // one-shot reentrancy guard prevents recursive crashes (e.g., if the handler itself @@ -753,6 +757,12 @@ class InProcessCrashHandler final : public ICrashHandler { timestamp_ms, getpid() ); + snprintf( + s_crash_context_filename, + sizeof(s_crash_context_filename), + "%s.ctx", + s_crash_filename + ); // Preemptively open the crash report file and keep it open indefinitely s_crash_fd = open(s_crash_filename, O_CREAT | O_WRONLY | O_APPEND, 0644); @@ -901,6 +911,14 @@ class InProcessCrashHandler final : public ICrashHandler { int result = unlink(s_crash_filename); (void)result; } + + // Delete the crash context file: it's only meaningful if a crash occurred, + // and on clean shutdown we don't want it to surface as stale context + DeleteCrashContext(s_crash_context_filename); + } + + void SetRumContext(const impl::RumFeatureContext& rum_ctx) override { + WriteCrashContext(s_crash_context_filename, rum_ctx); } private: diff --git a/src/datadog/impl/platform/crash_handler_inprocess_windows.cpp b/src/datadog/impl/platform/crash_handler_inprocess_windows.cpp index faff4188..3ee4cb4a 100644 --- a/src/datadog/impl/platform/crash_handler_inprocess_windows.cpp +++ b/src/datadog/impl/platform/crash_handler_inprocess_windows.cpp @@ -12,6 +12,7 @@ // clang-format off #define WIN32_LEAN_AND_MEAN +#define NOMINMAX #include #include // ToolHelp32 snapshot APIs for enumerating loaded modules // clang-format on @@ -20,7 +21,9 @@ #include #include "datadog/impl/assert.hpp" +#include "datadog/impl/core/feature_types/rum.hpp" #include "datadog/impl/diagnostics.hpp" +#include "datadog/impl/platform/crash_context_write.hpp" #include "datadog/impl/platform/crash_handler.hpp" #include "datadog/impl/platform/crash_handler_buildid_cache.hpp" #include "datadog/impl/platform/crash_report_write.hpp" @@ -41,7 +44,8 @@ static_assert( // Pre-opened file handle for crash report file to be written static HANDLE s_crash_file = INVALID_HANDLE_VALUE; -static char s_crash_filename[MAX_PATH]; // Path to crash report file +static char s_crash_filename[MAX_PATH]; // Path to crash report file +static char s_crash_context_filename[MAX_PATH]; // Path to companion crash context file // Original exception filter to restore on shutdown static LPTOP_LEVEL_EXCEPTION_FILTER s_old_filter = nullptr; @@ -267,6 +271,13 @@ class InProcessCrashHandler final : public ICrashHandler { timestamp_ms, static_cast(GetCurrentProcessId()) ); + _snprintf_s( + s_crash_context_filename, + sizeof(s_crash_context_filename), + _TRUNCATE, + "%s.ctx", + s_crash_filename + ); // Preemptively open the crash report file and keep it open indefinitely s_crash_file = CreateFileA( @@ -343,6 +354,14 @@ class InProcessCrashHandler final : public ICrashHandler { const BOOL deleted = DeleteFileA(s_crash_filename); (void)deleted; // Ignore result; file cleanup is best-effort } + + // Delete the crash context file: it's only meaningful if a crash occurred, + // and on clean shutdown we don't want it to surface as stale context + DeleteCrashContext(s_crash_context_filename); + } + + void SetRumContext(const impl::RumFeatureContext& rum_ctx) override { + WriteCrashContext(s_crash_context_filename, rum_ctx); } private: diff --git a/src/datadog/impl/platform/crash_handler_noop.cpp b/src/datadog/impl/platform/crash_handler_noop.cpp index 4dc748b3..71098312 100644 --- a/src/datadog/impl/platform/crash_handler_noop.cpp +++ b/src/datadog/impl/platform/crash_handler_noop.cpp @@ -43,6 +43,8 @@ class NoopCrashHandler final : public ICrashHandler { */ void Shutdown() override {} + void SetRumContext(const impl::RumFeatureContext&) override {} + private: impl::DiagnosticLogger _logger; }; diff --git a/tools/process-crash-report/lib/formatters.py b/tools/process-crash-report/lib/formatters.py index 0e52bb1b..f984fcd3 100644 --- a/tools/process-crash-report/lib/formatters.py +++ b/tools/process-crash-report/lib/formatters.py @@ -15,7 +15,7 @@ from enum import Enum from pathlib import Path from typing import Optional, TextIO -from .models import CrashReport, StackFrame, SymbolizedFrame, Module +from .models import CrashReport, StackFrame, SymbolizedFrame, Module, RumContext class OutputMode(str, Enum): @@ -73,6 +73,20 @@ def format_symbolicated_frame(sym_frame: SymbolizedFrame) -> str: return f" {sym_frame.frame_number}: {sym_frame.function} ({sym_frame.location})" +# === RUM Context Formatting === + +def _print_rum_context(rum_context: RumContext, output: TextIO) -> None: + """Print non-None RUM context fields as key-value lines.""" + if rum_context.application_id is not None: + output.write(f"Application ID: {rum_context.application_id}\n") + if rum_context.session_id is not None: + output.write(f"Session ID: {rum_context.session_id}\n") + if rum_context.view_id is not None: + output.write(f"View ID: {rum_context.view_id}\n") + if rum_context.action_id is not None: + output.write(f"Action ID: {rum_context.action_id}\n") + + # === Raw Crash Report Printing === def print_raw_crash_report( @@ -80,6 +94,7 @@ def print_raw_crash_report( metadata: dict[str, str], stack_addresses: list[int], modules: list[Module], + rum_context: Optional[RumContext] = None, output: TextIO = sys.stdout ) -> None: """ @@ -94,6 +109,7 @@ def print_raw_crash_report( metadata: Parsed metadata dictionary from crash report header stack_addresses: Raw addresses from stack frame section modules: Parsed Module objects from module section + rum_context: Optional RUM session context from the accompanying .ctx file output: Output stream (defaults to stdout) """ # === Section 1: Filename === @@ -106,6 +122,8 @@ def print_raw_crash_report( output.write("=== Crash Metadata ===\n") for key, value in metadata.items(): output.write(f"{key}: {value}\n") + if rum_context is not None: + _print_rum_context(rum_context, output) output.write("\n") # === Section 3: Raw Stack Trace === @@ -162,6 +180,8 @@ def print_crash_report( output.write("=== Crash Metadata ===\n") for key, value in report.metadata.items(): output.write(f"{key}: {value}\n") + if report.rum_context is not None: + _print_rum_context(report.rum_context, output) output.write("\n") # === Section 3: Resolved Stack Trace === diff --git a/tools/process-crash-report/lib/models.py b/tools/process-crash-report/lib/models.py index e96d343f..b8be773f 100644 --- a/tools/process-crash-report/lib/models.py +++ b/tools/process-crash-report/lib/models.py @@ -94,6 +94,22 @@ class SymbolizedFrame: address_info: Optional[str] = None +@dataclass +class RumContext: + """ + RUM session identifiers recovered from the crash context file. + + Written alongside crash reports by the SDK whenever the RUM context + changes. Each field is a UUID string (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) + or None if that identifier was not active at the time of the crash (i.e., + the binary UUID was all-zeros). + """ + application_id: Optional[str] + session_id: Optional[str] + view_id: Optional[str] + action_id: Optional[str] + + @dataclass class CrashReport: """ @@ -108,8 +124,11 @@ class CrashReport: metadata: Key-value pairs from the metadata section (Signal, PID, TID, etc.) stack_frames: Resolved stack frames with module+offset information modules: List of loaded modules, sorted by base_address + rum_context: RUM session identifiers from the accompanying .ctx file, or + None if the file was absent or could not be parsed """ file_path: Path metadata: dict[str, str] stack_frames: list[StackFrame] modules: list[Module] + rum_context: Optional[RumContext] = None diff --git a/tools/process-crash-report/lib/parser.py b/tools/process-crash-report/lib/parser.py index b2c366bc..5f7953c0 100644 --- a/tools/process-crash-report/lib/parser.py +++ b/tools/process-crash-report/lib/parser.py @@ -20,7 +20,7 @@ from pathlib import Path from datetime import datetime, timezone from typing import Optional -from .models import CrashReport, Module +from .models import CrashReport, Module, RumContext # === Magic Constants === @@ -36,6 +36,14 @@ UINT64_SIZE = 8 HEADER_SIZE = 64 # 8 uint64_t values +# Magic constants for .ctx files (crash_context.hpp) +CRASH_CONTEXT_HEADER_MAGIC = 0xdc01 +CRASH_CONTEXT_FILE_VERSION = 1 +CRASH_CONTEXT_FOOTER_MAGIC = 0xdcff + +# .ctx file size: 2× uint64_t (header magic + version) + 4× 16-byte UUID + 1× uint64_t (footer) +CRASH_CONTEXT_FILE_SIZE = 2 * UINT64_SIZE + 4 * 16 + UINT64_SIZE + # === Platform-Specific Signal/Exception Mapping === @@ -106,6 +114,90 @@ def _format_timestamp(timestamp: int) -> str: return str(timestamp) +# === Crash Context Parsing === + +def _uuid_bytes_to_str(data: bytes) -> Optional[str]: + """ + Convert 16 raw UUID bytes to the standard string representation. + + Returns None if all bytes are zero (indicating "not set"), otherwise + formats as xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx using the byte order + defined by RFC 4122 (bytes 0-3, 4-5, 6-7, 8-9, 10-15). + """ + if all(b == 0 for b in data): + return None + hex_str = data.hex() + return f"{hex_str[0:8]}-{hex_str[8:12]}-{hex_str[12:16]}-{hex_str[16:20]}-{hex_str[20:32]}" + + +def parse_crash_context(file_path: Path) -> Optional[RumContext]: + """ + Parse a crash context (.ctx) file produced by the SDK's crash handler. + + The .ctx file is written atomically whenever the active RUM context changes + and is deleted on clean shutdown. It contains the RUM session identifiers + (application, session, view, and action IDs) active at the time of the crash. + + The binary format is defined in crash_context.hpp: + 0x00 8B header magic (0xdc01, little-endian uint64_t) + 0x08 8B version (0x0001, little-endian uint64_t) + 0x10 16B application_id UUID (raw bytes) + 0x20 16B session_id UUID + 0x30 16B view_id UUID + 0x40 16B action_id UUID + 0x50 8B footer magic (0xdcff, little-endian uint64_t) + + Returns None (without raising) if the file is absent, has an unexpected + size, or contains an invalid magic number. UUID fields that are all-zeros + are represented as None in the returned RumContext. + """ + try: + data = file_path.read_bytes() + except FileNotFoundError: + return None + except OSError: + return None + + if len(data) != CRASH_CONTEXT_FILE_SIZE: + print( + f"Warning: .ctx file has unexpected size " + f"(expected {CRASH_CONTEXT_FILE_SIZE}, got {len(data)}): {file_path}", + file=sys.stderr + ) + return None + + header_magic, version = struct.unpack_from('<2Q', data, 0) + if header_magic != CRASH_CONTEXT_HEADER_MAGIC: + print( + f"Warning: .ctx file has invalid header magic " + f"(expected 0x{CRASH_CONTEXT_HEADER_MAGIC:x}, got 0x{header_magic:x}): {file_path}", + file=sys.stderr + ) + return None + + footer_magic, = struct.unpack_from(' Optional[Path]: @@ -127,7 +219,7 @@ def find_latest_crash_report(crashes_dir: Path) -> Optional[Path]: return None crash_files = sorted( - crashes_dir.glob('crash_*'), + (p for p in crashes_dir.glob('crash_*') if not p.suffix), reverse=True # Most recent first (lexical sort) ) @@ -327,9 +419,13 @@ def load_crash_report(file_path: Path) -> CrashReport: # Resolve addresses to stack frames stack_frames = resolve_stack_trace(stack_addresses, modules) + # Load the accompanying .ctx file if present + rum_context = parse_crash_context(file_path.with_suffix('.ctx')) + return CrashReport( file_path=file_path, metadata=metadata, stack_frames=stack_frames, - modules=modules + modules=modules, + rum_context=rum_context, ) diff --git a/tools/process-crash-report/main.py b/tools/process-crash-report/main.py index df5c54d3..8692b217 100755 --- a/tools/process-crash-report/main.py +++ b/tools/process-crash-report/main.py @@ -59,7 +59,7 @@ from pathlib import Path # Import lib modules -from lib.parser import find_latest_crash_report, load_crash_report, parse_crash_report +from lib.parser import find_latest_crash_report, load_crash_report, parse_crash_report, parse_crash_context from lib.symbolizers import get_symbolizer, SymbolizerTool from lib.formatters import print_crash_report, print_raw_crash_report, OutputMode @@ -122,7 +122,8 @@ def main() -> int: if args.output == 'raw': try: metadata, stack_addresses, modules = parse_crash_report(crash_report_path) - print_raw_crash_report(crash_report_path, metadata, stack_addresses, modules) + rum_context = parse_crash_context(crash_report_path.with_suffix('.ctx')) + print_raw_crash_report(crash_report_path, metadata, stack_addresses, modules, rum_context) return 0 except FileNotFoundError as e: print(f"Error: {e}", file=sys.stderr)