diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b16e691964..19158359923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,17 @@ - Use strlcpy to save session replay info path (#4740) - `sentryReplayUnmask` and `sentryReplayUnmask` preventing interaction (#4749) +- Fix missing `sample_rate` in baggage (#4751) ### Improvements - Add SentryHub to all log messages in the Hub (#4753) - More detailed log message when can't start session in SentryHub (#4752) +### Features + +- Add `sample_rand` to baggage (#4751) + ## 8.44.0-beta.1 ### Fixes diff --git a/Makefile b/Makefile index bf4a33ca047..755196221e5 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/SentryTestUtils/SentryLaunchProfiling+Tests.h b/SentryTestUtils/SentryLaunchProfiling+Tests.h index 4403e656716..1f33d2a48bf 100644 --- a/SentryTestUtils/SentryLaunchProfiling+Tests.h +++ b/SentryTestUtils/SentryLaunchProfiling+Tests.h @@ -39,7 +39,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 a4dbe7bf43b..7b3b7bcb82f 100644 --- a/Sources/Sentry/Profiling/SentryLaunchProfiling.m +++ b/Sources/Sentry/Profiling/SentryLaunchProfiling.m @@ -34,12 +34,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 +93,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; } @@ -153,9 +155,9 @@ 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, @1.0) hub:nil - configuration:sentry_config(profilesRate)]; + configuration:sentry_config(profilesRate, @1.0)]; } # pragma mark - Public diff --git a/Sources/Sentry/Public/SentryBaggage.h b/Sources/Sentry/Public/SentryBaggage.h index e306060f381..54223b08413 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/SentrySpanContext.h b/Sources/Sentry/Public/SentrySpanContext.h index 6c112743cda..6084673d94d 100644 --- a/Sources/Sentry/Public/SentrySpanContext.h +++ b/Sources/Sentry/Public/SentrySpanContext.h @@ -42,6 +42,16 @@ SENTRY_NO_INIT */ @property (nonatomic, readonly) SentrySampleDecision sampled; +/** + * Rate of sampling + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *sampleRate; + +/** + * Random value used to determine if the span is sampled. + */ +@property (nullable, nonatomic, strong, readonly) NSNumber *sampleRand; + /** * Short code identifying the type of operation the span is measuring. */ @@ -78,6 +88,19 @@ SENTRY_NO_INIT */ - (instancetype)initWithOperation:(NSString *)operation sampled:(SentrySampleDecision)sampled; +/** + * Init a @c SentryContext with an operation code and mark it as sampled or not. + * TraceId and SpanId with be randomly created. + * @param operation The operation this span is measuring. + * @param sampled Determines whether the trace should be sampled. + * @param sampleRate Rate of sampling + * @param sampleRand Random value used to determine if the trace is sampled. + */ +- (instancetype)initWithOperation:(NSString *)operation + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; + /** * @param traceId Determines which trace the Span belongs to. * @param spanId The Span Id. @@ -91,6 +114,23 @@ SENTRY_NO_INIT operation:(NSString *)operation sampled:(SentrySampleDecision)sampled; +/** + * @param traceId Determines which trace the Span belongs to. + * @param spanId The Span Id. + * @param operation The operation this span is measuring. + * @param parentId Id of a parent span. + * @param sampled Determines whether the trace should be sampled. + * @param sampleRate Rate of sampling + * @param sampleRand Random value used to determine if the trace is sampled. + */ +- (instancetype)initWithTraceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentId:(nullable SentrySpanId *)parentId + operation:(NSString *)operation + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; + /** * @param traceId Determines which trace the Span belongs to. * @param spanId The Span Id. @@ -106,6 +146,25 @@ SENTRY_NO_INIT spanDescription:(nullable NSString *)description sampled:(SentrySampleDecision)sampled; +/** + * @param traceId Determines which trace the Span belongs to. + * @param spanId The Span Id. + * @param operation The operation this span is measuring. + * @param parentId Id of a parent span. + * @param description The span description. + * @param sampled Determines whether the trace should be sampled. + * @param sampleRate Rate of sampling + * @param sampleRand Random value used to determine if the trace is sampled. + */ +- (instancetype)initWithTraceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentId:(nullable SentrySpanId *)parentId + operation:(NSString *)operation + spanDescription:(nullable NSString *)description + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryTraceContext.h b/Sources/Sentry/Public/SentryTraceContext.h index afb939b84a3..5959e103db9 100644 --- a/Sources/Sentry/Public/SentryTraceContext.h +++ b/Sources/Sentry/Public/SentryTraceContext.h @@ -52,6 +52,11 @@ NS_SWIFT_NAME(TraceContext) */ @property (nullable, nonatomic, readonly) NSString *sampleRate; +/** + * 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 f15693d8240..bcc1284652e 100644 --- a/Sources/Sentry/Public/SentryTransactionContext.h +++ b/Sources/Sentry/Public/SentryTransactionContext.h @@ -27,9 +27,14 @@ SENTRY_NO_INIT @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 @@ -56,6 +61,17 @@ SENTRY_NO_INIT operation:(NSString *)operation sampled:(SentrySampleDecision)sampled; +/** + * @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. @@ -71,6 +87,23 @@ SENTRY_NO_INIT parentSpanId:(nullable SentrySpanId *)parentSpanId parentSampled:(SentrySampleDecision)parentSampled; +/** + * @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 + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryBaggage.m b/Sources/Sentry/SentryBaggage.m index 28e9fe2d285..4f437817245 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 e59f8f697ff..1a588fad2d4 100644 --- a/Sources/Sentry/SentryBuildAppStartSpans.m +++ b/Sources/Sentry/SentryBuildAppStartSpans.m @@ -20,7 +20,9 @@ operation:operation spanDescription:description origin:SentryTraceOrigin.autoAppStart - sampled:tracer.sampled]; + sampled:tracer.sampled + sampleRate:tracer.sampleRate + sampleRand:tracer.sampleRand]; return [[SentrySpan alloc] initWithTracer:tracer context:context framesTracker:nil]; } diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 50a9199f5e5..3b682ccb579 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 @@ -689,8 +695,10 @@ - (SentryEnvelope *)updateSessionState:(SentryEnvelope *)envelope endSessionCrashedWithTimestamp:[SentryDependencyContainer.sharedInstance .dateProvider date]]; if (_client.options.diagnosticLevel == kSentryLevelDebug) { - SENTRY_LOG_DEBUG(@"Ending session with status: %@", - [self createSessionDebugString:currentSession]); + [SentryLog + logWithMessage:[NSString stringWithFormat:@"Ending session with status: %@", + [self createSessionDebugString:currentSession]] + andLevel:kSentryLevelDebug]; } if (startNewSession) { // Setting _session to nil so startSession doesn't capture it again diff --git a/Sources/Sentry/SentrySamplerDecision.m b/Sources/Sentry/SentrySamplerDecision.m index d3f2203cae9..b5d324a2f42 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 446dfca5139..a55c4babef1 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:[NSNumber numberWithDouble: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/SentrySpanContext.m b/Sources/Sentry/SentrySpanContext.m index 1c7d1010b40..27f6aa9ac64 100644 --- a/Sources/Sentry/SentrySpanContext.m +++ b/Sources/Sentry/SentrySpanContext.m @@ -16,12 +16,37 @@ - (instancetype)initWithOperation:(NSString *)operation } - (instancetype)initWithOperation:(NSString *)operation sampled:(SentrySampleDecision)sampled +{ + return [self initWithOperation:operation sampled:sampled sampleRate:nil sampleRand:nil]; +} + +- (instancetype)initWithOperation:(NSString *)operation + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand { return [self initWithTraceId:[[SentryId alloc] init] spanId:[[SentrySpanId alloc] init] parentId:nil operation:operation - sampled:sampled]; + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]; +} + +- (instancetype)initWithTraceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentId:(nullable SentrySpanId *)parentId + operation:(NSString *)operation + sampled:(SentrySampleDecision)sampled +{ + return [self initWithTraceId:traceId + spanId:spanId + parentId:parentId + operation:operation + sampled:sampled + sampleRate:nil + sampleRand:nil]; } - (instancetype)initWithTraceId:(SentryId *)traceId @@ -29,13 +54,17 @@ - (instancetype)initWithTraceId:(SentryId *)traceId parentId:(nullable SentrySpanId *)parentId operation:(NSString *)operation sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand { return [self initWithTraceId:traceId spanId:spanId parentId:parentId operation:operation spanDescription:nil - sampled:sampled]; + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]; } - (instancetype)initWithTraceId:(SentryId *)traceId @@ -51,7 +80,29 @@ - (instancetype)initWithTraceId:(SentryId *)traceId operation:operation spanDescription:description origin:SentryTraceOrigin.manual - sampled:sampled]; + sampled:sampled + sampleRate:nil + sampleRand:nil]; +} + +- (instancetype)initWithTraceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentId:(nullable SentrySpanId *)parentId + operation:(NSString *)operation + spanDescription:(nullable NSString *)description + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand +{ + return [self initWithTraceId:traceId + spanId:spanId + parentId:parentId + operation:operation + spanDescription:description + origin:SentryTraceOrigin.manual + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]; } #pragma mark - Private @@ -66,7 +117,45 @@ - (instancetype)initWithOperation:(NSString *)operation operation:operation spanDescription:nil origin:origin - sampled:sampled]; + sampled:sampled + sampleRate:nil + sampleRand:nil]; +} + +- (instancetype)initWithOperation:(NSString *)operation + origin:(NSString *)origin + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand +{ + return [self initWithTraceId:[[SentryId alloc] init] + spanId:[[SentrySpanId alloc] init] + parentId:nil + operation:operation + spanDescription:nil + origin:origin + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]; +} + +- (instancetype)initWithTraceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentId:(nullable SentrySpanId *)parentId + operation:(NSString *)operation + spanDescription:(nullable NSString *)description + origin:(NSString *)origin + sampled:(SentrySampleDecision)sampled +{ + return [self initWithTraceId:traceId + spanId:spanId + parentId:parentId + operation:operation + spanDescription:description + origin:origin + sampled:sampled + sampleRate:nil + sampleRand:nil]; } - (instancetype)initWithTraceId:(SentryId *)traceId @@ -76,12 +165,16 @@ - (instancetype)initWithTraceId:(SentryId *)traceId spanDescription:(nullable NSString *)description origin:(NSString *)origin sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand { if (self = [super init]) { _traceId = traceId; _spanId = spanId; _parentSpanId = parentId; _sampled = sampled; + _sampleRate = sampleRate; + _sampleRand = sampleRand; _operation = operation; _spanDescription = description; _origin = origin; @@ -111,6 +204,16 @@ - (instancetype)initWithTraceId:(SentryId *)traceId [mutabledictionary setValue:valueForSentrySampleDecision(self.sampled) forKey:@"sampled"]; } + if (self.sampleRate != nil) { + [mutabledictionary setValue:[NSString stringWithFormat:@"%f", self.sampleRate.floatValue] + forKey:@"sample_rate"]; + } + + if (self.sampleRand != nil) { + [mutabledictionary setValue:[NSString stringWithFormat:@"%f", self.sampleRate.floatValue] + forKey:@"sample_rand"]; + } + if (self.spanDescription != nil) { [mutabledictionary setValue:self.spanDescription forKey:@"description"]; } @@ -121,6 +224,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId return mutabledictionary; } + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryTraceContext.m b/Sources/Sentry/SentryTraceContext.m index c14486b23dd..ebb2f20292f 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/SentryTracer.m b/Sources/Sentry/SentryTracer.m index ef75ac05a08..521373a615b 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -383,7 +383,9 @@ - (void)cancelDeadlineTimer parentId:parentId operation:operation spanDescription:description - sampled:self.sampled]; + sampled:self.sampled + sampleRate:nil + sampleRand:nil]; SentrySpan *child = [[SentrySpan alloc] initWithTracer:self diff --git a/Sources/Sentry/SentryTransactionContext.mm b/Sources/Sentry/SentryTransactionContext.mm index 17b4daf830e..c3c4d794352 100644 --- a/Sources/Sentry/SentryTransactionContext.mm +++ b/Sources/Sentry/SentryTransactionContext.mm @@ -17,18 +17,59 @@ @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:kSentrySampleDecisionUndecided + 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 + sampleRate:sampleRate + sampleRand:sampleRand]; +} + +- (instancetype)initWithName:(NSString *)name + operation:(NSString *)operation + traceId:(SentryId *)traceId + spanId:(SentrySpanId *)spanId + parentSpanId:(nullable SentrySpanId *)parentSpanId + parentSampled:(SentrySampleDecision)parentSampled { return [self initWithName:name nameSource:kSentryTransactionNameSourceCustom operation:operation origin:SentryTraceOrigin.manual - sampled:sampled]; + traceId:traceId + spanId:spanId + parentSpanId:parentSpanId + sampled:kSentrySampleDecisionUndecided + parentSampled:parentSampled + sampleRate:nil + parentSampleRate:nil + sampleRand:nil + parentSampleRand:nil]; } - (instancetype)initWithName:(NSString *)name @@ -37,6 +78,8 @@ - (instancetype)initWithName:(NSString *)name spanId:(SentrySpanId *)spanId parentSpanId:(nullable SentrySpanId *)parentSpanId parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand { return [self initWithName:name nameSource:kSentryTransactionNameSourceCustom @@ -45,7 +88,12 @@ - (instancetype)initWithName:(NSString *)name traceId:traceId spanId:spanId parentSpanId:parentSpanId - parentSampled:parentSampled]; + 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,30 @@ - (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]; + if (self = [super initWithOperation:operation + origin:origin + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]) { + [self commonInitWithName:name + source:source + 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 { if (self = [super initWithTraceId:traceId spanId:spanId @@ -90,8 +148,14 @@ - (instancetype)initWithName:(NSString *)name operation:operation spanDescription:nil origin:origin - sampled:kSentryDefaultSamplingDecision]) { - [self commonInitWithName:name source:source parentSampled:parentSampled]; + sampled:kSentrySampleDecisionUndecided + sampleRate:nil + sampleRand:nil]) { + [self commonInitWithName:name + source:source + parentSampled:kSentrySampleDecisionUndecided + parentSampleRate:nil + parentSampleRand:nil]; } return self; } @@ -105,6 +169,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 @@ -112,11 +180,14 @@ - (instancetype)initWithName:(NSString *)name operation:operation spanDescription:nil origin:origin - sampled:sampled]) { - _name = [NSString stringWithString:name]; - _nameSource = source; - self.parentSampled = parentSampled; - [self getThreadInfo]; + sampled:sampled + sampleRate:sampleRate + sampleRand:sampleRand]) { + [self commonInitWithName:name + source:source + parentSampled:parentSampled + parentSampleRate:parentSampleRate + parentSampleRand:parentSampleRand]; } return self; } @@ -139,10 +210,14 @@ - (SentryThread *)sentry_threadInfo - (void)commonInitWithName:(NSString *)name source:(SentryTransactionNameSource)source parentSampled:(SentrySampleDecision)parentSampled + parentSampleRate:(nullable NSNumber *)parentSampleRate + parentSampleRand:(nullable NSNumber *)parentSampleRand { _name = [NSString stringWithString:name]; _nameSource = source; 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/SentrySamplerDecision.h b/Sources/Sentry/include/SentrySamplerDecision.h index 8b115653524..ec482ca17cf 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/SentrySpan.h b/Sources/Sentry/include/SentrySpan.h index 7fd111eed5b..6f07d7fae69 100644 --- a/Sources/Sentry/include/SentrySpan.h +++ b/Sources/Sentry/include/SentrySpan.h @@ -37,6 +37,16 @@ SENTRY_NO_INIT */ @property (nonatomic) SentrySampleDecision sampled; +/** + * Rate of sampling + */ +@property (nullable, nonatomic, strong) NSNumber *sampleRate; + +/** + * Random value used to determine if the span is sampled. + */ +@property (nullable, nonatomic, strong) NSNumber *sampleRand; + /** * Short code identifying the type of operation the span is measuring. */ diff --git a/Sources/Sentry/include/SentrySpanContext+Private.h b/Sources/Sentry/include/SentrySpanContext+Private.h index 57f9772b7dc..6454dc0e959 100644 --- a/Sources/Sentry/include/SentrySpanContext+Private.h +++ b/Sources/Sentry/include/SentrySpanContext+Private.h @@ -6,7 +6,9 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithOperation:(NSString *)operation origin:(NSString *)origin - sampled:(SentrySampleDecision)sampled; + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; - (instancetype)initWithTraceId:(SentryId *)traceId spanId:(SentrySpanId *)spanId @@ -14,7 +16,9 @@ NS_ASSUME_NONNULL_BEGIN operation:(NSString *)operation spanDescription:(nullable NSString *)description origin:(NSString *)origin - sampled:(SentrySampleDecision)sampled; + sampled:(SentrySampleDecision)sampled + sampleRate:(nullable NSNumber *)sampleRate + sampleRand:(nullable NSNumber *)sampleRand; @end diff --git a/Sources/Sentry/include/SentryTransactionContext+Private.h b/Sources/Sentry/include/SentryTransactionContext+Private.h index 67cf02bc9bd..1d7e995b09b 100644 --- a/Sources/Sentry/include/SentryTransactionContext+Private.h +++ b/Sources/Sentry/include/SentryTransactionContext+Private.h @@ -14,16 +14,17 @@ 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; + parentSpanId:(nullable SentrySpanId *)parentSpanId; - (instancetype)initWithName:(NSString *)name nameSource:(SentryTransactionNameSource)source @@ -33,7 +34,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 f3ff29ec503..e056f5deab2 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) diff --git a/Tests/SentryTests/Helper/SentrySerializationTests.swift b/Tests/SentryTests/Helper/SentrySerializationTests.swift index 72f894b4087..94bdd9f8e39 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) } - 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) + 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/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index 797ea66345d..951eaf5e289 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -162,6 +162,24 @@ class SentryEnvelopeTests: XCTestCase { XCTAssertEqual(eventId, envelopeHeader.eventId) XCTAssertEqual(traceContext, envelopeHeader.traceContext) } + + func testInitSentryEnvelopeHeader_withSampleRand_SetIdAndTraceState() { + // -- Arrange -- + let eventId = SentryId() + let traceContext = TraceContext( + trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", + transaction: "transaction", userSegment: nil, + sampleRate: nil, sampleRand: nil, sampled: nil, + replayId: nil + ) + + // -- Act -- + let envelopeHeader = SentryEnvelopeHeader(id: eventId, traceContext: traceContext) + + // -- Assert -- + XCTAssertEqual(eventId, envelopeHeader.eventId) + XCTAssertEqual(traceContext, envelopeHeader.traceContext) + } func testInitSentryEnvelopeWithSession_DefaultSdkInfoIsSet() { let envelope = SentryEnvelope(session: SentrySession(releaseName: "1.1.1", distinctId: "some-id")) diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 12a7c6164ed..7577104e87d 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -372,26 +372,62 @@ class SentryHubTests: XCTestCase { 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) } - + 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) } 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 +437,28 @@ 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) + } + 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,16 +479,74 @@ 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) + } 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) @@ -442,7 +558,27 @@ class SentryHubTests: XCTestCase { 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/Transaction/SentryBaggageTests.swift b/Tests/SentryTests/Transaction/SentryBaggageTests.swift index e8bed7def58..c981a524110 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/SentrySpanContextTests.swift b/Tests/SentryTests/Transaction/SentrySpanContextTests.swift index 44a0d013896..cea53857ddc 100644 --- a/Tests/SentryTests/Transaction/SentrySpanContextTests.swift +++ b/Tests/SentryTests/Transaction/SentrySpanContextTests.swift @@ -1,8 +1,22 @@ +@testable import Sentry import XCTest class SentrySpanContextTests: XCTestCase { + let operation = "ui.load" + let transactionName = "Screen Load" + let origin = "auto.ui.swift_ui" + let spanDescription = "span description" + let traceID = SentryId() + let spanID = SpanId() + let parentSpanID = SpanId() + let sampled = SentrySampleDecision.yes + let sampleRate = NSNumber(value: 0.123456789) + let sampleRand = NSNumber(value: 0.333) + + // MARK: - Legacy Tests + let someOperation = "Some Operation" - + func testInit() { let spanContext = SpanContext(operation: someOperation) XCTAssertEqual(spanContext.sampled, SentrySampleDecision.undecided) @@ -96,5 +110,299 @@ 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, + expectedSampleRate: nil, + expectedSampleRand: nil + ) + } + + 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, + expectedSampleRate: nil, + expectedSampleRand: nil + ) + } + + func testPublicInit_WithOperationSampledSampleRateSampleRand() { + // Act + let context = SpanContext( + operation: operation, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand + ) + } + + 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, + expectedSampleRate: nil, + expectedSampleRand: nil + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSampledSampleRateSampleRand() { + // Act + let context = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand + ) + } + + 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: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSpanDescriptionSampledSampleRateSampleRand() { + // Act + let context = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: spanDescription, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: spanDescription, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand + ) + } + + // MARK: - Serialization + + func testSerialization_minimalData_shouldNotIncludeNilValues() { + // Arrange + let spanContext = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: nil, + sampled: .undecided, + sampleRate: nil, + sampleRand: nil + ) + + // 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["sample_rate"]) + XCTAssertNil(data["sample_rand"]) + 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.traceId.sentryIdString) + XCTAssertEqual(data["op"] as? String, operation) + XCTAssertNil(data["origin"]) + XCTAssertNil(data["sampled"]) + XCTAssertNil(data["sample_rate"]) + XCTAssertNil(data["sample_rand"]) + XCTAssertNil(data["description"]) + XCTAssertNil(data["parent_span_id"]) + } + + func testSerialization_sampledDecisionYes_shouldSerializeToTrue() { + // Arrange + let spanContext = SpanContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: nil, + sampled: .yes, + sampleRate: nil, + sampleRand: nil + ) + + // 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, + sampleRate: nil, + sampleRand: nil + ) + + // 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, + sampleRate: nil, + sampleRand: nil + ) + + // 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, + expectedSampleRate: NSNumber?, + expectedSampleRand: NSNumber?, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertNotNil(context.traceId, file: file, line: line) + XCTAssertNotNil(context.spanId, file: file, line: line) + if let expectedParentSpanId = expectedParentSpanId { + XCTAssertEqual(context.parentSpanId, expectedParentSpanId, file: file, line: line) + } else { + XCTAssertNil(context.parentSpanId, file: file, line: line) + } + + XCTAssertEqual(context.sampled, expectedSampled, file: file, line: line) + XCTAssertEqual(context.sampleRate, expectedSampleRate, file: file, line: line) + XCTAssertEqual(context.sampleRand, expectedSampleRand, file: file, line: line) + + XCTAssertEqual(context.operation, expectedOperation, file: file, line: line) + XCTAssertEqual(context.spanDescription, expectedSpanDescription, file: file, line: line) + XCTAssertEqual(context.origin, expectedOrigin, file: file, line: line) + } } diff --git a/Tests/SentryTests/Transaction/SentrySpanTests.swift b/Tests/SentryTests/Transaction/SentrySpanTests.swift index 409a129d2c0..233d1f3b87b 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/SentryTraceStateTests.swift index 2503cbf0909..964f908ec96 100644 --- a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift +++ b/Tests/SentryTests/Transaction/SentryTraceStateTests.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,79 @@ 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 assertTraceState(traceContext: traceContext) } 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 +150,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 +172,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 +186,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,18 +201,21 @@ 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) } - func assertTraceState(traceContext: TraceContext) { - XCTAssertEqual(traceContext.traceId, fixture.traceId) - XCTAssertEqual(traceContext.publicKey, fixture.publicKey) - XCTAssertEqual(traceContext.releaseName, fixture.releaseName) - XCTAssertEqual(traceContext.environment, fixture.environment) - XCTAssertEqual(traceContext.transaction, fixture.transactionName) - XCTAssertEqual(traceContext.userSegment, fixture.userSegment) - XCTAssertEqual(traceContext.sampled, fixture.sampled) - XCTAssertEqual(traceContext.replayId, fixture.replayId) + func assertTraceState(traceContext: TraceContext, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(traceContext.traceId, fixture.traceId, file: file, line: line) + XCTAssertEqual(traceContext.publicKey, fixture.publicKey, file: file, line: line) + XCTAssertEqual(traceContext.releaseName, fixture.releaseName, file: file, line: line) + XCTAssertEqual(traceContext.environment, fixture.environment, file: file, line: line) + XCTAssertEqual(traceContext.transaction, fixture.transactionName, file: file, line: line) + XCTAssertEqual(traceContext.userSegment, fixture.userSegment, file: file, line: line) + XCTAssertEqual(traceContext.sampled, fixture.sampled, file: file, line: line) + XCTAssertEqual(traceContext.sampleRate, fixture.sampleRate, file: file, line: line) + XCTAssertEqual(traceContext.sampleRand, fixture.sampleRand, file: file, line: line) + XCTAssertEqual(traceContext.replayId, fixture.replayId, file: file, line: line) } } diff --git a/Tests/SentryTests/Transaction/SentryTransactionContextTests.swift b/Tests/SentryTests/Transaction/SentryTransactionContextTests.swift index 353b27e3a49..b55049435bc 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 { let operation = "ui.load" let transactionName = "Screen Load" let origin = "auto.ui.swift_ui" + let spanDescription = "span description" let traceID = SentryId() let spanID = SpanId() let parentSpanID = SpanId() let nameSource = SentryTransactionNameSource.route let sampled = SentrySampleDecision.yes let parentSampled = SentrySampleDecision.no - + let sampleRate = NSNumber(value: 0.123456789) + let parentSampleRate = NSNumber(value: 0.987654321) + let sampleRand = NSNumber(value: 0.333) + let parentSampleRand = NSNumber(value: 0.666) + + // MARK: - Legacy Tests + func testPublicInit_WithOperation() { let context = TransactionContext(operation: operation) @@ -58,8 +66,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 +81,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 +98,749 @@ 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: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + 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: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithOperationSampledSampleRateSampleRand() { + // Act + let context = TransactionContext( + 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: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + 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: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSampledSampleRateSampleRand() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + 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: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSpanDescriptionSampled() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: spanDescription, + sampled: sampled + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithTraceIdSpanIdParentIdOperationSpanDescriptionSampledSampleRateSampleRand() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: spanDescription, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: spanDescription, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand, + expectedName: nil, + expectedNameSource: nil, + expectedParentSampled: nil, + 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: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + 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: nil, + expectedParentSampled: nil, + 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: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithNameTraceIdSpanIdParentSpanIdParentSampled() { + // 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: transactionName, + expectedNameSource: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithNameTraceIdSpanIdParentSpanIdParentSampled_withNilValues() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: nil, + 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: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithNameTraceIdSpanIdParentSpanIdParentSampledSampleRateSampleRand() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: sampled, + sampleRate: sampleRate, + sampleRand: sampleRand + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: parentSpanID, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: sampleRate, + expectedSampleRand: sampleRand, + expectedName: transactionName, + expectedNameSource: nil, + expectedParentSampled: nil, + expectedParentSampleRate: nil, + expectedParentSampleRand: nil + ) + } + + func testPublicInit_WithNameTraceIdSpanIdParentSpanIdParentSampledSampleRateSampleRand_withNilValues() { + // Act + let context = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: nil, + operation: operation, + sampled: sampled, + sampleRate: nil, + sampleRand: nil + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + expectedOperation: operation, + expectedOrigin: SentryTraceOrigin.manual, + expectedSpanDescription: nil, + expectedSampled: sampled, + expectedSampleRate: nil, + expectedSampleRand: nil, + expectedName: transactionName, + expectedNameSource: nil, + expectedParentSampled: nil, + 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: nil, + expectedParentSampled: nil, + 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: nil, + expectedParentSampled: nil, + 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: nil, + expectedParentSampled: nil, + 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 + ) + + // 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_WithNameSourceOperationOriginTraceIdSpanIdParentSpanId_withNilValues() { + // Act + let context = TransactionContext( + name: transactionName, + nameSource: nameSource, + operation: operation, + origin: origin, + trace: traceID, + spanId: spanID, + parentSpanId: nil + ) + + // Assert + assertFullContext( + context: context, + expectedParentSpanId: nil, + 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() { + // 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: sampleRate, + expectedSampleRand: sampleRand, + expectedName: transactionName, + expectedNameSource: nameSource, + expectedParentSampled: nil, + 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 TransactionContext = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: nil, + sampled: .undecided, + sampleRate: nil, + sampleRand: nil + ) + + // Act + let data = TransactionContext.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 TransactionContext = TransactionContext(operation: operation) + + // Act + let data = TransactionContext.serialize() + + // Assert + XCTAssertEqual(data["type"] as? String, SENTRY_TRACE_TYPE) + XCTAssertEqual(data["trace_id"] as? String, TransactionContext.traceId.sentryIdString) + XCTAssertEqual(data["span_id"] as? String, TransactionContext.traceId.sentryIdString) + XCTAssertEqual(data["op"] as? String, operation) + XCTAssertNil(data["origin"]) + 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 TransactionContext = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + spanDescription: nil, + sampled: .yes, + sampleRate: nil, + sampleRand: nil + ) + + // Act + let data = TransactionContext.serialize() + + // Assert + XCTAssertEqual(data["sampled"] as? Bool, true) + } + + func testSerializationWithSampleRand_sampledDecisionNo_shouldSerializeToFalse() { + // Arrange + let TransactionContext = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .no, + sampleRate: nil, + sampleRand: nil + ) + + // Act + let data = TransactionContext.serialize() + + // Assert + XCTAssertEqual(data["sampled"] as? Bool, false) + } + + func testSerializationWithSampleRand_sampledDecisionUndecided_shouldNotSerialize() { + // Arrange + let TransactionContext = TransactionContext( + trace: traceID, + spanId: spanID, + parentId: parentSpanID, + operation: operation, + sampled: .undecided, + sampleRate: nil, + sampleRand: nil + ) + + // Act + let data = TransactionContext.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 +859,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, file: file, line: line) + XCTAssertNotNil(context.spanId, file: file, line: line) + if let expectedParentSpanId = expectedParentSpanId { + XCTAssertEqual(context.parentSpanId, expectedParentSpanId, file: file, line: line) + } else { + XCTAssertNil(context.parentSpanId, file: file, line: line) + } + + XCTAssertEqual(context.sampled, expectedSampled, file: file, line: line) + XCTAssertEqual(context.sampleRate, expectedSampleRate, file: file, line: line) + XCTAssertEqual(context.sampleRand, expectedSampleRand, file: file, line: line) + + XCTAssertEqual(context.operation, expectedOperation, file: file, line: line) + XCTAssertEqual(context.spanDescription, expectedSpanDescription, file: file, line: line) + XCTAssertEqual(context.origin, expectedOrigin, file: file, line: line) + + XCTAssertEqual(context.name, expectedName, file: file, line: line) + XCTAssertEqual(context.nameSource, expectedNameSource, file: file, line: line) + XCTAssertEqual(context.parentSampled, expectedParentSampled, file: file, line: line) + XCTAssertEqual(context.parentSampleRate, expectedParentSampleRate, file: file, line: line) + XCTAssertEqual(context.parentSampleRand, expectedParentSampleRand, file: file, line: line) + } }