Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,10 @@
D2268ADC2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268ADA2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift */; };
D2268ADE2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */; };
D2268ADF2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */; };
D2268AE72EB50F4C00875E44 /* DatadogDiagnosticFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE62EB50F4C00875E44 /* DatadogDiagnosticFilter.swift */; };
D2268AE82EB50F4C00875E44 /* DatadogDiagnosticFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE62EB50F4C00875E44 /* DatadogDiagnosticFilter.swift */; };
D2268AEA2EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE92EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift */; };
D2268AEB2EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE92EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift */; };
D22743D829DEB8B4001A7EF9 /* VitalInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */; };
D22743D929DEB8B4001A7EF9 /* VitalInfoSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */; };
D22743DA29DEB8B4001A7EF9 /* VitalMemoryReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */; };
Expand Down Expand Up @@ -3326,6 +3330,8 @@
D2268AD72EB4DAC400875E44 /* AnyCrashReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCrashReport.swift; sourceTree = "<group>"; };
D2268ADA2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashFieldDictionaryTests.swift; sourceTree = "<group>"; };
D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogTypeSafeFilterTests.swift; sourceTree = "<group>"; };
D2268AE62EB50F4C00875E44 /* DatadogDiagnosticFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogDiagnosticFilter.swift; sourceTree = "<group>"; };
D2268AE92EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogDiagnosticFilterTests.swift; sourceTree = "<group>"; };
D22789352D64A0D3007E9DB0 /* UploadQualityMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadQualityMetric.swift; sourceTree = "<group>"; };
D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = "<group>"; };
D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6539,6 +6545,7 @@
D22689C22EB12D3D00875E44 /* KSCrashPlugin.swift */,
D2268AD42EB4DA5800875E44 /* CrashFieldDictionary.swift */,
D2268AD12EB4DA4100875E44 /* DatadogTypeSafeFilter.swift */,
D2268AE62EB50F4C00875E44 /* DatadogDiagnosticFilter.swift */,
D2268AD72EB4DAC400875E44 /* AnyCrashReport.swift */,
);
path = KSCrashIntegration;
Expand All @@ -6550,6 +6557,7 @@
D22689C62EB2151F00875E44 /* KSCrashPluginTests.swift */,
D2268ADA2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift */,
D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */,
D2268AE92EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift */,
);
path = KSCrashIntegration;
sourceTree = "<group>";
Expand Down Expand Up @@ -9393,6 +9401,7 @@
612556BB268DD9BF002BCE74 /* DDCrashReportExporter.swift in Sources */,
61FDBA1326971953001D9D43 /* CrashReportMinifier.swift in Sources */,
D214DA8329DF2D5E004D0AE8 /* CrashReporting.swift in Sources */,
D2268AE72EB50F4C00875E44 /* DatadogDiagnosticFilter.swift in Sources */,
D2268AD82EB4DAC400875E44 /* AnyCrashReport.swift in Sources */,
61F2728B25C9561A00D54BF8 /* PLCrashReporterIntegration.swift in Sources */,
D214DA8129DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */,
Expand All @@ -9415,6 +9424,7 @@
D2268ADF2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift in Sources */,
D2268ADC2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift in Sources */,
D243BBC0276C9D640019C857 /* PLCrashReporterIntegrationTests.swift in Sources */,
D2268AEB2EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift in Sources */,
61FDBA15269722B4001D9D43 /* CrashReportInfoMinifierTests.swift in Sources */,
D22689C82EB2151F00875E44 /* KSCrashPluginTests.swift in Sources */,
61E95D882695C00200EA3115 /* DDCrashReportExporterTests.swift in Sources */,
Expand Down Expand Up @@ -10607,6 +10617,7 @@
D2CB6FC127C5348200A62B57 /* DDCrashReportExporter.swift in Sources */,
D2CB6FC227C5348200A62B57 /* CrashReportMinifier.swift in Sources */,
D214DA8429DF2D5E004D0AE8 /* CrashReporting.swift in Sources */,
D2268AE82EB50F4C00875E44 /* DatadogDiagnosticFilter.swift in Sources */,
D2268AD92EB4DAC400875E44 /* AnyCrashReport.swift in Sources */,
D2CB6FC327C5348200A62B57 /* PLCrashReporterIntegration.swift in Sources */,
D214DA8229DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */,
Expand All @@ -10629,6 +10640,7 @@
D2268ADE2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift in Sources */,
D2268ADB2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift in Sources */,
D2CB6FDC27C5352300A62B57 /* PLCrashReporterIntegrationTests.swift in Sources */,
D2268AEA2EB50F7F00875E44 /* DatadogDiagnosticFilterTests.swift in Sources */,
D2CB6FDD27C5352300A62B57 /* CrashReportInfoMinifierTests.swift in Sources */,
D22689C72EB2151F00875E44 /* KSCrashPluginTests.swift in Sources */,
D2CB6FDE27C5352300A62B57 /* DDCrashReportExporterTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* 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 2019-Present Datadog, Inc.
*/

import Foundation

// swiftlint:disable duplicate_imports
#if COCOAPODS
import KSCrash
#elseif swift(>=6.0)
internal import KSCrashRecording
#else
@_implementationOnly import KSCrashRecording
#endif
// swiftlint:enable duplicate_imports

/// A KSCrash filter that generates human-readable diagnostic messages for crash reports.
///
/// This filter is modeled after KSCrash's `KSCrashReportFilterDoctor` and serves the same
/// purpose: analyzing crash reports and producing descriptive diagnostic messages that explain
/// the cause of the crash in plain language.
///
/// ## Crash Types Handled
///
/// 1. **Uncaught Exceptions**: Objective-C/Swift exceptions that weren't caught by the application
/// - Extracts exception name and reason
/// - Format: `"Terminating app due to uncaught exception 'ExceptionName', reason: 'detailed reason'."`
///
/// 2. **Signal-based Crashes**: Low-level crashes caused by Unix signals
/// - Maps signal names to human-readable descriptions (SIGSEGV → Segmentation fault, etc.)
/// - Format: `"Application crash: SIGSEGV (Segmentation fault)"`
///
/// ## Processing Flow
///
/// For each crash report:
/// 1. Validates the report structure
/// 2. Analyzes crash data to determine the crash type
/// 3. Generates an appropriate diagnostic message
/// 4. Injects the diagnosis into both the main crash and recrash reports (if present)
/// 5. Returns the updated report
///
/// ## Integration
///
/// This filter should be placed early in the KSCrash filter chain, before the
/// `DatadogCrashReportFilter`, to ensure diagnostic information is available for
/// subsequent processing and reporting.
internal final class DatadogDiagnosticFilter: NSObject, CrashReportFilter {
/// Placeholder text used when crash information is unavailable.
private let unknown = "<unknown>"

/// Placeholder text used for optional fields that are not present.
private let unavailable = "???"

/// Mapping of Unix signal names to human-readable descriptions.
///
/// This dictionary provides user-friendly descriptions for all standard POSIX signals,
/// matching the behavior of KSCrash's diagnostic filter.
private let knownSignalDescriptionByName = [
"SIGSIGNAL 0": "Signal 0",
"SIGHUP": "Hangup",
"SIGINT": "Interrupt",
"SIGQUIT": "Quit",
"SIGILL": "Illegal instruction",
"SIGTRAP": "Trace/BPT trap",
"SIGABRT": "Abort trap",
"SIGEMT": "EMT trap",
"SIGFPE": "Floating point exception",
"SIGKILL": "Killed",
"SIGBUS": "Bus error",
"SIGSEGV": "Segmentation fault",
"SIGSYS": "Bad system call",
"SIGPIPE": "Broken pipe",
"SIGALRM": "Alarm clock",
"SIGTERM": "Terminated",
"SIGURG": "Urgent I/O condition",
"SIGSTOP": "Suspended (signal)",
"SIGTSTP": "Suspended",
"SIGCONT": "Continued",
"SIGCHLD": "Child exited",
"SIGTTIN": "Stopped (tty input)",
"SIGTTOU": "Stopped (tty output)",
"SIGIO": "I/O possible",
"SIGXCPU": "Cputime limit exceeded",
"SIGXFSZ": "Filesize limit exceeded",
"SIGVTALRM": "Virtual timer expired",
"SIGPROF": "Profiling timer expired",
"SIGWINCH": "Window size changes",
"SIGINFO": "Information request",
"SIGUSR1": "User defined signal 1",
"SIGUSR2": "User defined signal 2",
]

/// Processes crash reports by adding human-readable diagnostic messages.
///
/// This method analyzes each crash report, generates a diagnostic message describing
/// the crash cause, and injects it into the report. The diagnosis is added to both
/// the main crash report and any recrash reports present.
///
/// - Parameters:
/// - reports: The crash reports to process and enhance with diagnostic information
/// - onCompletion: Completion handler called with the processed reports. If diagnosis
/// generation fails for any report, the original reports are returned
/// along with the error.
func filterReports(
_ reports: [CrashReport],
onCompletion: (([CrashReport]?, (Error)?) -> Void)?
) {
do {
let reports = try reports.map { report in
// Validate and extract the type-safe report dictionary
guard var dict = report.untypedValue as? CrashFieldDictionary else {
throw CrashReportException(description: "KSCrash report untypedValue is not a CrashDictionary")
}

if let crash: CrashFieldDictionary = try dict.valueIfPresent(forKey: .crash), let diagnosis = try diagnose(crash: crash) {
dict.setValue(forKey: .crash, .diagnosis, value: diagnosis)
}

if let crash: CrashFieldDictionary = try dict.valueIfPresent(forKey: .recrashReport, .crash), let diagnosis = try diagnose(crash: crash) {
dict.setValue(forKey: .recrashReport, .crash, .diagnosis, value: diagnosis)
}

return AnyCrashReport(dict)
}

onCompletion?(reports, nil)
} catch {
onCompletion?(reports, error)
}
}

/// Analyzes a crash report and generates a human-readable diagnostic message.
///
/// This method examines the crash data to determine the crash type and constructs
/// an appropriate diagnostic message. It follows a prioritized analysis:
///
/// 1. **Exception-based crashes**: Checks for uncaught NSException
/// 2. **Signal-based crashes**: Checks for Unix signals with known descriptions
/// 3. **Unknown crashes**: Returns a generic message for unrecognized crash types
///
/// - Parameter dict: The crash report dictionary containing crash information
/// - Returns: A diagnostic message describing the crash, or `nil` if no crash data is found
/// - Throws: `CrashReportException` if the report structure is invalid
///
/// ## Example Messages
///
/// - Exception: `"Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'unrecognized selector'."`
/// - Signal: `"Application crash: SIGSEGV (Segmentation fault)"`
/// - Unknown: `"Application crash: <unknown>"`
func diagnose(crash dict: CrashFieldDictionary) throws -> String? {
// Check for uncaught exception
if let exception: CrashFieldDictionary = try dict.valueIfPresent(forKey: .error, .nsException) {
let name: String = try exception.valueIfPresent(forKey: .name) ?? unknown // e.g. `NSInvalidArgumentException`
let reason = try dict.valueIfPresent(forKey: .error, .reason) ?? unknown // e.g. `-[NSObject objectForKey:]: unrecognized selector sent to instance 0x...`
return "Terminating app due to uncaught exception '\(name)', reason: '\(reason)'."
}

// Check for signal-based crash with known description
if
let name: String = try dict.valueIfPresent(forKey: .error, .signal, .name),
let description = knownSignalDescriptionByName[name] {
return "Application crash: \(name) (\(description))"
}

// Fallback for unknown crash types
return "Application crash: \(unknown)"
}
}
Loading