diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d4143f03f..ab2750d049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## vNext + +- Add support for dynamic transaction sampling. (#753) @Tyrrrz + ## 3.0.0-beta.0 - Add instruction_addr to SentryStackFrame. (#744) @lucas-zimerman @@ -717,4 +721,4 @@ Also available via NuGet: [Sentry](https://www.nuget.org/packages/Sentry/0.0.1-preview1) [Sentry.AspNetCore](https://www.nuget.org/packages/Sentry.AspNetCore/0.0.1-preview1) -[Sentry.Extensions.Logging](https://www.nuget.org/packages/Sentry.Extensions.Logging/0.0.1-preview1) \ No newline at end of file +[Sentry.Extensions.Logging](https://www.nuget.org/packages/Sentry.Extensions.Logging/0.0.1-preview1) diff --git a/modules/Ben.Demystifier b/modules/Ben.Demystifier index 84e9d0fd1c..a6fe04f902 160000 --- a/modules/Ben.Demystifier +++ b/modules/Ben.Demystifier @@ -1 +1 @@ -Subproject commit 84e9d0fd1cc5ecc9a8277e4e72c6eb0d4dc58975 +Subproject commit a6fe04f9023d7cf304e676ea2319f31b2f5fda72 diff --git a/samples/Sentry.Samples.AspNetCore.Mvc/SpecialExceptionProcessor.cs b/samples/Sentry.Samples.AspNetCore.Mvc/SpecialExceptionProcessor.cs index 55064627fc..8dfac42907 100644 --- a/samples/Sentry.Samples.AspNetCore.Mvc/SpecialExceptionProcessor.cs +++ b/samples/Sentry.Samples.AspNetCore.Mvc/SpecialExceptionProcessor.cs @@ -1,5 +1,6 @@ using Sentry; using Sentry.Extensibility; +using Sentry.Protocol; namespace Samples.AspNetCore.Mvc { diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index d4e52abb32..aeaf7223f2 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.Net.Http.Headers; using Sentry.AspNetCore.Extensions; using Sentry.Extensibility; +using Sentry.Protocol; namespace Sentry.AspNetCore { diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 603372be27..4e9cbf922a 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Sentry.Protocol; @@ -55,10 +56,11 @@ public void WithScope(Action scopeCallback) /// /// Returns a dummy transaction. /// - public ITransaction StartTransaction(string name, string operation) => new Transaction(this, name, operation) - { - IsSampled = false - }; + public ITransaction StartTransaction( + ITransactionContext context, + IReadOnlyDictionary customSamplingContext) => + // Transactions from DisabledHub are always sampled out + new Transaction(this, context) {IsSampled = false}; /// /// Returns null. diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index fd3d4a1e7d..dad8d90278 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -75,8 +75,10 @@ public void WithScope(Action scopeCallback) /// Forwards the call to . /// [DebuggerStepThrough] - public ITransaction StartTransaction(string name, string operation) - => SentrySdk.StartTransaction(name, operation); + public ITransaction StartTransaction( + ITransactionContext context, + IReadOnlyDictionary customSamplingContext) + => SentrySdk.StartTransaction(context, customSamplingContext); /// /// Forwards the call to . diff --git a/src/Sentry/HubExtensions.cs b/src/Sentry/HubExtensions.cs index 0797781878..4670db541e 100644 --- a/src/Sentry/HubExtensions.cs +++ b/src/Sentry/HubExtensions.cs @@ -12,6 +12,21 @@ namespace Sentry [EditorBrowsable(EditorBrowsableState.Never)] public static class HubExtensions { + /// + /// Starts a transaction. + /// + public static ITransaction StartTransaction(this IHub hub, ITransactionContext context) => + hub.StartTransaction(context, new Dictionary()); + + /// + /// Starts a transaction. + /// + public static ITransaction StartTransaction( + this IHub hub, + string name, + string operation) => + hub.StartTransaction(new TransactionContext(name, operation)); + /// /// Starts a transaction. /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 99e88cd7a0..6bfb0c5887 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Sentry.Protocol; namespace Sentry @@ -24,7 +25,10 @@ public interface IHub : /// /// Starts a transaction. /// - ITransaction StartTransaction(string name, string operation); + ITransaction StartTransaction( + ITransactionContext context, + IReadOnlyDictionary customSamplingContext + ); /// /// Gets the sentry trace header. diff --git a/src/Sentry/ISentryTraceSampler.cs b/src/Sentry/ISentryTraceSampler.cs deleted file mode 100644 index 8b26d3d045..0000000000 --- a/src/Sentry/ISentryTraceSampler.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Sentry -{ - /// - /// Trace sampler. - /// - public interface ISentryTraceSampler - { - /// - /// Gets the sample rate based on context. - /// - double GetSampleRate(TraceSamplingContext context); - } -} diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index dc58d58949..224a1a50da 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Threading.Tasks; using Sentry.Extensibility; using Sentry.Integrations; @@ -103,9 +105,11 @@ public void WithScope(Action scopeCallback) public void BindClient(ISentryClient client) => ScopeManager.BindClient(client); - public ITransaction StartTransaction(string name, string operation) + public ITransaction StartTransaction( + ITransactionContext context, + IReadOnlyDictionary customSamplingContext) { - var transaction = new Transaction(this, name, operation); + var transaction = new Transaction(this, context); var nameAndVersion = MainSentryEventProcessor.NameAndVersion; var protocolPackageName = MainSentryEventProcessor.ProtocolPackageName; @@ -121,6 +125,34 @@ public ITransaction StartTransaction(string name, string operation) transaction.Sdk.AddPackage(protocolPackageName, nameAndVersion.Version); } + // Make a sampling decision + var samplingContext = new TransactionSamplingContext( + context, + customSamplingContext + ); + + var sampleRate = + // Custom sampler may not exist or may return null, in which case we fallback + // to the static sample rate. + _options.TracesSampler?.Invoke(samplingContext) + ?? _options.TracesSampleRate; + + transaction.IsSampled = sampleRate switch + { + // Sample rate >= 1 means always sampled *in* + >= 1 => true, + // Sample rate <= 0 means always sampled *out* + <= 0 => false, + // Otherwise roll the dice + _ => SynchronizedRandom.NextDouble() > sampleRate + }; + + // A sampled out transaction still appears fully functional to the user + // but will be dropped by the client and won't reach Sentry's servers. + + // Sampling decision must have been made at this point + Debug.Assert(transaction.IsSampled != null, "Started transaction without a sampling decision."); + return transaction; } diff --git a/src/Sentry/Internal/SynchronizedRandom.cs b/src/Sentry/Internal/SynchronizedRandom.cs new file mode 100644 index 0000000000..858b3b923a --- /dev/null +++ b/src/Sentry/Internal/SynchronizedRandom.cs @@ -0,0 +1,17 @@ +using System; + +namespace Sentry.Internal +{ + internal static class SynchronizedRandom + { + private static readonly Random Random = new(); + + public static double NextDouble() + { + lock (Random) + { + return Random.NextDouble(); + } + } + } +} diff --git a/src/Sentry/Protocol/Context/ITraceContext.cs b/src/Sentry/Protocol/Context/ITraceContext.cs new file mode 100644 index 0000000000..be27932c32 --- /dev/null +++ b/src/Sentry/Protocol/Context/ITraceContext.cs @@ -0,0 +1,42 @@ +namespace Sentry.Protocol.Context +{ + /// + /// Trace metadata stored in 'contexts.trace' on a n event or transaction. + /// + public interface ITraceContext + { + /// + /// Span ID. + /// + SpanId SpanId { get; } + + /// + /// Parent ID. + /// + SpanId? ParentSpanId { get; } + + /// + /// Trace ID. + /// + SentryId TraceId { get; } + + /// + /// Operation. + /// + string Operation { get; } + + /// + /// Status. + /// + SpanStatus? Status { get; } + + // Note: this may need to be mutated internally, + // but the user should never be able to change it + // on their own. + + /// + /// Whether the span or transaction is sampled in (i.e. eligible for sending to Sentry). + /// + bool? IsSampled { get; } + } +} diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs index 2351c2d77b..0d9224ddcd 100644 --- a/src/Sentry/Protocol/Context/Trace.cs +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -1,12 +1,13 @@ using System.Text.Json; using Sentry.Internal.Extensions; +using Sentry.Protocol.Context; namespace Sentry.Protocol { /// /// Trace context data. /// - public class Trace : ISpanContext, IJsonSerializable + public class Trace : ITraceContext, IJsonSerializable { /// /// Tells Sentry which type of context this is. @@ -29,7 +30,7 @@ public class Trace : ISpanContext, IJsonSerializable public SpanStatus? Status { get; set; } /// - public bool IsSampled { get; internal set; } = true; + public bool? IsSampled { get; internal set; } /// /// Clones this instance. @@ -76,7 +77,10 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("status", status.ToString().ToSnakeCase()); } - writer.WriteBoolean("sampled", IsSampled); + if (IsSampled is {} isSampled) + { + writer.WriteBoolean("sampled", isSampled); + } writer.WriteEndObject(); } @@ -91,7 +95,7 @@ public static Trace FromJson(JsonElement json) var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var operation = json.GetPropertyOrNull("op")?.GetString() ?? ""; var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.Replace("_", "").ParseEnum()); - var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? true; + var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean(); return new Trace { diff --git a/src/Sentry/Protocol/IEventLike.cs b/src/Sentry/Protocol/IEventLike.cs index 1691da75d3..2884b323a3 100644 --- a/src/Sentry/Protocol/IEventLike.cs +++ b/src/Sentry/Protocol/IEventLike.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using System.Linq; namespace Sentry.Protocol { /// /// Models members common between types that represent event-like data. /// - public interface IEventLike : IHasTags, IHasExtra + public interface IEventLike : IHasBreadcrumbs, IHasTags, IHasExtra { /// /// Sentry level. @@ -70,16 +71,27 @@ public interface IEventLike : IHasTags, IHasExtra /// { "fingerprint": ["myrpc", "POST", "/foo.bar"] } /// { "fingerprint": ["{{ default }}", "http://example.com/my.url"] } IReadOnlyList Fingerprint { get; set; } + } + /// + /// Extensions for . + /// + public static class EventLikeExtensions + { /// - /// A trail of events which happened prior to an issue. + /// Whether a has been set to the object with any of its fields non null. /// - /// - IReadOnlyCollection Breadcrumbs { get; } + public static bool HasUser(this IEventLike eventLike) + => eventLike.User.Email is not null + || eventLike.User.Id is not null + || eventLike.User.Username is not null + || eventLike.User.InternalOther?.Count > 0 + || eventLike.User.IpAddress is not null; /// - /// Adds a breadcrumb. + /// Sets the fingerprint to the object. /// - void AddBreadcrumb(Breadcrumb breadcrumb); + public static void SetFingerprint(this IEventLike eventLike, IEnumerable fingerprint) + => eventLike.Fingerprint = fingerprint as IReadOnlyList ?? fingerprint.ToArray(); } } diff --git a/src/Sentry/Protocol/IHasBreadcrumbs.cs b/src/Sentry/Protocol/IHasBreadcrumbs.cs new file mode 100644 index 0000000000..534e5dcce0 --- /dev/null +++ b/src/Sentry/Protocol/IHasBreadcrumbs.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Sentry.Protocol +{ + /// + /// Implemented by objects that contain breadcrumbs. + /// + public interface IHasBreadcrumbs + { + /// + /// A trail of events which happened prior to an issue. + /// + /// + IReadOnlyCollection Breadcrumbs { get; } + + /// + /// Adds a breadcrumb. + /// + void AddBreadcrumb(Breadcrumb breadcrumb); + } + + /// + /// Extensions for . + /// + public static class HasBreadcrumbsExtensions + { +#if HAS_VALUE_TUPLE + /// + /// Adds a breadcrumb to the object. + /// + /// The object. + /// The message. + /// The category. + /// The type. + /// The data key-value pair. + /// The level. + public static void AddBreadcrumb( + this IHasBreadcrumbs hasBreadcrumbs, + string message, + string? category, + string? type, + (string, string)? dataPair = null, + BreadcrumbLevel level = default) + { + // Not to throw on code that ignores nullability warnings. + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (hasBreadcrumbs is null) + { + return; + } + + Dictionary? data = null; + + if (dataPair != null) + { + data = new Dictionary + { + {dataPair.Value.Item1, dataPair.Value.Item2} + }; + } + + hasBreadcrumbs.AddBreadcrumb( + null, + message, + category, + type, + data, + level); + } +#endif + + /// + /// Adds a breadcrumb to the object. + /// + /// The object. + /// The message. + /// The category. + /// The type. + /// The data. + /// The level. + public static void AddBreadcrumb( + this IHasBreadcrumbs hasBreadcrumbs, + string message, + string? category = null, + string? type = null, + Dictionary? data = null, + BreadcrumbLevel level = default) + { + // Not to throw on code that ignores nullability warnings. + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (hasBreadcrumbs is null) + { + return; + } + + hasBreadcrumbs.AddBreadcrumb( + null, + message, + category, + type, + data, + level); + } + + /// + /// Adds a breadcrumb to the object. + /// + /// + /// This overload is used for testing. + /// + /// The object. + /// The timestamp + /// The message. + /// The category. + /// The type. + /// The data + /// The level. + [EditorBrowsable(EditorBrowsableState.Never)] + public static void AddBreadcrumb( + this IHasBreadcrumbs hasBreadcrumbs, + DateTimeOffset? timestamp, + string message, + string? category = null, + string? type = null, + IReadOnlyDictionary? data = null, + BreadcrumbLevel level = default) + { + // Not to throw on code that ignores nullability warnings. + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (hasBreadcrumbs is null) + { + return; + } + + hasBreadcrumbs.AddBreadcrumb(new Breadcrumb( + timestamp, + message, + type, + data, + category, + level)); + } + } +} diff --git a/src/Sentry/Protocol/IHasExtra.cs b/src/Sentry/Protocol/IHasExtra.cs index 6dc660ed3b..a85d058fea 100644 --- a/src/Sentry/Protocol/IHasExtra.cs +++ b/src/Sentry/Protocol/IHasExtra.cs @@ -17,4 +17,21 @@ public interface IHasExtra /// void SetExtra(string key, object? value); } + + /// + /// Extensions for . + /// + public static class HasExtraExtensions + { + /// + /// Sets the extra key-value pairs to the object. + /// + public static void SetExtras(this IHasExtra hasExtra, IEnumerable> values) + { + foreach (var (key, value) in values) + { + hasExtra.SetExtra(key, value); + } + } + } } diff --git a/src/Sentry/Protocol/IHasTags.cs b/src/Sentry/Protocol/IHasTags.cs index 1c97e261a9..1085b92863 100644 --- a/src/Sentry/Protocol/IHasTags.cs +++ b/src/Sentry/Protocol/IHasTags.cs @@ -16,5 +16,27 @@ public interface IHasTags /// Sets a tag. /// void SetTag(string key, string value); + + /// + /// Removes a tag. + /// + void UnsetTag(string key); + } + + /// + /// Extensions for . + /// + public static class HasTagsExtensions + { + /// + /// Set all items as tags. + /// + public static void SetTags(this IHasTags hasTags, IEnumerable> tags) + { + foreach (var (key, value) in tags) + { + hasTags.SetTag(key, value); + } + } } } diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 057860e763..db7be3f1e9 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -8,9 +8,22 @@ namespace Sentry.Protocol public interface ISpan : ISpanContext, IHasTags, IHasExtra { /// - /// Description. + /// Span description. /// - string? Description { get; set; } + // 'new' because it adds a setter. + new string? Description { get; set; } + + /// + /// Span operation. + /// + // 'new' because it adds a setter. + new string Operation { get; set; } + + /// + /// Span status. + /// + // 'new' because it adds a setter. + new SpanStatus? Status { get; set; } /// /// Start timestamp. diff --git a/src/Sentry/Protocol/ISpanContext.cs b/src/Sentry/Protocol/ISpanContext.cs index ddcd996519..0d1cfaeabc 100644 --- a/src/Sentry/Protocol/ISpanContext.cs +++ b/src/Sentry/Protocol/ISpanContext.cs @@ -1,44 +1,15 @@ +using Sentry.Protocol.Context; + namespace Sentry.Protocol { - // Parts of transaction (which is a span) are stored in a context - // for some unknown reason. This interface defines those fields. - /// /// Span metadata. /// - public interface ISpanContext + public interface ISpanContext : ITraceContext { /// - /// Span ID. - /// - SpanId SpanId { get; } - - /// - /// Parent ID. - /// - SpanId? ParentSpanId { get; } - - /// - /// Trace ID. - /// - SentryId TraceId { get; } - - /// - /// Operation. - /// - string Operation { get; set; } - - /// - /// Status. - /// - SpanStatus? Status { get; set; } - - // Note: this may need to be mutated internally, - // but the user should never be able to change it - // on their own. - /// - /// Is sampled. + /// Description. /// - bool IsSampled { get; } + string? Description { get; } } } diff --git a/src/Sentry/Protocol/ITransaction.cs b/src/Sentry/Protocol/ITransaction.cs index de3c080aa6..29b2bd9ed6 100644 --- a/src/Sentry/Protocol/ITransaction.cs +++ b/src/Sentry/Protocol/ITransaction.cs @@ -5,7 +5,7 @@ namespace Sentry.Protocol /// /// Transaction. /// - public interface ITransaction : ISpan, IEventLike + public interface ITransaction : ISpan, ITransactionContext, IEventLike { /// /// Transaction event ID. @@ -15,7 +15,8 @@ public interface ITransaction : ISpan, IEventLike /// /// Transaction name. /// - string Name { get; set; } + // 'new' because it adds a setter + new string Name { get; set; } /// /// Flat list of spans within this transaction. diff --git a/src/Sentry/Protocol/ITransactionContext.cs b/src/Sentry/Protocol/ITransactionContext.cs new file mode 100644 index 0000000000..a269ab8bf2 --- /dev/null +++ b/src/Sentry/Protocol/ITransactionContext.cs @@ -0,0 +1,13 @@ +namespace Sentry.Protocol +{ + /// + /// Transaction metadata. + /// + public interface ITransactionContext : ISpanContext + { + /// + /// Transaction name. + /// + string Name { get; } + } +} diff --git a/src/Sentry/Protocol/SentryEvent.cs b/src/Sentry/Protocol/SentryEvent.cs index 615e1e8a48..e38ffafee2 100644 --- a/src/Sentry/Protocol/SentryEvent.cs +++ b/src/Sentry/Protocol/SentryEvent.cs @@ -208,6 +208,10 @@ public void SetExtra(string key, object? value) => public void SetTag(string key, string value) => (_tags ??= new Dictionary())[key] = value; + /// + public void UnsetTag(string key) => + (_tags ??= new Dictionary()).Remove(key); + /// public void WriteTo(Utf8JsonWriter writer) { diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index bfd38b0fcd..02271c0358 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -33,17 +33,17 @@ public class Span : ISpan, IJsonSerializable /// public DateTimeOffset? EndTimestamp { get; private set; } - /// + /// public string Operation { get; set; } - /// + /// public string? Description { get; set; } - /// + /// public SpanStatus? Status { get; set; } /// - public bool IsSampled { get; internal set; } + public bool? IsSampled { get; internal set; } private ConcurrentDictionary? _tags; @@ -54,6 +54,10 @@ public class Span : ISpan, IJsonSerializable public void SetTag(string key, string value) => (_tags ??= new ConcurrentDictionary())[key] = value; + /// + public void UnsetTag(string key) => + (_tags ??= new ConcurrentDictionary()).TryRemove(key, out _); + private ConcurrentDictionary? _data; /// @@ -111,7 +115,10 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("status", status.ToString().ToSnakeCase()); } - writer.WriteBoolean("sampled", IsSampled); + if (IsSampled is {} isSampled) + { + writer.WriteBoolean("sampled", isSampled); + } writer.WriteString("start_timestamp", StartTimestamp); @@ -146,7 +153,7 @@ public static Span FromJson(Transaction parentTransaction, JsonElement json) var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.Replace("_", "").ParseEnum()); - var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? true; + var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean(); var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); diff --git a/src/Sentry/Protocol/SpanContext.cs b/src/Sentry/Protocol/SpanContext.cs new file mode 100644 index 0000000000..0b398a1da3 --- /dev/null +++ b/src/Sentry/Protocol/SpanContext.cs @@ -0,0 +1,50 @@ +namespace Sentry.Protocol +{ + /// + /// Span metadata used for sampling. + /// + public class SpanContext : ISpanContext + { + /// + public SpanId SpanId { get; } + + /// + public SpanId? ParentSpanId { get; } + + /// + public SentryId TraceId { get; } + + /// + public string Operation { get; } + + /// + public string? Description { get; } + + /// + public SpanStatus? Status { get; } + + /// + public bool? IsSampled { get; } + + /// + /// Initializes an instance of . + /// + public SpanContext( + SpanId spanId, + SpanId? parentSpanId, + SentryId traceId, + string operation, + string description, + SpanStatus? status, + bool? isSampled) + { + SpanId = spanId; + ParentSpanId = parentSpanId; + TraceId = traceId; + Operation = operation; + Description = description; + Status = status; + IsSampled = isSampled; + } + } +} diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 546995ae3b..3d05d18181 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -44,6 +44,16 @@ public SpanId SpanId private set => Contexts.Trace.SpanId = value; } + // A transaction normally does not have a parent because it represents + // the top node in the span hierarchy. + // However, a transaction may also be continued from a trace header + // (i.e. when another service sends a request to this service), + // in which case the newly created transaction refers to the incoming + // transaction as the parent. + + /// + public SpanId? ParentSpanId { get; } + /// public SentryId TraceId { @@ -51,7 +61,7 @@ public SentryId TraceId private set => Contexts.Trace.TraceId = value; } - /// + /// public string Name { get; set; } /// @@ -60,17 +70,17 @@ public SentryId TraceId /// public DateTimeOffset? EndTimestamp { get; private set; } - /// + /// public string Operation { get => Contexts.Trace.Operation; set => Contexts.Trace.Operation = value; } - /// + /// public string? Description { get; set; } - /// + /// public SpanStatus? Status { get => Contexts.Trace.Status; @@ -78,7 +88,7 @@ public SpanStatus? Status } /// - public bool IsSampled + public bool? IsSampled { get => Contexts.Trace.IsSampled; internal set => Contexts.Trace.IsSampled = value; @@ -137,19 +147,19 @@ public IReadOnlyList Fingerprint } // Not readonly because of deserialization - private Lazy> _breadcrumbsLazy = new(); + private Lazy> _breadcrumbsLazy = new(); /// public IReadOnlyCollection Breadcrumbs => _breadcrumbsLazy.Value; // Not readonly because of deserialization - private Lazy> _extraLazy = new(); + private Lazy> _extraLazy = new(); /// public IReadOnlyDictionary Extra => _extraLazy.Value; // Not readonly because of deserialization - private Lazy> _tagsLazy = new(); + private Lazy> _tagsLazy = new(); /// public IReadOnlyDictionary Tags => _tagsLazy.Value; @@ -160,23 +170,19 @@ public IReadOnlyList Fingerprint /// public IReadOnlyCollection Spans => _spansLazy.Value; - // Transaction never has a parent - SpanId? ISpanContext.ParentSpanId => null; - // This constructor is used for deserialization purposes. // It's required because some of the fields are mapped on 'contexts.trace'. // When deserializing, we don't parse those fields explicitly, but // instead just parse the trace context and resolve them later. // Hence why we need a constructor that doesn't take the operation to avoid // overwriting it. - private Transaction(ISentryClient client, string name) + private Transaction( + ISentryClient client, + string name) { _client = client; EventId = SentryId.Create(); - SpanId = SpanId.Create(); - TraceId = SentryId.Create(); Name = name; - IsSampled = true; } /// @@ -185,9 +191,25 @@ private Transaction(ISentryClient client, string name) public Transaction(ISentryClient client, string name, string operation) : this(client, name) { + SpanId = SpanId.Create(); + TraceId = SentryId.Create(); Operation = operation; } + /// + /// Initializes an instance of . + /// + public Transaction(ISentryClient client, ITransactionContext context) + : this(client, context.Name) + { + SpanId = context.SpanId; + ParentSpanId = context.ParentSpanId; + TraceId = context.TraceId; + Operation = context.Operation; + Description = context.Description; + Status = context.Status; + } + /// public void AddBreadcrumb(Breadcrumb breadcrumb) => _breadcrumbsLazy.Value.Add(breadcrumb); @@ -200,6 +222,10 @@ public void SetExtra(string key, object? value) => public void SetTag(string key, string value) => _tagsLazy.Value[key] = value; + /// + public void UnsetTag(string key) => + _tagsLazy.Value.TryRemove(key, out _); + internal ISpan StartChild(SpanId parentSpanId, string operation) { var span = new Span(this, parentSpanId, operation) @@ -366,9 +392,9 @@ public static Transaction FromJson(JsonElement json) var environment = json.GetPropertyOrNull("environment")?.GetString(); var sdk = json.GetPropertyOrNull("sdk")?.Pipe(SdkVersion.FromJson) ?? new SdkVersion(); var fingerprint = json.GetPropertyOrNull("fingerprint")?.EnumerateArray().Select(j => j.GetString()!).ToArray(); - var breadcrumbs = json.GetPropertyOrNull("breadcrumbs")?.EnumerateArray().Select(Breadcrumb.FromJson).ToList(); - var extra = json.GetPropertyOrNull("extra")?.GetObjectDictionary()?.ToDictionary(); - var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.ToDictionary(); + var breadcrumbs = json.GetPropertyOrNull("breadcrumbs")?.EnumerateArray().Select(Breadcrumb.FromJson).Pipe(v => new ConcurrentBag(v)); + var extra = json.GetPropertyOrNull("extra")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v)); + var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var transaction = new Transaction(hub, name) { @@ -382,10 +408,7 @@ public static Transaction FromJson(JsonElement json) _user = user, Environment = environment, Sdk = sdk, - _fingerprint = fingerprint, - _breadcrumbsLazy = new(() => breadcrumbs!), - _extraLazy = new(() => extra!), - _tagsLazy = new(() => tags!) + _fingerprint = fingerprint }; if (breadcrumbs is not null) @@ -400,7 +423,7 @@ public static Transaction FromJson(JsonElement json) if (tags is not null) { - transaction._tagsLazy = new(() => tags!); + transaction._tagsLazy = new(() => tags); } var spans = json diff --git a/src/Sentry/Protocol/TransactionContext.cs b/src/Sentry/Protocol/TransactionContext.cs new file mode 100644 index 0000000000..8221359955 --- /dev/null +++ b/src/Sentry/Protocol/TransactionContext.cs @@ -0,0 +1,59 @@ +namespace Sentry.Protocol +{ + /// + /// Transaction metadata used for sampling. + /// + public class TransactionContext : SpanContext, ITransactionContext + { + /// + public string Name { get; } + + /// + /// Initializes an instance of . + /// + public TransactionContext( + SpanId spanId, + SpanId? parentSpanId, + SentryId traceId, + string name, + string operation, + string description, + SpanStatus? status, + bool? isSampled) + : base(spanId, parentSpanId, traceId, operation, description, status, isSampled) + { + Name = name; + } + + /// + /// Initializes an instance of . + /// + public TransactionContext( + string name, + string operation, + string description, + bool? isSampled) + : this(SpanId.Create(), null, SentryId.Create(), name, operation, description, null, isSampled) + { + } + + /// + /// Initializes an instance of . + /// + public TransactionContext( + string name, + string operation, + bool? isSampled) + : this(name, operation, "", isSampled) + { + } + + /// + /// Initializes an instance of . + /// + public TransactionContext(string name, string operation) + : this(name, operation, null) + { + } + } +} diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index e005ad1f9f..0b4c3a23da 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -224,6 +224,9 @@ public void AddBreadcrumb(Breadcrumb breadcrumb) /// public void SetTag(string key, string value) => _tags[key] = value; + /// + public void UnsetTag(string key) => _tags.TryRemove(key, out _); + /// /// Adds an attachment. /// diff --git a/src/Sentry/ScopeExtensions.cs b/src/Sentry/ScopeExtensions.cs index 6c0e8f5e94..ab1b0f5c09 100644 --- a/src/Sentry/ScopeExtensions.cs +++ b/src/Sentry/ScopeExtensions.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.IO; using Sentry.Extensibility; -using System.Linq; using Sentry.Internal; using Sentry.Internal.Extensions; using Sentry.Protocol; @@ -16,178 +15,6 @@ namespace Sentry [EditorBrowsable(EditorBrowsableState.Never)] public static class ScopeExtensions { - /// - /// Whether a has been set to the scope with any of its fields non null. - /// - /// - /// True if a User was set to the scope. Otherwise, false. - public static bool HasUser(this IEventLike eventLike) - => eventLike.User.Email is not null - || eventLike.User.Id is not null - || eventLike.User.Username is not null - || eventLike.User.InternalOther?.Count > 0 - || eventLike.User.IpAddress is not null; - -#if HAS_VALUE_TUPLE - /// - /// Adds a breadcrumb to the scope. - /// - /// The scope. - /// The message. - /// The category. - /// The type. - /// The data key-value pair. - /// The level. - public static void AddBreadcrumb( - this IEventLike eventLike, - string message, - string? category, - string? type, - (string, string)? dataPair = null, - BreadcrumbLevel level = default) - { - // Not to throw on code that ignores nullability warnings. - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (eventLike is null) - { - return; - } - - Dictionary? data = null; - - if (dataPair != null) - { - data = new Dictionary - { - {dataPair.Value.Item1, dataPair.Value.Item2} - }; - } - - eventLike.AddBreadcrumb( - null, - message, - category, - type, - data, - level); - } -#endif - - /// - /// Adds a breadcrumb to the scope. - /// - /// The scope. - /// The message. - /// The category. - /// The type. - /// The data. - /// The level. - public static void AddBreadcrumb( - this IEventLike eventLike, - string message, - string? category = null, - string? type = null, - Dictionary? data = null, - BreadcrumbLevel level = default) - { - // Not to throw on code that ignores nullability warnings. - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (eventLike is null) - { - return; - } - - eventLike.AddBreadcrumb( - null, - message, - category, - type, - data, - level); - } - - /// - /// Adds a breadcrumb to the scope. - /// - /// - /// This overload is used for testing. - /// - /// The scope. - /// The timestamp - /// The message. - /// The category. - /// The type. - /// The data - /// The level. - [EditorBrowsable(EditorBrowsableState.Never)] - public static void AddBreadcrumb( - this IEventLike eventLike, - DateTimeOffset? timestamp, - string message, - string? category = null, - string? type = null, - IReadOnlyDictionary? data = null, - BreadcrumbLevel level = default) - { - // Not to throw on code that ignores nullability warnings. - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (eventLike is null) - { - return; - } - - eventLike.AddBreadcrumb(new Breadcrumb( - timestamp, - message, - type, - data, - category, - level)); - } - - /// - /// Sets the fingerprint to the . - /// - /// The scope. - /// The fingerprint. - public static void SetFingerprint(this IEventLike eventLike, IEnumerable fingerprint) - => eventLike.Fingerprint = fingerprint as IReadOnlyList ?? fingerprint.ToArray(); - - /// - /// Sets the extra key-value pairs to the object. - /// - public static void SetExtras(this IHasExtra hasExtra, IEnumerable> values) - { - foreach (var (key, value) in values) - { - hasExtra.SetExtra(key, value); - } - } - - /// - /// Set all items as tags. - /// - public static void SetTags(this IHasTags hasTags, IEnumerable> tags) - { - foreach (var (key, value) in tags) - { - hasTags.SetTag(key, value); - } - } - - /// - /// Removes a tag from the . - /// - /// The scope. - /// - public static void UnsetTag(this IEventLike eventLike, string key) - { - if (eventLike.Tags is IDictionary tags) - { - tags.Remove(key); - } - } - /// /// Invokes all event processor providers available. /// diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 9029004b25..89dd6e23bf 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Sentry.Extensibility; using Sentry.Internal; @@ -24,9 +24,6 @@ public class SentryClient : ISentryClient, IDisposable private volatile bool _disposed; private readonly SentryOptions _options; - private readonly Lazy _random = new(() => new Random(), LazyThreadSafetyMode.PublicationOnly); - internal Random Random => _random.Value; - // Internal for testing. internal IBackgroundWorker Worker { get; } @@ -115,33 +112,33 @@ public void CaptureTransaction(Transaction transaction) if (transaction.SpanId.Equals(SpanId.Empty)) { - _options.DiagnosticLogger?.LogWarning("Transaction dropped due to empty id."); + _options.DiagnosticLogger?.LogWarning( + "Transaction dropped due to empty id." + ); + return; } if (string.IsNullOrWhiteSpace(transaction.Name) || string.IsNullOrWhiteSpace(transaction.Operation)) { - _options.DiagnosticLogger?.LogWarning("Transaction discarded due to one or more required fields missing."); - return; - } + _options.DiagnosticLogger?.LogWarning( + "Transaction discarded due to one or more required fields missing." + ); - // A transaction may have already been sampled somehow or the - // field may have been set directly. To be safe, we check that. - if (!transaction.IsSampled) - { - _options.DiagnosticLogger?.LogDebug("Transaction dropped due to sampling."); return; } - if (_options.TracesSampleRate < 1) + // Sampling decision MUST have been made at this point + Debug.Assert(transaction.IsSampled != null, "Attempt to capture transaction without sampling decision."); + + if (transaction.IsSampled != true) { - if (Random.NextDouble() > _options.TracesSampleRate) - { - transaction.IsSampled = false; - _options.DiagnosticLogger?.LogDebug("Transaction dropped due to random sampling."); - return; - } + _options.DiagnosticLogger?.LogDebug( + "Transaction dropped by sampling." + ); + + return; } CaptureEnvelope(Envelope.FromTransaction(transaction)); @@ -159,7 +156,7 @@ private SentryId DoSendEvent(SentryEvent @event, Scope? scope) { if (_options.SampleRate != null) { - if (Random.NextDouble() > _options.SampleRate.Value) + if (SynchronizedRandom.NextDouble() > _options.SampleRate.Value) { _options.DiagnosticLogger?.LogDebug("Event sampled."); return SentryId.Empty; diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 58c9998ac9..4e7463e63a 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -419,9 +419,11 @@ public double TracesSampleRate } /// - /// Custom logic that determines trace sample rate depending on the context. + /// Custom delegate that returns sample rate dynamically for a specific transaction context. + /// The delegate may also return null to fallback to default sample rate as + /// defined by the property. /// - public ISentryTraceSampler? TracesSampler { get; set; } + public Func? TracesSampler { get; set; } /// /// ATTENTION: This option will change how issues are grouped in Sentry! diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 8b822b320c..d38ae20693 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -315,6 +315,22 @@ public static void CaptureUserFeedback(SentryId eventId, string email, string co public static void CaptureTransaction(Transaction transaction) => _hub.CaptureTransaction(transaction); + /// + /// Starts a transaction. + /// + [DebuggerStepThrough] + public static ITransaction StartTransaction( + ITransactionContext context, + IReadOnlyDictionary customSamplingContext) + => _hub.StartTransaction(context, customSamplingContext); + + /// + /// Starts a transaction. + /// + [DebuggerStepThrough] + public static ITransaction StartTransaction(ITransactionContext context) + => _hub.StartTransaction(context); + /// /// Starts a transaction. /// diff --git a/src/Sentry/TraceSamplingContext.cs b/src/Sentry/TraceSamplingContext.cs deleted file mode 100644 index 99860a5ded..0000000000 --- a/src/Sentry/TraceSamplingContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Sentry.Protocol; - -namespace Sentry -{ - /// - /// Trace sampling context. - /// - public class TraceSamplingContext - { - /// - /// Span. - /// - public ISpan Span { get; } - - /// - /// Span's parent. - /// - public ISpan? ParentSpan { get; } - - /// - /// Initializes an instance of . - /// - /// - /// - public TraceSamplingContext(ISpan span, ISpan? parentSpan = null) - { - Span = span; - ParentSpan = parentSpan; - } - } -} diff --git a/src/Sentry/TransactionSamplingContext.cs b/src/Sentry/TransactionSamplingContext.cs new file mode 100644 index 0000000000..2f5c85908d --- /dev/null +++ b/src/Sentry/TransactionSamplingContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Sentry.Protocol; + +namespace Sentry +{ + /// + /// Context used by a dynamic sampler to determine whether a transaction should be sampled. + /// + public class TransactionSamplingContext + { + /// + /// Transaction context. + /// + public ITransactionContext TransactionContext { get; } + + /// + /// Custom data used for sampling. + /// + public IReadOnlyDictionary CustomSamplingContext { get; } + + /// + /// Initializes an instance of . + /// + public TransactionSamplingContext( + ITransactionContext transactionContext, + IReadOnlyDictionary customSamplingContext) + { + TransactionContext = transactionContext; + CustomSamplingContext = customSamplingContext; + } + } +} diff --git a/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs b/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs index 719684ab57..b912434b85 100644 --- a/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs +++ b/test/Sentry.Log4Net.Tests/SentryAppenderTests.cs @@ -2,6 +2,7 @@ using log4net; using log4net.Core; using NSubstitute; +using Sentry.Protocol; using Sentry.Reflection; using Xunit; diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index dbb9aaf600..f78fa3ae13 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -7,6 +7,7 @@ using Sentry; using Sentry.Extensibility; using Sentry.Internal; +using Sentry.Protocol; using Sentry.Protocol.Envelopes; using Xunit; @@ -144,7 +145,7 @@ public void CaptureMessage_SuccessQueued_LastEventIdSetToReturnedId() } [Fact] - public void StartTransaction_Works() + public void StartTransaction_NameOpDescription_Works() { // Arrange var hub = new Hub(new SentryOptions @@ -160,5 +161,149 @@ public void StartTransaction_Works() transaction.Operation.Should().Be("operation"); transaction.Description.Should().Be("description"); } + + [Fact] + public void StartTransaction_StaticSampling_SampledIn() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampleRate = 1 + }); + + // Act + var transaction = hub.StartTransaction("name", "operation"); + + // Assert + transaction.IsSampled.Should().BeTrue(); + } + + [Fact] + public void StartTransaction_StaticSampling_SampledOut() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampleRate = 0 + }); + + // Act + var transaction = hub.StartTransaction("name", "operation"); + + // Assert + transaction.IsSampled.Should().BeFalse(); + } + + [Fact] + public void StartTransaction_DynamicSampling_SampledIn() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampler = ctx => ctx.TransactionContext.Name == "foo" ? 1 : 0 + }); + + // Act + var transaction = hub.StartTransaction("foo", "op"); + + // Assert + transaction.IsSampled.Should().BeTrue(); + } + + [Fact] + public void StartTransaction_DynamicSampling_SampledOut() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampler = ctx => ctx.TransactionContext.Name == "foo" ? 1 : 0 + }); + + // Act + var transaction = hub.StartTransaction("bar", "op"); + + // Assert + transaction.IsSampled.Should().BeFalse(); + } + + [Fact] + public void StartTransaction_DynamicSampling_WithCustomContext_SampledIn() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampler = ctx => ctx.CustomSamplingContext.GetValueOrDefault("xxx") as string == "zzz" ? 1 : 0 + }); + + // Act + var transaction = hub.StartTransaction( + new TransactionContext("foo", "op"), + new Dictionary {["xxx"] = "zzz"} + ); + + // Assert + transaction.IsSampled.Should().BeTrue(); + } + + [Fact] + public void StartTransaction_DynamicSampling_WithCustomContext_SampledOut() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampler = ctx => ctx.CustomSamplingContext.GetValueOrDefault("xxx") as string == "zzz" ? 1 : 0 + }); + + // Act + var transaction = hub.StartTransaction( + new TransactionContext("foo", "op"), + new Dictionary {["xxx"] = "yyy"} + ); + + // Assert + transaction.IsSampled.Should().BeFalse(); + } + + [Fact] + public void StartTransaction_DynamicSampling_FallbackToStatic_SampledIn() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampler = _ => null, + TracesSampleRate = 1 + }); + + // Act + var transaction = hub.StartTransaction("foo", "bar"); + + // Assert + transaction.IsSampled.Should().BeTrue(); + } + + [Fact] + public void StartTransaction_DynamicSampling_FallbackToStatic_SampledOut() + { + // Arrange + var hub = new Hub(new SentryOptions + { + Dsn = DsnSamples.ValidDsnWithSecret, + TracesSampler = _ => null, + TracesSampleRate = 0 + }); + + // Act + var transaction = hub.StartTransaction("foo", "bar"); + + // Assert + transaction.IsSampled.Should().BeFalse(); + } } } diff --git a/test/Sentry.Tests/Protocol/Context/TraceTests.cs b/test/Sentry.Tests/Protocol/Context/TraceTests.cs index a6a4fd139f..aabc467926 100644 --- a/test/Sentry.Tests/Protocol/Context/TraceTests.cs +++ b/test/Sentry.Tests/Protocol/Context/TraceTests.cs @@ -15,7 +15,7 @@ public void Ctor_NoPropertyFilled_SerializesEmptyObject() var actual = trace.ToJsonString(); // Assert - Assert.Equal("{\"type\":\"trace\",\"sampled\":true}", actual); + Assert.Equal("{\"type\":\"trace\"}", actual); } [Fact] diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs index c84cc48ae3..f24ef486bd 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -126,6 +126,45 @@ public void StartChild_LevelTwo_Works() grandChild.ParentSpanId.Should().Be(child.SpanId); } + [Fact] + public void StartChild_SamplingInherited_Null() + { + // Arrange + var transaction = new Transaction(DisabledHub.Instance, "my name", "my op") {IsSampled = null}; + + // Act + var child = transaction.StartChild("child op", "child desc"); + + // Assert + child.IsSampled.Should().BeNull(); + } + + [Fact] + public void StartChild_SamplingInherited_True() + { + // Arrange + var transaction = new Transaction(DisabledHub.Instance, "my name", "my op") {IsSampled = true}; + + // Act + var child = transaction.StartChild("child op", "child desc"); + + // Assert + child.IsSampled.Should().BeTrue(); + } + + [Fact] + public void StartChild_SamplingInherited_False() + { + // Arrange + var transaction = new Transaction(DisabledHub.Instance, "my name", "my op") {IsSampled = false}; + + // Act + var child = transaction.StartChild("child op", "child desc"); + + // Assert + child.IsSampled.Should().BeFalse(); + } + [Fact] public void Finish_RecordsTime() { diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index b97a9e6c0d..85a380d0bc 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -342,70 +342,22 @@ public void CaptureUserFeedback_DisposedClient_ThrowsObjectDisposedException() } [Fact] - public void CaptureTransaction_AlreadySampled_Drops() + public void CaptureTransaction_SampledOut_Dropped() { // Arrange var sut = _fixture.GetSut(); - var transaction = new Transaction( + // Act + sut.CaptureTransaction(new Transaction( sut, "test name", "test operation" - ); - - transaction.Contexts.Trace.IsSampled = false; - - // Act - sut.CaptureTransaction(transaction); - - // Assert - _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); - } - - [Fact] - public void CaptureTransaction_SamplingLowest_Drops() - { - // Arrange - var sut = _fixture.GetSut(); - - // Three decimal places longer than what Random returns. Should always drop - _fixture.SentryOptions.TracesSampleRate = 0.00000000000000000001; - - // Act - sut.CaptureTransaction( - new Transaction( - sut, - "test name", - "test operation" - ) - ); + ) {IsSampled = false}); // Assert _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); } - [Fact] - public void CaptureTransaction_SamplingHighest_Sends() - { - // Arrange - var sut = _fixture.GetSut(); - - // Three decimal places longer than what Random returns. Should always send - _fixture.SentryOptions.TracesSampleRate = 0.99999999999999999999; - - // Act - sut.CaptureTransaction( - new Transaction( - sut, - "test name", - "test operation" - ) - ); - - // Assert - _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); - } - [Fact] public void CaptureTransaction_ValidTransaction_Sent() { @@ -418,7 +370,7 @@ public void CaptureTransaction_ValidTransaction_Sent() sut, "test name", "test operation" - ) + ) {IsSampled = true} ); // Assert @@ -435,7 +387,7 @@ public void CaptureTransaction_NoSpanId_Ignored() sut, "test name", "test operation" - ); + ) {IsSampled = true}; transaction.Contexts.Trace.SpanId = SpanId.Empty; @@ -458,7 +410,7 @@ public void CaptureTransaction_NoName_Ignored() sut, null!, "test operation" - ) + ) {IsSampled = true} ); // Assert @@ -477,7 +429,7 @@ public void CaptureTransaction_NoOperation_Ignored() sut, "test name", null! - ) + ) {IsSampled = true} ); // Assert