Skip to content

Commit

Permalink
feat: Transactions for crashes (#4504)
Browse files Browse the repository at this point in the history
Finishes the transaction bound to the scope when the app crashes and stores it to disk. This experimental feature is disabled by default.
  • Loading branch information
philipphofmann authored Nov 8, 2024
1 parent 05ac767 commit 0d38ada
Show file tree
Hide file tree
Showing 41 changed files with 715 additions and 67 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Transactions for crashes (#4504): Finish the transaction bound to the scope when the app crashes. This __experimental__ feature is disabled by default. You can enable it via the option `enablePersistingTracesWhenCrashing`.

### Fixes

- Keep PropagationContext when cloning scope (#4518)
Expand All @@ -26,6 +30,7 @@
- Time-of-check time-of-use filesystem race condition (#4473)
- Capture all touches with session replay (#4477)


### Improvements

- Improve frames tracker performance (#4469)
Expand Down
1 change: 1 addition & 0 deletions Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
options.enableSwizzling = !args.contains("--disable-swizzling")
options.enableCrashHandler = !args.contains("--disable-crash-handler")
options.enableTracing = !args.contains("--disable-tracing")
options.enablePersistingTracesWhenCrashing = true

// because we run CPU for 15 seconds at full throttle, we trigger ANR issues being sent. disable such during benchmarks.
options.enableAppHangTracking = !isBenchmarking && !args.contains("--disable-anr-tracking")
Expand Down
9 changes: 8 additions & 1 deletion Samples/iOS-Swift/iOS-Swift/ErrorsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ class ErrorsViewController: UIViewController {
}

@IBAction func crash(_ sender: UIButton) {
SentrySDK.crash()
let transaction = SentrySDK.startTransaction(name: "Crashing Transaction", operation: "ui.load", bindToScope: true)

transaction.startChild(operation: "operation explode")

DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
transaction.startChild(operation: "operation crash")
SentrySDK.crash()
}
}

// swiftlint:disable force_unwrapping
Expand Down
5 changes: 5 additions & 0 deletions SentryTestUtils/TestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ public class TestClient: SentryClient {
return SentryId()
}

public var saveCrashTransactionInvocations = Invocations<(event: Event, scope: Scope)>()
public override func saveCrashTransaction(transaction: Transaction, scope: Scope) {
saveCrashTransactionInvocations.record((transaction, scope))
}

public var captureUserFeedbackInvocations = Invocations<UserFeedback>()
public override func capture(userFeedback: UserFeedback) {
captureUserFeedbackInvocations.record(userFeedback)
Expand Down
7 changes: 6 additions & 1 deletion SentryTestUtils/TestTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import _SentryPrivate
import Foundation

public class TestTransport: NSObject, Transport {

public var sentEnvelopes = Invocations<SentryEnvelope>()
public func send(envelope: SentryEnvelope) {
sentEnvelopes.record(envelope)
}

public var storedEnvelopes = Invocations<SentryEnvelope>()
public func store(_ envelope: SentryEnvelope) {
storedEnvelopes.record(envelope)
}

public var recordLostEvents = Invocations<(category: SentryDataCategory, reason: SentryDiscardReason)>()
public func recordLostEvent(_ category: SentryDataCategory, reason: SentryDiscardReason) {
recordLostEvents.record((category, reason))
Expand Down
5 changes: 5 additions & 0 deletions SentryTestUtils/TestTransportAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public class TestTransportAdapter: SentryTransportAdapter {
public override func send(event: Event, traceContext: TraceContext?, attachments: [Attachment], additionalEnvelopeItems: [SentryEnvelopeItem]) {
sendEventWithTraceStateInvocations.record((event, traceContext, attachments, additionalEnvelopeItems))
}

public var storeEventInvocations = Invocations<(event: Event, traceContext: TraceContext?)>()
public override func store(_ event: Event, traceContext: TraceContext?) {
storeEventInvocations.record((event, traceContext))
}

public var userFeedbackInvocations = Invocations<UserFeedback>()
public override func send(userFeedback: UserFeedback) {
Expand Down
11 changes: 11 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,17 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, assign) BOOL enablePerformanceV2;

/**
* @warning This is an experimental feature and may still have bugs.
*
* When enabled, the SDK finishes the ongoing transaction bound to the scope and links them to the
* crash event when your app crashes. The SDK skips adding profiles to increase the chance of
* keeping the transaction.
*
* @note The default is @c NO .
*/
@property (nonatomic, assign) BOOL enablePersistingTracesWhenCrashing;

/**
* A block that configures the initial scope when starting the SDK.
* @discussion The block receives a suggested default scope. You can either
Expand Down
16 changes: 16 additions & 0 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,22 @@ - (SentryId *)captureCrashEvent:(SentryEvent *)event
return [self sendEvent:preparedEvent withSession:session withScope:scope];
}

- (void)saveCrashTransaction:(SentryTransaction *)transaction withScope:(SentryScope *)scope
{
SentryEvent *preparedEvent = [self prepareEvent:transaction
withScope:scope
alwaysAttachStacktrace:NO
isCrashEvent:NO];

if (preparedEvent == nil) {
return;
}

SentryTraceContext *traceContext = [self getTraceStateWithEvent:transaction withScope:scope];

[self.transportAdapter storeEvent:preparedEvent traceContext:traceContext];
}

- (SentryId *)captureEvent:(SentryEvent *)event
{
return [self captureEvent:event withScope:[[SentryScope alloc] init]];
Expand Down
25 changes: 25 additions & 0 deletions Sources/Sentry/SentryCrashIntegration.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "SentryCrashIntegration.h"
#import "SentryCrashInstallationReporter.h"

#import "SentryCrashC.h"
#include "SentryCrashMonitor_Signal.h"
#import "SentryCrashWrapper.h"
#import "SentryDispatchQueueWrapper.h"
Expand All @@ -11,6 +12,8 @@
#import "SentrySDK+Private.h"
#import "SentryScope+Private.h"
#import "SentrySessionCrashedHandler.h"
#import "SentrySpan+Private.h"
#import "SentryTracer.h"
#import "SentryWatchdogTerminationLogic.h"
#import <SentryAppStateManager.h>
#import <SentryClient+Private.h>
Expand All @@ -34,6 +37,17 @@
static NSString *const DEVICE_KEY = @"device";
static NSString *const LOCALE_KEY = @"locale";

void
sentry_finishAndSaveTransaction(void)
{
SentrySpan *span = SentrySDK.currentHub.scope.span;

if (span != nil) {
SentryTracer *tracer = [span tracer];
[tracer finishForCrash];
}
}

@interface SentryCrashIntegration ()

@property (nonatomic, weak) SentryOptions *options;
Expand Down Expand Up @@ -110,6 +124,10 @@ - (BOOL)installWithOptions:(nonnull SentryOptions *)options

[self configureScope];

if (options.enablePersistingTracesWhenCrashing) {
[self configureTracingWhenCrashing];
}

return YES;
}

Expand Down Expand Up @@ -193,6 +211,8 @@ - (void)uninstall
installationToken = 0;
}

sentrycrash_setSaveTransaction(NULL);

[NSNotificationCenter.defaultCenter removeObserver:self
name:NSCurrentLocaleDidChangeNotification
object:nil];
Expand Down Expand Up @@ -243,4 +263,9 @@ - (void)currentLocaleDidChange
}];
}

- (void)configureTracingWhenCrashing
{
sentrycrash_setSaveTransaction(&sentry_finishAndSaveTransaction);
}

@end
10 changes: 9 additions & 1 deletion Sources/Sentry/SentryCrashReportConverter.m
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,15 @@ - (SentryEvent *_Nullable)convertReportToEvent

event.dist = self.userContext[@"dist"];
event.environment = self.userContext[@"environment"];
event.context = self.userContext[@"context"];

NSMutableDictionary *mutableContext =
[[NSMutableDictionary alloc] initWithDictionary:self.userContext[@"context"]];
if (self.userContext[@"traceContext"]) {
mutableContext[@"trace"] = self.userContext[@"traceContext"];
}

event.context = mutableContext;

event.extra = self.userContext[@"extra"];
event.tags = self.userContext[@"tags"];
// event.level we do not set the level here since this always resulted
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/SentryCrashScopeObserver.m
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ - (void)setContext:(nullable NSDictionary<NSString *, id> *)context
syncToSentryCrash:^(const void *bytes) { sentrycrash_scopesync_setContext(bytes); }];
}

- (void)setTraceContext:(nullable NSDictionary<NSString *, id> *)traceContext
{
[self syncScope:traceContext
syncToSentryCrash:^(const void *bytes) { sentrycrash_scopesync_setTraceContext(bytes); }];
}

- (void)setExtras:(nullable NSDictionary<NSString *, id> *)extras
{
[self syncScope:extras
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentryHttpTransport.m
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope
}];
}

- (void)storeEnvelope:(SentryEnvelope *)envelope
{
[self.fileManager storeEnvelope:envelope];
}

- (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason
{
[self recordLostEvent:category reason:reason quantity:1];
Expand Down
15 changes: 15 additions & 0 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,21 @@ - (void)captureTransaction:(SentryTransaction *)transaction
}];
}

- (void)saveCrashTransaction:(SentryTransaction *)transaction
{
SentrySampleDecision decision = transaction.trace.sampled;

if (decision != kSentrySampleDecisionYes) {
// No need to update client reports when we're crashing cause they get lost anyways.
return;
}

SentryClient *client = _client;
if (client != nil) {
[client saveCrashTransaction:transaction withScope:self.scope];
}
}

- (SentryId *)captureEvent:(SentryEvent *)event
{
return [self captureEvent:event withScope:self.scope];
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ - (instancetype)init
self.sendDefaultPii = NO;
self.enableAutoPerformanceTracing = YES;
self.enablePerformanceV2 = NO;
self.enablePersistingTracesWhenCrashing = NO;
self.enableCaptureFailedRequests = YES;
self.environment = kSentryDefaultEnvironment;
self.enableTimeToFullDisplayTracing = NO;
Expand Down Expand Up @@ -413,6 +414,9 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
[self setBool:options[@"enablePerformanceV2"]
block:^(BOOL value) { self->_enablePerformanceV2 = value; }];

[self setBool:options[@"enablePersistingTracesWhenCrashing"]
block:^(BOOL value) { self->_enablePersistingTracesWhenCrashing = value; }];

[self setBool:options[@"enableCaptureFailedRequests"]
block:^(BOOL value) { self->_enableCaptureFailedRequests = value; }];

Expand Down
38 changes: 30 additions & 8 deletions Sources/Sentry/SentryScope.m
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ - (void)setSpan:(nullable id<SentrySpan>)span
{
@synchronized(_spanLock) {
_span = span;

for (id<SentryScopeObserver> observer in self.observers) {
[observer setTraceContext:[self buildTraceContext:span]];
}
}
}

Expand Down Expand Up @@ -453,9 +457,18 @@ - (void)clearAttachments
if (self.extras.count > 0) {
[serializedData setValue:[self extras] forKey:@"extra"];
}
if (self.context.count > 0) {
[serializedData setValue:[self context] forKey:@"context"];

NSDictionary *traceContext = nil;
@synchronized(_spanLock) {
traceContext = [self buildTraceContext:_span];
}
serializedData[@"traceContext"] = traceContext;

NSDictionary *context = [self context];
if (context.count > 0) {
[serializedData setValue:context forKey:@"context"];
}

[serializedData setValue:[self.userObject serialize] forKey:@"user"];
[serializedData setValue:self.distString forKey:@"dist"];
[serializedData setValue:self.environmentString forKey:@"environment"];
Expand Down Expand Up @@ -571,8 +584,9 @@ - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event
return event;
}

id<SentrySpan> span;

if (self.span != nil) {
id<SentrySpan> span;
@synchronized(_spanLock) {
span = self.span;
}
Expand All @@ -583,14 +597,10 @@ - (SentryEvent *__nullable)applyToEvent:(SentryEvent *)event
[span isKindOfClass:[SentryTracer class]]) {
event.transaction = [[(SentryTracer *)span transactionContext] name];
}
newContext[@"trace"] = [span serialize];
}
}

if (newContext[@"trace"] == nil) {
// If this is an error event we need to add the distributed trace context.
newContext[@"trace"] = [self.propagationContext traceContextForEvent];
}
newContext[@"trace"] = [self buildTraceContext:span];

event.context = newContext;
return event;
Expand All @@ -601,6 +611,18 @@ - (void)addObserver:(id<SentryScopeObserver>)observer
[self.observers addObject:observer];
}

/**
* Make sure to call this inside @c synchronized(_spanLock) caus this method isn't thread safe.
*/
- (NSDictionary *)buildTraceContext:(nullable id<SentrySpan>)span
{
if (span != nil) {
return [span serialize];
} else {
return [self.propagationContext traceContextForEvent];
}
}

@end

NS_ASSUME_NONNULL_END
6 changes: 6 additions & 0 deletions Sources/Sentry/SentryScopeSyncC.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ sentrycrash_scopesync_setContext(const char *const jsonEncodedCString)
setField(jsonEncodedCString, &scope.context);
}

void
sentrycrash_scopesync_setTraceContext(const char *const jsonEncodedCString)
{
setField(jsonEncodedCString, &scope.traceContext);
}

void
sentrycrash_scopesync_setEnvironment(const char *const jsonEncodedCString)
{
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/SentrySpotlightTransport.m
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope
}];
}

- (void)storeEnvelope:(SentryEnvelope *)envelope
{
[self sendEnvelope:envelope];
}

- (SentryFlushResult)flush:(NSTimeInterval)timeout
{
// Empty on purpose
Expand Down
Loading

0 comments on commit 0d38ada

Please sign in to comment.