diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8049860b19..bc20ec278c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3 + uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 477c567677..6bd1a8a138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 5.2.0 + +### Features + +- Serilog scope properties are now sent with Sentry events ([#3976](https://github.com/getsentry/sentry-dotnet/pull/3976)) +- The sample seed used for sampling decisions is now propagated, for use in downstream custom trace samplers ([#3951](https://github.com/getsentry/sentry-dotnet/pull/3951)) +- Add Azure Function UseSentry overloads for easier wire ups ([#3971](https://github.com/getsentry/sentry-dotnet/pull/3971)) + +### Fixes + +- Fix mismapped breadcrumb levels coming in from native to dotnet SDK ([#3993](https://github.com/getsentry/sentry-dotnet/pull/3993)) + +### Dependencies + +- Bump CLI from v2.41.1 to v2.42.1 ([#3979](https://github.com/getsentry/sentry-dotnet/pull/3979)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2421) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.41.1...2.42.1) + ## 5.1.1 ### Fixes @@ -8,10 +26,9 @@ - Native SIGSEGV errors resulting from managed NullReferenceExceptions are now suppressed on Android ([#3903](https://github.com/getsentry/sentry-dotnet/pull/3903)) - OTel activities that are marked as not recorded are no longer sent to Sentry ([#3890](https://github.com/getsentry/sentry-dotnet/pull/3890)) - Fixed envelopes with oversized attachments getting stuck in __processing ([#3938](https://github.com/getsentry/sentry-dotnet/pull/3938)) -- Unknown stack frames in profiles on .NET 8+ ([#3942](https://github.com/getsentry/sentry-dotnet/pull/3942)) -- Deduplicate profiling stack frames ([#3941](https://github.com/getsentry/sentry-dotnet/pull/3941)) - OperatingSystem will now return macOS as OS name instead of 'Darwin' as well as the proper version. ([#2710](https://github.com/getsentry/sentry-dotnet/pull/3956)) - Ignore null value on CocoaScopeObserver.SetTag ([#3948](https://github.com/getsentry/sentry-dotnet/pull/3948)) +- Deduplicate profiling stack frames ([#3969](https://github.com/getsentry/sentry-dotnet/pull/3969)) ## 5.1.0 diff --git a/Directory.Build.props b/Directory.Build.props index d012b7c186..fdf535fdec 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 5.1.1 + 5.2.0 13 true true @@ -83,7 +83,7 @@ - 2.41.1 + 2.42.1 $(MSBuildThisFileDirectory)tools\sentry-cli\$(SentryCLIVersion)\ diff --git a/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerApplicationBuilderExtensions.cs b/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerApplicationBuilderExtensions.cs index d20d1880f7..250db84e34 100644 --- a/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerApplicationBuilderExtensions.cs +++ b/src/Sentry.Azure.Functions.Worker/SentryFunctionsWorkerApplicationBuilderExtensions.cs @@ -26,6 +26,20 @@ public static IFunctionsWorkerApplicationBuilder UseSentry(this IFunctionsWorker public static IFunctionsWorkerApplicationBuilder UseSentry(this IFunctionsWorkerApplicationBuilder builder, HostBuilderContext context, string dsn) => builder.UseSentry(context, o => o.Dsn = dsn); + /// + /// Uses Sentry integration. + /// + public static IFunctionsWorkerApplicationBuilder UseSentry( + this IFunctionsWorkerApplicationBuilder builder, + Action? optionsConfiguration) + { + if (builder is IHostApplicationBuilder appBuilder) + { + return builder.UseSentry(appBuilder.Configuration, optionsConfiguration); + } + throw new InvalidOperationException("Builder is not of type " + typeof(IHostApplicationBuilder)); + } + /// /// Uses Sentry integration. /// @@ -33,11 +47,21 @@ public static IFunctionsWorkerApplicationBuilder UseSentry( this IFunctionsWorkerApplicationBuilder builder, HostBuilderContext context, Action? optionsConfiguration) + => builder.UseSentry(context.Configuration, optionsConfiguration); + + + /// + /// Uses Sentry integration. + /// + public static IFunctionsWorkerApplicationBuilder UseSentry( + this IFunctionsWorkerApplicationBuilder builder, + IConfiguration configuration, + Action? optionsConfiguration) { builder.UseMiddleware(); var services = builder.Services; - var section = context.Configuration.GetSection("Sentry"); + var section = configuration.GetSection("Sentry"); #if NET8_0_OR_GREATER services.AddSingleton>(_ => new SentryAzureFunctionsOptionsSetup(section) diff --git a/src/Sentry.Profiling/SampleProfileBuilder.cs b/src/Sentry.Profiling/SampleProfileBuilder.cs index bdef768d85..56927be4a9 100644 --- a/src/Sentry.Profiling/SampleProfileBuilder.cs +++ b/src/Sentry.Profiling/SampleProfileBuilder.cs @@ -16,8 +16,12 @@ internal class SampleProfileBuilder // Output profile being built. public readonly SampleProfile Profile = new(); - // A sparse array that maps from StackSourceFrameIndex to an index in the output Profile.frames. - private readonly Dictionary _frameIndexes = new(); + // A sparse array that maps from CodeAddressIndex to an index in the output Profile.frames. + private readonly Dictionary _frameIndexesByCodeAddressIndex = new(); + + // A sparse array that maps from MethodIndex to an index in the output Profile.frames. + // This deduplicates frames that map to the same method but have a different CodeAddressIndex. + private readonly Dictionary _frameIndexesByMethodIndex = new(); // A dictionary from a CallStackIndex to an index in the output Profile.stacks. private readonly Dictionary _stackIndexes = new(); @@ -85,13 +89,14 @@ private int AddStackTrace(CallStackIndex callstackIndex) { var key = (int)callstackIndex; - if (!_stackIndexes.ContainsKey(key)) + if (!_stackIndexes.TryGetValue(key, out var value)) { Profile.Stacks.Add(CreateStackTrace(callstackIndex)); - _stackIndexes[key] = Profile.Stacks.Count - 1; + value = Profile.Stacks.Count - 1; + _stackIndexes[key] = value; } - return _stackIndexes[key]; + return value; } private Internal.GrowableArray CreateStackTrace(CallStackIndex callstackIndex) @@ -116,21 +121,71 @@ private Internal.GrowableArray CreateStackTrace(CallStackIndex callstackInd return stackTrace; } + private int PushNewFrame(SentryStackFrame frame) + { + Profile.Frames.Add(frame); + return Profile.Frames.Count - 1; + } + /// /// Check if the frame is already stored in the output Profile, or adds it. /// /// The index to the output Profile frames array. private int AddStackFrame(CodeAddressIndex codeAddressIndex) { - var key = (int)codeAddressIndex; + if (_frameIndexesByCodeAddressIndex.TryGetValue((int)codeAddressIndex, out var value)) + { + return value; + } - if (!_frameIndexes.ContainsKey(key)) + var methodIndex = _traceLog.CodeAddresses.MethodIndex(codeAddressIndex); + if (methodIndex != MethodIndex.Invalid) + { + value = AddStackFrame(methodIndex); + _frameIndexesByCodeAddressIndex[(int)codeAddressIndex] = value; + return value; + } + + // Fall back if the method info is unknown, see more info on Symbol resolution in + // https://github.com/getsentry/perfview/blob/031250ffb4f9fcadb9263525d6c9f274be19ca51/src/PerfView/SupportFiles/UsersGuide.htm#L7745-L7784 + if (_traceLog.CodeAddresses[codeAddressIndex] is { } codeAddressInfo) { - Profile.Frames.Add(CreateStackFrame(codeAddressIndex)); - _frameIndexes[key] = Profile.Frames.Count - 1; + var frame = new SentryStackFrame + { + InstructionAddress = (long?)codeAddressInfo.Address, + Module = codeAddressInfo.ModuleFile?.Name, + }; + frame.ConfigureAppFrame(_options); + + return _frameIndexesByCodeAddressIndex[(int)codeAddressIndex] = PushNewFrame(frame); } - return _frameIndexes[key]; + // If all else fails, it's a completely unknown frame. + // TODO check this - maybe we would be able to resolve it later in the future? + return PushNewFrame(new SentryStackFrame { InApp = false }); + } + + /// + /// Check if the frame is already stored in the output Profile, or adds it. + /// + /// The index to the output Profile frames array. + private int AddStackFrame(MethodIndex methodIndex) + { + if (_frameIndexesByMethodIndex.TryGetValue((int)methodIndex, out var value)) + { + return value; + } + + var method = _traceLog.CodeAddresses.Methods[methodIndex]; + + var frame = new SentryStackFrame + { + Function = method.FullMethodName, + Module = method.MethodModuleFile?.Name + }; + frame.ConfigureAppFrame(_options); + + return _frameIndexesByMethodIndex[(int)methodIndex] = PushNewFrame(frame); } /// @@ -141,52 +196,17 @@ private int AddThread(TraceThread thread) { var key = (int)thread.ThreadIndex; - if (!_threadIndexes.ContainsKey(key)) + if (!_threadIndexes.TryGetValue(key, out var value)) { Profile.Threads.Add(new() { Name = thread.ThreadInfo ?? $"Thread {thread.ThreadID}", }); - _threadIndexes[key] = Profile.Threads.Count - 1; + value = Profile.Threads.Count - 1; + _threadIndexes[key] = value; _downsampler.NewThreadAdded(_threadIndexes[key]); } - return _threadIndexes[key]; - } - - private SentryStackFrame CreateStackFrame(CodeAddressIndex codeAddressIndex) - { - var frame = new SentryStackFrame(); - - var methodIndex = _traceLog.CodeAddresses.MethodIndex(codeAddressIndex); - if (_traceLog.CodeAddresses.Methods[methodIndex] is { } method) - { - frame.Function = method.FullMethodName; - - if (method.MethodModuleFile is { } moduleFile) - { - frame.Module = moduleFile.Name; - } - - frame.ConfigureAppFrame(_options); - } - else - { - // Fall back if the method info is unknown, see more info on Symbol resolution in - // https://github.com/getsentry/perfview/blob/031250ffb4f9fcadb9263525d6c9f274be19ca51/src/PerfView/SupportFiles/UsersGuide.htm#L7745-L7784 - frame.InstructionAddress = (long?)_traceLog.CodeAddresses.Address(codeAddressIndex); - - if (_traceLog.CodeAddresses.ModuleFile(codeAddressIndex) is { } moduleFile) - { - frame.Module = moduleFile.Name; - frame.ConfigureAppFrame(_options); - } - else - { - frame.InApp = false; - } - } - - return frame; + return value; } } diff --git a/src/Sentry.Profiling/SampleProfilerSession.cs b/src/Sentry.Profiling/SampleProfilerSession.cs index 3bb606a960..e54cf62020 100644 --- a/src/Sentry.Profiling/SampleProfilerSession.cs +++ b/src/Sentry.Profiling/SampleProfilerSession.cs @@ -17,14 +17,16 @@ internal class SampleProfilerSession : IDisposable private readonly IDiagnosticLogger? _logger; private readonly SentryStopwatch _stopwatch; private bool _stopped = false; + private Task _processing; - private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession session, TraceLogEventSource eventSource, IDiagnosticLogger? logger) + private SampleProfilerSession(SentryStopwatch stopwatch, EventPipeSession session, TraceLogEventSource eventSource, Task processing, IDiagnosticLogger? logger) { _session = session; _logger = logger; _eventSource = eventSource; _sampleEventParser = new SampleProfilerTraceEventParser(_eventSource); _stopwatch = stopwatch; + _processing = processing; } // Exposed only for benchmarks. @@ -86,7 +88,7 @@ public static SampleProfilerSession StartNew(IDiagnosticLogger? logger = null) var eventSource = TraceLog.CreateFromEventPipeSession(session, TraceLog.EventPipeRundownConfiguration.Enable(client)); // Process() blocks until the session is stopped so we need to run it on a separate thread. - Task.Factory.StartNew(eventSource.Process, TaskCreationOptions.LongRunning) + var processing = Task.Factory.StartNew(eventSource.Process, TaskCreationOptions.LongRunning) .ContinueWith(_ => { if (_.Exception?.InnerException is { } e) @@ -95,7 +97,7 @@ public static SampleProfilerSession StartNew(IDiagnosticLogger? logger = null) } }, TaskContinuationOptions.OnlyOnFaulted); - return new SampleProfilerSession(stopWatch, session, eventSource, logger); + return new SampleProfilerSession(stopWatch, session, eventSource, processing, logger); } catch (Exception ex) { @@ -128,6 +130,7 @@ public void Stop() { _stopped = true; _session.Stop(); + _processing.Wait(); _session.Dispose(); _eventSource.Dispose(); } diff --git a/src/Sentry.Serilog/SentryOptionExtensions.cs b/src/Sentry.Serilog/SentryOptionExtensions.cs new file mode 100644 index 0000000000..94dfbc5212 --- /dev/null +++ b/src/Sentry.Serilog/SentryOptionExtensions.cs @@ -0,0 +1,21 @@ +namespace Sentry.Serilog; + +/// +/// Extensions for to add Serilog specific configuration. +/// +public static class SentryOptionExtensions +{ + /// + /// Ensures Serilog scope properties get applied to Sentry events. If you are not initialising Sentry when + /// configuring the Sentry sink for Serilog then you should call this method in the options callback for whichever + /// Sentry integration you are using to initialise Sentry. + /// + /// + /// + /// + public static T ApplySerilogScopeToEvents(this T options) where T : SentryOptions + { + options.AddEventProcessor(new SerilogScopeEventProcessor(options)); + return options; + } +} diff --git a/src/Sentry.Serilog/SentrySinkExtensions.cs b/src/Sentry.Serilog/SentrySinkExtensions.cs index f023819a41..3eb5b56c3a 100644 --- a/src/Sentry.Serilog/SentrySinkExtensions.cs +++ b/src/Sentry.Serilog/SentrySinkExtensions.cs @@ -326,6 +326,14 @@ internal static void ConfigureSentrySerilogOptions( sentrySerilogOptions.DefaultTags.Add(tag.Key, tag.Value); } } + + // This only works when the SDK is initialized using the LoggerSinkConfiguration extensions. If the SDK is + // initialized using some other integration then the processor will need to be added manually to whichever + // options are used to initialize the SDK. + if (sentrySerilogOptions.InitializeSdk) + { + sentrySerilogOptions.ApplySerilogScopeToEvents(); + } } /// diff --git a/src/Sentry.Serilog/SerilogScopeEventProcessor.cs b/src/Sentry.Serilog/SerilogScopeEventProcessor.cs new file mode 100644 index 0000000000..4b3de83fe0 --- /dev/null +++ b/src/Sentry.Serilog/SerilogScopeEventProcessor.cs @@ -0,0 +1,57 @@ +using Serilog.Context; + +namespace Sentry.Serilog; + +/// +/// Sentry event processor that applies properties from the Serilog scope to Sentry events. +/// +internal class SerilogScopeEventProcessor : ISentryEventProcessor +{ + private readonly SentryOptions _options; + + /// + /// This processor extracts properties from the Serilog context and applies these to Sentry events. + /// + public SerilogScopeEventProcessor(SentryOptions options) + { + _options = options; + _options.LogDebug("Initializing Serilog scope event processor."); + } + + /// + public SentryEvent Process(SentryEvent @event) + { + _options.LogDebug("Running Serilog scope event processor on: Event {0}", @event.EventId); + + // This is a bit of a hack. Serilog doesn't have any hooks that let us inspect the context. We can, however, + // apply the context to a dummy log event and then copy across the properties from that log event to our Sentry + // event. + // See: https://github.com/getsentry/sentry-dotnet/issues/3544#issuecomment-2307884977 + var enricher = LogContext.Clone(); + var logEvent = new LogEvent(DateTimeOffset.Now, LogEventLevel.Error, null, MessageTemplate.Empty, []); + enricher.Enrich(logEvent, new LogEventPropertyFactory()); + foreach (var (key, value) in logEvent.Properties) + { + if (!@event.Tags.ContainsKey(key)) + { + // Potentially we could be doing SetData here instead of SetTag. See DefaultSentryScopeStateProcessor. + @event.SetTag( + key, + value is ScalarValue { Value: string stringValue } + ? stringValue + : value.ToString() + ); + } + } + return @event; + } + + private class LogEventPropertyFactory : ILogEventPropertyFactory + { + public LogEventProperty CreateProperty(string name, object? value, bool destructureObjects = false) + { + var scalarValue = new ScalarValue(value); + return new LogEventProperty(name, scalarValue); + } + } +} diff --git a/src/Sentry/BaggageHeader.cs b/src/Sentry/BaggageHeader.cs index 83fe9f0a84..66846a1fcd 100644 --- a/src/Sentry/BaggageHeader.cs +++ b/src/Sentry/BaggageHeader.cs @@ -26,7 +26,7 @@ private BaggageHeader(IEnumerable> members) => // We can safely return a dictionary of Sentry members, as we are in control over the keys added. // Just to be safe though, we'll group by key and only take the first of each one. - internal IReadOnlyDictionary GetSentryMembers() => + internal Dictionary GetSentryMembers() => Members .Where(kvp => kvp.Key.StartsWith(SentryKeyPrefix)) .GroupBy(kvp => kvp.Key, kvp => kvp.Value) diff --git a/src/Sentry/Breadcrumb.cs b/src/Sentry/Breadcrumb.cs index e7d447edf6..b3b58606b0 100644 --- a/src/Sentry/Breadcrumb.cs +++ b/src/Sentry/Breadcrumb.cs @@ -157,8 +157,18 @@ public static Breadcrumb FromJson(JsonElement json) var type = json.GetPropertyOrNull("type")?.GetString(); var data = json.GetPropertyOrNull("data")?.GetStringDictionaryOrNull(); var category = json.GetPropertyOrNull("category")?.GetString(); - var level = json.GetPropertyOrNull("level")?.GetString()?.ParseEnum() ?? default; + var levelString = json.GetPropertyOrNull("level")?.GetString(); + var level = levelString?.ToUpper() switch + { + "DEBUG" => BreadcrumbLevel.Debug, + "INFO" => BreadcrumbLevel.Info, + "WARNING" => BreadcrumbLevel.Warning, + "ERROR" => BreadcrumbLevel.Error, + "CRITICAL" => BreadcrumbLevel.Critical, + "FATAL" => BreadcrumbLevel.Critical, + _ => default + }; return new Breadcrumb(timestamp, message, type, data!, category, level); } } diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index 26056d17cc..e85ec88b84 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -1,3 +1,4 @@ +using Sentry.Internal; using Sentry.Internal.Extensions; namespace Sentry; @@ -24,6 +25,7 @@ private DynamicSamplingContext( string publicKey, bool? sampled, double? sampleRate = null, + double? sampleRand = null, string? release = null, string? environment = null, string? transactionName = null) @@ -31,20 +33,25 @@ private DynamicSamplingContext( // Validate and set required values if (traceId == SentryId.Empty) { - throw new ArgumentOutOfRangeException(nameof(traceId)); + throw new ArgumentOutOfRangeException(nameof(traceId), "cannot be empty"); } if (string.IsNullOrWhiteSpace(publicKey)) { - throw new ArgumentException(default, nameof(publicKey)); + throw new ArgumentException("cannot be empty", nameof(publicKey)); } if (sampleRate is < 0.0 or > 1.0) { - throw new ArgumentOutOfRangeException(nameof(sampleRate)); + throw new ArgumentOutOfRangeException(nameof(sampleRate), "Arg invalid if < 0.0 or > 1.0"); } - var items = new Dictionary(capacity: 7) + if (sampleRand is < 0.0 or >= 1.0) + { + throw new ArgumentOutOfRangeException(nameof(sampleRand), "Arg invalid if < 0.0 or >= 1.0"); + } + + var items = new Dictionary(capacity: 8) { ["trace_id"] = traceId.ToString(), ["public_key"] = publicKey, @@ -61,6 +68,11 @@ private DynamicSamplingContext( items.Add("sample_rate", sampleRate.Value.ToString(CultureInfo.InvariantCulture)); } + if (sampleRand is not null) + { + items.Add("sample_rand", sampleRand.Value.ToString("N4", CultureInfo.InvariantCulture)); + } + if (!string.IsNullOrWhiteSpace(release)) { items.Add("release", release); @@ -99,7 +111,7 @@ private DynamicSamplingContext( return null; } - if (items.TryGetValue("sampled", out var sampledString) && !bool.TryParse(sampledString, out _)) + if (items.TryGetValue("sampled", out var sampledString) && !bool.TryParse(sampledString, out var sampled)) { return null; } @@ -111,6 +123,27 @@ private DynamicSamplingContext( return null; } + // See https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value + if (items.TryGetValue("sample_rand", out var sampleRand)) + { + if (!double.TryParse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture, out var rand) || + rand is < 0.0 or >= 1.0) + { + return null; + } + } + else + { + var rand = SampleRandHelper.GenerateSampleRand(traceId); + if (!string.IsNullOrEmpty(sampledString)) + { + // Ensure sample_rand is consistent with the sampling decision that has already been made + rand = bool.Parse(sampledString) + ? rand * rate // 0 <= sampleRand < rate + : rate + (1 - rate) * rand; // rate < sampleRand < 1 + } + items.Add("sample_rand", rand.ToString("N4", CultureInfo.InvariantCulture)); + } return new DynamicSamplingContext(items); } @@ -121,6 +154,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra var traceId = transaction.TraceId; var sampled = transaction.IsSampled; var sampleRate = transaction.SampleRate!.Value; + var sampleRand = transaction.SampleRand; var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null; // These two may not have been set yet on the transaction, but we can get them directly. @@ -132,6 +166,7 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra publicKey, sampled, sampleRate, + sampleRand, release, environment, transactionName); diff --git a/src/Sentry/Internal/FnvHash.cs b/src/Sentry/Internal/FnvHash.cs new file mode 100644 index 0000000000..10281e90bd --- /dev/null +++ b/src/Sentry/Internal/FnvHash.cs @@ -0,0 +1,43 @@ +namespace Sentry.Internal; + +/// +/// FNV is a non-cryptographic hash. +/// +/// See https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV_hash_parameters +/// +/// +/// We use a struct to avoid heap allocations. +/// +internal struct FnvHash +{ + public FnvHash() + { + } + + private const int Offset = unchecked((int)2166136261); + private const int Prime = 16777619; + + private int HashCode { get; set; } = Offset; + + private void Combine(byte data) + { + unchecked + { + HashCode ^= data; + HashCode *= Prime; + } + } + + private static int ComputeHash(byte[] data) + { + var result = new FnvHash(); + foreach (var b in data) + { + result.Combine(b); + } + + return result.HashCode; + } + + public static int ComputeHash(string data) => ComputeHash(Encoding.UTF8.GetBytes(data)); +} diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index b90f4ca603..a564ab1791 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -127,7 +127,12 @@ internal ITransactionTracer StartTransaction( IReadOnlyDictionary customSamplingContext, DynamicSamplingContext? dynamicSamplingContext) { - var transaction = new TransactionTracer(this, context); + var transaction = new TransactionTracer(this, context) + { + SampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var sampleRand) ?? false + ? double.Parse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture) + : SampleRandHelper.GenerateSampleRand(context.TraceId.ToString()) + }; // If the hub is disabled, we will always sample out. In other words, starting a transaction // after disposing the hub will result in that transaction not being sent to Sentry. @@ -151,7 +156,7 @@ internal ITransactionTracer StartTransaction( if (tracesSampler(samplingContext) is { } sampleRate) { - transaction.IsSampled = _randomValuesFactory.NextBool(sampleRate); + transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate); transaction.SampleRate = sampleRate; } } @@ -160,7 +165,7 @@ internal ITransactionTracer StartTransaction( if (transaction.IsSampled == null) { var sampleRate = _options.TracesSampleRate ?? 0.0; - transaction.IsSampled = _randomValuesFactory.NextBool(sampleRate); + transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate); transaction.SampleRate = sampleRate; } diff --git a/src/Sentry/Internal/SampleRandHelper.cs b/src/Sentry/Internal/SampleRandHelper.cs new file mode 100644 index 0000000000..5e420c70f8 --- /dev/null +++ b/src/Sentry/Internal/SampleRandHelper.cs @@ -0,0 +1,15 @@ +namespace Sentry.Internal; + +internal static class SampleRandHelper +{ + internal static double GenerateSampleRand(string traceId) + => new Random(FnvHash.ComputeHash(traceId)).NextDouble(); + + internal static bool IsSampled(double sampleRand, double rate) => rate switch + { + >= 1 => true, + <= 0 => false, + _ => sampleRand < rate + }; + +} diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index 2a12b41a38..2799c161c5 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -120,13 +120,13 @@ <_OSArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) - - - - - - - + + + + + + + diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index e8059a2d00..c4da3d5933 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -100,6 +100,8 @@ internal set /// public double? SampleRate { get; internal set; } + internal double? SampleRand { get; set; } + /// public SentryLevel? Level { get; set; } diff --git a/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs index 3c2d440c7f..13b34e7385 100644 --- a/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/SentryTracingMiddlewareTests.cs @@ -1,4 +1,3 @@ -#if NETCOREAPP3_1_OR_GREATER using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -285,6 +284,7 @@ public async Task Baggage_header_propagates_to_outbound_requests(bool shouldProp const string incomingBaggage = "sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " + "sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " + + "sentry-sample_rand=0.1234, " + "sentry-sample_rate=0.5, " + "foo-bar=abc123"; @@ -299,6 +299,7 @@ public async Task Baggage_header_propagates_to_outbound_requests(bool shouldProp "other-value=abc123, " + "sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " + "sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " + + "sentry-sample_rand=0.1234, " + "sentry-sample_rate=0.5"; } else @@ -382,6 +383,7 @@ public async Task Baggage_header_sets_dynamic_sampling_context() const string baggage = "sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8, " + "sentry-public_key=d4d82fc1c2c4032a83f3a29aa3a3aff, " + + "sentry-sample_rand=0.1234, " + "sentry-sample_rate=0.5"; // Arrange @@ -677,5 +679,3 @@ public async Task Transaction_TransactionNameProviderSetUnset_TransactionNameSet transaction.NameSource.Should().Be(TransactionNameSource.Url); } } - -#endif diff --git a/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index f6955863c0..f6ee752df8 100644 --- a/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -7,6 +7,8 @@ public static class SentryFunctionsWorkerApplicationBuilderExtensions { public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action? optionsConfiguration) { } public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, System.Action? optionsConfiguration) { } public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, string dsn) { } } diff --git a/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index f6955863c0..f6ee752df8 100644 --- a/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Azure.Functions.Worker.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -7,6 +7,8 @@ public static class SentryFunctionsWorkerApplicationBuilderExtensions { public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, System.Action? optionsConfiguration) { } + public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action? optionsConfiguration) { } public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, System.Action? optionsConfiguration) { } public static Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder UseSentry(this Microsoft.Azure.Functions.Worker.IFunctionsWorkerApplicationBuilder builder, Microsoft.Extensions.Hosting.HostBuilderContext context, string dsn) { } } diff --git a/test/Sentry.Profiling.Tests/TraceLogProcessorTests.ProfileInfo_Serialization_Works.verified.txt b/test/Sentry.Profiling.Tests/TraceLogProcessorTests.ProfileInfo_Serialization_Works.verified.txt index 714859c86d..481fcf2e42 100644 --- a/test/Sentry.Profiling.Tests/TraceLogProcessorTests.ProfileInfo_Serialization_Works.verified.txt +++ b/test/Sentry.Profiling.Tests/TraceLogProcessorTests.ProfileInfo_Serialization_Works.verified.txt @@ -107,11 +107,12 @@ 36 ], [ - 37 + 17 ], [ 0, 1, + 37, 38, 39, 40, @@ -119,35 +120,37 @@ 42, 43, 44, - 45, - 46, + 21, 22 ], [ + 45, + 46, 47, 48, 49, 50, - 51, - 52, - 37 + 17 ], [ + 51, + 52, 53, 54, 55, - 56, - 57, - 37 + 17 ], [ + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ + 60, + 61, + 62, 63, 64, 65, @@ -155,48 +158,55 @@ 67, 68, 69, + 56, + 57, + 58, + 59, + 17 + ], + [ 70, 71, 72, + 73, + 74, + 69, + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ - 73, - 74, 75, - 76, - 77, - 78, + 74, + 69, + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ - 79, - 80, - 78, + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ + 76, + 77, + 78, + 79, + 80, 81, + 82, + 83, 59, - 60, - 61, - 62 + 17 ], [ - 82, - 83, 84, 85, 86, @@ -204,31 +214,22 @@ 88, 89, 90, - 62 - ], - [ 91, 92, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - 62 + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 59, + 17 ], [ 0, 1, + 37, 38, 39, 40, @@ -236,15 +237,14 @@ 42, 43, 44, - 45, - 46, + 21, 22 ], [ 18, 19, 20, - 101, + 21, 22 ] ], @@ -340,7 +340,6 @@ in_app: true }, { - in_app: false, instruction_addr: 0x7ff947fd8378 }, { @@ -433,11 +432,6 @@ module: System.Private.CoreLib.il, in_app: false }, - { - function: Aura.UI.Gallery.NetCore.Program.Main(class System.String[]), - module: Aura.UI.Gallery.NetCore, - in_app: true - }, { function: System.IO.Stream+<>c.b__40_0(class System.Object), module: System.Private.CoreLib.il, @@ -478,11 +472,6 @@ module: System.Private.CoreLib.il, in_app: false }, - { - function: System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart(), - module: System.Private.CoreLib.il, - in_app: false - }, { function: System.Collections.Concurrent.ConcurrentDictionary`2[System.__Canon,System.IntPtr].TryAddInternal(!0,value class System.Nullable`1,!1,bool,bool,!1&), module: System.Collections.Concurrent.il, @@ -509,7 +498,6 @@ in_app: true }, { - in_app: false, instruction_addr: 0x7ff94be7aed3 }, { @@ -557,11 +545,6 @@ module: Avalonia.Controls, in_app: true }, - { - function: Aura.UI.Gallery.NetCore.Program.Main(class System.String[]), - module: Aura.UI.Gallery.NetCore, - in_app: true - }, { function: Avalonia.Animation.Animation..cctor(), module: Avalonia.Animation, @@ -637,26 +620,11 @@ module: Avalonia.Controls, in_app: true }, - { - function: Avalonia.Controls.Primitives.TemplatedControl..cctor(), - module: Avalonia.Controls, - in_app: true - }, { function: System.Reactive.Linq.Observable.Merge(class System.IObservable`1[]), module: system.reactive, in_app: false }, - { - function: Avalonia.Controls.TextBlock..cctor(), - module: Avalonia.Controls, - in_app: true - }, - { - function: Avalonia.Controls.Window..cctor(), - module: Avalonia.Controls, - in_app: true - }, { function: Avalonia.Win32.Win32Platform.SetDpiAwareness(), module: avalonia.win32, @@ -697,11 +665,6 @@ module: Avalonia.Controls, in_app: true }, - { - function: Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(!!0,class System.String[],value class Avalonia.Controls.ShutdownMode), - module: Avalonia.Controls, - in_app: true - }, { function: System.Drawing.SafeNativeMethods+Gdip..cctor(), module: system.drawing.common, @@ -738,23 +701,12 @@ in_app: true }, { - in_app: false, instruction_addr: 0x7ffa0d468281 }, { function: Avalonia.Win32.Win32Platform.CreateMessageWindow(), module: avalonia.win32, in_app: true - }, - { - function: Avalonia.Win32.Win32Platform..ctor(), - module: avalonia.win32, - in_app: true - }, - { - function: System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart(), - module: System.Private.CoreLib.il, - in_app: false } ], samples: [ @@ -1070,4 +1022,4 @@ } ] } -} \ No newline at end of file +} diff --git a/test/Sentry.Profiling.Tests/TraceLogProcessorTests.Profile_Serialization_Works.verified.txt b/test/Sentry.Profiling.Tests/TraceLogProcessorTests.Profile_Serialization_Works.verified.txt index 6eb35f3c22..7720ab6755 100644 --- a/test/Sentry.Profiling.Tests/TraceLogProcessorTests.Profile_Serialization_Works.verified.txt +++ b/test/Sentry.Profiling.Tests/TraceLogProcessorTests.Profile_Serialization_Works.verified.txt @@ -84,11 +84,12 @@ 36 ], [ - 37 + 17 ], [ 0, 1, + 37, 38, 39, 40, @@ -96,35 +97,37 @@ 42, 43, 44, - 45, - 46, + 21, 22 ], [ + 45, + 46, 47, 48, 49, 50, - 51, - 52, - 37 + 17 ], [ + 51, + 52, 53, 54, 55, - 56, - 57, - 37 + 17 ], [ + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ + 60, + 61, + 62, 63, 64, 65, @@ -132,48 +135,55 @@ 67, 68, 69, + 56, + 57, + 58, + 59, + 17 + ], + [ 70, 71, 72, + 73, + 74, + 69, + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ - 73, - 74, 75, - 76, - 77, - 78, + 74, + 69, + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ - 79, - 80, - 78, + 56, + 57, 58, 59, - 60, - 61, - 62 + 17 ], [ + 76, + 77, + 78, + 79, + 80, 81, + 82, + 83, 59, - 60, - 61, - 62 + 17 ], [ - 82, - 83, 84, 85, 86, @@ -181,31 +191,22 @@ 88, 89, 90, - 62 - ], - [ 91, 92, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 84, - 85, - 86, - 87, - 88, - 89, - 90, - 62 + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 59, + 17 ], [ 0, 1, + 37, 38, 39, 40, @@ -213,15 +214,14 @@ 42, 43, 44, - 45, - 46, + 21, 22 ], [ 18, 19, 20, - 101, + 21, 22 ] ], @@ -317,7 +317,6 @@ in_app: true }, { - in_app: false, instruction_addr: 0x7ff947fd8378 }, { @@ -410,11 +409,6 @@ module: System.Private.CoreLib.il, in_app: false }, - { - function: Aura.UI.Gallery.NetCore.Program.Main(class System.String[]), - module: Aura.UI.Gallery.NetCore, - in_app: true - }, { function: System.IO.Stream+<>c.b__40_0(class System.Object), module: System.Private.CoreLib.il, @@ -455,11 +449,6 @@ module: System.Private.CoreLib.il, in_app: false }, - { - function: System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart(), - module: System.Private.CoreLib.il, - in_app: false - }, { function: System.Collections.Concurrent.ConcurrentDictionary`2[System.__Canon,System.IntPtr].TryAddInternal(!0,value class System.Nullable`1,!1,bool,bool,!1&), module: System.Collections.Concurrent.il, @@ -486,7 +475,6 @@ in_app: true }, { - in_app: false, instruction_addr: 0x7ff94be7aed3 }, { @@ -534,11 +522,6 @@ module: Avalonia.Controls, in_app: true }, - { - function: Aura.UI.Gallery.NetCore.Program.Main(class System.String[]), - module: Aura.UI.Gallery.NetCore, - in_app: true - }, { function: Avalonia.Animation.Animation..cctor(), module: Avalonia.Animation, @@ -614,26 +597,11 @@ module: Avalonia.Controls, in_app: true }, - { - function: Avalonia.Controls.Primitives.TemplatedControl..cctor(), - module: Avalonia.Controls, - in_app: true - }, { function: System.Reactive.Linq.Observable.Merge(class System.IObservable`1[]), module: system.reactive, in_app: false }, - { - function: Avalonia.Controls.TextBlock..cctor(), - module: Avalonia.Controls, - in_app: true - }, - { - function: Avalonia.Controls.Window..cctor(), - module: Avalonia.Controls, - in_app: true - }, { function: Avalonia.Win32.Win32Platform.SetDpiAwareness(), module: avalonia.win32, @@ -674,11 +642,6 @@ module: Avalonia.Controls, in_app: true }, - { - function: Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(!!0,class System.String[],value class Avalonia.Controls.ShutdownMode), - module: Avalonia.Controls, - in_app: true - }, { function: System.Drawing.SafeNativeMethods+Gdip..cctor(), module: system.drawing.common, @@ -715,23 +678,12 @@ in_app: true }, { - in_app: false, instruction_addr: 0x7ffa0d468281 }, { function: Avalonia.Win32.Win32Platform.CreateMessageWindow(), module: avalonia.win32, in_app: true - }, - { - function: Avalonia.Win32.Win32Platform..ctor(), - module: avalonia.win32, - in_app: true - }, - { - function: System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart(), - module: System.Private.CoreLib.il, - in_app: false } ], samples: [ @@ -1046,4 +998,4 @@ stack_id: 18 } ] -} \ No newline at end of file +} diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 893bc8a67e..1455bbc51b 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -1,6 +1,11 @@ [assembly: System.CLSCompliant(true)] namespace Sentry.Serilog { + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } public class SentrySerilogOptions : Sentry.SentryOptions { public SentrySerilogOptions() { } diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 893bc8a67e..1455bbc51b 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -1,6 +1,11 @@ [assembly: System.CLSCompliant(true)] namespace Sentry.Serilog { + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } public class SentrySerilogOptions : Sentry.SentryOptions { public SentrySerilogOptions() { } diff --git a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 893bc8a67e..1455bbc51b 100644 --- a/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Serilog.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -1,6 +1,11 @@ [assembly: System.CLSCompliant(true)] namespace Sentry.Serilog { + public static class SentryOptionExtensions + { + public static T ApplySerilogScopeToEvents(this T options) + where T : Sentry.SentryOptions { } + } public class SentrySerilogOptions : Sentry.SentryOptions { public SentrySerilogOptions() { } diff --git a/test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs b/test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs new file mode 100644 index 0000000000..378849dc9f --- /dev/null +++ b/test/Sentry.Serilog.Tests/SerilogScopeEventProcessorTests.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; + +namespace Sentry.Serilog.Tests; + +public class SerilogScopeEventProcessorTests +{ + [Theory] + [InlineData("42", "42")] + [InlineData(42, "42")] + public void Emit_WithException_CreatesEventWithException(object value, string expected) + { + // Arrange + var options = new SentryOptions(); + var sut = new SerilogScopeEventProcessor(options); + + using var log = new LoggerConfiguration().CreateLogger(); + var factory = new LoggerFactory().AddSerilog(log); + var logger = factory.CreateLogger(); + + // Act + SentryEvent evt; + using (logger.BeginScope(new Dictionary { ["Answer"] = value })) + { + evt = new SentryEvent(); + sut.Process(evt); + } + + // Assert + evt.Tags.Should().ContainKey("Answer"); + evt.Tags["Answer"].Should().Be(expected); + } +} diff --git a/test/Sentry.Testing/VerifyExtensions.cs b/test/Sentry.Testing/VerifyExtensions.cs index 7027580f9c..c3b3a1585f 100644 --- a/test/Sentry.Testing/VerifyExtensions.cs +++ b/test/Sentry.Testing/VerifyExtensions.cs @@ -11,6 +11,7 @@ public static SettingsTask IgnoreStandardSentryMembers(this SettingsTask setting return settings .ScrubMachineName() .ScrubUserName() + .ScrubMember("sample_rand") .AddExtraSettings(_ => { _.Converters.Add(new SpansConverter()); diff --git a/test/Sentry.Tests/DynamicSamplingContextTests.cs b/test/Sentry.Tests/DynamicSamplingContextTests.cs index b23a1c4962..374be0c371 100644 --- a/test/Sentry.Tests/DynamicSamplingContextTests.cs +++ b/test/Sentry.Tests/DynamicSamplingContextTests.cs @@ -142,6 +142,105 @@ public void CreateFromBaggage_SampleRate_TooHigh() Assert.Null(dsc); } + [Fact] + public void CreateFromBaggage_SampleRand_Invalid() + { + var baggage = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "1.0"}, + {"sentry-sample_rand", "not-a-number"}, + }); + + var dsc = baggage.CreateDynamicSamplingContext(); + + Assert.Null(dsc); + } + + [Fact] + public void CreateFromBaggage_SampleRand_TooLow() + { + var baggage = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "1.0"}, + {"sentry-sample_rand", "-0.1"} + }); + + var dsc = baggage.CreateDynamicSamplingContext(); + + Assert.Null(dsc); + } + + [Fact] + public void CreateFromBaggage_SampleRand_TooHigh() + { + var baggage = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "1.0"}, + {"sentry-sample_rand", "1.0"} // Must be less than 1 + }); + + var dsc = baggage.CreateDynamicSamplingContext(); + + Assert.Null(dsc); + } + + [Fact] + public void CreateFromBaggage_NotSampledNoSampleRand_GeneratesSampleRand() + { + var baggage = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "0.5"} + }); + + var dsc = baggage.CreateDynamicSamplingContext(); + + using var scope = new AssertionScope(); + Assert.NotNull(dsc); + var sampleRandItem = Assert.Contains("sample_rand", dsc.Items); + var sampleRand = double.Parse(sampleRandItem, NumberStyles.Float, CultureInfo.InvariantCulture); + Assert.True(sampleRand >= 0.0); + Assert.True(sampleRand < 1.0); + } + + [Theory] + [InlineData("true")] + [InlineData("false")] + public void CreateFromBaggage_SampledNoSampleRand_GeneratesConsistentSampleRand(string sampled) + { + var baggage = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "0.5"}, + {"sentry-sampled", sampled}, + }); + + var dsc = baggage.CreateDynamicSamplingContext(); + + using var scope = new AssertionScope(); + Assert.NotNull(dsc); + var sampleRandItem = Assert.Contains("sample_rand", dsc.Items); + var sampleRand = double.Parse(sampleRandItem, NumberStyles.Float, CultureInfo.InvariantCulture); + if (sampled == "true") + { + Assert.True(sampleRand >= 0.0); + Assert.True(sampleRand < 0.5); + } + else + { + Assert.True(sampleRand >= 0.5); + Assert.True(sampleRand < 1.0); + } + } + [Fact] public void CreateFromBaggage_Sampled_MalFormed() { @@ -171,10 +270,11 @@ public void CreateFromBaggage_Valid_Minimum() var dsc = baggage.CreateDynamicSamplingContext(); Assert.NotNull(dsc); - Assert.Equal(3, dsc.Items.Count); + Assert.Equal(4, dsc.Items.Count); Assert.Equal("43365712692146d08ee11a729dfbcaca", Assert.Contains("trace_id", dsc.Items)); Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); Assert.Equal("1.0", Assert.Contains("sample_rate", dsc.Items)); + Assert.Contains("sample_rand", dsc.Items); } [Fact] @@ -186,6 +286,7 @@ public void CreateFromBaggage_Valid_Complete() {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, {"sentry-sampled", "true"}, {"sentry-sample_rate", "1.0"}, + {"sentry-sample_rand", "0.1234"}, {"sentry-release", "test@1.0.0+abc"}, {"sentry-environment", "production"}, {"sentry-user_segment", "Group B"}, @@ -200,6 +301,7 @@ public void CreateFromBaggage_Valid_Complete() Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); Assert.Equal("true", Assert.Contains("sampled", dsc.Items)); Assert.Equal("1.0", Assert.Contains("sample_rate", dsc.Items)); + Assert.Equal("0.1234", Assert.Contains("sample_rand", dsc.Items)); Assert.Equal("test@1.0.0+abc", Assert.Contains("release", dsc.Items)); Assert.Equal("production", Assert.Contains("environment", dsc.Items)); Assert.Equal("Group B", Assert.Contains("user_segment", dsc.Items)); @@ -214,6 +316,7 @@ public void ToBaggageHeader() {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, {"sentry-sample_rate", "1.0"}, + {"sentry-sample_rand", "0.1234"}, {"sentry-release", "test@1.0.0+abc"}, {"sentry-environment", "production"}, {"sentry-user_segment", "Group B"}, @@ -253,6 +356,7 @@ public void CreateFromTransaction(bool? isSampled) NameSource = TransactionNameSource.Route, IsSampled = isSampled, SampleRate = 0.5, + SampleRand = (isSampled ?? true) ? 0.4000 : 0.6000, // Lower than the sample rate means sampled == true User = { }, @@ -261,7 +365,7 @@ public void CreateFromTransaction(bool? isSampled) var dsc = transaction.CreateDynamicSamplingContext(options); Assert.NotNull(dsc); - Assert.Equal(isSampled.HasValue ? 7 : 6, dsc.Items.Count); + Assert.Equal(isSampled.HasValue ? 8 : 7, dsc.Items.Count); Assert.Equal(traceId.ToString(), Assert.Contains("trace_id", dsc.Items)); Assert.Equal("d4d82fc1c2c4032a83f3a29aa3a3aff", Assert.Contains("public_key", dsc.Items)); if (transaction.IsSampled is { } sampled) @@ -273,6 +377,7 @@ public void CreateFromTransaction(bool? isSampled) Assert.DoesNotContain("sampled", dsc.Items); } Assert.Equal("0.5", Assert.Contains("sample_rate", dsc.Items)); + Assert.Equal((isSampled ?? true) ? "0.4000" : "0.6000", Assert.Contains("sample_rand", dsc.Items)); Assert.Equal("foo@2.4.5", Assert.Contains("release", dsc.Items)); Assert.Equal("staging", Assert.Contains("environment", dsc.Items)); Assert.Equal("GET /person/{id}", Assert.Contains("transaction", dsc.Items)); diff --git a/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt b/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt index af9d84138f..a39b4497ea 100644 --- a/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt +++ b/test/Sentry.Tests/EventProcessorTests.WithTransaction.verified.txt @@ -9,6 +9,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_2, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt index 34439cfbcc..c510d50d43 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet8_0.verified.txt @@ -31,6 +31,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_3, @@ -146,6 +147,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_3, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt index 34439cfbcc..c510d50d43 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.DotNet9_0.verified.txt @@ -31,6 +31,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_3, @@ -146,6 +147,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_3, diff --git a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt index b453712534..f41bcaa626 100644 --- a/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt +++ b/test/Sentry.Tests/HubTests.CaptureEvent_ActiveTransaction_UnhandledExceptionTransactionEndedAsCrashed.Net4_8.verified.txt @@ -31,6 +31,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_3, @@ -146,6 +147,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_3, diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index 17206a5c4f..b00cc89e7f 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -680,6 +680,136 @@ public void StartTransaction_SameInstrumenter_SampledIn() transaction.IsSampled.Should().BeTrue(); } + [Fact] + public void StartTransaction_NoDynamicSamplingContext_GeneratesSampleRand() + { + // Arrange + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.SampleRand.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext!.Items.Should().ContainKey("sample_rand"); + transactionTracer.DynamicSamplingContext.Items["sample_rand"].Should().Be(transactionTracer.SampleRand!.Value.ToString("N4", CultureInfo.InvariantCulture)); + } + + [Fact] + public void StartTransaction_DynamicSamplingContextWithoutSampleRand_SampleRandNotPropagated() + { + // Arrange + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext, DynamicSamplingContext.Empty); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.SampleRand.Should().NotBeNull(); + transactionTracer.DynamicSamplingContext.Should().NotBeNull(); + // See https://develop.sentry.dev/sdk/telemetry/traces/dynamic-sampling-context/#freezing-dynamic-sampling-context + transactionTracer.DynamicSamplingContext!.Items.Should().NotContainKey("sample_rand"); + } + + [Fact] + public void StartTransaction_DynamicSamplingContextWithSampleRand_InheritsSampleRand() + { + // Arrange + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + var dsc = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sampled", "true"}, + {"sentry-sample_rate", "0.5"}, // Required in the baggage header, but ignored by sampling logic + {"sentry-sample_rand", "0.1234"} + }).CreateDynamicSamplingContext(); + + _fixture.Options.TracesSampleRate = 0.4; + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext, dsc); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(true); + transactionTracer.SampleRate.Should().Be(0.4); + transactionTracer.SampleRand.Should().Be(0.1234); + transactionTracer.DynamicSamplingContext.Should().Be(dsc); + } + + [Theory] + [InlineData(0.1, false)] + [InlineData(0.2, true)] + public void StartTransaction_TraceSampler_UsesSampleRand(double sampleRate, bool expectedIsSampled) + { + // Arrange + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + var dsc = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sampled", "true"}, + {"sentry-sample_rate", "0.5"}, + {"sentry-sample_rand", "0.1234"} + }).CreateDynamicSamplingContext(); + + _fixture.Options.TracesSampler = _ => sampleRate; + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext, dsc); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(expectedIsSampled); + transactionTracer.SampleRate.Should().Be(sampleRate); + transactionTracer.SampleRand.Should().Be(0.1234); + transactionTracer.DynamicSamplingContext.Should().Be(dsc); + } + + [Theory] + [InlineData(0.1, false)] + [InlineData(0.2, true)] + public void StartTransaction_StaticSampler_UsesSampleRand(double sampleRate, bool expectedIsSampled) + { + // Arrange + var transactionContext = new TransactionContext("name", "operation"); + var customContext = new Dictionary(); + var dsc = BaggageHeader.Create(new List> + { + {"sentry-trace_id", "43365712692146d08ee11a729dfbcaca"}, + {"sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff"}, + {"sentry-sample_rate", "0.5"}, // Static sampling ignores this and uses options.TracesSampleRate instead + {"sentry-sample_rand", "0.1234"} + }).CreateDynamicSamplingContext(); + + _fixture.Options.TracesSampleRate = sampleRate; + var hub = _fixture.GetSut(); + + // Act + var transaction = hub.StartTransaction(transactionContext, customContext, dsc); + + // Assert + var transactionTracer = ((TransactionTracer)transaction); + transactionTracer.IsSampled.Should().Be(expectedIsSampled); + transactionTracer.SampleRate.Should().Be(sampleRate); + transactionTracer.SampleRand.Should().Be(0.1234); + transactionTracer.DynamicSamplingContext.Should().Be(dsc); + } + [Fact] public void StartTransaction_DifferentInstrumenter_SampledIn() { diff --git a/test/Sentry.Tests/Internals/FnvHashTests.cs b/test/Sentry.Tests/Internals/FnvHashTests.cs new file mode 100644 index 0000000000..757882c3de --- /dev/null +++ b/test/Sentry.Tests/Internals/FnvHashTests.cs @@ -0,0 +1,24 @@ +namespace Sentry.Tests.Internals; + +public class FnvHashTests +{ + [Theory] + [InlineData("", 2_166_136_261)] + [InlineData("h", 3_977_000_791)] + [InlineData("he", 1_547_363_254)] + [InlineData("hel", 179_613_742)] + [InlineData("hell", 477_198_310)] + [InlineData("hello", 1_335_831_723)] + [InlineData("hello ", 3_801_292_497)] + [InlineData("hello w", 1_402_552_146)] + [InlineData("hello wo", 3_611_200_775)] + [InlineData("hello wor", 1_282_977_583)] + [InlineData("hello worl", 2_767_971_961)] + [InlineData("hello world", 3_582_672_807)] + public void ComputeHash_WithString_ReturnsExpected(string input, uint expected) + { + var actual = FnvHash.ComputeHash(input); + + Assert.Equal(unchecked((int)expected), actual); + } +} diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs index 89dfa87957..dc658d2ed9 100644 --- a/test/Sentry.Tests/SentryPropagationContextTests.cs +++ b/test/Sentry.Tests/SentryPropagationContextTests.cs @@ -83,6 +83,7 @@ public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWith var baggageHeader = BaggageHeader.Create(new List> { { "sentry-sample_rate", "1.0" }, + { "sentry-sample_rand", "0.1234" }, { "sentry-trace_id", "75302ac48a024bde9a3b3734a82e36c8" }, { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" }, { "sentry-replay_id", "bfd31b89a59d41c99d96dc2baf840ecd" } @@ -90,6 +91,6 @@ public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWith var propagationContext = SentryPropagationContext.CreateFromHeaders(null, traceHeader, baggageHeader); - Assert.Equal(4, propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions()).Items.Count); + Assert.Equal(5, propagationContext.GetOrCreateDynamicSamplingContext(new SentryOptions()).Items.Count); } } diff --git a/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt index 6ffc1f77f4..1c801c9b72 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Discard.verified.txt @@ -9,6 +9,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_2, diff --git a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt index b8a5f7a62b..0e9cc2a73b 100644 --- a/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt +++ b/test/Sentry.Tests/TransactionProcessorTests.Simple.verified.txt @@ -9,6 +9,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_2, @@ -53,6 +54,7 @@ environment: production, public_key: d4d82fc1c2c4032a83f3a29aa3a3aff, release: release, + sample_rand: {Scrubbed}, sample_rate: 1, sampled: true, trace_id: Guid_2,