Skip to content

Commit

Permalink
feat: Add beforeSendSpan callback (#4095)
Browse files Browse the repository at this point in the history
Add a callback to drop or change spans of a transaction.

Fixes GH-4083
  • Loading branch information
philipphofmann authored Jun 21, 2024
1 parent 8123181 commit b9fc537
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Restart replay session with mobile session (#4085)
- Add pause and resume AppHangTracking API (#4077). You can now pause and resume app hang tracking with `SentrySDK.pauseAppHangTracking()` and `SentrySDK.resumeAppHangTracking()`.
- Add `beforeSendSpan` callback (#4095)

### Fixes

Expand Down
3 changes: 3 additions & 0 deletions Samples/iOS-Swift/iOS-Swift/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
options.beforeSend = { event in
return event
}
options.beforeSendSpan = { span in
return span
}
options.enableSigtermReporting = true
options.beforeCaptureScreenshot = { _ in
return true
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/Public/SentryDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ typedef SentryBreadcrumb *_Nullable (^SentryBeforeBreadcrumbCallback)(
*/
typedef SentryEvent *_Nullable (^SentryBeforeSendEventCallback)(SentryEvent *_Nonnull event);

/**
* Use this block to drop or modify a span before the SDK sends it to Sentry. Return @c nil to drop
* the span.
*/
typedef id<SentrySpan> _Nullable (^SentryBeforeSendSpanCallback)(id<SentrySpan> _Nonnull span);

/**
* Block can be used to decide if the SDK should capture a screenshot or not. Return @c true if the
* SDK should capture a screenshot, return @c false if not. This callback doesn't work for crashes.
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ NS_SWIFT_NAME(Options)
*/
@property (nullable, nonatomic, copy) SentryBeforeSendEventCallback beforeSend;

/**
* Use this callback to drop or modify a span before the SDK sends it to Sentry. Return @c nil to
* drop the span.
*/
@property (nullable, nonatomic, copy) SentryBeforeSendSpanCallback beforeSendSpan;

/**
* This block can be used to modify the event before it will be serialized and sent.
*/
Expand Down
15 changes: 15 additions & 0 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,21 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
[self recordLost:eventIsNotATransaction reason:kSentryDiscardReasonEventProcessor];
}

BOOL eventIsATransaction
= event.type != nil && [event.type isEqualToString:SentryEnvelopeItemTypeTransaction];
if (event != nil && eventIsATransaction && self.options.beforeSendSpan != nil) {
SentryTransaction *transaction = (SentryTransaction *)event;
NSMutableArray<id<SentrySpan>> *processedSpans = [NSMutableArray array];
for (id<SentrySpan> span in transaction.spans) {
id<SentrySpan> processedSpan = self.options.beforeSendSpan(span);
if (processedSpan) {
[processedSpans addObject:processedSpan];
}
}

transaction.spans = processedSpans;
}

if (event != nil && nil != self.options.beforeSend) {
event = self.options.beforeSend(event);

Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
self.beforeSend = options[@"beforeSend"];
}

if ([self isBlock:options[@"beforeSendSpan"]]) {
self.beforeSendSpan = options[@"beforeSendSpan"];
}

if ([self isBlock:options[@"beforeBreadcrumb"]]) {
self.beforeBreadcrumb = options[@"beforeBreadcrumb"];
}
Expand Down
7 changes: 0 additions & 7 deletions Sources/Sentry/SentryTransaction.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@

NS_ASSUME_NONNULL_BEGIN

@interface
SentryTransaction ()

@property (nonatomic, strong) NSArray<id<SentrySpan>> *spans;

@end

@implementation SentryTransaction

- (instancetype)initWithTrace:(SentryTracer *)trace children:(NSArray<id<SentrySpan>> *)children
Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/include/SentryTransaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ SENTRY_NO_INIT

@property (nonatomic, strong) SentryTracer *trace;
@property (nonatomic, copy, nullable) NSArray<NSString *> *viewNames;
@property (nonatomic, strong) NSArray<id<SentrySpan>> *spans;

- (instancetype)initWithTrace:(SentryTracer *)trace children:(NSArray<id<SentrySpan>> *)children;

Expand Down
78 changes: 78 additions & 0 deletions Tests/SentryTests/SentryClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,76 @@ class SentryClientTest: XCTestCase {
XCTAssertEqual([], actual.threads)
}
}

func testBeforeSendSpanDitchOneSpan_OtherChangedSpanSent() throws {
let spanOne = getSpan(operation: "operation.one", tracer: fixture.trace)
let spanTwo = getSpan(operation: "operation.two", tracer: fixture.trace)
let transaction = Transaction(trace: fixture.trace, children: [spanOne, spanTwo])

fixture.getSut(configureOptions: { options in
options.beforeSendSpan = { span in
if span.operation == "operation.one" {
span.operation = "changed"
return span
}

return nil
}
}).capture(event: transaction)

try assertLastSentEvent { actual in
let serialized = actual.serialize()
let serializedSpans = try XCTUnwrap(serialized["spans"] as? [[String: Any]])
XCTAssertEqual(1, serializedSpans.count)

let serializedSpan = try XCTUnwrap(serializedSpans.first)

XCTAssertEqual("changed", serializedSpan["op"] as? String)
}
}

func testBeforeSendSpanIsNil_SpansUntouched() throws {
let tracer = fixture.trace
let span = getSpan(operation: "operation", tracer: tracer)
let transaction = Transaction(trace: fixture.trace, children: [span])
fixture.getSut().capture(event: transaction)

try assertLastSentEvent { actual in

let serialized = actual.serialize()
let serializedSpans = try XCTUnwrap(serialized["spans"] as? [[String: Any]])
XCTAssertEqual(1, serializedSpans.count)
let serializedSpan = try XCTUnwrap(serializedSpans.first)

XCTAssertEqual("operation", serializedSpan["op"] as? String)
}
}

/// Ensure that you can't start and finish new spans in the beforeSendSpan Callback
func testBeforeSendSpan_StartSpan_ReturnsNoOpSpan() throws {
let tracer = fixture.trace
let span = getSpan(operation: "operation", tracer: tracer)
tracer.finish()

let transaction = Transaction(trace: tracer, children: [span])

fixture.getSut(configureOptions: { options in
options.beforeSendSpan = { span in
let childSpan = span.startChild(operation: "op")

XCTAssert(childSpan.isKind(of: SentryNoOpSpan.self))

return span
}
}).capture(event: transaction)

try assertLastSentEvent { actual in

let serialized = actual.serialize()
let serializedSpans = try XCTUnwrap(serialized["spans"] as? [[String: Any]])
XCTAssertEqual(1, serializedSpans.count)
}
}

func testNoDsn_MessageNotSent() {
let sut = fixture.getSutWithNoDsn()
Expand Down Expand Up @@ -1698,6 +1768,14 @@ class SentryClientTest: XCTestCase {
return event
}

private func getSpan(operation: String, tracer: SentryTracer) -> Span {
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
return SentrySpan(tracer: tracer, context: SpanContext(operation: operation), framesTracker: nil)
#else
return SentrySpan(tracer: tracer, context: SpanContext(operation: operation))
#endif
}

private func beforeSendReturnsNil(capture: (SentryClient) -> Void) {
capture(fixture.getSut(configureOptions: { options in
options.beforeSend = { _ in
Expand Down
16 changes: 16 additions & 0 deletions Tests/SentryTests/SentryOptionsTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#import "SentryError.h"
#import "SentryOptions+HybridSDKs.h"
#import "SentrySDK.h"
#import "SentrySpan.h"
#import "SentryTests-Swift.h"
#import <XCTest/XCTest.h>
@import Nimble;
Expand Down Expand Up @@ -299,6 +300,21 @@ - (void)testNSNullBeforeSend_ReturnsNil
XCTAssertFalse([options.beforeSend isEqual:[NSNull null]]);
}

- (void)testBeforeSendSpan
{
SentryBeforeSendSpanCallback callback = ^(id<SentrySpan> span) { return span; };
SentryOptions *options = [self getValidOptions:@{ @"beforeSendSpan" : callback }];

XCTAssertEqual(callback, options.beforeSendSpan);
}

- (void)testDefaultBeforeSendSpan
{
SentryOptions *options = [self getValidOptions:@{}];

XCTAssertNil(options.beforeSendSpan);
}

- (void)testBeforeBreadcrumb
{
SentryBeforeBreadcrumbCallback callback
Expand Down

0 comments on commit b9fc537

Please sign in to comment.