From 1f3cd4a174e9bae29081172b0409047d58d481db Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 May 2026 19:08:01 +0200 Subject: [PATCH] fix(dart): Add span v2 envelope metadata Include the span v2 payload version and automatic ingest settings when building pre-encoded span envelope items. Co-Authored-By: GPT-5.5 Co-authored-by: Cursor --- packages/dart/lib/src/sentry_envelope.dart | 48 ++++++++++++-- packages/dart/test/sentry_envelope_test.dart | 67 +++++++++++++++++++- 2 files changed, 108 insertions(+), 7 deletions(-) diff --git a/packages/dart/lib/src/sentry_envelope.dart b/packages/dart/lib/src/sentry_envelope.dart index 54a53f2294..e2088c2a3d 100644 --- a/packages/dart/lib/src/sentry_envelope.dart +++ b/packages/dart/lib/src/sentry_envelope.dart @@ -17,6 +17,14 @@ class SentryEnvelope { SentryEnvelope(this.header, this.items, {this.containsUnhandledException = false}); + static const _spanV2PayloadProperties = { + 'version': 2, + 'ingest_settings': { + 'infer_ip': 'auto', + 'infer_user_agent': 'auto', + }, + }; + /// Header describing envelope content. final SentryEnvelopeHeader header; @@ -127,7 +135,9 @@ class SentryEnvelope { dsn: dsn, traceContext: traceContext), [ SentryEnvelopeItem.fromSpansData( - _buildItemsPayload(encodedSpans), encodedSpans.length) + _buildItemsPayload(encodedSpans, + additionalTopLevelProperties: _spanV2PayloadProperties), + encodedSpans.length) ], ); @@ -177,13 +187,41 @@ class SentryEnvelope { } } - /// Builds a payload in the format {"items": [item1, item2, ...]} - static Uint8List _buildItemsPayload(List> encodedItems) { + /// Builds a payload with optional top-level properties and an items array. + /// + /// [additionalTopLevelProperties] are serialized before `items` and are used + /// for signal-specific envelope item metadata, such as the span v2 `version` + /// and `ingest_settings` fields. + static Uint8List _buildItemsPayload( + List> encodedItems, { + Map additionalTopLevelProperties = const {}, + }) { final builder = BytesBuilder(copy: false); - builder.add(utf8.encode('{"items":[')); + final comma = utf8.encode(','); + final colon = utf8.encode(':'); + + builder.add(utf8.encode('{')); + + var needsComma = false; + void addCommaIfNeeded() { + if (needsComma) { + builder.add(comma); + } + needsComma = true; + } + + for (final entry in additionalTopLevelProperties.entries) { + addCommaIfNeeded(); + builder.add(utf8JsonEncoder.convert(entry.key)); + builder.add(colon); + builder.add(utf8JsonEncoder.convert(entry.value)); + } + + addCommaIfNeeded(); + builder.add(utf8.encode('"items":[')); for (int i = 0; i < encodedItems.length; i++) { if (i > 0) { - builder.add(utf8.encode(',')); + builder.add(comma); } builder.add(encodedItems[i]); } diff --git a/packages/dart/test/sentry_envelope_test.dart b/packages/dart/test/sentry_envelope_test.dart index b17c735ea2..4a48a96a35 100644 --- a/packages/dart/test/sentry_envelope_test.dart +++ b/packages/dart/test/sentry_envelope_test.dart @@ -27,6 +27,12 @@ void main() { return utf8.decode(expectedItem); } + Future> decodedItemPayload( + SentryEnvelope envelope) async { + final data = await envelope.items.single.dataFactory(); + return jsonDecode(utf8.decode(data)) as Map; + } + test('serialize', () async { final eventId = SentryId.newId(); @@ -214,8 +220,11 @@ void main() { expect(sut.items.length, 1); final expectedEnvelopeItem = SentryEnvelopeItem.fromSpansData( - // The envelope should create the final payload with {"items": [...]} wrapper - utf8.encode('{"items":[') + + utf8.encode( + '{"version":2,' + '"ingest_settings":{"infer_ip":"auto","infer_user_agent":"auto"},' + '"items":[', + ) + span1 + utf8.encode(',') + span2 + @@ -233,6 +242,26 @@ void main() { expect(actualItem, expectedItem); }); + test('fromSpansData includes span v2 payload metadata', () async { + final encodedSpans = [ + utf8.encode('{"span_id":"span-id"}'), + ]; + final sdkVersion = + SdkVersion(name: 'fixture-name', version: 'fixture-version'); + final sut = SentryEnvelope.fromSpansData(encodedSpans, sdkVersion); + + final payload = await decodedItemPayload(sut); + + expect(payload['version'], 2); + expect(payload['ingest_settings'], { + 'infer_ip': 'auto', + 'infer_user_agent': 'auto', + }); + expect(payload['items'], [ + {'span_id': 'span-id'}, + ]); + }); + test('fromLogsData', () async { final encodedLogs = [ utf8.encode( @@ -269,6 +298,23 @@ void main() { expect(actualItem, expectedItem); }); + test('fromLogsData keeps a plain items payload', () async { + final encodedLogs = [ + utf8.encode('{"message":"log"}'), + ]; + final sdkVersion = + SdkVersion(name: 'fixture-name', version: 'fixture-version'); + final sut = SentryEnvelope.fromLogsData(encodedLogs, sdkVersion); + + final payload = await decodedItemPayload(sut); + + expect(payload, { + 'items': [ + {'message': 'log'}, + ], + }); + }); + test('fromMetricsData creates envelope with wrapped metrics payload', () async { final encodedMetrics = [ @@ -300,6 +346,23 @@ void main() { expect(actualItem, expectedPayload); }); + test('fromMetricsData keeps a plain items payload', () async { + final encodedMetrics = [ + utf8.encode('{"name":"metric","value":1}'), + ]; + final sdkVersion = + SdkVersion(name: 'fixture-name', version: 'fixture-version'); + final sut = SentryEnvelope.fromMetricsData(encodedMetrics, sdkVersion); + + final payload = await decodedItemPayload(sut); + + expect(payload, { + 'items': [ + {'name': 'metric', 'value': 1}, + ], + }); + }); + test('max attachment size', () async { final attachment = SentryAttachment.fromLoader( loader: () => Uint8List.fromList([1, 2, 3, 4]),