Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Unreleased

* Feat: Add support for attachments (#505)

# 6.0.0-beta.1

* Feat: Browser detection (#502)
* Feat: Enrich events with more context (#452)
* Feat: Add Culture Context (#491)
* Feat: Add DeduplicationEventProcessor (#498)
* Feat: Capture failed requests as event (#473)
* Feat: `beforeSend` callback accepts async code (#494)

## Breaking Changes:

Expand Down
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export 'src/transport/transport.dart';
export 'src/integration.dart';
export 'src/event_processor.dart';
export 'src/http_client/sentry_http_client.dart';
export 'src/sentry_attachment/sentry_attachment.dart';
2 changes: 2 additions & 0 deletions dart/lib/sentry_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'sentry.dart';
export 'src/sentry_attachment/io_sentry_attachment.dart';
30 changes: 21 additions & 9 deletions dart/lib/src/protocol/sentry_id.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
import 'package:meta/meta.dart';
import 'package:uuid/uuid.dart';

/// Hexadecimal string representing a uuid4 value
/// Hexadecimal string representing a uuid4 value.
/// The length is exactly 32
/// characters. Dashes are not allowed. Has to be lowercase.
@immutable
class SentryId {
static final SentryId _emptyId =
SentryId.fromId('00000000-0000-0000-0000-000000000000');

/// The ID Sentry.io assigned to the submitted event for future reference.
final String _id;

static final Uuid _uuidGenerator = Uuid();

SentryId._internal({String? id}) : _id = id ?? _uuidGenerator.v4();
SentryId._internal({String? id})
: _id =
id?.replaceAll('-', '') ?? _uuidGenerator.v4().replaceAll('-', '');

/// Generates a new SentryId
factory SentryId.newId() => SentryId._internal();
SentryId.newId() : this._internal();

/// Generates a SentryId with the given UUID
factory SentryId.fromId(String id) => SentryId._internal(id: id);
SentryId.fromId(String id) : this._internal(id: id);

/// SentryId with an empty UUID
factory SentryId.empty() => _emptyId;
const SentryId.empty() : _id = '00000000000000000000000000000000';

@override
String toString() => _id;

@override
int get hashCode => _id.hashCode;

@override
String toString() => _id.replaceAll('-', '');
bool operator ==(o) {
if (o is SentryId) {
return o._id == _id;
}
return false;
}
}
19 changes: 18 additions & 1 deletion dart/lib/src/scope.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dart:collection';

import 'sentry_attachment/sentry_attachment.dart';
import 'event_processor.dart';
import 'protocol.dart';
import 'sentry_options.dart';
Expand Down Expand Up @@ -81,6 +81,10 @@ class Scope {

final SentryOptions _options;

final List<SentryAttachment> _attachements = [];

List<SentryAttachment> get attachements => List.unmodifiable(_attachements);

Scope(this._options);

/// Adds a breadcrumb to the breadcrumbs queue
Expand Down Expand Up @@ -116,6 +120,14 @@ class Scope {
_breadcrumbs.add(breadcrumb);
}

void addAttachment(SentryAttachment attachment) {
_attachements.add(attachment);
}

void clearAttachments() {
_attachements.clear();
}

/// Clear all the breadcrumbs
void clearBreadcrumbs() {
_breadcrumbs.clear();
Expand All @@ -129,6 +141,7 @@ class Scope {
/// Resets the Scope to its default state
void clear() {
clearBreadcrumbs();
clearAttachments();
level = null;
transaction = null;
user = null;
Expand Down Expand Up @@ -285,6 +298,10 @@ class Scope {
}
});

for (final attachment in _attachements) {
clone.addAttachment(attachment);
}

return clone;
}
}
35 changes: 35 additions & 0 deletions dart/lib/src/sentry_attachment/io_sentry_attachment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'dart:io';

import 'sentry_attachment.dart';

class IoSentryAttachment extends SentryAttachment {
/// Creates an attachment from a given path.
/// Only available on `dart:io` platforms.
/// Not available on web.
IoSentryAttachment.fromPath(
String path, {
String? filename,
String? attachmentType,
String? contentType,
}) : this.fromFile(
File(path),
attachmentType: attachmentType,
contentType: contentType,
filename: filename,
);

/// Creates an attachment from a given [File].
/// Only available on `dart:io` platforms.
/// Not available on web.
IoSentryAttachment.fromFile(
File file, {
String? filename,
String? attachmentType,
String? contentType,
}) : super.fromLoader(
loader: () => file.readAsBytes(),
filename: filename ?? file.uri.pathSegments.last,
attachmentType: attachmentType,
contentType: contentType,
);
}
94 changes: 94 additions & 0 deletions dart/lib/src/sentry_attachment/sentry_attachment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:async';
import 'dart:typed_data';

// https://develop.sentry.dev/sdk/features/#attachments
// https://develop.sentry.dev/sdk/envelopes/#attachment

typedef ContentLoader = FutureOr<Uint8List> Function();

/// Arbitrary content which gets attached to an event.
class SentryAttachment {
/// Standard attachment without special meaning.
static const String typeAttachmentDefault = 'event.attachment';

/// Minidump file that creates an error event and is symbolicated.
/// The file should start with the `MDMP` magic bytes.
static const String typeMinidump = 'event.minidump';

/// Apple crash report file that creates an error event and is symbolicated.
static const String typeAppleCrashReport = 'event.applecrashreport';

/// XML file containing UE4 crash meta data.
/// During event ingestion, event contexts and extra fields are extracted from
/// this file.
static const String typeUnrealContext = 'unreal.context';

/// Plain-text log file obtained from UE4 crashes.
/// During event ingestion, the last logs are extracted into event
/// breadcrumbs.
static const String typeUnrealLogs = 'unreal.logs';

SentryAttachment.fromLoader({
required ContentLoader loader,
required this.filename,
String? attachmentType,
this.contentType,
}) : _loader = loader,
attachmentType = attachmentType ?? typeAttachmentDefault;

/// Creates an [SentryAttachment] from a [Uint8List]
SentryAttachment.fromUint8List(
Uint8List bytes,
String fileName, {
String? contentType,
String? attachmentType,
}) : this.fromLoader(
attachmentType: attachmentType,
loader: () => bytes,
filename: fileName,
contentType: contentType,
);

/// Creates an [SentryAttachment] from a [List<int>]
SentryAttachment.fromIntList(
List<int> bytes,
String fileName, {
String? contentType,
String? attachmentType,
}) : this.fromLoader(
attachmentType: attachmentType,
loader: () => Uint8List.fromList(bytes),
filename: fileName,
contentType: contentType,
);

/// Creates an [SentryAttachment] from [ByteData]
SentryAttachment.fromByteData(
ByteData bytes,
String fileName, {
String? contentType,
String? attachmentType,
}) : this.fromLoader(
attachmentType: attachmentType,
loader: () => bytes.buffer.asUint8List(),
filename: fileName,
contentType: contentType,
);

/// Attachment type.
/// Should be one of types given in [AttachmentType].
final String attachmentType;

/// Attachment content.
/// Is loaded while sending this attachment.
FutureOr<Uint8List> get bytes => _loader();

final ContentLoader _loader;

/// Attachment file name.
final String filename;

/// Attachment content type.
/// Inferred by Sentry if it's not given.
final String? contentType;
}
10 changes: 8 additions & 2 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,14 @@ class SentryClient {
return _sentryId;
}
}
final envelope = SentryEnvelope.fromEvent(preparedEvent, _options.sdk);
return await _options.transport.send(envelope);
final envelope = SentryEnvelope.fromEvent(
preparedEvent,
_options.sdk,
attachments: scope?.attachements,
);

final id = await captureEnvelope(envelope);
return id ?? SentryId.empty();
}

SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) {
Expand Down
24 changes: 18 additions & 6 deletions dart/lib/src/sentry_envelope.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:convert';

import 'sentry_attachment/sentry_attachment.dart';
import 'sentry_envelope_header.dart';
import 'sentry_envelope_item.dart';
import 'protocol/sentry_event.dart';
Expand All @@ -16,19 +17,30 @@ class SentryEnvelope {
final List<SentryEnvelopeItem> items;

/// Create an `SentryEnvelope` with containing one `SentryEnvelopeItem` which holds the `SentyEvent` data.
factory SentryEnvelope.fromEvent(SentryEvent event, SdkVersion sdkVersion) {
return SentryEnvelope(SentryEnvelopeHeader(event.eventId, sdkVersion),
[SentryEnvelopeItem.fromEvent(event)]);
factory SentryEnvelope.fromEvent(
SentryEvent event,
SdkVersion sdkVersion, {
List<SentryAttachment>? attachments,
}) {
return SentryEnvelope(
SentryEnvelopeHeader(event.eventId, sdkVersion),
[
SentryEnvelopeItem.fromEvent(event),
if (attachments != null)
...attachments.map((e) => SentryEnvelopeItem.fromAttachment(e))
],
);
}

/// Stream binary data representation of `Envelope` file encoded.
Stream<List<int>> envelopeStream() async* {
yield utf8.encode(jsonEncode(header.toJson()));
final newLineData = utf8.encode('\n');
for (final item in items) {
yield newLineData;
await for (final chunk in item.envelopeItemStream()) {
yield chunk;
final itemStream = await item.envelopeItemStream();
if (itemStream.isNotEmpty) {
yield newLineData;
yield itemStream;
}
}
}
Expand Down
54 changes: 42 additions & 12 deletions dart/lib/src/sentry_envelope_item.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:convert';

import 'sentry_attachment/sentry_attachment.dart';
import 'sentry_item_type.dart';
import 'protocol/sentry_event.dart';
import 'sentry_envelope_item_header.dart';
Expand All @@ -8,13 +9,24 @@ import 'sentry_envelope_item_header.dart';
class SentryEnvelopeItem {
SentryEnvelopeItem(this.header, this.dataFactory);

/// Header with info about type and length of data in bytes.
final SentryEnvelopeItemHeader header;
factory SentryEnvelopeItem.fromAttachment(SentryAttachment attachment) {
final cachedItem = _CachedItem(() async {
return await attachment.bytes;
});

/// Create binary data representation of item data.
final Future<List<int>> Function() dataFactory;
final getLength = () async => (await cachedItem.getData()).length;

final header = SentryEnvelopeItemHeader(
SentryItemType.attachment,
getLength,
contentType: attachment.contentType,
fileName: attachment.filename,
attachmentType: attachment.attachmentType,
);
return SentryEnvelopeItem(header, cachedItem.getData);
}

/// Create an `SentryEnvelopeItem` which holds the `SentyEvent` data.
/// Create an [SentryEnvelopeItem] which holds the [SentryEvent] data.
factory SentryEnvelopeItem.fromEvent(SentryEvent event) {
final cachedItem = _CachedItem(() async {
final jsonEncoded = jsonEncode(event.toJson());
Expand All @@ -26,16 +38,34 @@ class SentryEnvelopeItem {
};

return SentryEnvelopeItem(
SentryEnvelopeItemHeader(SentryItemType.event, getLength,
contentType: 'application/json'),
cachedItem.getData);
SentryEnvelopeItemHeader(
SentryItemType.event,
getLength,
contentType: 'application/json',
),
cachedItem.getData,
);
}

/// Header with info about type and length of data in bytes.
final SentryEnvelopeItemHeader header;

/// Create binary data representation of item data.
final Future<List<int>> Function() dataFactory;

/// Stream binary data of `Envelope` item.
Stream<List<int>> envelopeItemStream() async* {
yield utf8.encode(jsonEncode(await header.toJson()));
yield utf8.encode('\n');
yield await dataFactory();
Future<List<int>> envelopeItemStream() async {
// Each item needs to be encoded as one unit.
// Otherwise the header alredy got yielded if the content throws
// an exception.
try {
final itemHeader = utf8.encode(jsonEncode(await header.toJson()));
final newLine = utf8.encode('\n');
final data = await dataFactory();
return [...itemHeader, ...newLine, ...data];
} catch (e) {
return [];
}
}
}

Expand Down
Loading