Skip to content

Commit 69803b7

Browse files
committed
feat: add attachment support
1 parent 018bd69 commit 69803b7

14 files changed

Lines changed: 399 additions & 1 deletion

src/Attachment/Attachment.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Attachment;
6+
7+
abstract class Attachment
8+
{
9+
private const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
10+
11+
/**
12+
* @var string
13+
*/
14+
private $filename;
15+
16+
/**
17+
* @var string
18+
*/
19+
private $contentType;
20+
21+
public function __construct(string $filename, string $contentType)
22+
{
23+
$this->filename = $filename;
24+
$this->contentType = $contentType;
25+
}
26+
27+
public function getFilename(): string
28+
{
29+
return $this->filename;
30+
}
31+
32+
public function getContentType(): string
33+
{
34+
return $this->contentType;
35+
}
36+
37+
/**
38+
* Returns the size in bytes for the attachment. This method should aim to use a low overhead
39+
* way of determining the size because it will be called more than once.
40+
* For example, for file attachments it should read the file size from the filesystem instead of
41+
* reading the file in memory and then calculating the length.
42+
* If no low overhead way exists, then the result should be cached so that calling it multiple times
43+
* does not decrease performance.
44+
*
45+
* @return int the size in bytes or null if the length could not be determined, for example if the file
46+
* does not exist
47+
*/
48+
abstract public function getSize(): ?int;
49+
50+
/**
51+
* Fetches and returns the data. Calling this can have a non-trivial impact on memory usage, depending
52+
* on the type and size of attachment.
53+
*
54+
* @return string the content as bytes or null if the content could not be retrieved, for example if the file
55+
* does not exist
56+
*/
57+
abstract public function getData(): ?string;
58+
59+
/**
60+
* Creates a new attachment representing a file referenced by a path.
61+
* The file is not validated and the content is not read when creating the attachment.
62+
*/
63+
public static function fromFile(string $path, string $contentType = self::DEFAULT_CONTENT_TYPE): Attachment
64+
{
65+
return new FileAttachment($path, $contentType);
66+
}
67+
68+
/**
69+
* Creates a new attachment representing a slice of bytes that lives in memory.
70+
*/
71+
public static function fromBytes(string $filename, string $data, string $contentType = self::DEFAULT_CONTENT_TYPE): Attachment
72+
{
73+
return new ByteAttachment($filename, $contentType, $data);
74+
}
75+
}

src/Attachment/ByteAttachment.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Sentry\Attachment;
4+
5+
/**
6+
* Represents an attachment that is stored in memory and will not be read from the filesystem.
7+
*/
8+
class ByteAttachment extends Attachment
9+
{
10+
/**
11+
* @var string
12+
*/
13+
private $data;
14+
15+
public function __construct(string $filename, string $contentType, string $data)
16+
{
17+
parent::__construct($filename, $contentType);
18+
$this->data = $data;
19+
}
20+
21+
public function getSize(): ?int
22+
{
23+
return \strlen($this->data);
24+
}
25+
26+
public function getData(): ?string
27+
{
28+
return $this->data;
29+
}
30+
}

src/Attachment/FileAttachment.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Sentry\Attachment;
4+
5+
/**
6+
* Represents a file that is readable by using a path.
7+
*/
8+
class FileAttachment extends Attachment
9+
{
10+
11+
/**
12+
* @var string
13+
*/
14+
private $path;
15+
16+
public function __construct(string $path, string $contentType)
17+
{
18+
parent::__construct(basename($path), $contentType);
19+
$this->path = $path;
20+
}
21+
22+
public function getSize(): ?int
23+
{
24+
return @filesize($this->path) ?: null;
25+
}
26+
27+
public function getData(): ?string
28+
{
29+
return @file_get_contents($this->path) ?: null;
30+
}
31+
}

src/Event.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Sentry;
66

7+
use Sentry\Attachment\Attachment;
78
use Sentry\Context\OsContext;
89
use Sentry\Context\RuntimeContext;
910
use Sentry\Logs\Log;
@@ -204,6 +205,11 @@ final class Event
204205
*/
205206
private $profile;
206207

208+
/**
209+
* @var Attachment[]
210+
*/
211+
private $attachments = [];
212+
207213
private function __construct(?EventId $eventId, EventType $eventType)
208214
{
209215
$this->id = $eventId ?? EventId::generate();
@@ -241,6 +247,11 @@ public static function createLogs(?EventId $eventId = null): self
241247
return new self($eventId, EventType::logs());
242248
}
243249

250+
public static function createAttachments(?EventId $eventId = null): self
251+
{
252+
return new self($eventId, EventType::attachment());
253+
}
254+
244255
/**
245256
* Gets the ID of this event.
246257
*/
@@ -933,4 +944,20 @@ public function getTraceId(): ?string
933944

934945
return null;
935946
}
947+
948+
/**
949+
* @return Attachment[]
950+
*/
951+
public function getAttachments(): array
952+
{
953+
return $this->attachments;
954+
}
955+
956+
/**
957+
* @param Attachment[] $attachments
958+
*/
959+
public function setAttachments(array $attachments): void
960+
{
961+
$this->attachments = $attachments;
962+
}
936963
}

src/EventType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public static function logs(): self
4747
return self::getInstance('log');
4848
}
4949

50+
public static function attachment(): self
51+
{
52+
return self::getInstance('attachment');
53+
}
54+
5055
/**
5156
* List of all cases on the enum.
5257
*
@@ -59,6 +64,7 @@ public static function cases(): array
5964
self::transaction(),
6065
self::checkIn(),
6166
self::logs(),
67+
self::attachment()
6268
];
6369
}
6470

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Serializer\EnvelopItems;
6+
7+
use Sentry\Attachment\Attachment;
8+
use Sentry\Event;
9+
use Sentry\Util\JSON;
10+
11+
class AttachmentItem implements EnvelopeItemInterface
12+
{
13+
/**
14+
* {@inheritDoc}
15+
*/
16+
public static function toEnvelopeItem(Event $event)
17+
{
18+
$attachments = $event->getAttachments();
19+
20+
$items = [];
21+
22+
foreach ($attachments as $attachment) {
23+
$header = [
24+
'filename' => $attachment->getFilename(),
25+
'content_type' => $attachment->getContentType(),
26+
'attachment_type' => 'event.attachment',
27+
];
28+
29+
$items[] = \sprintf("%s\n%s", JSON::encode($header), file_get_contents($attachment->getFilename()));
30+
}
31+
32+
return $items;
33+
}
34+
35+
public static function toAttachmentItem(Attachment $attachment): ?string
36+
{
37+
$data = $attachment->getData();
38+
if ($data === null) {
39+
return null;
40+
}
41+
42+
$header = [
43+
'type' => 'attachment',
44+
'filename' => $attachment->getFilename(),
45+
'content_type' => $attachment->getContentType(),
46+
'attachment_type' => 'event.attachment',
47+
'length' => $attachment->getSize(),
48+
];
49+
50+
return \sprintf("%s\n%s", JSON::encode($header), $data);
51+
}
52+
53+
/**
54+
* Returns the total size of all attachments in bytes.
55+
*
56+
* @param Attachment[] $attachments
57+
*/
58+
public static function totalAttachmentSize(array $attachments): int
59+
{
60+
$sum = 0;
61+
foreach ($attachments as $attachment) {
62+
$sum += $attachment->getSize();
63+
}
64+
65+
return $sum;
66+
}
67+
}

src/Serializer/EnvelopItems/EnvelopeItemInterface.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@
1111
*/
1212
interface EnvelopeItemInterface
1313
{
14-
public static function toEnvelopeItem(Event $event): ?string;
14+
/**
15+
* @param Event $event
16+
* @return ?string|string[]
17+
*/
18+
public static function toEnvelopeItem(Event $event);
1519
}

src/Serializer/PayloadSerializer.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Sentry\Event;
88
use Sentry\EventType;
99
use Sentry\Options;
10+
use Sentry\Serializer\EnvelopItems\AttachmentItem;
1011
use Sentry\Serializer\EnvelopItems\CheckInItem;
1112
use Sentry\Serializer\EnvelopItems\EventItem;
1213
use Sentry\Serializer\EnvelopItems\LogsItem;
@@ -23,6 +24,11 @@
2324
*/
2425
final class PayloadSerializer implements PayloadSerializerInterface
2526
{
27+
/**
28+
* Attachments have a limit of 100MB before compression.
29+
*/
30+
private const MAX_ATTACHMENT_SIZE = 100000000;
31+
2632
/**
2733
* @var Options The SDK client options
2834
*/
@@ -75,6 +81,13 @@ public function serialize(Event $event): string
7581
break;
7682
}
7783

84+
// If attachments are exceeding the limit, then we drop all attachments
85+
if (AttachmentItem::totalAttachmentSize($event->getAttachments()) <= self::MAX_ATTACHMENT_SIZE) {
86+
foreach ($event->getAttachments() as $attachment) {
87+
$items[] = AttachmentItem::toAttachmentItem($attachment);
88+
}
89+
}
90+
7891
return \sprintf("%s\n%s", JSON::encode($envelopeHeader), implode("\n", array_filter($items)));
7992
}
8093
}

src/State/Hub.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Sentry\State;
66

77
use Psr\Log\NullLogger;
8+
use Sentry\Attachment\Attachment;
89
use Sentry\Breadcrumb;
910
use Sentry\CheckIn;
1011
use Sentry\CheckInStatus;
@@ -231,6 +232,19 @@ public function addBreadcrumb(Breadcrumb $breadcrumb): bool
231232
return $breadcrumb !== null;
232233
}
233234

235+
public function addAttachment(Attachment $attachment): bool
236+
{
237+
$client = $this->getClient();
238+
239+
if ($client === null) {
240+
return false;
241+
}
242+
243+
$this->getScope()->addAttachment($attachment);
244+
245+
return true;
246+
}
247+
234248
/**
235249
* {@inheritdoc}
236250
*/

src/State/HubInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Sentry\State;
66

7+
use Sentry\Attachment\Attachment;
78
use Sentry\Breadcrumb;
89
use Sentry\CheckInStatus;
910
use Sentry\ClientInterface;
@@ -152,4 +153,6 @@ public function getSpan(): ?Span;
152153
* Sets the span on the Hub.
153154
*/
154155
public function setSpan(?Span $span): HubInterface;
156+
157+
public function addAttachment(Attachment $attachment): bool;
155158
}

0 commit comments

Comments
 (0)