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
23 changes: 23 additions & 0 deletions src/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Sentry\Context\OsContext;
use Sentry\Context\RuntimeContext;
use Sentry\Logs\Log;
use Sentry\Profiles\ProfileChunk;
use Sentry\Profiling\Profile;
use Sentry\Tracing\Span;

Expand Down Expand Up @@ -71,6 +72,11 @@ final class Event
*/
private $logs = [];

/**
* @var ProfileChunk|null
*/
private $profileChunk;

/**
* @var string|null The name of the server (e.g. the host name)
*/
Expand Down Expand Up @@ -241,6 +247,11 @@ public static function createLogs(?EventId $eventId = null): self
return new self($eventId, EventType::logs());
}

public static function createProfileChunk(?EventId $eventId = null): self
{
return new self($eventId, EventType::profileChunk());
}

/**
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
*/
Expand Down Expand Up @@ -445,6 +456,18 @@ public function setLogs(array $logs): self
return $this;
}

public function getProfileChunk(): ?ProfileChunk
{
return $this->profileChunk;
}

public function setProfileChunk(?ProfileChunk $profileChunk): self
{
$this->profileChunk = $profileChunk;

return $this;
}

/**
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/EventType.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ public static function logs(): self
return self::getInstance('log');
}

public static function profileChunk(): self
{
return self::getInstance('profile_chunk');
}

/**
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
*/
Expand Down
38 changes: 34 additions & 4 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,7 @@ public function setTracesSampleRate(?float $sampleRate): self

public function getProfilesSampleRate(): ?float
{
/** @var int|float|null $value */
$value = $this->options['profiles_sample_rate'] ?? null;

return $value ?? null;
return $this->options['profiles_sample_rate'];
}

public function setProfilesSampleRate(?float $sampleRate): self
Expand All @@ -209,6 +206,34 @@ public function setProfilesSampleRate(?float $sampleRate): self
return $this;
}

public function getProfilesSessionSampleRate(): ?float
{
return $this->options['profiles_session_sample_rate'];
}

public function setProfilesSessionSampleRate(?float $sampleRate): self
{
$options = array_merge($this->options, ['profiles_session_sample_rate' => $sampleRate]);

$this->options = $this->resolver->resolve($options);

return $this;
}

public function getProfilesLifecycle(): ?string
{
return $this->options['profiles_lifecycle'];
}

public function setProfilesLifecycle(?string $lifecycle): self
{
$options = array_merge($this->options, ['profiles_lifecycle' => $lifecycle]);

$this->options = $this->resolver->resolve($options);

return $this;
}

/**
* Gets whether tracing is enabled or not. The feature is enabled when at
* least one of the `traces_sample_rate` and `traces_sampler` options is
Expand Down Expand Up @@ -1223,6 +1248,8 @@ private function configureOptions(OptionsResolver $resolver): void
'traces_sample_rate' => null,
'traces_sampler' => null,
'profiles_sample_rate' => null,
'profiles_session_sample_rate' => null,
'profiles_lifecycle' => 'trace',
'attach_stacktrace' => false,
/**
* @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x.
Expand Down Expand Up @@ -1293,6 +1320,8 @@ private function configureOptions(OptionsResolver $resolver): void
$resolver->setAllowedTypes('traces_sample_rate', ['null', 'int', 'float']);
$resolver->setAllowedTypes('traces_sampler', ['null', 'callable']);
$resolver->setAllowedTypes('profiles_sample_rate', ['null', 'int', 'float']);
$resolver->setAllowedTypes('profiles_session_sample_rate', ['null', 'int', 'float']);
$resolver->setAllowedTypes('profiles_lifecycle', ['string']);
$resolver->setAllowedTypes('attach_stacktrace', 'bool');
$resolver->setAllowedTypes('attach_metric_code_locations', 'bool');
$resolver->setAllowedTypes('context_lines', ['null', 'int']);
Expand Down Expand Up @@ -1335,6 +1364,7 @@ private function configureOptions(OptionsResolver $resolver): void
$resolver->setAllowedTypes('class_serializers', 'array');

$resolver->setAllowedValues('max_request_body_size', ['none', 'never', 'small', 'medium', 'always']);
$resolver->setAllowedValues('profiles_lifecycle', ['trace', 'manual']);
$resolver->setAllowedValues('dsn', \Closure::fromCallable([$this, 'validateDsnOption']));
$resolver->setAllowedValues('max_breadcrumbs', \Closure::fromCallable([$this, 'validateMaxBreadcrumbsOptions']));
$resolver->setAllowedValues('class_serializers', \Closure::fromCallable([$this, 'validateClassSerializersOption']));
Expand Down
255 changes: 255 additions & 0 deletions src/Profiles/ProfileChunk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<?php

declare(strict_types=1);

namespace Sentry\Profiles;

use Sentry\Event;
use Sentry\Options;
use Sentry\Util\PrefixStripper;
use Sentry\Util\SentryUid;

/**
* Type definition of the Sentry v2 profile format (continuous profiling).
* All fields are none otpional.
*
* @see https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/
*
* @phpstan-type SentryProfileFrame array{
* abs_path: string,
* filename: string,
* function: string,
* module: string|null,
* lineno: int|null,
* }
* @phpstan-type SentryV2Profile array{
* profiler_id: string,
* chunk_id: string,
* platform: string,
* release: string,
* environment: string,
* version: string,
* profile: array{
* frames: array<int, SentryProfileFrame>,
* samples: array<int, array{
* thread_id: string,
* stack_id: int,
* timestamp: float,
* }>,
* stacks: array<int, array<int, int>>,
* },
* client_sdk: array{
* name: string,
* version: string,
* },
* }
* @phpstan-type ExcimerLogStackEntryTrace array{
* file: string,
* line: int,
* class?: string,
* function?: string,
* closure_line?: int,
* }
* @phpstan-type ExcimerLogStackEntry array{
* trace: array<int, ExcimerLogStackEntryTrace>,
* timestamp: float
* }
*
* @internal
*/
final class ProfileChunk
{
use PrefixStripper;

/**
* @var string The thread ID
*/
public const THREAD_ID = '0';

/**
* @var string The thread name
*/
public const THREAD_NAME = 'main';

/**
* @var string The version of the profile format
*/
private const VERSION = '2';

/**
* @var float|null The start time of the profile as a Unix timestamp with microseconds
*/
private $startTimeStamp;

/**
* @var string|null The profiler ID
*/
private $profilerId;

/**
* @var string|null The chunk ID (null = auto-generate)
*/
private $chunkId;

/**
* @var array<int, \ExcimerLog> The data of the profile
*/
private $excimerLogs;

/**
* @var Options|null
*/
private $options;

public function __construct(?Options $options = null)
{
$this->options = $options;
}

public function setStartTimeStamp(?float $startTimeStamp): void
{
$this->startTimeStamp = $startTimeStamp;
}

public function setProfilerId(?string $profilerId): void
{
$this->profilerId = $profilerId;
}

public function setChunkId(string $chunkId): void
{
$this->chunkId = $chunkId;
}

/**
* @param array<int, \ExcimerLog> $excimerLogs
*/
public function setExcimerLogs($excimerLogs): void
{
$this->excimerLogs = $excimerLogs;
}

/**
* @return SentryV2Profile|null
*/
public function getFormattedData(Event $event): ?array
{
$frames = [];
$frameHashMap = [];

$stacks = [];
$stackHashMap = [];

$registerStack = static function (array $stack) use (&$stacks, &$stackHashMap): int {
$stackHash = md5(serialize($stack));

if (\array_key_exists($stackHash, $stackHashMap) === false) {
$stackHashMap[$stackHash] = \count($stacks);
$stacks[] = $stack;
}

return $stackHashMap[$stackHash];
};

$samples = [];

$loggedStacks = $this->prepareStacks();
foreach ($loggedStacks as $stack) {
$stackFrames = [];

foreach ($stack['trace'] as $frame) {
$absolutePath = $frame['file'];
$lineno = $frame['line'];

$frameKey = "{$absolutePath}:{$lineno}";

$frameIndex = $frameHashMap[$frameKey] ?? null;

if ($frameIndex === null) {
$file = $this->stripPrefixFromFilePath($this->options, $absolutePath);
$module = null;

if (isset($frame['class'], $frame['function'])) {
// Class::method
$function = $frame['class'] . '::' . $frame['function'];
$module = $frame['class'];
} elseif (isset($frame['function'])) {
// {closure}
$function = $frame['function'];
} else {
// /index.php
$function = $file;
}

$frameHashMap[$frameKey] = $frameIndex = \count($frames);
$frames[] = [
'filename' => $file,
'abs_path' => $absolutePath,
'module' => $module,
'function' => $function,
'lineno' => $lineno,
];
}

$stackFrames[] = $frameIndex;
}

$stackId = $registerStack($stackFrames);

$samples[] = [
'stack_id' => $stackId,
'thread_id' => self::THREAD_ID,
'timestamp' => $this->startTimeStamp + $stack['timestamp'],
];
}

return [
'profiler_id' => $this->profilerId,
'chunk_id' => $this->chunkId ?? SentryUid::generate(),
'platform' => 'php',
'release' => $event->getRelease() ?? '',
'environment' => $event->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT,
'version' => self::VERSION,
'profile' => [
'frames' => $frames,
'samples' => $samples,
'stacks' => $stacks,
'thread_metadata' => (object) [
self::THREAD_ID => [
'name' => self::THREAD_NAME,
],
],
],
'client_sdk' => [
'name' => $event->getSdkIdentifier(),
'version' => $event->getSdkVersion(),
],
];
}

/**
* This method is mainly used to be able to mock the ExcimerLog class in the tests.
*
* @return array<int, ExcimerLogStackEntry>
*/
private function prepareStacks(): array
{
$stacks = [];

foreach ($this->excimerLogs as $excimerLog) {
foreach ($excimerLog as $stack) {
if ($stack instanceof \ExcimerLogEntry) {
$stacks[] = [
'trace' => $stack->getTrace(),
'timestamp' => $stack->getTimestamp(),
];
} else {
/** @var ExcimerLogStackEntry $stack */
$stacks[] = $stack;
}
}
}

return $stacks;
}
}
Loading