Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [FEATURE] Add an experimental Core Animation recording pipeline for Session Replay, available through the `compositionTreeRecording` feature flag. See [#3127][]
- [FEATURE] Add `disallowList` to `RUM.Configuration.URLSessionTracking` to exclude URLs from automatic RUM resource tracking, with `*` wildcard support. [#3097][]
- [FEATURE] Add `CrashReporting.Configuration.appHangBacktraceEnabled` to opt out of stack trace collection in App Hang errors while keeping Crash Reporting enabled. See [#3136][]
- [IMPROVEMENT] Forward `local_cache_hit` signal on RUM resources [#3074][]
- [FIX] Fix `EXC_BREAKPOINT` crash when a log or RUM attribute's `encode(to:)` throws after partially encoding a value. [#3134][]

Expand Down Expand Up @@ -1227,6 +1228,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
[#3127]: https://github.com/DataDog/dd-sdk-ios/pull/3127
[#3097]: https://github.com/DataDog/dd-sdk-ios/pull/3097
[#3134]: https://github.com/DataDog/dd-sdk-ios/pull/3134
[#3136]: https://github.com/DataDog/dd-sdk-ios/pull/3136

[@00fa9a]: https://github.com/00FA9A
[@britton-earnin]: https://github.com/Britton-Earnin
Expand Down
60 changes: 60 additions & 0 deletions Datadog/Example/Base.lproj/Main iOS.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,65 @@
<constraint firstAttribute="height" constant="20" id="WDI-xM-t2N"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App Hang" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ah1-Ti-tLe">
<rect key="frame" x="0.0" y="392" width="384" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ah2-Gp-g10" userLabel="Vertical gap 5">
<rect key="frame" x="0.0" y="412.5" width="384" height="5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="5" id="Ah3-Cn-st5"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Backtraces: ON" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ah4-St-lbl">
<rect key="frame" x="0.0" y="417.5" width="384" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" systemColor="systemGrayColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ah5-Gp-g10" userLabel="Vertical gap 10">
<rect key="frame" x="0.0" y="432" width="384" height="10"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="10" id="Ah6-Cn-st10"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" spacing="5" translatesAutoresizingMaskIntoConstraints="NO" id="Ah7-Sv-stk">
<rect key="frame" x="0.0" y="442" width="384" height="44"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Ah8-Bt-btn">
<rect key="frame" x="0.0" y="0.0" width="384" height="44"/>
<color key="backgroundColor" red="0.38823529410000002" green="0.17254901959999999" blue="0.65098039220000004" alpha="1" colorSpace="calibratedRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Hang main thread for 2s">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="titleShadowColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="boolean" keyPath="layer.masksToBounds" value="YES"/>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="7"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<action selector="didTapHangMainThread:" destination="dzc-N5-XBR" eventType="touchUpInside" id="Ah9-Ac-act"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="44" id="AhA-Cn-h44"/>
</constraints>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AhB-Gp-g20" userLabel="Vertical gap 20">
<rect key="frame" x="0.0" y="486" width="384" height="20"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="AhC-Cn-h20"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
Expand All @@ -1382,6 +1441,7 @@
</view>
<navigationItem key="navigationItem" id="UG5-SU-NkM"/>
<connections>
<outlet property="appHangBacktraceStatusLabel" destination="Ah4-St-lbl" id="AhD-Ou-out"/>
<outlet property="rumServiceNameTextField" destination="Bco-7y-w7S" id="cBg-pr-cZ4"/>
<outlet property="viewNameTextField" destination="bXI-gy-gbl" id="ZaZ-Sh-Nrd"/>
</connections>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class DebugCrashReportingWithRUMViewController: UIViewController {
super.viewDidLoad()
rumServiceNameTextField.text = serviceName
viewNameTextField.placeholder = viewName
appHangBacktraceStatusLabel.text = Environment.isAppHangBacktraceEnabled()
? "Backtraces: ON — launch with `DD_DISABLE_APP_HANG_BACKTRACES` to turn them off"
: "Backtraces: OFF — `appHangBacktraceEnabled: false`"
}

private func crash() {
Expand Down Expand Up @@ -48,6 +51,22 @@ class DebugCrashReportingWithRUMViewController: UIViewController {
}
}

// MARK: - App Hang

@IBOutlet weak var appHangBacktraceStatusLabel: UILabel!

/// Blocks the main thread for longer than the `appHangThreshold` configured in `ExampleAppDelegate`,
/// so RUM reports an App Hang error. Whether that error carries a stack trace depends on
/// `CrashReporting.Configuration.appHangBacktraceEnabled`.
@IBAction func didTapHangMainThread(_ sender: Any) {
(sender as? UIButton)?.disableFor(seconds: 0.5)

rumMonitor.startView(key: viewName, name: viewName, attributes: [:])
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Thread.sleep(forTimeInterval: 2)
}
}

// MARK: - OOM Crash

@IBAction func didTapOOMCrash(_ sender: UIButton) {
Expand Down
9 changes: 9 additions & 0 deletions Datadog/Example/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ internal struct Environment {
struct Argument {
static let isRunningUnitTests = "IS_RUNNING_UNIT_TESTS"
static let isRunningUITests = "IS_RUNNING_UI_TESTS"
/// Launches the app with `CrashReporting.Configuration.appHangBacktraceEnabled` set to `false`.
static let disableAppHangBacktraces = "DD_DISABLE_APP_HANG_BACKTRACES"
}

struct InfoPlistKey {
Expand All @@ -37,6 +39,13 @@ internal struct Environment {
return !isRunningUITests() && !isRunningUnitTests()
}

/// Whether App Hangs detected by RUM should carry a stack trace.
///
/// Add `DD_DISABLE_APP_HANG_BACKTRACES` to the scheme's launch arguments to exercise the opt-out.
static func isAppHangBacktraceEnabled() -> Bool {
return !ProcessInfo.processInfo.arguments.contains(Argument.disableAppHangBacktraces)
}

// MARK: - Info.plist

static func readClientToken() -> String {
Expand Down
7 changes: 6 additions & 1 deletion Datadog/Example/ExampleAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate {
)

// Enable Crash Reporting
CrashReporting.enable()
CrashReporting.enable(
with: CrashReporting.Configuration(
appHangBacktraceEnabled: Environment.isAppHangBacktraceEnabled()
)
)

// Set highest verbosity level to see debugging logs from the SDK
Datadog.verbosityLevel = .debug
Expand All @@ -87,6 +91,7 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate {
}
),
trackBackgroundEvents: true,
appHangThreshold: 0.5,
trackWatchdogTerminations: true,
customEndpoint: Environment.readCustomRUMURL(),
telemetrySampleRate: 100
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ class GeneratingBacktraceTests: XCTestCase {
)
}

func testGivenAppHangBacktracesDisabled_whenGeneratingBacktrace_itStillGeneratesIt() throws {
#if os(watchOS)
throw XCTSkip("Backtrace generation is not supported on watchOS")
#endif
// Given
CrashReporting.enable(with: .init(appHangBacktraceEnabled: false), in: core)

// Then
XCTAssertNotNil(core.get(feature: BacktraceReportingFeature.self), "`BacktraceReportingFeature` must still be registered")
XCTAssertFalse(core.isAppHangBacktraceEnabled)

// Only the App Hangs consumer is gated - other consumers must keep working:
let backtrace = try XCTUnwrap(core.backtraceReporter.generateBacktrace())
XCTAssertGreaterThan(backtrace.threads.count, 0, "Some thread(s) should be recorded")
XCTAssertGreaterThan(backtrace.binaryImages.count, 0, "Some binary image(s) should be recorded")
}

func testGivenCrashReportingNotEnabled_thenAppHangBacktracesAreNotDisabled() {
// Then (backtrace generation is *unavailable*, not *disabled*)
XCTAssertNil(core.get(feature: BacktraceReportingFeature.self))
XCTAssertTrue(core.isAppHangBacktraceEnabled)
}

func testGeneratingBacktraceOfTheMainThread() throws {
#if os(watchOS)
throw XCTSkip("Backtrace generation is not supported on watchOS")
Expand Down
42 changes: 42 additions & 0 deletions Datadog/IntegrationUnitTests/RUM/AppHangsMonitoringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,48 @@ class AppHangsMonitoringTests: XCTestCase {
#endif
}

func testGivenAppHangBacktracesDisabledInCrashReporting_whenRUMIsEnabledFirst_itTracksAppHangWithNoStackTrace() throws {
try assertAppHangIsTrackedWithNoStackTrace { crashReportingConfig in
RUM.enable(with: self.rumConfig, in: self.core)
CrashReporting.enable(with: crashReportingConfig, in: self.core)
}
}

func testGivenAppHangBacktracesDisabledInCrashReporting_whenCrashReportingIsEnabledFirst_itTracksAppHangWithNoStackTrace() throws {
try assertAppHangIsTrackedWithNoStackTrace { crashReportingConfig in
CrashReporting.enable(with: crashReportingConfig, in: self.core)
RUM.enable(with: self.rumConfig, in: self.core)
}
}

/// Asserts that a hang is tracked with no stack trace, with the SDK enabled by `enableSDK`.
///
/// Both enablement orders get their own test rather than being picked at random: only the RUM-first order
/// proves that the opt-out is read per hang instead of being captured when RUM is enabled, so randomizing
/// would let that regression pass half of the runs.
private func assertAppHangIsTrackedWithNoStackTrace(enableSDK: (CrashReporting.Configuration) -> Void) throws {
// Given (initialize SDK on the main thread)
enableSDK(CrashReporting.Configuration(appHangBacktraceEnabled: false))

// When
mainQueue.sync {
Thread.sleep(forTimeInterval: hangDuration)
}

// Then
try flushHangsMonitoring()
let errors = core.waitAndReturnEvents(ofFeature: RUMFeature.name, ofType: RUMErrorEvent.self)
let appHangError = try XCTUnwrap(errors.first)

XCTAssertEqual(appHangError.error.message, AppHangsMonitor.Constants.appHangErrorMessage)
XCTAssertEqual(appHangError.error.type, AppHangsMonitor.Constants.appHangErrorType)
XCTAssertEqual(appHangError.error.stack, AppHangsMonitor.Constants.appHangStackDisabledErrorMessage)
XCTAssertEqual(appHangError.error.source, .source)
XCTAssertNil(appHangError.error.threads, "Threads should be unavailable as App Hang backtraces were disabled")
XCTAssertNil(appHangError.error.binaryImages, "Binary Images should be unavailable as App Hang backtraces were disabled")
XCTAssertNil(appHangError.error.wasTruncated, "Truncation flag should be unavailable as App Hang backtraces were disabled")
}

func testGivenOnlyRUMEnabled_whenMainThreadHangs_itTracksAppHangWithNoStackTrace() throws {
// Given
mainQueue.sync {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ - (void)testDDConfigurationBuilderAPI {

- (void)testDatadogCrashReporterAPI {
[DDCrashReporter enable];

DDCrashReporterConfiguration *configuration = [DDCrashReporterConfiguration new];
XCTAssertTrue(configuration.appHangBacktraceEnabled, @"App Hang backtraces are enabled by default");
configuration.appHangBacktraceEnabled = NO;
XCTAssertFalse(configuration.appHangBacktraceEnabled, @"The setter must write through to the wrapped configuration");

[DDCrashReporter enableWith:configuration];
}

#pragma clang diagnostic pop
Expand Down
Loading