Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions packages/dart/lib/src/sentry_envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ class SentryEnvelope {
SentryEnvelope(this.header, this.items,
{this.containsUnhandledException = false});

static const _spanV2PayloadProperties = <String, Object>{
'version': 2,
'ingest_settings': <String, String>{
'infer_ip': 'auto',
'infer_user_agent': 'auto',
},
};

/// Header describing envelope content.
final SentryEnvelopeHeader header;

Expand Down Expand Up @@ -127,7 +135,9 @@ class SentryEnvelope {
dsn: dsn, traceContext: traceContext),
[
SentryEnvelopeItem.fromSpansData(
_buildItemsPayload(encodedSpans), encodedSpans.length)
_buildItemsPayload(encodedSpans,
additionalTopLevelProperties: _spanV2PayloadProperties),
encodedSpans.length)
],
);

Expand Down Expand Up @@ -177,13 +187,41 @@ class SentryEnvelope {
}
}

/// Builds a payload in the format {"items": [item1, item2, ...]}
static Uint8List _buildItemsPayload(List<List<int>> 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<List<int>> encodedItems, {
Map<String, Object?> 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]);
}
Expand Down
67 changes: 65 additions & 2 deletions packages/dart/test/sentry_envelope_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ void main() {
return utf8.decode(expectedItem);
}

Future<Map<String, dynamic>> decodedItemPayload(
SentryEnvelope envelope) async {
final data = await envelope.items.single.dataFactory();
return jsonDecode(utf8.decode(data)) as Map<String, dynamic>;
}

test('serialize', () async {
final eventId = SentryId.newId();

Expand Down Expand Up @@ -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 +
Expand All @@ -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(
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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]),
Expand Down
Loading