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)