diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e809f55540..7797d82dedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 08d88d09313..b34643d0145 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -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 diff --git a/Sources/Sentry/Public/SentryDefines.h b/Sources/Sentry/Public/SentryDefines.h index c71be224b32..ac550383f24 100644 --- a/Sources/Sentry/Public/SentryDefines.h +++ b/Sources/Sentry/Public/SentryDefines.h @@ -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 _Nullable (^SentryBeforeSendSpanCallback)(id _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. diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 852ffa67c8c..01cf2c25618 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -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. */ diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 3517f7713b6..8af4b75e600 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -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> *processedSpans = [NSMutableArray array]; + for (id span in transaction.spans) { + id 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); diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 760409ff49f..54fa1f38528 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -343,6 +343,10 @@ - (BOOL)validateOptions:(NSDictionary *)options self.beforeSend = options[@"beforeSend"]; } + if ([self isBlock:options[@"beforeSendSpan"]]) { + self.beforeSendSpan = options[@"beforeSendSpan"]; + } + if ([self isBlock:options[@"beforeBreadcrumb"]]) { self.beforeBreadcrumb = options[@"beforeBreadcrumb"]; } diff --git a/Sources/Sentry/SentryTransaction.m b/Sources/Sentry/SentryTransaction.m index 5aa4459fa3f..dff36566dfe 100644 --- a/Sources/Sentry/SentryTransaction.m +++ b/Sources/Sentry/SentryTransaction.m @@ -9,13 +9,6 @@ NS_ASSUME_NONNULL_BEGIN -@interface -SentryTransaction () - -@property (nonatomic, strong) NSArray> *spans; - -@end - @implementation SentryTransaction - (instancetype)initWithTrace:(SentryTracer *)trace children:(NSArray> *)children diff --git a/Sources/Sentry/include/SentryTransaction.h b/Sources/Sentry/include/SentryTransaction.h index 2eceb4aad87..77a12e3b008 100644 --- a/Sources/Sentry/include/SentryTransaction.h +++ b/Sources/Sentry/include/SentryTransaction.h @@ -12,6 +12,7 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentryTracer *trace; @property (nonatomic, copy, nullable) NSArray *viewNames; +@property (nonatomic, strong) NSArray> *spans; - (instancetype)initWithTrace:(SentryTracer *)trace children:(NSArray> *)children; diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 035bba36041..6ad0c9f8995 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -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() @@ -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 diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 6fea3d117db..e8d9414a8e7 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -2,6 +2,7 @@ #import "SentryError.h" #import "SentryOptions+HybridSDKs.h" #import "SentrySDK.h" +#import "SentrySpan.h" #import "SentryTests-Swift.h" #import @import Nimble; @@ -299,6 +300,21 @@ - (void)testNSNullBeforeSend_ReturnsNil XCTAssertFalse([options.beforeSend isEqual:[NSNull null]]); } +- (void)testBeforeSendSpan +{ + SentryBeforeSendSpanCallback callback = ^(id 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