Skip to content

Commit aad2e28

Browse files
authored
feat: add attachment support (#1925)
1 parent 38c5b2c commit aad2e28

14 files changed

Lines changed: 383 additions & 0 deletions

File tree

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

src/Attachment/FileAttachment.php

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

src/Event.php

Lines changed: 22 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;
@@ -205,6 +206,11 @@ final class Event
205206
*/
206207
private $profile;
207208

209+
/**
210+
* @var Attachment[]
211+
*/
212+
private $attachments = [];
213+
208214
private function __construct(?EventId $eventId, EventType $eventType)
209215
{
210216
$this->id = $eventId ?? EventId::generate();
@@ -934,4 +940,20 @@ public function getTraceId(): ?string
934940

935941
return null;
936942
}
943+
944+
/**
945+
* @return Attachment[]
946+
*/
947+
public function getAttachments(): array
948+
{
949+
return $this->attachments;
950+
}
951+
952+
/**
953+
* @param Attachment[] $attachments
954+
*/
955+
public function setAttachments(array $attachments): void
956+
{
957+
$this->attachments = $attachments;
958+
}
937959
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Serializer\EnvelopItems;
6+
7+
use Sentry\Attachment\Attachment;
8+
use Sentry\Util\JSON;
9+
10+
class AttachmentItem
11+
{
12+
public static function toAttachmentItem(Attachment $attachment): ?string
13+
{
14+
$data = $attachment->getData();
15+
if ($data === null) {
16+
return null;
17+
}
18+
19+
$header = [
20+
'type' => 'attachment',
21+
'filename' => $attachment->getFilename(),
22+
'content_type' => $attachment->getContentType(),
23+
'attachment_type' => 'event.attachment',
24+
'length' => $attachment->getSize(),
25+
];
26+
27+
return \sprintf("%s\n%s", JSON::encode($header), $data);
28+
}
29+
}

src/Serializer/PayloadSerializer.php

Lines changed: 7 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;
@@ -60,12 +61,18 @@ public function serialize(Event $event): string
6061
switch ($event->getType()) {
6162
case EventType::event():
6263
$items[] = EventItem::toEnvelopeItem($event);
64+
foreach ($event->getAttachments() as $attachment) {
65+
$items[] = AttachmentItem::toAttachmentItem($attachment);
66+
}
6367
break;
6468
case EventType::transaction():
6569
$items[] = TransactionItem::toEnvelopeItem($event);
6670
if ($event->getSdkMetadata('profile') !== null) {
6771
$items[] = ProfileItem::toEnvelopeItem($event);
6872
}
73+
foreach ($event->getAttachments() as $attachment) {
74+
$items[] = AttachmentItem::toAttachmentItem($attachment);
75+
}
6976
break;
7077
case EventType::checkIn():
7178
$items[] = CheckInItem::toEnvelopeItem($event);

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/HubAdapter.php

Lines changed: 9 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;
@@ -155,6 +156,14 @@ public function addBreadcrumb(Breadcrumb $breadcrumb): bool
155156
return SentrySdk::getCurrentHub()->addBreadcrumb($breadcrumb);
156157
}
157158

159+
/**
160+
* {@inheritDoc}
161+
*/
162+
public function addAttachment(Attachment $attachment): bool
163+
{
164+
return SentrySdk::getCurrentHub()->addAttachment($attachment);
165+
}
166+
158167
/**
159168
* {@inheritdoc}
160169
*/

src/State/HubInterface.php

Lines changed: 6 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,9 @@ public function getSpan(): ?Span;
152153
* Sets the span on the Hub.
153154
*/
154155
public function setSpan(?Span $span): HubInterface;
156+
157+
/**
158+
* Records a new attachment that will be attached to error and transaction events.
159+
*/
160+
public function addAttachment(Attachment $attachment): bool;
155161
}

src/State/Scope.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
namespace Sentry\State;
66

7+
use Sentry\Attachment\Attachment;
78
use Sentry\Breadcrumb;
89
use Sentry\Event;
910
use Sentry\EventHint;
11+
use Sentry\EventType;
1012
use Sentry\Options;
1113
use Sentry\Severity;
1214
use Sentry\Tracing\DynamicSamplingContext;
@@ -76,6 +78,11 @@ class Scope
7678
*/
7779
private $span;
7880

81+
/**
82+
* @var Attachment[]
83+
*/
84+
private $attachments = [];
85+
7986
/**
8087
* @var callable[] List of event processors
8188
*
@@ -334,6 +341,7 @@ public function clear(): self
334341
$this->tags = [];
335342
$this->extra = [];
336343
$this->contexts = [];
344+
$this->attachments = [];
337345

338346
return $this;
339347
}
@@ -412,6 +420,12 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op
412420
$hint = new EventHint();
413421
}
414422

423+
if ($event->getType() === EventType::event() || $event->getType() === EventType::transaction()) {
424+
if (empty($event->getAttachments())) {
425+
$event->setAttachments($this->attachments);
426+
}
427+
}
428+
415429
foreach (array_merge(self::$globalEventProcessors, $this->eventProcessors) as $processor) {
416430
$event = $processor($event, $hint);
417431

@@ -482,4 +496,18 @@ public function __clone()
482496
$this->propagationContext = clone $this->propagationContext;
483497
}
484498
}
499+
500+
public function addAttachment(Attachment $attachment): self
501+
{
502+
$this->attachments[] = $attachment;
503+
504+
return $this;
505+
}
506+
507+
public function clearAttachments(): self
508+
{
509+
$this->attachments = [];
510+
511+
return $this;
512+
}
485513
}

0 commit comments

Comments
 (0)