diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bfd2c9b2..5c32c92d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +## Improvements + +- Add `sample_rand` to baggage (#4751) + +## Fixes + +- Fix missing `sample_rate` in baggage (#4751) + ## 8.44.0 ### Fixes diff --git a/Makefile b/Makefile index bf4a33ca04..755196221e 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,12 @@ test: run-test-server: cd ./test-server && swift build cd ./test-server && swift run & -.PHONY: run-test-server + +run-test-server-sync: + cd ./test-server && swift build + cd ./test-server && swift run + +.PHONY: run-test-server run-test-server-sync test-alamofire: ./scripts/test-alamofire.sh diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 7d6732ed87..94772880f4 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -787,6 +787,7 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; + D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; @@ -894,7 +895,7 @@ D8853C842833EABC00700D64 /* SentryANRTrackerV1.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BCFA71427D0BAB7008C662C /* SentryANRTrackerV1.h */; }; D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceContext.m */; }; D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; + D88817DD26D72BA500BF2251 /* SentryTraceContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceContextTests.swift */; }; D88B30A92D48D8C3008DE513 /* SentryMaskingPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88B30A82D48D88E008DE513 /* SentryMaskingPreviewView.swift */; }; D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8A3649C2C91AA3300AC569B /* SentryReplayApi.m in Sources */ = {isa = PBXBuildFile; fileRef = D8A3649B2C91AA3300AC569B /* SentryReplayApi.m */; }; @@ -1897,6 +1898,7 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; + D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D41909922D48FFF6002B83D0 /* SentryNSDictionarySanitize+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryNSDictionarySanitize+Tests.h"; sourceTree = ""; }; D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryNSDictionarySanitize+Tests.m"; sourceTree = ""; }; D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; @@ -2013,7 +2015,7 @@ D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFileIOTrackingIntegrationTests.swift; sourceTree = ""; }; D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = Public/SentryTraceContext.h; sourceTree = ""; }; - D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; + D88817DB26D72B7B00BF2251 /* SentryTraceContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceContextTests.swift; sourceTree = ""; }; D88B30A82D48D88E008DE513 /* SentryMaskingPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryMaskingPreviewView.swift; sourceTree = ""; }; D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; @@ -3016,17 +3018,18 @@ 7B6C5ED4264E62B60010D138 /* Transaction */ = { isa = PBXGroup; children = ( - 7B6C5ED5264E62CA0010D138 /* SentryTransactionTests.swift */, + D880E3A628573E87008A90DB /* SentryBaggageTests.swift */, + D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */, + 7BE912AE272166DD00E49E62 /* SentryNoOpSpanTests.swift */, 8EC4CF4F25C3A0070093DEE9 /* SentrySpanContextTests.swift */, 8E70B0FC25CB72BE002B3155 /* SentrySpanTests.swift */, - D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */, - 7BE912AE272166DD00E49E62 /* SentryNoOpSpanTests.swift */, - D880E3A628573E87008A90DB /* SentryBaggageTests.swift */, - 8EAC7FF7265C8910005B44E5 /* SentryTracerTests.swift */, + D88817DB26D72B7B00BF2251 /* SentryTraceContextTests.swift */, 7BBEB16026AEE5EF00C06C03 /* SentryTracer+Test.h */, + 8EAC7FF7265C8910005B44E5 /* SentryTracerTests.swift */, + 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */, + 7B6C5ED5264E62CA0010D138 /* SentryTransactionTests.swift */, D8137D52272B53070082656C /* TestSentrySpan.h */, D8137D53272B53070082656C /* TestSentrySpan.m */, - 62950F0F29E7FE0100A42624 /* SentryTransactionContextTests.swift */, ); path = Transaction; sourceTree = ""; @@ -5146,7 +5149,7 @@ 62CFD9AD2C99770B00834E1B /* SentryInvalidJSONString.m in Sources */, 62375FB92B47F9F000CC55F1 /* SentryDependencyContainerTests.swift in Sources */, 7BC6EC08255C36DE0059822A /* SentryStacktraceTests.swift in Sources */, - D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */, + D88817DD26D72BA500BF2251 /* SentryTraceContextTests.swift in Sources */, 7B26BBFB24C0A66D00A79CCC /* SentrySdkInfoNilTests.m in Sources */, D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */, 7B984A9F28E572AF001F4BEE /* CrashReport.swift in Sources */, @@ -5245,6 +5248,7 @@ 849AC40029E0C1FF00889C16 /* SentryFormatterTests.swift in Sources */, 7BDDE3CC2966BD4700EB9177 /* SentryMXManagerTests.swift in Sources */, 7BC6EC0C255C3DF80059822A /* SentryThreadTests.swift in Sources */, + D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */, D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */, 8E70B10125CB8695002B3155 /* SentrySpanIdTests.swift in Sources */, 84EB21962BF01CEA00EDDA28 /* SentryCrashInstallationTests.swift in Sources */, diff --git a/SentryTestUtils/SentryLaunchProfiling+Tests.h b/SentryTestUtils/SentryLaunchProfiling+Tests.h index 4403e65671..fbd4278e25 100644 --- a/SentryTestUtils/SentryLaunchProfiling+Tests.h +++ b/SentryTestUtils/SentryLaunchProfiling+Tests.h @@ -17,7 +17,9 @@ typedef struct { } SentryLaunchProfileConfig; SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate; +SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyTracesSampleRand; SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate; +SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRand; SENTRY_EXTERN NSString *const kSentryLaunchProfileConfigKeyContinuousProfiling; SENTRY_EXTERN SentryTracer *_Nullable sentry_launchTracer; @@ -39,7 +41,7 @@ BOOL sentry_willProfileNextLaunch(SentryOptions *options); */ void _sentry_nondeduplicated_startLaunchProfile(void); -SentryTransactionContext *sentry_context(NSNumber *tracesRate); +SentryTransactionContext *sentry_context(NSNumber *tracesRate, NSNumber *tracesRand); NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Profiling/SentryLaunchProfiling.m b/Sources/Sentry/Profiling/SentryLaunchProfiling.m index a4dbe7bf43..782605ef70 100644 --- a/Sources/Sentry/Profiling/SentryLaunchProfiling.m +++ b/Sources/Sentry/Profiling/SentryLaunchProfiling.m @@ -25,7 +25,9 @@ BOOL isProfilingAppLaunch; NSString *const kSentryLaunchProfileConfigKeyTracesSampleRate = @"traces"; +NSString *const kSentryLaunchProfileConfigKeyTracesSampleRand = @"traces.sample_rand"; NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRate = @"profiles"; +NSString *const kSentryLaunchProfileConfigKeyProfilesSampleRand = @"profiles.sample_rand"; NSString *const kSentryLaunchProfileConfigKeyContinuousProfiling = @"continuous-profiling"; static SentryTracer *_Nullable launchTracer; @@ -34,12 +36,13 @@ SentryTracer *_Nullable sentry_launchTracer; SentryTracerConfiguration * -sentry_config(NSNumber *profilesRate) +sentry_config(NSNumber *profilesRate, NSNumber *profilesRand) { SentryTracerConfiguration *config = [SentryTracerConfiguration defaultConfiguration]; config.profilesSamplerDecision = [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionYes - forSampleRate:profilesRate]; + forSampleRate:profilesRate + withSampleRand:profilesRand]; return config; } @@ -92,15 +95,16 @@ } SentryTransactionContext * -sentry_context(NSNumber *tracesRate) +sentry_context(NSNumber *tracesRate, NSNumber *tracesRand) { SentryTransactionContext *context = [[SentryTransactionContext alloc] initWithName:@"launch" nameSource:kSentryTransactionNameSourceCustom operation:SentrySpanOperation.appLifecycle origin:SentryTraceOrigin.autoAppStartProfile - sampled:kSentrySampleDecisionYes]; - context.sampleRate = tracesRate; + sampled:kSentrySampleDecisionYes + sampleRate:tracesRate + sampleRand:tracesRand]; return context; } @@ -143,6 +147,13 @@ return; } + NSNumber *profilesRand = launchConfig[kSentryLaunchProfileConfigKeyProfilesSampleRand]; + if (profilesRate == nil) { + SENTRY_LOG_DEBUG(@"Received a nil configured launch profile sample rand, will not " + @"start trace profiler for launch."); + return; + } + NSNumber *tracesRate = launchConfig[kSentryLaunchProfileConfigKeyTracesSampleRate]; if (tracesRate == nil) { SENTRY_LOG_DEBUG(@"Received a nil configured launch trace sample rate, will not start " @@ -150,12 +161,19 @@ return; } + NSNumber *tracesRand = launchConfig[kSentryLaunchProfileConfigKeyTracesSampleRand]; + if (tracesRate == nil) { + SENTRY_LOG_DEBUG(@"Received a nil configured launch trace sample rand, will not start " + @"trace profiler for launch."); + return; + } + SENTRY_LOG_INFO(@"Starting app launch trace profile at %llu.", getAbsoluteTime()); sentry_isTracingAppLaunch = YES; sentry_launchTracer = - [[SentryTracer alloc] initWithTransactionContext:sentry_context(tracesRate) + [[SentryTracer alloc] initWithTransactionContext:sentry_context(tracesRate, tracesRand) hub:nil - configuration:sentry_config(profilesRate)]; + configuration:sentry_config(profilesRate, profilesRand)]; } # pragma mark - Public @@ -179,8 +197,12 @@ } else { configDict[kSentryLaunchProfileConfigKeyTracesSampleRate] = config.tracesDecision.sampleRate; + configDict[kSentryLaunchProfileConfigKeyTracesSampleRand] + = config.tracesDecision.sampleRand; configDict[kSentryLaunchProfileConfigKeyProfilesSampleRate] = config.profilesDecision.sampleRate; + configDict[kSentryLaunchProfileConfigKeyProfilesSampleRand] + = config.profilesDecision.sampleRand; } writeAppLaunchProfilingConfigFile(configDict); }]; diff --git a/Sources/Sentry/Public/SentryBaggage.h b/Sources/Sentry/Public/SentryBaggage.h index e306060f38..54223b0841 100644 --- a/Sources/Sentry/Public/SentryBaggage.h +++ b/Sources/Sentry/Public/SentryBaggage.h @@ -45,6 +45,13 @@ NS_SWIFT_NAME(Baggage) */ @property (nullable, nonatomic, readonly) NSString *userSegment; +/** + * The random value used to determine if the trace is sampled. + * + * A float (`0.1234` notation) in the range of `[0, 1)` (including 0.0, excluding 1.0). + */ +@property (nullable, nonatomic, readonly) NSString *sampleRand; + /** * The sample rate. */ @@ -67,6 +74,17 @@ NS_SWIFT_NAME(Baggage) sampled:(nullable NSString *)sampled replayId:(nullable NSString *)replayId; +- (instancetype)initWithTraceId:(SentryId *)traceId + publicKey:(NSString *)publicKey + releaseName:(nullable NSString *)releaseName + environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction + userSegment:(nullable NSString *)userSegment + sampleRate:(nullable NSString *)sampleRate + sampleRand:(nullable NSString *)sampleRand + sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId; + - (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalBaggage; @end diff --git a/Sources/Sentry/Public/SentryTraceContext.h b/Sources/Sentry/Public/SentryTraceContext.h index afb939b84a..790440489c 100644 --- a/Sources/Sentry/Public/SentryTraceContext.h +++ b/Sources/Sentry/Public/SentryTraceContext.h @@ -48,10 +48,15 @@ NS_SWIFT_NAME(TraceContext) @property (nullable, nonatomic, readonly) NSString *userSegment; /** - * Sample rate used for this trace. + * Serialized sample rate used for this trace. */ @property (nullable, nonatomic, readonly) NSString *sampleRate; +/** + * Serialized random value used to determine if the trace is sampled. + */ +@property (nullable, nonatomic, readonly) NSString *sampleRand; + /** * Value indicating whether the trace was sampled. */ @@ -75,6 +80,20 @@ NS_SWIFT_NAME(TraceContext) sampled:(nullable NSString *)sampled replayId:(nullable NSString *)replayId; +/** + * Initializes a SentryTraceContext with given properties. + */ +- (instancetype)initWithTraceId:(SentryId *)traceId + publicKey:(NSString *)publicKey + releaseName:(nullable NSString *)releaseName + environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction + userSegment:(nullable NSString *)userSegment + sampleRate:(nullable NSString *)sampleRate + sampleRand:(nullable NSString *)sampleRand + sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId; + /** * Initializes a SentryTraceContext with data from scope and options. */ diff --git a/Sources/Sentry/Public/SentryTransactionContext.h b/Sources/Sentry/Public/SentryTransactionContext.h index f15693d824..16ccfe40da 100644 --- a/Sources/Sentry/Public/SentryTransactionContext.h +++ b/Sources/Sentry/Public/SentryTransactionContext.h @@ -21,15 +21,30 @@ SENTRY_NO_INIT @property (nonatomic, readonly) NSString *name; @property (nonatomic, readonly) SentryTransactionNameSource nameSource; +/** + * Rate of sampling + */ +@property (nonatomic, strong, nullable) NSNumber *sampleRate; + +/** + * Random value used to determine if the span is sampled. + */ +@property (nonatomic, strong, nullable) NSNumber *sampleRand; + /** * Parent sampled */ @property (nonatomic) SentrySampleDecision parentSampled; /** - * Sample rate used for this transaction + * Parent sample rate used for this transaction */ -@property (nonatomic, strong, nullable) NSNumber *sampleRate; +@property (nonatomic, strong, nullable) NSNumber *parentSampleRate; + +/** + * Parent random value used to determine if the trace is sampled. + */ +@property (nonatomic, strong, nullable) NSNumber *parentSampleRand; /** * If app launch profiling is enabled via @c SentryOptions.enableAppLaunchProfiling and @@ -54,7 +69,37 @@ SENTRY_NO_INIT */ - (instancetype)initWithName:(NSString *)name operation:(NSString *)operation - sampled:(SentrySampleDecision)sampled; + sampled:(SentrySampleDecision)sampled + DEPRECATED_MSG_ATTRIBUTE("Use initWithName:operation:sampled:sampleRate:sampleRand instead"); + +/** + * @param name Transaction name + * @param operation The operation this span is measuring. + * @param sampled Determines whether the trace should be sampled. + */ +- (instancetype)initWithName:(NSString *)name + operation:(NSString *)operation + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; + +/** + * @param name Transaction name + * @param operation The operation this span is measuring. + * @param traceId Trace Id + * @param spanId Span Id + * @param parentSpanId Parent span id + * @param parentSampled Whether the parent is sampled + */ +- (instancetype)initWithName:(NSString *)name + operation:(NSString *)operation + traceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentSpanId:(nullable SentrySpanId *)parentSpanId + parentSampled:(SentrySampleDecision)parentSampled + DEPRECATED_MSG_ATTRIBUTE("Use " + "initWithName:operation:traceId:spanId:parentSpanId:parentSampled:" + "parentSampleRate:parentSampleRand instead"); /** * @param name Transaction name @@ -69,7 +114,9 @@ SENTRY_NO_INIT traceId:(SentryId *)traceId spanId:(SentrySpanId *)spanId parentSpanId:(nullable SentrySpanId *)parentSpanId - parentSampled:(SentrySampleDecision)parentSampled; + parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand; @end diff --git a/Sources/Sentry/SentryBaggage.m b/Sources/Sentry/SentryBaggage.m index 28e9fe2d28..4f43781724 100644 --- a/Sources/Sentry/SentryBaggage.m +++ b/Sources/Sentry/SentryBaggage.m @@ -20,6 +20,29 @@ - (instancetype)initWithTraceId:(SentryId *)traceId sampled:(nullable NSString *)sampled replayId:(nullable NSString *)replayId { + return [self initWithTraceId:traceId + publicKey:publicKey + releaseName:releaseName + environment:environment + transaction:transaction + userSegment:userSegment + sampleRate:sampleRate + sampleRand:nil + sampled:sampled + replayId:replayId]; +} + +- (instancetype)initWithTraceId:(SentryId *)traceId + publicKey:(NSString *)publicKey + releaseName:(nullable NSString *)releaseName + environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction + userSegment:(nullable NSString *)userSegment + sampleRate:(nullable NSString *)sampleRate + sampleRand:(nullable NSString *)sampleRand + sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId +{ if (self = [super init]) { _traceId = traceId; @@ -29,6 +52,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId _transaction = transaction; _userSegment = userSegment; _sampleRate = sampleRate; + _sampleRand = sampleRand; _sampled = sampled; _replayId = replayId; } @@ -60,6 +84,10 @@ - (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalB [information setValue:_userSegment forKey:@"sentry-user_segment"]; } + if (_sampleRand != nil) { + [information setValue:_sampleRand forKey:@"sentry-sample_rand"]; + } + if (_sampleRate != nil) { [information setValue:_sampleRate forKey:@"sentry-sample_rate"]; } diff --git a/Sources/Sentry/SentryBuildAppStartSpans.m b/Sources/Sentry/SentryBuildAppStartSpans.m index e59f8f697f..61422b4cb1 100644 --- a/Sources/Sentry/SentryBuildAppStartSpans.m +++ b/Sources/Sentry/SentryBuildAppStartSpans.m @@ -26,7 +26,8 @@ } NSArray * -sentryBuildAppStartSpans(SentryTracer *tracer, SentryAppStartMeasurement *appStartMeasurement) +sentryBuildAppStartSpans( + SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement) { if (appStartMeasurement == nil) { diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 50a9199f5e..4c5eb6d567 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -393,8 +393,9 @@ - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent - (SentryTransactionContext *)transactionContext:(SentryTransactionContext *)context withSampled:(SentrySampleDecision)sampleDecision + sampleRate:(NSNumber *)sampleRate + sampleRand:(NSNumber *)sampleRand { - return [[SentryTransactionContext alloc] initWithName:context.name nameSource:context.nameSource operation:context.operation @@ -403,7 +404,11 @@ - (SentryTransactionContext *)transactionContext:(SentryTransactionContext *)con spanId:context.spanId parentSpanId:context.parentSpanId sampled:sampleDecision - parentSampled:context.parentSampled]; + parentSampled:context.parentSampled + sampleRate:sampleRate + parentSampleRate:context.parentSampleRate + sampleRand:sampleRand + parentSampleRand:context.parentSampleRand]; } - (SentryTracer *)startTransactionWithContext:(SentryTransactionContext *)transactionContext @@ -418,8 +423,9 @@ - (SentryTracer *)startTransactionWithContext:(SentryTransactionContext *)transa SentrySamplerDecision *tracesSamplerDecision = sentry_sampleTrace(samplingContext, self.client.options); transactionContext = [self transactionContext:transactionContext - withSampled:tracesSamplerDecision.decision]; - transactionContext.sampleRate = tracesSamplerDecision.sampleRate; + withSampled:tracesSamplerDecision.decision + sampleRate:tracesSamplerDecision.sampleRate + sampleRand:tracesSamplerDecision.sampleRand]; #if SENTRY_TARGET_PROFILING_SUPPORTED SentrySamplerDecision *profilesSamplerDecision diff --git a/Sources/Sentry/SentrySamplerDecision.m b/Sources/Sentry/SentrySamplerDecision.m index d3f2203cae..b5d324a2f4 100644 --- a/Sources/Sentry/SentrySamplerDecision.m +++ b/Sources/Sentry/SentrySamplerDecision.m @@ -4,10 +4,12 @@ @implementation SentrySamplerDecision - (instancetype)initWithDecision:(SentrySampleDecision)decision forSampleRate:(nullable NSNumber *)sampleRate + withSampleRand:(nullable NSNumber *)sampleRand { if (self = [super init]) { _decision = decision; _sampleRate = sampleRate; + _sampleRand = sampleRand; } return self; } diff --git a/Sources/Sentry/SentrySampling.m b/Sources/Sentry/SentrySampling.m index 446dfca513..5bd22ee461 100644 --- a/Sources/Sentry/SentrySampling.m +++ b/Sources/Sentry/SentrySampling.m @@ -38,7 +38,9 @@ double random = [SentryDependencyContainer.sharedInstance.random nextNumber]; SentrySampleDecision decision = random <= rate.doubleValue ? kSentrySampleDecisionYes : kSentrySampleDecisionNo; - return [[SentrySamplerDecision alloc] initWithDecision:decision forSampleRate:rate]; + return [[SentrySamplerDecision alloc] initWithDecision:decision + forSampleRate:rate + withSampleRand:@(random)]; } SentrySamplerDecision * @@ -46,7 +48,8 @@ { if (rate == nil) { return [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionNo - forSampleRate:nil]; + forSampleRate:nil + withSampleRand:nil]; } return _sentry_calcSample(rate); @@ -61,7 +64,8 @@ if (context.transactionContext.sampled != kSentrySampleDecisionUndecided) { return [[SentrySamplerDecision alloc] initWithDecision:context.transactionContext.sampled - forSampleRate:context.transactionContext.sampleRate]; + forSampleRate:context.transactionContext.sampleRate + withSampleRand:context.transactionContext.sampleRand]; } NSNumber *callbackRate = _sentry_samplerCallbackRate( @@ -74,7 +78,8 @@ if (context.transactionContext.parentSampled != kSentrySampleDecisionUndecided) { return [[SentrySamplerDecision alloc] initWithDecision:context.transactionContext.parentSampled - forSampleRate:context.transactionContext.sampleRate]; + forSampleRate:context.transactionContext.sampleRate + withSampleRand:context.transactionContext.sampleRand]; } return _sentry_calcSampleFromNumericalRate(options.tracesSampleRate); @@ -91,7 +96,8 @@ // whether the associated profile should be sampled. if (tracesSamplerDecision.decision != kSentrySampleDecisionYes) { return [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionNo - forSampleRate:nil]; + forSampleRate:nil + withSampleRand:nil]; } // Backward compatibility for clients that are still using the enableProfiling option. @@ -99,7 +105,8 @@ # pragma clang diagnostic ignored "-Wdeprecated-declarations" if (options.enableProfiling) { return [[SentrySamplerDecision alloc] initWithDecision:kSentrySampleDecisionYes - forSampleRate:@1.0]; + forSampleRate:@1.0 + withSampleRand:@1.0]; } # pragma clang diagnostic pop diff --git a/Sources/Sentry/SentryTraceContext.m b/Sources/Sentry/SentryTraceContext.m index c14486b23d..ebb2f20292 100644 --- a/Sources/Sentry/SentryTraceContext.m +++ b/Sources/Sentry/SentryTraceContext.m @@ -25,6 +25,29 @@ - (instancetype)initWithTraceId:(SentryId *)traceId sampleRate:(nullable NSString *)sampleRate sampled:(nullable NSString *)sampled replayId:(nullable NSString *)replayId +{ + return [self initWithTraceId:traceId + publicKey:publicKey + releaseName:releaseName + environment:environment + transaction:transaction + userSegment:userSegment + sampleRate:sampleRate + sampleRand:nil + sampled:sampled + replayId:replayId]; +} + +- (instancetype)initWithTraceId:(SentryId *)traceId + publicKey:(NSString *)publicKey + releaseName:(nullable NSString *)releaseName + environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction + userSegment:(nullable NSString *)userSegment + sampleRate:(nullable NSString *)sampleRate + sampleRand:(nullable NSString *)sampleRand + sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId { if (self = [super init]) { _traceId = traceId; @@ -33,6 +56,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId _releaseName = releaseName; _transaction = transaction; _userSegment = userSegment; + _sampleRand = sampleRand; _sampleRate = sampleRate; _sampled = sampled; _replayId = replayId; @@ -66,12 +90,17 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer } #pragma clang diagnostic pop - NSString *sampleRate = nil; - if ([tracer isKindOfClass:[SentryTransactionContext class]]) { - sampleRate = - [NSString stringWithFormat:@"%@", [(SentryTransactionContext *)tracer sampleRate]]; + NSString *serializedSampleRand = nil; + NSNumber *sampleRand = [tracer.transactionContext sampleRand]; + if (sampleRand != nil) { + serializedSampleRand = [NSString stringWithFormat:@"%f", sampleRand.doubleValue]; } + NSString *serializedSampleRate = nil; + NSNumber *sampleRate = [tracer.transactionContext sampleRate]; + if (sampleRate != nil) { + serializedSampleRate = [NSString stringWithFormat:@"%f", sampleRate.doubleValue]; + } NSString *sampled = nil; if (tracer.sampled != kSentrySampleDecisionUndecided) { sampled @@ -84,7 +113,8 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer environment:options.environment transaction:tracer.transactionContext.name userSegment:userSegment - sampleRate:sampleRate + sampleRate:serializedSampleRate + sampleRand:serializedSampleRand sampled:sampled replayId:scope.replayId]; } @@ -101,6 +131,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId transaction:nil userSegment:userSegment sampleRate:nil + sampleRand:nil sampled:nil replayId:replayId]; } @@ -128,6 +159,7 @@ - (nullable instancetype)initWithDict:(NSDictionary *)dictionary transaction:dictionary[@"transaction"] userSegment:userSegment sampleRate:dictionary[@"sample_rate"] + sampleRand:dictionary[@"sample_rand"] sampled:dictionary[@"sampled"] replayId:dictionary[@"replay_id"]]; } @@ -141,6 +173,7 @@ - (SentryBaggage *)toBaggage transaction:_transaction userSegment:_userSegment sampleRate:_sampleRate + sampleRand:_sampleRand sampled:_sampled replayId:_replayId]; return result; @@ -167,6 +200,10 @@ - (SentryBaggage *)toBaggage [result setValue:_userSegment forKey:@"user_segment"]; } + if (_sampleRand != nil) { + [result setValue:_sampleRand forKey:@"sample_rand"]; + } + if (_sampleRate != nil) { [result setValue:_sampleRate forKey:@"sample_rate"]; } diff --git a/Sources/Sentry/SentryTransactionContext.mm b/Sources/Sentry/SentryTransactionContext.mm index 17b4daf830..67ee34e1bc 100644 --- a/Sources/Sentry/SentryTransactionContext.mm +++ b/Sources/Sentry/SentryTransactionContext.mm @@ -17,18 +17,37 @@ @implementation SentryTransactionContext - (instancetype)initWithName:(NSString *)name operation:(NSString *)operation { - return [self initWithName:name operation:operation sampled:kSentrySampleDecisionUndecided]; + return [self initWithName:name + operation:operation + sampled:kSentrySampleDecisionUndecided + sampleRate:nil + sampleRand:nil]; +} + +- (instancetype)initWithName:(NSString *)name + operation:(NSString *)operation + sampled:(SentrySampleDecision)sampled +{ + return [self initWithName:name + operation:operation + sampled:sampled + sampleRate:nil + sampleRand:nil]; } - (instancetype)initWithName:(NSString *)name operation:(NSString *)operation sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand { return [self initWithName:name nameSource:kSentryTransactionNameSourceCustom operation:operation origin:SentryTraceOrigin.manual - sampled:sampled]; + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]; } - (instancetype)initWithName:(NSString *)name @@ -45,7 +64,36 @@ - (instancetype)initWithName:(NSString *)name traceId:traceId spanId:spanId parentSpanId:parentSpanId - parentSampled:parentSampled]; + sampled:kSentrySampleDecisionUndecided + parentSampled:parentSampled + sampleRate:nil + parentSampleRate:nil + sampleRand:nil + parentSampleRand:nil]; +} + +- (instancetype)initWithName:(NSString *)name + operation:(NSString *)operation + traceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentSpanId:(nullable SentrySpanId *)parentSpanId + parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand +{ + return [self initWithName:name + nameSource:kSentryTransactionNameSourceCustom + operation:operation + origin:SentryTraceOrigin.manual + traceId:traceId + spanId:spanId + parentSpanId:parentSpanId + sampled:kSentrySampleDecisionUndecided + parentSampled:parentSampled + sampleRate:nil + parentSampleRate:parentSampleRate + sampleRand:nil + parentSampleRand:parentSampleRand]; } #pragma mark - Private @@ -55,12 +103,13 @@ - (instancetype)initWithName:(NSString *)name operation:(NSString *)operation origin:(NSString *)origin { - if (self = [super initWithOperation:operation - origin:origin - sampled:kSentryDefaultSamplingDecision]) { - [self commonInitWithName:name source:source parentSampled:kSentryDefaultSamplingDecision]; - } - return self; + return [self initWithName:name + nameSource:source + operation:operation + origin:origin + sampled:kSentryDefaultSamplingDecision + sampleRate:nil + sampleRand:nil]; } - (instancetype)initWithName:(NSString *)name @@ -68,21 +117,31 @@ - (instancetype)initWithName:(NSString *)name operation:(NSString *)operation origin:(NSString *)origin sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand { if (self = [super initWithOperation:operation origin:origin sampled:sampled]) { - [self commonInitWithName:name source:source parentSampled:kSentryDefaultSamplingDecision]; + [self commonInitWithName:name + source:source + sampleRate:sampleRate + sampleRand:sampleRand + parentSampled:kSentryDefaultSamplingDecision + parentSampleRate:NULL + parentSampleRand:NULL]; } return self; } - (instancetype)initWithName:(NSString *)name nameSource:(SentryTransactionNameSource)source - operation:(nonnull NSString *)operation + operation:(NSString *)operation origin:(NSString *)origin traceId:(SentryId *)traceId spanId:(SentrySpanId *)spanId parentSpanId:(nullable SentrySpanId *)parentSpanId parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand; { if (self = [super initWithTraceId:traceId spanId:spanId @@ -90,8 +149,14 @@ - (instancetype)initWithName:(NSString *)name operation:operation spanDescription:nil origin:origin - sampled:kSentryDefaultSamplingDecision]) { - [self commonInitWithName:name source:source parentSampled:parentSampled]; + sampled:kSentrySampleDecisionUndecided]) { + [self commonInitWithName:name + source:source + sampleRate:nil + sampleRand:nil + parentSampled:parentSampled + parentSampleRate:parentSampleRate + parentSampleRand:parentSampleRand]; } return self; } @@ -105,6 +170,10 @@ - (instancetype)initWithName:(NSString *)name parentSpanId:(nullable SentrySpanId *)parentSpanId sampled:(SentrySampleDecision)sampled parentSampled:(SentrySampleDecision)parentSampled + sampleRate:(nullable NSNumber *)sampleRate + parentSampleRate:(nullable NSNumber *)parentSampleRate + sampleRand:(nullable NSNumber *)sampleRand + parentSampleRand:(nullable NSNumber *)parentSampleRand { if (self = [super initWithTraceId:traceId spanId:spanId @@ -113,10 +182,13 @@ - (instancetype)initWithName:(NSString *)name spanDescription:nil origin:origin sampled:sampled]) { - _name = [NSString stringWithString:name]; - _nameSource = source; - self.parentSampled = parentSampled; - [self getThreadInfo]; + [self commonInitWithName:name + source:source + sampleRate:sampleRate + sampleRand:sampleRand + parentSampled:parentSampled + parentSampleRate:parentSampleRate + parentSampleRand:parentSampleRand]; } return self; } @@ -138,11 +210,19 @@ - (SentryThread *)sentry_threadInfo - (void)commonInitWithName:(NSString *)name source:(SentryTransactionNameSource)source + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand { _name = [NSString stringWithString:name]; _nameSource = source; + self.sampleRate = sampleRate; + self.sampleRand = sampleRand; self.parentSampled = parentSampled; + self.parentSampleRate = parentSampleRate; + self.parentSampleRand = parentSampleRand; [self getThreadInfo]; SENTRY_LOG_DEBUG(@"Created transaction context with name %@", name); } diff --git a/Sources/Sentry/include/SentryBuildAppStartSpans.h b/Sources/Sentry/include/SentryBuildAppStartSpans.h index 22014687c7..23b19aabca 100644 --- a/Sources/Sentry/include/SentryBuildAppStartSpans.h +++ b/Sources/Sentry/include/SentryBuildAppStartSpans.h @@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN #if SENTRY_HAS_UIKIT NSArray *sentryBuildAppStartSpans( - SentryTracer *tracer, SentryAppStartMeasurement *appStartMeasurement); + SentryTracer *tracer, SentryAppStartMeasurement *_Nullable appStartMeasurement); #endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySamplerDecision.h b/Sources/Sentry/include/SentrySamplerDecision.h index 8b11565352..ec482ca17c 100644 --- a/Sources/Sentry/include/SentrySamplerDecision.h +++ b/Sources/Sentry/include/SentrySamplerDecision.h @@ -7,10 +7,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) SentrySampleDecision decision; +@property (nonatomic, nullable, strong, readonly) NSNumber *sampleRand; + @property (nullable, nonatomic, strong, readonly) NSNumber *sampleRate; - (instancetype)initWithDecision:(SentrySampleDecision)decision - forSampleRate:(nullable NSNumber *)sampleRate; + forSampleRate:(nullable NSNumber *)sampleRate + withSampleRand:(nullable NSNumber *)sampleRand; @end diff --git a/Sources/Sentry/include/SentryTransactionContext+Private.h b/Sources/Sentry/include/SentryTransactionContext+Private.h index 67cf02bc9b..4baef4cbb3 100644 --- a/Sources/Sentry/include/SentryTransactionContext+Private.h +++ b/Sources/Sentry/include/SentryTransactionContext+Private.h @@ -14,16 +14,20 @@ NS_ASSUME_NONNULL_BEGIN nameSource:(SentryTransactionNameSource)source operation:(NSString *)operation origin:(NSString *)origin - sampled:(SentrySampleDecision)sampled; + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; - (instancetype)initWithName:(NSString *)name nameSource:(SentryTransactionNameSource)source - operation:(nonnull NSString *)operation + operation:(NSString *)operation origin:(NSString *)origin traceId:(SentryId *)traceId spanId:(SentrySpanId *)spanId parentSpanId:(nullable SentrySpanId *)parentSpanId - parentSampled:(SentrySampleDecision)parentSampled; + parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand; - (instancetype)initWithName:(NSString *)name nameSource:(SentryTransactionNameSource)source @@ -33,7 +37,11 @@ NS_ASSUME_NONNULL_BEGIN spanId:(SentrySpanId *)spanId parentSpanId:(nullable SentrySpanId *)parentSpanId sampled:(SentrySampleDecision)sampled - parentSampled:(SentrySampleDecision)parentSampled; + parentSampled:(SentrySampleDecision)parentSampled + sampleRate:(nullable NSNumber *)sampleRate + parentSampleRate:(nullable NSNumber *)parentSampleRate + sampleRand:(nullable NSNumber *)sampleRand + parentSampleRand:(nullable NSNumber *)parentSampleRand; #if SENTRY_TARGET_PROFILING_SUPPORTED // This is currently only exposed for testing purposes, see -[SentryProfilerTests diff --git a/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.swift b/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.swift index 1838dd55a3..a201f87a52 100644 --- a/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.swift +++ b/Tests/SentryProfilerTests/SentryAppLaunchProfilingTests.swift @@ -16,7 +16,7 @@ final class SentryAppLaunchProfilingSwiftTests: XCTestCase { } func testContentsOfLaunchTraceProfileTransactionContext() { - let context = sentry_context(NSNumber(value: 1)) + let context = sentry_context(NSNumber(value: 1), NSNumber(value: 1)) XCTAssertEqual(context.nameSource.rawValue, 0) XCTAssertEqual(context.origin, "auto.app.start.profile") XCTAssertEqual(context.sampled, .yes) @@ -116,18 +116,29 @@ final class SentryAppLaunchProfilingSwiftTests: XCTestCase { } func testLaunchTraceProfileConfiguration() throws { + // -- Arrange -- let expectedProfilesSampleRate: NSNumber = 0.567 + let expectedProfilesSampleRand: NSNumber = fixture.fixedRandomValue as NSNumber let expectedTracesSampleRate: NSNumber = 0.789 + let expectedTracesSampleRand: NSNumber = fixture.fixedRandomValue as NSNumber + + // -- Act -- let options = Options() options.enableAppLaunchProfiling = true options.profilesSampleRate = expectedProfilesSampleRate options.tracesSampleRate = expectedTracesSampleRate + + // Smoke test that the file doesn't exist yet XCTAssertFalse(appLaunchProfileConfigFileExists()) sentry_manageTraceProfilerOnStartSDK(options, TestHub(client: nil, andScope: nil)) + + // -- Assert -- XCTAssert(appLaunchProfileConfigFileExists()) let dict = try XCTUnwrap(appLaunchProfileConfiguration()) XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyTracesSampleRate], expectedTracesSampleRate) + XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyTracesSampleRand], expectedTracesSampleRand) XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyProfilesSampleRate], expectedProfilesSampleRate) + XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyProfilesSampleRand], expectedProfilesSampleRand) } // test that after configuring for a launch profile, a subsequent @@ -176,23 +187,37 @@ final class SentryAppLaunchProfilingSwiftTests: XCTestCase { // the next launch, configuring profiling for continuous mode, that the // configuration file switches from trace-based to continuous-style config func testSwitchFromTraceBasedToContinuousLaunchProfileConfiguration() throws { + // -- Arrange -- let options = Options() options.enableAppLaunchProfiling = true options.profilesSampleRate = 0.567 options.tracesSampleRate = 0.789 + + // Smoke test that the file doesn't exist XCTAssertFalse(appLaunchProfileConfigFileExists()) + + // -- Act -- sentry_manageTraceProfilerOnStartSDK(options, TestHub(client: nil, andScope: nil)) + + // -- Assert -- XCTAssert(appLaunchProfileConfigFileExists()) let dict = try XCTUnwrap(appLaunchProfileConfiguration()) XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyTracesSampleRate], options.tracesSampleRate) + XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyTracesSampleRand] as? Double, fixture.fixedRandomValue) XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyProfilesSampleRate], options.profilesSampleRate) - - options.profilesSampleRate = nil + XCTAssertEqual(dict[kSentryLaunchProfileConfigKeyProfilesSampleRand] as? Double, fixture.fixedRandomValue) + + // -- Act -- + options.profilesSampleRate = nil sentry_manageTraceProfilerOnStartSDK(options, TestHub(client: nil, andScope: nil)) + + // -- Assert -- let newDict = try XCTUnwrap(appLaunchProfileConfiguration()) XCTAssertEqual(newDict[kSentryLaunchProfileConfigKeyContinuousProfiling], true) XCTAssertNil(newDict[kSentryLaunchProfileConfigKeyTracesSampleRate]) + XCTAssertNil(newDict[kSentryLaunchProfileConfigKeyTracesSampleRand]) XCTAssertNil(newDict[kSentryLaunchProfileConfigKeyProfilesSampleRate]) + XCTAssertNil(newDict[kSentryLaunchProfileConfigKeyProfilesSampleRand]) } func testTraceProfilerStartsWhenBothSampleRatesAreSetAboveZero() { diff --git a/Tests/SentryProfilerTests/SentryProfileTestFixture.swift b/Tests/SentryProfilerTests/SentryProfileTestFixture.swift index 4050e592a1..fddd4b98e4 100644 --- a/Tests/SentryProfilerTests/SentryProfileTestFixture.swift +++ b/Tests/SentryProfilerTests/SentryProfileTestFixture.swift @@ -13,7 +13,7 @@ class SentryProfileTestFixture { } private static let dsnAsString = TestConstants.dsnAsString(username: "SentryProfileTestFixture") - + let options: Options let client: TestClient? let hub: SentryHub @@ -21,7 +21,9 @@ class SentryProfileTestFixture { let message = "some message" let transactionName = "Some Transaction" let transactionOperation = "Some Operation" - + + let fixedRandomValue = 0.5 + let systemWrapper = TestSentrySystemWrapper() let processInfoWrapper = TestSentryNSProcessInfoWrapper() let dispatchFactory = TestDispatchFactory() @@ -40,7 +42,7 @@ class SentryProfileTestFixture { init() { SentryDependencyContainer.sharedInstance().dispatchQueueWrapper = dispatchQueueWrapper SentryDependencyContainer.sharedInstance().dateProvider = currentDateProvider - SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.5) + SentryDependencyContainer.sharedInstance().random = TestRandom(value: fixedRandomValue) SentryDependencyContainer.sharedInstance().systemWrapper = systemWrapper SentryDependencyContainer.sharedInstance().processInfoWrapper = processInfoWrapper SentryDependencyContainer.sharedInstance().dispatchFactory = dispatchFactory diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index f753fc60be..7e5de448a0 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -938,14 +938,30 @@ extension SentryFileManagerTests { } func testAppLaunchProfileConfiguration() throws { + // -- Assert -- let expectedTracesSampleRate = 0.12 + let expectedTracesSampleRand = 0.55 let expectedProfilesSampleRate = 0.34 - try ensureAppLaunchProfileConfig(tracesSampleRate: expectedTracesSampleRate, profilesSampleRate: expectedProfilesSampleRate) + let expectedProfilesSampleRand = 0.66 + + // -- Act -- + try ensureAppLaunchProfileConfig( + tracesSampleRate: expectedTracesSampleRate, + tracesSampleRand: expectedTracesSampleRand, + profilesSampleRate: expectedProfilesSampleRate, + profilesSampleRand: expectedProfilesSampleRand + ) let config = appLaunchProfileConfiguration() + + // -- Assert -- let actualTracesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRate]).doubleValue + let actualTracesSampleRand = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRand]).doubleValue let actualProfilesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRate]).doubleValue + let actualProfilesSampleRand = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRand]).doubleValue XCTAssertEqual(actualTracesSampleRate, expectedTracesSampleRate) + XCTAssertEqual(actualTracesSampleRand, expectedTracesSampleRand) XCTAssertEqual(actualProfilesSampleRate, expectedProfilesSampleRate) + XCTAssertEqual(actualProfilesSampleRand, expectedProfilesSampleRand) } // if a file isn't present when we expect it to be, like if there was an issue when we went to write it to disk @@ -955,40 +971,61 @@ extension SentryFileManagerTests { } func testWriteAppLaunchProfilingConfigFile_noCurrentFileExists() throws { + // -- Arrange -- try ensureAppLaunchProfileConfig(exists: false) let expectedTracesSampleRate = 0.12 + let expectedTracesSampleRand = 0.55 let expectedProfilesSampleRate = 0.34 + let expectedProfilesSampleRand = 0.66 writeAppLaunchProfilingConfigFile([ kSentryLaunchProfileConfigKeyTracesSampleRate: expectedTracesSampleRate, - kSentryLaunchProfileConfigKeyProfilesSampleRate: expectedProfilesSampleRate + kSentryLaunchProfileConfigKeyTracesSampleRand: expectedTracesSampleRand, + kSentryLaunchProfileConfigKeyProfilesSampleRate: expectedProfilesSampleRate, + kSentryLaunchProfileConfigKeyProfilesSampleRand: expectedProfilesSampleRand ]) let config = NSDictionary(contentsOf: launchProfileConfigFileURL()) let actualTracesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRate] as? NSNumber).doubleValue + let actualTracesSampleRand = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRand] as? NSNumber).doubleValue let actualProfilesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRate] as? NSNumber).doubleValue + let actualProfilesSampleRand = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRand] as? NSNumber).doubleValue XCTAssertEqual(actualTracesSampleRate, expectedTracesSampleRate) + XCTAssertEqual(actualTracesSampleRand, expectedTracesSampleRand) XCTAssertEqual(actualProfilesSampleRate, expectedProfilesSampleRate) + XCTAssertEqual(actualProfilesSampleRand, expectedProfilesSampleRand) } // if a file is still present in the primary location, like if a crash occurred before it could be removed, or an error occurred when trying to remove it or move it to the backup location, make sure we overwrite it func testWriteAppLaunchProfilingConfigFile_fileAlreadyExists() throws { - try ensureAppLaunchProfileConfig(exists: true, tracesSampleRate: 0.75, profilesSampleRate: 0.75) - + // -- Arrange -- + try ensureAppLaunchProfileConfig(exists: true, tracesSampleRate: 0.75, tracesSampleRand: 0.25, profilesSampleRate: 0.75, profilesSampleRand: 0.35) + let expectedTracesSampleRate = 0.12 + let expectedTracesSampleRand = 0.55 let expectedProfilesSampleRate = 0.34 + let expectedProfilesSampleRand = 0.66 + + // -- Act -- writeAppLaunchProfilingConfigFile([ kSentryLaunchProfileConfigKeyTracesSampleRate: expectedTracesSampleRate, - kSentryLaunchProfileConfigKeyProfilesSampleRate: expectedProfilesSampleRate + kSentryLaunchProfileConfigKeyTracesSampleRand: expectedTracesSampleRand, + kSentryLaunchProfileConfigKeyProfilesSampleRate: expectedProfilesSampleRate, + kSentryLaunchProfileConfigKeyProfilesSampleRand: expectedProfilesSampleRand ]) + // -- Assert -- let config = NSDictionary(contentsOf: launchProfileConfigFileURL()) let actualTracesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRate] as? NSNumber).doubleValue + let actualTracesSampleRand = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyTracesSampleRand] as? NSNumber).doubleValue let actualProfilesSampleRate = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRate] as? NSNumber).doubleValue + let actualProfilesSampleRand = try XCTUnwrap(config?[kSentryLaunchProfileConfigKeyProfilesSampleRand] as? NSNumber).doubleValue XCTAssertEqual(actualTracesSampleRate, expectedTracesSampleRate) + XCTAssertEqual(actualTracesSampleRand, expectedTracesSampleRand) XCTAssertEqual(actualProfilesSampleRate, expectedProfilesSampleRate) + XCTAssertEqual(actualProfilesSampleRand, expectedProfilesSampleRand) } func testRemoveAppLaunchProfilingConfigFile() throws { @@ -1023,11 +1060,16 @@ extension SentryFileManagerTests { // MARK: Private profiling tests private extension SentryFileManagerTests { - func ensureAppLaunchProfileConfig(exists: Bool = true, tracesSampleRate: Double = 1, profilesSampleRate: Double = 1) throws { + func ensureAppLaunchProfileConfig(exists: Bool = true, tracesSampleRate: Double = 1, tracesSampleRand: Double = 1.0, profilesSampleRate: Double = 1, profilesSampleRand: Double = 1.0) throws { let url = launchProfileConfigFileURL() if exists { - let dict = [kSentryLaunchProfileConfigKeyTracesSampleRate: tracesSampleRate, kSentryLaunchProfileConfigKeyProfilesSampleRate: profilesSampleRate] + let dict = [ + kSentryLaunchProfileConfigKeyTracesSampleRate: tracesSampleRate, + kSentryLaunchProfileConfigKeyTracesSampleRand: tracesSampleRand, + kSentryLaunchProfileConfigKeyProfilesSampleRate: profilesSampleRate, + kSentryLaunchProfileConfigKeyProfilesSampleRand: profilesSampleRand + ] try (dict as NSDictionary).write(to: url) } else { let fm = FileManager.default diff --git a/Tests/SentryTests/Helper/SentrySerializationTests.swift b/Tests/SentryTests/Helper/SentrySerializationTests.swift index 501a41706e..d0b8bb42f3 100644 --- a/Tests/SentryTests/Helper/SentrySerializationTests.swift +++ b/Tests/SentryTests/Helper/SentrySerializationTests.swift @@ -6,7 +6,18 @@ class SentrySerializationTests: XCTestCase { private class Fixture { static var invalidData = "hi".data(using: .utf8)! - static var traceContext = TraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: "some segment", sampleRate: "0.25", sampled: "true", replayId: nil) + static var traceContext = TraceContext( + trace: SentryId(), + publicKey: "PUBLIC_KEY", + releaseName: "RELEASE_NAME", + environment: "TEST", + transaction: "transaction", + userSegment: "some segment", + sampleRate: "0.25", + sampleRand: "0.6543", + sampled: "true", + replayId: nil + ) } override func setUp() { @@ -163,6 +174,32 @@ class SentrySerializationTests: XCTestCase { XCTAssertNotNil(deserializedEnvelope.header.traceContext) assertTraceState(firstTrace: trace, secondTrace: deserializedEnvelope.header.traceContext!) } + + func testEnvelopeWithDataWithSampleRand_TraceContextWithoutUser_ReturnsTraceContext() throws { + // -- Arrange -- + let trace = TraceContext( + trace: SentryId(), + publicKey: "PUBLIC_KEY", + releaseName: "RELEASE_NAME", + environment: "TEST", + transaction: "transaction", + userSegment: nil, + sampleRate: nil, + sampleRand: nil, + sampled: nil, + replayId: nil + ) + + // -- Act -- + let envelopeHeader = SentryEnvelopeHeader(id: nil, traceContext: trace) + let envelope = SentryEnvelope(header: envelopeHeader, singleItem: createItemWithEmptyAttachment()) + + let deserializedEnvelope = try XCTUnwrap(SentrySerialization.envelope(with: serializeEnvelope(envelope: envelope))) + + // -- Assert -- + XCTAssertNotNil(deserializedEnvelope.header.traceContext) + assertTraceState(firstTrace: trace, secondTrace: deserializedEnvelope.header.traceContext!) + } func testEnvelopeWithData_SdkInfoIsNil_ReturnsNil() throws { let envelopeHeader = SentryEnvelopeHeader(id: nil, sdkInfo: nil, traceContext: nil) @@ -534,18 +571,19 @@ class SentrySerializationTests: XCTestCase { return SentryEnvelopeItem(header: itemHeader, data: itemData) } - private func assertDefaultSdkInfoSet(deserializedEnvelope: SentryEnvelope) { + private func assertDefaultSdkInfoSet(deserializedEnvelope: SentryEnvelope, file: StaticString = #file, line: UInt = #line) { let sdkInfo = SentrySdkInfo(name: SentryMeta.sdkName, version: SentryMeta.versionString, integrations: [], features: [], packages: []) - XCTAssertEqual(sdkInfo, deserializedEnvelope.header.sdkInfo) + XCTAssertEqual(sdkInfo, deserializedEnvelope.header.sdkInfo, file: file, line: line) } - private func assertTraceState(firstTrace: TraceContext, secondTrace: TraceContext) { - XCTAssertEqual(firstTrace.traceId, secondTrace.traceId) - XCTAssertEqual(firstTrace.publicKey, secondTrace.publicKey) - XCTAssertEqual(firstTrace.releaseName, secondTrace.releaseName) - XCTAssertEqual(firstTrace.environment, secondTrace.environment) - XCTAssertEqual(firstTrace.userSegment, secondTrace.userSegment) - XCTAssertEqual(firstTrace.sampleRate, secondTrace.sampleRate) + private func assertTraceState(firstTrace: TraceContext, secondTrace: TraceContext, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(firstTrace.traceId, secondTrace.traceId, "Trace ID is not equal", file: file, line: line) + XCTAssertEqual(firstTrace.publicKey, secondTrace.publicKey, "Public key is not equal", file: file, line: line) + XCTAssertEqual(firstTrace.releaseName, secondTrace.releaseName, "Release name is not equal", file: file, line: line) + XCTAssertEqual(firstTrace.environment, secondTrace.environment, "Environment is not equal", file: file, line: line) + XCTAssertEqual(firstTrace.userSegment, secondTrace.userSegment, "User segment is not equal", file: file, line: line) + XCTAssertEqual(firstTrace.sampleRand, secondTrace.sampleRand, "Sample rand is not equal", file: file, line: line) + XCTAssertEqual(firstTrace.sampleRate, secondTrace.sampleRate, "Sample rate is not equal", file: file, line: line) } } diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 12a7c6164e..5fc30984a5 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -370,28 +370,68 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(span.sampled, .no) } + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testCaptureTransaction_CapturesEventAsync() throws { let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .yes)) + + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) + XCTAssertEqual(self.fixture.client.captureEventWithScopeInvocations.count, 1) + XCTAssertEqual(self.fixture.dispatchQueueWrapper.dispatchAsyncInvocations.count, 1) + } + + func testCaptureTransaction_withSampleRateRand_CapturesEventAsync() throws { + let transaction = sut.startTransaction( + transactionContext: TransactionContext( + name: fixture.transactionName, + operation: fixture.transactionOperation, + sampled: .yes, + sampleRate: 0.123456789, + sampleRand: 0.987654321 + ) + ) + let trans = Dynamic(transaction).toTransaction().asAnyObject sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) XCTAssertEqual(self.fixture.client.captureEventWithScopeInvocations.count, 1) XCTAssertEqual(self.fixture.dispatchQueueWrapper.dispatchAsyncInvocations.count, 1) } - + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testCaptureSampledTransaction_DoesNotCaptureEvent() throws { let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .no)) + + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) + XCTAssertEqual(self.fixture.client.captureEventWithScopeInvocations.count, 0) + } + + func testCaptureSampledTransaction_withSampleRateRand_DoesNotCaptureEvent() throws { + // Arrange + let transaction = sut.startTransaction( + transactionContext: TransactionContext( + name: fixture.transactionName, + operation: fixture.transactionOperation, + sampled: .no, + sampleRate: 0.123456789, + sampleRand: 0.987654321 + ) + ) + // Act let trans = Dynamic(transaction).toTransaction().asAnyObject sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) + // Assert XCTAssertEqual(self.fixture.client.captureEventWithScopeInvocations.count, 0) } + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testCaptureSampledTransaction_RecordsLostEvent() throws { let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .no)) - + let trans = Dynamic(transaction).toTransaction().asAnyObject sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) @@ -401,6 +441,29 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(.sampleRate, lostEvent?.reason) } + func testCaptureSampledTransaction_withSampleRateRand_RecordsLostEvent() throws { + // Arrange + let transaction = sut.startTransaction( + transactionContext: TransactionContext( + name: fixture.transactionName, + operation: fixture.transactionOperation, + sampled: .no, + sampleRate: 0.123456789, + sampleRand: 0.987654321 + ) + ) + // Act + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) + + // Assert + XCTAssertEqual(1, fixture.client.recordLostEvents.count) + let lostEvent = fixture.client.recordLostEvents.first + XCTAssertEqual(lostEvent?.category, .transaction) + XCTAssertEqual(lostEvent?.reason, .sampleRate) + } + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testCaptureSampledTransaction_RecordsLostSpans() throws { let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .no)) let trans = Dynamic(transaction).toTransaction().asAnyObject @@ -421,28 +484,108 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(.sampleRate, lostEvent?.reason) XCTAssertEqual(4, lostEvent?.quantity) } + + func testCaptureSampledTransaction_withSampleRateRand_RecordsLostSpans() throws { + // Arrange + let transaction = sut.startTransaction( + transactionContext: TransactionContext( + name: fixture.transactionName, + operation: fixture.transactionOperation, + sampled: .no, + sampleRate: 0.123456789, + sampleRand: 0.987654321 + ) + ) + // Act + let trans = Dynamic(transaction).toTransaction().asAnyObject + + if let tracer = transaction as? SentryTracer { + (trans as? Transaction)?.spans = [ + tracer.startChild(operation: "child1"), + tracer.startChild(operation: "child2"), + tracer.startChild(operation: "child3") + ] + } + + sut.capture(try XCTUnwrap(trans as? Transaction), with: Scope()) + + // Assert + XCTAssertEqual(1, fixture.client.recordLostEventsWithQauntity.count) + let lostEvent = fixture.client.recordLostEventsWithQauntity.first + XCTAssertEqual(lostEvent?.category, .span) + XCTAssertEqual(lostEvent?.reason, .sampleRate) + XCTAssertEqual(lostEvent?.quantity, 4) + } + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testSaveCrashTransaction_SavesTransaction() throws { let scope = fixture.scope let sut = SentryHub(client: fixture.client, andScope: scope) let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .yes)) + + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.saveCrash(try XCTUnwrap(trans as? Transaction)) + let client = fixture.client + XCTAssertEqual(1, client.saveCrashTransactionInvocations.count) + XCTAssertEqual(scope, client.saveCrashTransactionInvocations.first?.scope) + XCTAssertEqual(0, client.recordLostEvents.count) + } + + func testSaveCrashTransaction_withSampleRateRand_SavesTransaction() throws { + // Arrange + let scope = fixture.scope + let sut = SentryHub(client: fixture.client, andScope: scope) + + let transaction = sut.startTransaction( + transactionContext: TransactionContext( + name: fixture.transactionName, + operation: fixture.transactionOperation, + sampled: .yes, + sampleRate: 0.123456789, + sampleRand: 0.987654321 + ) + ) + + // Act let trans = Dynamic(transaction).toTransaction().asAnyObject sut.saveCrash(try XCTUnwrap(trans as? Transaction)) + // Assert let client = fixture.client XCTAssertEqual(1, client.saveCrashTransactionInvocations.count) XCTAssertEqual(scope, client.saveCrashTransactionInvocations.first?.scope) XCTAssertEqual(0, client.recordLostEvents.count) } + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testSaveCrashTransaction_NotSampled_DoesNotSaveTransaction() throws { let scope = fixture.scope let sut = SentryHub(client: fixture.client, andScope: scope) let transaction = sut.startTransaction(transactionContext: TransactionContext(name: fixture.transactionName, operation: fixture.transactionOperation, sampled: .no)) + + let trans = Dynamic(transaction).toTransaction().asAnyObject + sut.saveCrash(try XCTUnwrap(trans as? Transaction)) + XCTAssertEqual(self.fixture.client.saveCrashTransactionInvocations.count, 0) + } + + func testSaveCrashTransaction_NotSampledWithSampleRateRand_DoesNotSaveTransaction() throws { + let scope = fixture.scope + let sut = SentryHub(client: fixture.client, andScope: scope) + + let transaction = sut.startTransaction( + transactionContext: TransactionContext( + name: fixture.transactionName, + operation: fixture.transactionOperation, + sampled: .no, + sampleRate: 0.123456789, + sampleRand: 0.987654321 + ) + ) + let trans = Dynamic(transaction).toTransaction().asAnyObject sut.saveCrash(try XCTUnwrap(trans as? Transaction)) diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index e6dd458623..1b4d4e21ee 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -60,6 +60,7 @@ #import "SentryBreadcrumb+Private.h" #import "SentryBreadcrumbDelegate.h" #import "SentryBreadcrumbTracker.h" +#import "SentryBuildAppStartSpans.h" #import "SentryByteCountFormatter.h" #import "SentryClassRegistrator.h" #import "SentryClient+Private.h" diff --git a/Tests/SentryTests/Transaction/SentryBaggageTests.swift b/Tests/SentryTests/Transaction/SentryBaggageTests.swift index e8bed7def5..c981a52411 100644 --- a/Tests/SentryTests/Transaction/SentryBaggageTests.swift +++ b/Tests/SentryTests/Transaction/SentryBaggageTests.swift @@ -4,6 +4,8 @@ import Sentry import XCTest class SentryBaggageTests: XCTestCase { + // MARK: - Tests without sampleRand + func test_baggageToHeader_AppendToOriginal() { let header = Baggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", transaction: "transaction", userSegment: "test user", sampleRate: "0.49", sampled: "true", replayId: "some_replay_id").toHTTPHeader(withOriginalBaggage: ["a": "a", "sentry-trace_id": "to-be-overwritten"]) @@ -15,4 +17,157 @@ class SentryBaggageTests: XCTestCase { XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000") } + + // MARK: - Tests with sampleRand + + func testWithSampleRand_baggageToHeader_AppendToOriginal() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", + transaction: "transaction", userSegment: "test user", + sampleRate: "0.49", sampleRand: "0.6543", sampled: "true", + replayId: "some_replay_id" + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["a": "a", "sentry-trace_id": "to-be-overwritten"]) + + // -- Assert -- + XCTAssertEqual(header, "a=a,sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-replay_id=some_replay_id,sentry-sample_rand=0.6543,sentry-sample_rate=0.49,sentry-sampled=true,sentry-trace_id=00000000000000000000000000000000,sentry-transaction=transaction,sentry-user_segment=test%20user") + } + + func testWithSampleRand_baggageToHeader_onlyTrace_ignoreNils() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: nil, userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: nil) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000") + } + + func testToHTTPHeader_releaseNameInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: nil, + transaction: nil, userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-release": "original release name"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-release=release%20name,sentry-trace_id=00000000000000000000000000000000") + } + + func testToHTTPHeader_environmentInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: "environment", + transaction: nil, userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-environment": "original environment"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-environment=environment,sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000") + } + + func testToHTTPHeader_transactionInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: "transaction", userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-transaction": "original transaction"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000,sentry-transaction=transaction") + } + + func testToHTTPHeader_userSegmentInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: nil, userSegment: "segment", + sampleRate: nil, sampleRand: nil, sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-user_segment": "original segment"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000,sentry-user_segment=segment") + } + + func testToHTTPHeader_sampleRateInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: nil, userSegment: nil, + sampleRate: "1.0", sampleRand: nil, sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-sample_rate": "0.1"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-sample_rate=1.0,sentry-trace_id=00000000000000000000000000000000") + } + + func testToHTTPHeader_sampleRandInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: nil, userSegment: nil, + sampleRate: nil, sampleRand: "0.5", sampled: nil, replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-sample_rand": "0.1"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-sample_rand=0.5,sentry-trace_id=00000000000000000000000000000000") + } + + func testToHTTPHeader_sampledInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: nil, userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: "true", replayId: nil + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-sampled": "false"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-sampled=true,sentry-trace_id=00000000000000000000000000000000") + } + + func testToHTTPHeader_replayIdInOriginalBaggage_shouldBeOverwritten() { + // -- Arrange -- + let baggage = Baggage( + trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, + transaction: nil, userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: nil, replayId: "replay-id" + ) + + // -- Act -- + let header = baggage.toHTTPHeader(withOriginalBaggage: ["sentry-replay_id": "original-replay-id"]) + + // -- Assert -- + XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-replay_id=replay-id,sentry-trace_id=00000000000000000000000000000000") + } } diff --git a/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift b/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift new file mode 100644 index 0000000000..b78bd129b3 --- /dev/null +++ b/Tests/SentryTests/Transaction/SentryBuildAppStartSpansTests.swift @@ -0,0 +1,293 @@ +@testable import Sentry +import XCTest + +#if canImport(UIKit) +class SentryBuildAppStartSpansTests: XCTestCase { + + func testSentryBuildAppStartSpans_appStartMeasurementIsNil_shouldNotReturnAnySpans() { + // Arrange + let context = SpanContext(operation: "operation") + let tracer = SentryTracer(context: context, framesTracker: nil) + let appStartMeasurement: SentryAppStartMeasurement? = nil + + // Act + let result = sentryBuildAppStartSpans(tracer, appStartMeasurement) + + // Assert + XCTAssertEqual(result, []) + } + + func testSentryBuildAppStartSpans_appStartMeasurementIsNotColdOrWarm_shouldNotReturnAnySpans() { + // Arrange + let context = SpanContext(operation: "operation") + let tracer = SentryTracer(context: context, framesTracker: nil) + let appStartMeasurement = SentryAppStartMeasurement( + type: SentryAppStartType.unknown, + isPreWarmed: false, + appStartTimestamp: Date(timeIntervalSince1970: 1_000), + runtimeInitSystemTimestamp: 1_100, + duration: 1_200, + runtimeInitTimestamp: Date(timeIntervalSince1970: 1_300), + moduleInitializationTimestamp: Date(timeIntervalSince1970: 1_400), + sdkStartTimestamp: Date(timeIntervalSince1970: 1_500), + didFinishLaunchingTimestamp: Date(timeIntervalSince1970: 1_600) + ) + + // Act + let result = sentryBuildAppStartSpans(tracer, appStartMeasurement) + + // Assert + XCTAssertEqual(result, []) + } + + func testSentryBuildAppStartSpans_appStartMeasurementIsColdAndNotPrewarmed_shouldNotIncludePreRuntimeSpans() { + // Arrange + let context = SpanContext(operation: "operation") + let tracer = SentryTracer(context: context, framesTracker: nil) + let appStartMeasurement = SentryAppStartMeasurement( + type: SentryAppStartType.cold, + isPreWarmed: false, + appStartTimestamp: Date(timeIntervalSince1970: 1_000), + runtimeInitSystemTimestamp: 1_100, + duration: 935, + runtimeInitTimestamp: Date(timeIntervalSince1970: 1_300), + moduleInitializationTimestamp: Date(timeIntervalSince1970: 1_400), + sdkStartTimestamp: Date(timeIntervalSince1970: 1_500), + didFinishLaunchingTimestamp: Date(timeIntervalSince1970: 1_600) + ) + + // Act + let result = sentryBuildAppStartSpans(tracer, appStartMeasurement) + + // Assert + XCTAssertEqual(result.count, 6, "Number of spans do not match") + assertSpan( + span: result[0], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: tracer.spanId.sentrySpanIdString, + expectedOperation: "app.start.cold", + expectedDescription: "Cold Start", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_000), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_935), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[1], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.cold", + expectedDescription: "Pre Runtime Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_000), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_300), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[2], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.cold", + expectedDescription: "Runtime Init to Pre Main Initializers", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_300), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_400), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[3], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.cold", + expectedDescription: "UIKit Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_400), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_500), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[4], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.cold", + expectedDescription: "Application Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_500), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_600), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[5], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.cold", + expectedDescription: "Initial Frame Render", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_600), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_935), + expectedSampled: tracer.sampled + ) + } + + func testSentryBuildAppStartSpans_appStartMeasurementIsWarmAndNotPrewarmed_shouldNotIncludePreRuntimeSpans() { + // Arrange + let context = SpanContext(operation: "operation") + let tracer = SentryTracer(context: context, framesTracker: nil) + let appStartMeasurement = SentryAppStartMeasurement( + type: SentryAppStartType.warm, + isPreWarmed: false, + appStartTimestamp: Date(timeIntervalSince1970: 1_000), + runtimeInitSystemTimestamp: 1_100, + duration: 935, + runtimeInitTimestamp: Date(timeIntervalSince1970: 1_300), + moduleInitializationTimestamp: Date(timeIntervalSince1970: 1_400), + sdkStartTimestamp: Date(timeIntervalSince1970: 1_500), + didFinishLaunchingTimestamp: Date(timeIntervalSince1970: 1_600) + ) + + // Act + let result = sentryBuildAppStartSpans(tracer, appStartMeasurement) + + // Assert + XCTAssertEqual(result.count, 6, "Number of spans do not match") + assertSpan( + span: result[0], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: tracer.spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Warm Start", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_000), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_935), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[1], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Pre Runtime Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_000), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_300), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[2], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Runtime Init to Pre Main Initializers", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_300), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_400), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[3], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "UIKit Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_400), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_500), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[4], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Application Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_500), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_600), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[5], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Initial Frame Render", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_600), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_935), + expectedSampled: tracer.sampled + ) + } + + func testSentryBuildAppStartSpans_appStartMeasurementIsPreWarmed_shouldIncludePreRuntimeSpans() { + // Arrange + let context = SpanContext(operation: "operation") + let tracer = SentryTracer(context: context, framesTracker: nil) + let appStartMeasurement = SentryAppStartMeasurement( + type: SentryAppStartType.warm, + isPreWarmed: true, + appStartTimestamp: Date(timeIntervalSince1970: 1_000), + runtimeInitSystemTimestamp: 1_100, + duration: 935, + runtimeInitTimestamp: Date(timeIntervalSince1970: 1_300), + moduleInitializationTimestamp: Date(timeIntervalSince1970: 1_400), + sdkStartTimestamp: Date(timeIntervalSince1970: 1_500), + didFinishLaunchingTimestamp: Date(timeIntervalSince1970: 1_600) + ) + + // Act + let result = sentryBuildAppStartSpans(tracer, appStartMeasurement) + + // Assert + XCTAssertEqual(result.count, 4, "Number of spans do not match") + assertSpan( + span: result[0], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: tracer.spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Warm Start", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_000), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_935), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[1], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "UIKit Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_400), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_500), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[2], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Application Init", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_500), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_600), + expectedSampled: tracer.sampled + ) + assertSpan( + span: result[3], + expectedTraceId: tracer.traceId.sentryIdString, + expectedParentSpanId: result[0].spanId.sentrySpanIdString, + expectedOperation: "app.start.warm", + expectedDescription: "Initial Frame Render", + expectedStartTimestamp: Date(timeIntervalSince1970: 1_600), + expectedEndTimestamp: Date(timeIntervalSince1970: 1_935), + expectedSampled: tracer.sampled + ) + } + + private func assertSpan( + span: Span, + expectedTraceId: String, + expectedParentSpanId: String, + expectedOperation: String, + expectedDescription: String, + expectedStartTimestamp: Date, + expectedEndTimestamp: Date, + expectedSampled: SentrySampleDecision, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertEqual(span.traceId.sentryIdString, expectedTraceId, "TraceId does not match", file: file, line: line) + XCTAssertEqual(span.parentSpanId?.sentrySpanIdString, expectedParentSpanId, "ParentSpanId does not match", file: file, line: line) + XCTAssertEqual(span.operation, expectedOperation, "Operation does not match", file: file, line: line) + XCTAssertEqual(span.spanDescription, expectedDescription, "Description does not match", file: file, line: line) + XCTAssertEqual(span.startTimestamp, expectedStartTimestamp, "StartTimestamp does not match", file: file, line: line) + XCTAssertEqual(span.timestamp, expectedEndTimestamp, "EndTimestamp does not match", file: file, line: line) + XCTAssertEqual(span.sampled, expectedSampled, "Sampled does not match", file: file, line: line) + } +} +#endif diff --git a/Tests/SentryTests/Transaction/SentrySpanContextTests.swift b/Tests/SentryTests/Transaction/SentrySpanContextTests.swift index dfea1e3305..6f4e5985b4 100644 --- a/Tests/SentryTests/Transaction/SentrySpanContextTests.swift +++ b/Tests/SentryTests/Transaction/SentrySpanContextTests.swift @@ -1,8 +1,20 @@ +@testable import Sentry import XCTest class SentrySpanContextTests: XCTestCase { + private let operation = "ui.load" + private let transactionName = "Screen Load" + private let origin = "auto.ui.swift_ui" + private let spanDescription = "span description" + private let traceID = SentryId() + private let spanID = SpanId() + private let parentSpanID = SpanId() + private let sampled = SentrySampleDecision.yes + + // MARK: - Legacy Tests + private let someOperation = "Some Operation" - + func testInit() { let spanContext = SpanContext(operation: someOperation) XCTAssertEqual(spanContext.sampled, SentrySampleDecision.undecided) @@ -96,5 +108,201 @@ class SentrySpanContextTests: XCTestCase { XCTAssertNil(data["sampled"] ) } - + + // MARK: - SentrySpanContext - Public Initializers + + func testPublicInit_WithOperation() { + // Act + let context = SpanContext(operation: operation) + + // Assert + assertContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided + ) + } + + func testPublicInit_WithOperationSampled() { + // Act + let context = SpanContext(operation: operation, sampled: sampled) + + // Assert + assertContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSampled() { + // Act + let context = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: sampled + ) + + // Assert + assertContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSpanDescriptionSampled() { + // Act + let context = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: spanDescription, + sampled: sampled + ) + + // Assert + assertContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: spanDescription, + expectedSampled: sampled + ) + } + + // MARK: - Serialization + + func testSerialization_minimalData_shouldNotIncludeNilValues() { + // Arrange + let spanContext = SpanContext( + trace: traceID, + spanId: spanID, + parentId: nil, + operation: operation, + spanDescription: nil, + sampled: .undecided + ) + + // Act + let data = spanContext.serialize() + + // Assert + XCTAssertEqual(data["type"] as? String, SENTRY_TRACE_TYPE) + XCTAssertEqual(data["trace_id"] as? String, traceID.sentryIdString) + XCTAssertEqual(data["span_id"] as? String, spanID.sentrySpanIdString) + XCTAssertEqual(data["op"] as? String, operation) + XCTAssertNil(data["sampled"]) + XCTAssertNil(data["description"]) + XCTAssertNil(data["parent_span_id"]) + } + + func testSerialization_notSettingProperties_shouldNotSerialize() { + // Arrange + let spanContext = SpanContext(operation: operation) + + // Act + let data = spanContext.serialize() + + // Assert + XCTAssertEqual(data["type"] as? String, SENTRY_TRACE_TYPE) + XCTAssertEqual(data["trace_id"] as? String, spanContext.traceId.sentryIdString) + XCTAssertEqual(data["span_id"] as? String, spanContext.spanId.sentrySpanIdString) + XCTAssertEqual(data["op"] as? String, operation) + XCTAssertEqual(data["origin"] as? String, "manual") + XCTAssertNil(data["sampled"]) + XCTAssertNil(data["description"]) + XCTAssertNil(data["parent_span_id"]) + } + + func testSerialization_sampledDecisionYes_shouldSerializeToTrue() { + // Arrange + let spanContext = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .yes + ) + + // Act + let data = spanContext.serialize() + + // Assert + XCTAssertEqual(data["sampled"] as? Bool, true) + } + + func testSerialization_sampledDecisionNo_shouldSerializeToFalse() { + // Arrange + let spanContext = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .no + ) + + // Act + let data = spanContext.serialize() + + // Assert + XCTAssertEqual(data["sampled"] as? Bool, false) + } + + func testSerialization_sampledDecisionUndecided_shouldNotSerialize() { + // Arrange + let spanContext = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .undecided + ) + + // Act + let data = spanContext.serialize() + + // Assert + XCTAssertNil(data["sampled"]) + } + + // MARK: - Assertion Helper + + private func assertContext( + context: SpanContext, + expectedParentSpanId: SpanId?, + expectedOperation: String, + expectedOrigin: String?, + expectedSpanDescription: String?, + expectedSampled: SentrySampleDecision, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertNotNil(context.traceId, "Trace ID is nil", file: file, line: line) + XCTAssertNotNil(context.spanId, "Span ID is nil", file: file, line: line) + if let expectedParentSpanId = expectedParentSpanId { + XCTAssertEqual(context.parentSpanId, expectedParentSpanId, "Parent span ID does not match", file: file, line: line) + } else { + XCTAssertNil(context.parentSpanId, "Parent span ID is not nil", file: file, line: line) + } + + XCTAssertEqual(context.sampled, expectedSampled, "Sample ID does not match", file: file, line: line) + + XCTAssertEqual(context.operation, expectedOperation, "Operation does not match", file: file, line: line) + XCTAssertEqual(context.spanDescription, expectedSpanDescription, "Span description does not match", file: file, line: line) + XCTAssertEqual(context.origin, expectedOrigin, "Origin does not match", file: file, line: line) + } } diff --git a/Tests/SentryTests/Transaction/SentrySpanTests.swift b/Tests/SentryTests/Transaction/SentrySpanTests.swift index 3834321512..1f7c65bbbf 100644 --- a/Tests/SentryTests/Transaction/SentrySpanTests.swift +++ b/Tests/SentryTests/Transaction/SentrySpanTests.swift @@ -474,7 +474,6 @@ class SentrySpanTests: XCTestCase { XCTAssertEqual(serialization["timestamp"] as? TimeInterval, TestData.timestamp.timeIntervalSince1970) XCTAssertEqual(serialization["start_timestamp"] as? TimeInterval, TestData.timestamp.timeIntervalSince1970) XCTAssertEqual(serialization["type"] as? String, SENTRY_TRACE_TYPE) - XCTAssertEqual(serialization["sampled"] as? NSNumber, true) XCTAssertNotNil(serialization["data"]) XCTAssertNotNil(serialization["tags"]) diff --git a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift b/Tests/SentryTests/Transaction/SentryTraceContextTests.swift similarity index 64% rename from Tests/SentryTests/Transaction/SentryTraceStateTests.swift rename to Tests/SentryTests/Transaction/SentryTraceContextTests.swift index 72eb7f502c..21ad305cd7 100644 --- a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift +++ b/Tests/SentryTests/Transaction/SentryTraceContextTests.swift @@ -13,6 +13,7 @@ class SentryTraceContextTests: XCTestCase { let tracer: SentryTracer let userId = "SomeUserID" let userSegment = "Test Segment" + let sampleRand = "0.6543" let sampleRate = "0.45" let traceId: SentryId let publicKey = "SentrySessionTrackerTests" @@ -55,6 +56,7 @@ class SentryTraceContextTests: XCTestCase { } func testInit() { + // Act let traceContext = TraceContext( trace: fixture.traceId, publicKey: fixture.publicKey, @@ -67,40 +69,91 @@ class SentryTraceContextTests: XCTestCase { replayId: fixture.replayId ) + // Assert assertTraceState(traceContext: traceContext) } + func testInit_withSampleRateRand() { + // Act + let traceContext = TraceContext( + trace: fixture.traceId, + publicKey: fixture.publicKey, + releaseName: fixture.releaseName, + environment: fixture.environment, + transaction: fixture.transactionName, + userSegment: fixture.userSegment, + sampleRate: fixture.sampleRate, + sampleRand: fixture.sampleRand, + sampled: fixture.sampled, + replayId: fixture.replayId + ) + + // Assert + assertFullTraceState( + traceContext: traceContext, + expectedTraceId: fixture.traceId, + expectedPublicKey: fixture.publicKey, + expectedReleaseName: fixture.releaseName, + expectedEnvironment: fixture.environment, + expectedTransaction: fixture.transactionName, + expectedUserSegment: fixture.userSegment, + expectedSampled: fixture.sampled, + expectedSampleRate: fixture.sampleRate, + expectedSampleRand: fixture.sampleRand, + expectedReplayId: fixture.replayId + ) + } + func testInitWithScopeOptions() { + // Act let traceContext = TraceContext(scope: fixture.scope, options: fixture.options)! + // Assert assertTraceState(traceContext: traceContext) } func testInitWithTracerScopeOptions() { + // Act let traceContext = TraceContext(tracer: fixture.tracer, scope: fixture.scope, options: fixture.options) + + // Assert assertTraceState(traceContext: traceContext!) } func testInitWithTracerNotSampled() { + // Arrange let tracer = fixture.tracer tracer.sampled = .no + + // Act let traceContext = TraceContext(tracer: tracer, scope: fixture.scope, options: fixture.options) + + // Assert XCTAssertEqual(traceContext?.sampled, "false") } func testInitNil() { + // Arrange fixture.scope.span = nil + + // Act let traceContext = TraceContext(scope: fixture.scope, options: fixture.options) + + // Assert XCTAssertNil(traceContext) } func testInitTraceIdOptionsSegment_WithOptionsAndSegment() throws { + // Arrange let options = Options() options.dsn = TestConstants.realDSN let traceId = SentryId() + + // Act let traceContext = TraceContext(trace: traceId, options: options, userSegment: "segment", replayId: "replayId") + // Assert XCTAssertEqual(options.parsedDsn?.url.user, traceContext.publicKey) XCTAssertEqual(traceId, traceContext.traceId) XCTAssertEqual(options.releaseName, traceContext.releaseName) @@ -109,16 +162,21 @@ class SentryTraceContextTests: XCTestCase { XCTAssertEqual("segment", traceContext.userSegment) XCTAssertEqual(traceContext.replayId, "replayId") XCTAssertNil(traceContext.sampleRate) + XCTAssertNil(traceContext.sampleRand) XCTAssertNil(traceContext.sampled) } func testInitTraceIdOptionsSegment_WithOptionsOnly() throws { + // Arrange let options = Options() options.dsn = TestConstants.realDSN let traceId = SentryId() + + // Act let traceContext = TraceContext(trace: traceId, options: options, userSegment: nil, replayId: nil) + // Assert XCTAssertEqual(options.parsedDsn?.url.user, traceContext.publicKey) XCTAssertEqual(traceId, traceContext.traceId) XCTAssertEqual(options.releaseName, traceContext.releaseName) @@ -126,10 +184,12 @@ class SentryTraceContextTests: XCTestCase { XCTAssertNil(traceContext.transaction) XCTAssertNil(traceContext.userSegment) XCTAssertNil(traceContext.sampleRate) + XCTAssertNil(traceContext.sampleRand) XCTAssertNil(traceContext.sampled) } func test_toBaggage() { + // Arrange let traceContext = TraceContext( trace: fixture.traceId, publicKey: fixture.publicKey, @@ -138,11 +198,14 @@ class SentryTraceContextTests: XCTestCase { transaction: fixture.transactionName, userSegment: fixture.userSegment, sampleRate: fixture.sampleRate, + sampleRand: fixture.sampleRand, sampled: fixture.sampled, replayId: fixture.replayId) + // Act let baggage = traceContext.toBaggage() + // Assert XCTAssertEqual(baggage.traceId, fixture.traceId) XCTAssertEqual(baggage.publicKey, fixture.publicKey) XCTAssertEqual(baggage.releaseName, fixture.releaseName) @@ -150,6 +213,7 @@ class SentryTraceContextTests: XCTestCase { XCTAssertEqual(baggage.userSegment, fixture.userSegment) XCTAssertEqual(baggage.sampleRate, fixture.sampleRate) XCTAssertEqual(baggage.sampled, fixture.sampled) + XCTAssertEqual(baggage.sampleRand, fixture.sampleRand) XCTAssertEqual(baggage.replayId, fixture.replayId) } @@ -163,5 +227,30 @@ class SentryTraceContextTests: XCTestCase { XCTAssertEqual(traceContext.sampled, fixture.sampled) XCTAssertEqual(traceContext.replayId, fixture.replayId) } + + private func assertFullTraceState( + traceContext: TraceContext, + expectedTraceId: SentryId, + expectedPublicKey: String, + expectedReleaseName: String, + expectedEnvironment: String, + expectedTransaction: String, + expectedUserSegment: String, + expectedSampled: String, + expectedSampleRate: String, + expectedSampleRand: String, + expectedReplayId: String, + file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(traceContext.traceId, expectedTraceId, "Trace ID does not match", file: file, line: line) + XCTAssertEqual(traceContext.publicKey, expectedPublicKey, "Public Key does not match", file: file, line: line) + XCTAssertEqual(traceContext.releaseName, expectedReleaseName, "Release Name does not match", file: file, line: line) + XCTAssertEqual(traceContext.environment, expectedEnvironment, "Environment does not match", file: file, line: line) + XCTAssertEqual(traceContext.transaction, expectedTransaction, "Transaction does not match", file: file, line: line) + XCTAssertEqual(traceContext.userSegment, expectedUserSegment, "User Segment does not match", file: file, line: line) + XCTAssertEqual(traceContext.sampled, expectedSampled, "Sampled does not match", file: file, line: line) + XCTAssertEqual(traceContext.sampleRate, expectedSampleRate, "Sample Rate does not match", file: file, line: line) + XCTAssertEqual(traceContext.sampleRand, expectedSampleRand, "Sample Rand does not match", file: file, line: line) + XCTAssertEqual(traceContext.replayId, expectedReplayId, "Replay ID does not match", file: file, line: line) + } } diff --git a/Tests/SentryTests/Transaction/SentryTransactionContextTests.swift b/Tests/SentryTests/Transaction/SentryTransactionContextTests.swift index bbdadecd21..7dbb24fa15 100644 --- a/Tests/SentryTests/Transaction/SentryTransactionContextTests.swift +++ b/Tests/SentryTests/Transaction/SentryTransactionContextTests.swift @@ -1,4 +1,5 @@ import Foundation +@testable import Sentry import SentryTestUtils import XCTest @@ -7,13 +8,20 @@ class SentryTransactionContextTests: XCTestCase { private let operation = "ui.load" private let transactionName = "Screen Load" private let origin = "auto.ui.swift_ui" + private let spanDescription = "span description" private let traceID = SentryId() private let spanID = SpanId() private let parentSpanID = SpanId() private let nameSource = SentryTransactionNameSource.route private let sampled = SentrySampleDecision.yes private let parentSampled = SentrySampleDecision.no - + private let sampleRate = NSNumber(value: 0.123456789) + private let parentSampleRate = NSNumber(value: 0.987654321) + private let sampleRand = NSNumber(value: 0.333) + private let parentSampleRand = NSNumber(value: 0.666) + + // MARK: - Legacy Tests + func testPublicInit_WithOperation() { let context = TransactionContext(operation: operation) @@ -31,13 +39,15 @@ class SentryTransactionContextTests: XCTestCase { assertContext(context: context, transactionName: "", sampled: .yes) } - + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testPublicInit_WithNameOperationSampled() { let context = TransactionContext(name: transactionName, operation: operation, sampled: .yes) assertContext(context: context, sampled: .yes) } - + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") func testPublicInit_WithAllParams() { let context = TransactionContext(name: transactionName, operation: operation, trace: traceID, spanId: spanID, parentSpanId: parentSpanID, parentSampled: .no) @@ -58,8 +68,8 @@ class SentryTransactionContextTests: XCTestCase { func testPrivateInit_WithNameSourceOperationOriginSampled() { let nameSource = SentryTransactionNameSource.route let sampled = SentrySampleDecision.yes - let context = TransactionContext(name: transactionName, nameSource: nameSource, operation: operation, origin: origin, sampled: sampled) - + let context = TransactionContext(name: transactionName, nameSource: nameSource, operation: operation, origin: origin, sampled: sampled, sampleRate: nil, sampleRand: nil) + assertContext(context: context, sampled: sampled, nameSource: nameSource, origin: origin) } @@ -73,7 +83,7 @@ class SentryTransactionContextTests: XCTestCase { } private var contextWithAllParams: TransactionContext { - return TransactionContext(name: transactionName, nameSource: nameSource, operation: operation, origin: origin, trace: traceID, spanId: spanID, parentSpanId: parentSpanID, sampled: sampled, parentSampled: parentSampled) + return TransactionContext(name: transactionName, nameSource: nameSource, operation: operation, origin: origin, trace: traceID, spanId: spanID, parentSpanId: parentSpanID, sampled: sampled, parentSampled: parentSampled, sampleRate: nil, parentSampleRate: nil, sampleRand: nil, parentSampleRand: nil) } func testSerialize() { @@ -90,6 +100,643 @@ class SentryTransactionContextTests: XCTestCase { XCTAssertNotNil(actual) } + + // MARK: - SentryTransactionContext - Inherited Public Initializers + + func testPublicInit_WithOperation_shouldMatchExpectedContext() { + // Act + let context = TransactionContext(operation: operation) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: "", + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithOperationSampled_shouldMatchExpectedContext() { + // Act + let context = TransactionContext(operation: operation, sampled: sampled) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: "", + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSampled() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: sampled + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: "", + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + // MARK: - SentryTransactionContext - Public Initializers + + func testPublicInit_WithNameOperation_shouldMatchExpectedValues() { + // Act + let context = TransactionContext(name: transactionName, operation: operation) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") + func testPublicInit_WithNameOperationSampled_shouldMatchExpectedValues() { + // Act + let context = TransactionContext(name: transactionName, operation: operation, sampled: sampled) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithNameOperationSampledSampleRateSampleRand() { + // Act + let context = TransactionContext( + name: transactionName, + operation: operation, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") + func testPublicInit_WithNameTraceIdSpanIdParentSpanIdParentSampled() { + // Act + let context = TransactionContext( + name: transactionName, + operation: operation, + trace: traceID, + spanId: spanID, + parentSpanId: parentSpanID, + parentSampled: parentSampled + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: parentSampled, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + @available(*, deprecated, message: "The test is marked as deprecated to silence the deprecation warning of the initializer") + func testPublicInit_WithNameTraceIdSpanIdParentSpanIdParentSampled_withNilValues() { + // Act + let context = TransactionContext( + name: transactionName, + operation: operation, + trace: traceID, + spanId: spanID, + parentSpanId: nil, + parentSampled: parentSampled + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: parentSampled, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithNameOperationTraceIdSpanIdParentSpanIdParentSampled() { + // Act + let context = TransactionContext( + name: transactionName, + operation: operation, + trace: traceID, + spanId: spanID, + parentSpanId: parentSpanID, + parentSampled: parentSampled, + parentSampleRate: parentSampleRate, + parentSampleRand: parentSampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: parentSampled, + expectedParentSampleRate: parentSampleRate, + expectedParentSampleRand: parentSampleRand + ) + } + + func testPublicInit_WithNameOperationTraceIdSpanIdParentSpanIdParentSampled_withNilValues() { + // Act + let context = TransactionContext( + name: transactionName, + operation: operation, + trace: traceID, + spanId: spanID, + parentSpanId: nil, + parentSampled: parentSampled, + parentSampleRate: nil, + parentSampleRand: nil + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: SentryTransactionNameSource.custom, + expectedParentSampled: parentSampled, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + // MARK: - SentryTransactionContext - Private Initializers + + func testPrivateInit_WithNameSourceOperationOrigin_shouldMatchExpectedValues() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPrivateInit_WithNameSourceOperationOriginSampledSampleRateSampleRand() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPrivateInit_WithNameSourceOperationOriginSampledSampleRateSampleRand_withNilValues() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + sampled: sampled, + sampleRate: nil, + sampleRand: nil + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: .undecided, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPrivateInit_WithNameSourceOperationOriginTraceIdSpanIdParentSpanId() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: parentSpanID, + parentSampled: parentSampled, + parentSampleRate: parentSampleRate, + parentSampleRand: parentSampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: parentSampled, + expectedParentSampleRate: parentSampleRate, + expectedParentSampleRand: parentSampleRand + ) + } + + func testPrivateInit_WithNameSourceOperationOriginTraceIdSpanIdParentSpanId_withNilValues() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: nil, + parentSampled: parentSampled, + parentSampleRate: nil, + parentSampleRand: nil + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: .undecided, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: parentSampled, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPrivateInit_WithNameSourceOperationOriginTraceIdSpanIdParentSpanIdParentSampledParentSampleRateParentSampleRand() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: parentSpanID, + sampled: sampled, + parentSampled: parentSampled, + sampleRate: sampleRate, + parentSampleRate: parentSampleRate, + sampleRand: sampleRand, + parentSampleRand: parentSampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: parentSampled, + expectedParentSampleRate: parentSampleRate, + expectedParentSampleRand: parentSampleRand + ) + } + + func testPrivateInit_WithNameSourceOperationOriginTraceIdSpanIdParentSpanIdParentSampledParentSampleRateParentSampleRand_withNilValues() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: nil, + sampled: sampled, + parentSampled: parentSampled, + sampleRate: nil, + parentSampleRate: nil, + sampleRand: nil, + parentSampleRand: nil + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: origin, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: parentSampled, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + // MARK: - Serialization + + func testSerializeWithSampleRand() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: parentSpanID, + sampled: sampled, + parentSampled: parentSampled, + sampleRate: sampleRate, + parentSampleRate: parentSampleRate, + sampleRand: sampleRand, + parentSampleRand: parentSampleRand + ) + + // Assert + let actual = context.serialize() + XCTAssertEqual(context.traceId.sentryIdString, actual["trace_id"] as? String) + XCTAssertEqual(context.spanId.sentrySpanIdString, actual["span_id"] as? String) + XCTAssertEqual(context.origin, actual["origin"] as? String) + XCTAssertEqual(context.parentSpanId?.sentrySpanIdString, actual["parent_span_id"] as? String) + XCTAssertEqual("trace", actual["type"] as? String) + XCTAssertEqual(true, actual["sampled"] as? NSNumber) + XCTAssertEqual("ui.load", actual["op"] as? String) + + XCTAssertNotNil(actual) + } + + func testSerializationWithSampleRand_minimalData_shouldNotIncludeNilValues() { + // Arrange + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: nil, + sampled: .undecided, + parentSampled: parentSampled, + sampleRate: nil, + parentSampleRate: nil, + sampleRand: nil, + parentSampleRand: nil + ) + + // Act + let data = context.serialize() + + // Assert + XCTAssertEqual(data["type"] as? String, SENTRY_TRACE_TYPE) + XCTAssertEqual(data["trace_id"] as? String, traceID.sentryIdString) + XCTAssertEqual(data["span_id"] as? String, spanID.sentrySpanIdString) + XCTAssertEqual(data["op"] as? String, operation) + XCTAssertNil(data["sampled"]) + XCTAssertNil(data["sample_rate"]) + XCTAssertNil(data["sample_rand"]) + XCTAssertNil(data["description"]) + XCTAssertNil(data["parent_span_id"]) + } + + func testSerializationWithSampleRand_NotSettingProperties_PropertiesNotSerialized() { + // Arrange + let context = TransactionContext(operation: operation) + + // Act + let data = context.serialize() + + // Assert + XCTAssertEqual(data["type"] as? String, SENTRY_TRACE_TYPE) + XCTAssertEqual(data["trace_id"] as? String, context.traceId.sentryIdString) + XCTAssertEqual(data["span_id"] as? String, context.spanId.sentrySpanIdString) + XCTAssertEqual(data["op"] as? String, operation) + XCTAssertEqual(data["origin"] as? String, "manual") + XCTAssertNil(data["sampled"]) + XCTAssertNil(data["sample_rate"]) + XCTAssertNil(data["sample_rand"]) + XCTAssertNil(data["description"]) + XCTAssertNil(data["parent_span_id"]) + } + + func testSerializationWithSampleRand_sampledDecisionYes_shouldSerializeToTrue() { + // Arrange + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .yes + ) + + // Act + let data = context.serialize() + + // Assert + XCTAssertEqual(data["sampled"] as? Bool, true) + } + + func testSerializationWithSampleRand_sampledDecisionNo_shouldSerializeToFalse() { + // Arrange + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .no + ) + + // Act + let data = context.serialize() + + // Assert + XCTAssertEqual(data["sampled"] as? Bool, false) + } + + func testSerializationWithSampleRand_sampledDecisionUndecided_shouldNotSerialize() { + // Arrange + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .undecided + ) + + // Act + let data = context.serialize() + + // Assert + XCTAssertNil(data["sampled"]) + } + + // MARK: - Assertion Helpers private func assertContext(context: TransactionContext, transactionName: String? = nil, sampled: SentrySampleDecision = .undecided, isParentSpanIdNil: Bool = true, nameSource: SentryTransactionNameSource = SentryTransactionNameSource.custom, origin: String? = nil) { @@ -108,4 +755,47 @@ class SentryTransactionContextTests: XCTestCase { XCTAssertNotNil(context.parentSpanId) } } + + private func assertFullContext( + context: TransactionContext, + + expectedParentSpanId: SpanId?, + expectedOperation: String, + expectedOrigin: String?, + expectedSpanDescription: String?, + expectedSampled: SentrySampleDecision, + expectedSampleRate: NSNumber?, + expectedSampleRand: NSNumber?, + + expectedName: String, + expectedNameSource: SentryTransactionNameSource?, + expectedParentSampled: SentrySampleDecision, + expectedParentSampleRate: NSNumber?, + expectedParentSampleRand: NSNumber?, + + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertNotNil(context.traceId, "Trace ID is nil", file: file, line: line) + XCTAssertNotNil(context.spanId, "Span ID is nil", file: file, line: line) + if let expectedParentSpanId = expectedParentSpanId { + XCTAssertEqual(context.parentSpanId, expectedParentSpanId, "Parent Span ID is nil", file: file, line: line) + } else { + XCTAssertNil(context.parentSpanId, "Parent Span ID is not nil", file: file, line: line) + } + + XCTAssertEqual(context.sampled, expectedSampled, "Sampled is not equal", file: file, line: line) + XCTAssertEqual(context.sampleRate, expectedSampleRate, "Sample Rate is not equal", file: file, line: line) + XCTAssertEqual(context.sampleRand, expectedSampleRand, "Sample Rand is not equal", file: file, line: line) + + XCTAssertEqual(context.operation, expectedOperation, "Operation is not equal", file: file, line: line) + XCTAssertEqual(context.spanDescription, expectedSpanDescription, "Span Description is not equal", file: file, line: line) + XCTAssertEqual(context.origin, expectedOrigin, "Origin is not equal", file: file, line: line) + + XCTAssertEqual(context.name, expectedName, "Name is not equal", file: file, line: line) + XCTAssertEqual(context.nameSource, expectedNameSource, "Name Source is not equal", file: file, line: line) + XCTAssertEqual(context.parentSampled, expectedParentSampled, "Parent Sampled is not equal", file: file, line: line) + XCTAssertEqual(context.parentSampleRate, expectedParentSampleRate, "Parent Sample Rate is not equal", file: file, line: line) + XCTAssertEqual(context.parentSampleRand, expectedParentSampleRand, "Parent Sample Rand is not equal", file: file, line: line) + } }