diff --git a/CHANGELOG.md b/CHANGELOG.md index 844ba4106e..654821f034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Emit transaction.data inside contexts.trace.data ([#3936](https://github.com/getsentry/sentry-dotnet/pull/3936)) - 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)) diff --git a/src/Sentry/IHasData.cs b/src/Sentry/IHasData.cs new file mode 100644 index 0000000000..980f403731 --- /dev/null +++ b/src/Sentry/IHasData.cs @@ -0,0 +1,17 @@ +namespace Sentry; + +/// +/// Implemented by objects that contain a map of untyped additional metadata. +/// +public interface IHasData +{ + /// + /// An arbitrary mapping of additional metadata to store with the event. + /// + IReadOnlyDictionary Data { get; } + + /// + /// Sets an extra. + /// + void SetData(string key, object? value); +} diff --git a/src/Sentry/ISpanData.cs b/src/Sentry/ISpanData.cs index 373ff75c34..3a475102b8 100644 --- a/src/Sentry/ISpanData.cs +++ b/src/Sentry/ISpanData.cs @@ -5,7 +5,7 @@ namespace Sentry; /// /// Immutable data belonging to a span. /// -public interface ISpanData : ITraceContext, IHasTags, IHasExtra +public interface ISpanData : ITraceContext, IHasData, IHasTags, IHasExtra { /// /// Start timestamp. diff --git a/src/Sentry/Internal/NoOpSpan.cs b/src/Sentry/Internal/NoOpSpan.cs index babecc4a4d..f6e49e5a2c 100644 --- a/src/Sentry/Internal/NoOpSpan.cs +++ b/src/Sentry/Internal/NoOpSpan.cs @@ -19,6 +19,7 @@ protected NoOpSpan() public bool? IsSampled => default; public IReadOnlyDictionary Tags => ImmutableDictionary.Empty; public IReadOnlyDictionary Extra => ImmutableDictionary.Empty; + public IReadOnlyDictionary Data => ImmutableDictionary.Empty; public DateTimeOffset StartTimestamp => default; public DateTimeOffset? EndTimestamp => default; public bool IsFinished => default; @@ -71,6 +72,10 @@ public void SetExtra(string key, object? value) { } + public void SetData(string key, object? value) + { + } + public SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty; public IReadOnlyDictionary Measurements => ImmutableDictionary.Empty; diff --git a/src/Sentry/Protocol/Trace.cs b/src/Sentry/Protocol/Trace.cs index 289b2a4b08..8878aab170 100644 --- a/src/Sentry/Protocol/Trace.cs +++ b/src/Sentry/Protocol/Trace.cs @@ -50,6 +50,21 @@ internal set /// public bool? IsSampled { get; internal set; } + private Dictionary _data = new(); + + /// + /// Get the metadata + /// + public IReadOnlyDictionary Data => _data; + + /// + /// Adds metadata to the trace + /// + /// + /// + public void SetData(string key, object? value) + => _data[key] = value; + /// /// Clones this instance. /// @@ -63,7 +78,8 @@ internal set Operation = Operation, Origin = Origin, Status = Status, - IsSampled = IsSampled + IsSampled = IsSampled, + _data = _data.ToDict() }; /// @@ -103,6 +119,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteString("origin", Origin ?? Internal.OriginHelper.Manual); writer.WriteStringIfNotWhiteSpace("description", Description); writer.WriteStringIfNotWhiteSpace("status", Status?.ToString().ToSnakeCase()); + writer.WriteDictionaryIfNotEmpty("data", _data, logger); writer.WriteEndObject(); } @@ -120,6 +137,7 @@ public static Trace FromJson(JsonElement json) var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Replace("_", "").ParseEnum(); var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean(); + var data = json.GetPropertyOrNull("data")?.GetDictionaryOrNull() ?? new(); return new Trace { @@ -130,7 +148,8 @@ public static Trace FromJson(JsonElement json) Origin = origin, Description = description, Status = status, - IsSampled = isSampled + IsSampled = isSampled, + _data = data }; } } diff --git a/src/Sentry/SentrySpan.cs b/src/Sentry/SentrySpan.cs index 9031556c7d..e94ca99074 100644 --- a/src/Sentry/SentrySpan.cs +++ b/src/Sentry/SentrySpan.cs @@ -66,15 +66,26 @@ public void UnsetTag(string key) => (_tags ??= new Dictionary()).Remove(key); // Aka 'data' - private Dictionary? _extra; private readonly MetricsSummary? _metricsSummary; + + private Dictionary? _data; + + /// + public IReadOnlyDictionary Data => + _data ??= new Dictionary(); + + /// + public void SetData(string key, object? value) => + (_data ??= new Dictionary())[key] = value; + /// - public IReadOnlyDictionary Extra => _extra ??= new Dictionary(); + [Obsolete("Use SetData")] + public IReadOnlyDictionary Extra => Data; /// - public void SetExtra(string key, object? value) => - (_extra ??= new Dictionary())[key] = value; + [Obsolete("Use Data")] + public void SetExtra(string key, object? value) => SetData(key, value); /// /// Initializes an instance of . @@ -100,7 +111,7 @@ public SentrySpan(ISpan tracer) Description = tracer.Description; Status = tracer.Status; IsSampled = tracer.IsSampled; - _extra = tracer.Extra.ToDict(); + _data = tracer.Data.ToDict(); if (tracer is SpanTracer spanTracer) { @@ -138,7 +149,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteString("start_timestamp", StartTimestamp); writer.WriteStringIfNotNull("timestamp", EndTimestamp); writer.WriteStringDictionaryIfNotEmpty("tags", _tags!); - writer.WriteDictionaryIfNotEmpty("data", _extra!, logger); + writer.WriteDictionaryIfNotEmpty("data", _data!, logger); writer.WriteDictionaryIfNotEmpty("measurements", _measurements, logger); writer.WriteSerializableIfNotNull("_metrics_summary", _metricsSummary, logger); @@ -173,7 +184,7 @@ public static SentrySpan FromJson(JsonElement json) Status = status, IsSampled = isSampled, _tags = tags!, - _extra = data!, + _data = data!, _measurements = measurements, }; } diff --git a/src/Sentry/SentryTransaction.cs b/src/Sentry/SentryTransaction.cs index b69acd3f52..386891ec01 100644 --- a/src/Sentry/SentryTransaction.cs +++ b/src/Sentry/SentryTransaction.cs @@ -179,12 +179,6 @@ public IReadOnlyList Fingerprint /// public IReadOnlyCollection Breadcrumbs => _breadcrumbs; - // Not readonly because of deserialization - private Dictionary _extra = new(); - - /// - public IReadOnlyDictionary Extra => _extra; - // Not readonly because of deserialization private Dictionary _tags = new(); @@ -270,7 +264,6 @@ public SentryTransaction(ITransactionTracer tracer) Sdk = tracer.Sdk; Fingerprint = tracer.Fingerprint; _breadcrumbs = tracer.Breadcrumbs.ToList(); - _extra = tracer.Extra.ToDict(); _tags = tracer.Tags.ToDict(); _spans = FromTracerSpans(tracer); @@ -339,8 +332,20 @@ public void AddBreadcrumb(Breadcrumb breadcrumb) => _breadcrumbs.Add(breadcrumb); /// + public IReadOnlyDictionary Data => _contexts.Trace.Data; + + /// + [Obsolete("Use Data")] + public IReadOnlyDictionary Extra => _contexts.Trace.Data; + + /// + [Obsolete("Use SetData")] public void SetExtra(string key, object? value) => - _extra[key] = value; + SetData(key, value); + + /// + public void SetData(string key, object? value) => + _contexts.Trace.SetData(key, value); /// public void SetTag(string key, string value) => @@ -401,7 +406,6 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteSerializable("sdk", Sdk, logger); writer.WriteStringArrayIfNotEmpty("fingerprint", _fingerprint); writer.WriteArrayIfNotEmpty("breadcrumbs", _breadcrumbs, logger); - writer.WriteDictionaryIfNotEmpty("extra", _extra, logger); writer.WriteStringDictionaryIfNotEmpty("tags", _tags!); writer.WriteArrayIfNotEmpty("spans", _spans, logger); writer.WriteDictionaryIfNotEmpty("measurements", _measurements, logger); @@ -459,7 +463,6 @@ public static SentryTransaction FromJson(JsonElement json) Sdk = sdk, _fingerprint = fingerprint, _breadcrumbs = breadcrumbs, - _extra = extra, _tags = tags, _measurements = measurements, _spans = spans diff --git a/src/Sentry/SpanTracer.cs b/src/Sentry/SpanTracer.cs index 9dbbbc4b96..ed51d48eaa 100644 --- a/src/Sentry/SpanTracer.cs +++ b/src/Sentry/SpanTracer.cs @@ -85,10 +85,17 @@ public void UnsetTag(string key) => private readonly ConcurrentDictionary _data = new(); /// - public IReadOnlyDictionary Extra => _data; + public IReadOnlyDictionary Data => _data; /// - public void SetExtra(string key, object? value) => _data[key] = value; + public void SetData(string key, object? value) => + _data[key] = value; + + /// + public IReadOnlyDictionary Extra => Data; + + /// + public void SetExtra(string key, object? value) => SetData(key, value); internal Func? IsFiltered { get; set; } diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index 69376ad71f..e8059a2d00 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -157,10 +157,15 @@ public IReadOnlyList Fingerprint /// public IReadOnlyCollection Breadcrumbs => _breadcrumbs; - private readonly ConcurrentDictionary _extra = new(); + private readonly ConcurrentDictionary _data = new(); /// - public IReadOnlyDictionary Extra => _extra; + [Obsolete("Use Data")] + public IReadOnlyDictionary Extra => _data; + + /// + public IReadOnlyDictionary Data => _data; + private readonly ConcurrentDictionary _tags = new(); @@ -270,7 +275,11 @@ internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idle public void AddBreadcrumb(Breadcrumb breadcrumb) => _breadcrumbs.Add(breadcrumb); /// - public void SetExtra(string key, object? value) => _extra[key] = value; + [Obsolete("Use SetData")] + public void SetExtra(string key, object? value) => _data[key] = value; + + /// + public void SetData(string key, object? value) => _data[key] = value; /// public void SetTag(string key, string value) => _tags[key] = value; diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 597e275c15..4e0872ada1 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -207,7 +207,7 @@ public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() "category", BreadcrumbLevel.Warning)); - transaction.SetExtra("extra_key", "extra_value"); + transaction.SetData("extra_key", "extra_value"); transaction.Fingerprint = new[] { "fingerprint" }; transaction.SetTag("tag_key", "tag_value"); transaction.SetMeasurement("measurement_1", 111); diff --git a/test/Sentry.Tests/SerializationTests.cs b/test/Sentry.Tests/SerializationTests.cs new file mode 100644 index 0000000000..51a57bdd34 --- /dev/null +++ b/test/Sentry.Tests/SerializationTests.cs @@ -0,0 +1,50 @@ +using System.Text.Json.Nodes; + +namespace Sentry.Tests; + +public partial class SerializationTests +{ + private readonly IDiagnosticLogger _testOutputLogger; + + public SerializationTests(ITestOutputHelper output) + { + _testOutputLogger = new TestOutputDiagnosticLogger(output); + } + + [Fact] + public void Serialization_TransactionAndSpanData() + { + var hub = Substitute.For(); + var context = new TransactionContext("name", "operation", new SentryTraceHeader(SentryId.Empty, SpanId.Empty, false)); + var transactionTracer = new TransactionTracer(hub, context); + var span = transactionTracer.StartChild("childop"); + span.SetData("span1", "value1"); + + var transaction = new SentryTransaction(transactionTracer) + { + IsSampled = false + }; + transaction.SetData("transaction1", "transaction_value"); + var json = transaction.ToJsonString(_testOutputLogger); + _testOutputLogger.LogDebug(json); + + var node = JsonNode.Parse(json); + var dataNode = node?["contexts"]?["trace"]?["data"]?["transaction1"]?.GetValue(); + dataNode.Should().NotBeNull("contexts.trace.data.transaction1 not found"); + dataNode.Should().Be("transaction_value"); + + var spansNode = node?["spans"]?.AsArray(); + spansNode.Should().NotBeNull("spans not found"); + var spanDataNode = spansNode!.FirstOrDefault()?["data"]?["span1"]?.GetValue(); + spanDataNode.Should().NotBeNull("spans.data not found"); + spanDataNode.Should().Be("value1"); + + // verify deserialization + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + var el = JsonElement.ParseValue(ref reader); + var backTransaction = SentryTransaction.FromJson(el); + + backTransaction.Spans.First().Data["span1"].Should().Be("value1", "Span value missing"); + backTransaction.Contexts.Trace.Data["transaction1"].Should().Be("transaction_value", "Transaction value missing"); + } +} diff --git a/test/Sentry.Tests/SerializationTests.verify.cs b/test/Sentry.Tests/SerializationTests.verify.cs index 86a671cb3c..65b08857fa 100644 --- a/test/Sentry.Tests/SerializationTests.verify.cs +++ b/test/Sentry.Tests/SerializationTests.verify.cs @@ -4,15 +4,8 @@ namespace Sentry.Tests; -public class SerializationTests +public partial class SerializationTests { - private readonly IDiagnosticLogger _testOutputLogger; - - public SerializationTests(ITestOutputHelper output) - { - _testOutputLogger = new TestOutputDiagnosticLogger(output); - } - [Theory] [MemberData(nameof(GetData))] public async Task Serialization(string name, object target)