Skip to content
Open
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
35 changes: 35 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,28 @@
return $this->updateOptions(['max_value_length' => $maxValueLength]);
}

/**
* Gets the maximum number of items serialized from an array or an object,
* or null if there is no limit.
*/
public function getMaxBreadth(): ?int
{
$maxBreadth = $this->options['max_breadth'];

Check warning on line 1023 in src/Options.php

View workflow job for this annotation

GitHub Actions / Mago

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

return \is_int($maxBreadth) ? $maxBreadth : null;
}

/**
* Sets the maximum number of items serialized from an array or an object,
* such as the arguments of the stack frames.
*
* @param int|null $maxBreadth The number of items after which the next ones are left out, or null for no limit
*/
public function setMaxBreadth(?int $maxBreadth): self
{
return $this->updateOptions(['max_breadth' => $maxBreadth]);
}

/**
* Gets the http proxy setting.
*/
Expand Down Expand Up @@ -1292,6 +1314,7 @@
$resolver->setAllowedTypes('send_default_pii', 'bool');
$resolver->setAllowedTypes('default_integrations', 'bool');
$resolver->setAllowedTypes('max_value_length', 'int');
$resolver->setAllowedTypes('max_breadth', ['null', 'int']);
$resolver->setAllowedTypes('transport', ['null', TransportInterface::class]);
$resolver->setAllowedTypes('http_client', ['null', HttpClientInterface::class]);
$resolver->setAllowedTypes('http_proxy', ['null', 'string']);
Expand All @@ -1312,6 +1335,7 @@
$resolver->setAllowedValues('max_breadcrumbs', \Closure::fromCallable([$this, 'validateMaxBreadcrumbsOptions']));
$resolver->setAllowedValues('class_serializers', \Closure::fromCallable([$this, 'validateClassSerializersOption']));
$resolver->setAllowedValues('context_lines', \Closure::fromCallable([$this, 'validateContextLinesOption']));
$resolver->setAllowedValues('max_breadth', \Closure::fromCallable([$this, 'validateMaxBreadthOption']));
$resolver->setAllowedValues('log_flush_threshold', \Closure::fromCallable([$this, 'validateLogFlushThresholdOption']));
$resolver->setAllowedValues('metric_flush_threshold', \Closure::fromCallable([$this, 'validateMetricFlushThresholdOption']));

Expand Down Expand Up @@ -1401,6 +1425,7 @@
'in_app_include' => [],
'send_default_pii' => false,
'max_value_length' => 1024,
'max_breadth' => null,
'transport' => null,
'http_client' => null,
'http_proxy' => null,
Expand Down Expand Up @@ -1578,6 +1603,16 @@
return $contextLines === null || $contextLines >= 0;
}

/**
* Validates that the value passed to the "max_breadth" option is valid.
*
* @param int|null $maxBreadth The value to validate
*/
private function validateMaxBreadthOption(?int $maxBreadth): bool
{
return $maxBreadth === null || $maxBreadth > 0;
}

/**
* Validates that the value passed to the "log_flush_threshold" option is valid.
*
Expand Down
34 changes: 34 additions & 0 deletions src/Serializer/AbstractSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ abstract class AbstractSerializer
*/
public const WESTERN_MB_DETECT_ORDER = 'UTF-8, ASCII, ISO-8859-1, ISO-8859-2, ISO-8859-3, ISO-8859-4, ISO-8859-5, ISO-8859-6, ISO-8859-7, ISO-8859-8, ISO-8859-9, ISO-8859-10, ISO-8859-13, ISO-8859-14, ISO-8859-15, ISO-8859-16, Windows-1251, Windows-1252, Windows-1254';

/**
* The key under which the number of items left out by the `max_breadth`
* option is reported.
*/
private const TRUNCATED_KEY = '{truncated}';

/**
* The maximum depth to reach when serializing recursively.
*
Expand Down Expand Up @@ -108,8 +114,15 @@ protected function serializeRecursively($value, int $_depth = 0)

if (\is_array($value)) {
$serializedArray = [];
$maxBreadth = $this->options->getMaxBreadth();

foreach ($value as $k => $v) {
if ($maxBreadth !== null && \count($serializedArray) >= $maxBreadth) {
$serializedArray[self::TRUNCATED_KEY] = $this->formatTruncatedItems(\count($value) - $maxBreadth);

break;
}

$serializedArray[$k] = $this->serializeRecursively($v, $_depth + 1);
}

Expand Down Expand Up @@ -209,18 +222,39 @@ protected function serializeObject($object, int $_depth = 0, array $hashes = [])

$hashes[] = spl_object_hash($object);
$serializedObject = [];
$maxBreadth = $this->options->getMaxBreadth();
$truncatedItems = 0;

foreach ($object as $key => &$value) {
if ($maxBreadth !== null && \count($serializedObject) >= $maxBreadth) {
// the properties of an object cannot be counted without walking them, so only count the next ones
++$truncatedItems;

continue;
}

if (\is_object($value)) {
$serializedObject[$key] = $this->serializeObject($value, $_depth + 1, $hashes);
} else {
$serializedObject[$key] = $this->serializeRecursively($value, $_depth + 1);
}
}

if ($truncatedItems > 0) {
$serializedObject[self::TRUNCATED_KEY] = $this->formatTruncatedItems($truncatedItems);
}

return $serializedObject;
}

/**
* Describes the number of items left out because of the `max_breadth` option.
*/
private function formatTruncatedItems(int $count): string
{
return \sprintf('%d more item%s', $count, $count === 1 ? '' : 's');
}

/**
* Serializes the given value to a string.
*
Expand Down
1 change: 1 addition & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
* log_flush_threshold?: int|null,
* metric_flush_threshold?: int|null,
* max_breadcrumbs?: int,
* max_breadth?: int|null,
* max_request_body_size?: "none"|"never"|"small"|"medium"|"always",
* max_value_length?: int,
* org_id?: int|null,
Expand Down
27 changes: 27 additions & 0 deletions tests/OptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,13 @@ static function (array $integrations): array {
'setMaxValueLength',
];

yield [
'max_breadth',
10,
'getMaxBreadth',
'setMaxBreadth',
];

yield [
'transport',
new HttpTransport(new Options(), new HttpClient('foo', 'bar'), new PayloadSerializer(new Options())),
Expand Down Expand Up @@ -637,6 +644,7 @@ public function testDefaultOptionValues(): void
'send_default_pii' => false,
'data_collection' => null,
'max_value_length' => 1024,
'max_breadth' => null,
'transport' => null,
'http_client' => null,
'http_proxy' => null,
Expand Down Expand Up @@ -873,6 +881,25 @@ public static function maxBreadcrumbsOptionIsValidatedCorrectlyDataProvider(): a
];
}

/**
* @dataProvider maxBreadthOptionValidatesInputValueDataProvider
*/
public function testMaxBreadthOptionValidatesInputValue(?int $value, ?int $expectedValue): void
{
$options = new Options(['max_breadth' => $value]);

$this->assertSame($expectedValue, $options->getMaxBreadth());
}

public static function maxBreadthOptionValidatesInputValueDataProvider(): \Generator
{
yield [null, null];
yield [1, 1];
yield [10, 10];
yield [0, null];
yield [-1, null];
}

/**
* @dataProvider contextLinesOptionValidatesInputValueDataProvider
*/
Expand Down
54 changes: 53 additions & 1 deletion tests/Serializer/AbstractSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
namespace Sentry\Tests\Serializer;

use PHPUnit\Framework\TestCase;
use Sentry\Options;
use Sentry\Serializer\AbstractSerializer;
use Sentry\Serializer\RepresentationSerializerInterface;
use Sentry\Serializer\SerializerInterface;

abstract class AbstractSerializerTest extends TestCase
{
abstract protected function createSerializer(): AbstractSerializer;
abstract protected function createSerializer(?Options $options = null): AbstractSerializer;

/**
* This method is only existed because of testSerializeCallable.
Expand Down Expand Up @@ -463,6 +464,57 @@ public function testLongString(bool $serializeAllObjects): void
}
}

public function testArrayItemsAreNotLimitedByDefault(): void
{
$input = array_fill(0, 100, 'foo');

$this->assertSame($input, $this->invokeSerialization($this->createSerializer(), $input));
}

/**
* @dataProvider maxBreadthLimitsTheItemsOfAnArrayDataProvider
*/
public function testMaxBreadthLimitsTheItemsOfAnArray(int $maxBreadth, array $input, array $expectedResult): void
{
$serializer = $this->createSerializer(new Options(['max_breadth' => $maxBreadth]));

$this->assertSame($expectedResult, $this->invokeSerialization($serializer, $input));
}

public static function maxBreadthLimitsTheItemsOfAnArrayDataProvider(): \Generator
{
yield 'list longer than the limit' => [2, ['a', 'b', 'c', 'd', 'e'], ['a', 'b', '{truncated}' => '3 more items']];
yield 'map longer than the limit' => [2, ['a' => 'foo', 'b' => 'bar', 'c' => 'baz'], ['a' => 'foo', 'b' => 'bar', '{truncated}' => '1 more item']];
yield 'limit of one' => [1, ['a', 'b'], ['a', '{truncated}' => '1 more item']];
yield 'as many items as the limit' => [2, ['a', 'b'], ['a', 'b']];
yield 'fewer items than the limit' => [2, ['a'], ['a']];
yield 'empty array' => [2, [], []];
}

public function testMaxBreadthLimitsTheItemsOfNestedArrays(): void
{
$serializer = $this->createSerializer(new Options(['max_breadth' => 2]));
$input = [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i']];

$this->assertSame([['a', 'b', '{truncated}' => '1 more item'], ['d', 'e', '{truncated}' => '1 more item'], '{truncated}' => '1 more item'], $this->invokeSerialization($serializer, $input));
}

/**
* @dataProvider serializeAllObjectsDataProvider
*/
public function testMaxBreadthLimitsThePropertiesOfAnObject(bool $serializeAllObjects): void
{
$serializer = $this->createSerializer(new Options(['max_breadth' => 2]));

if ($serializeAllObjects) {
$serializer->setSerializeAllObjects(true);
}

$input = (object) ['a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'qux'];

$this->assertSame(['a' => 'foo', 'b' => 'bar', '{truncated}' => '2 more items'], $this->invokeSerialization($serializer, $input));
}

/**
* @dataProvider serializeAllObjectsDataProvider
*/
Expand Down
4 changes: 2 additions & 2 deletions tests/Serializer/RepresentationSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ public function testSerializeFloatOutsideOfIntegerRange(bool $serializeAllObject
/**
* @return RepresentationSerializer
*/
protected function createSerializer(): AbstractSerializer
protected function createSerializer(?Options $options = null): AbstractSerializer
{
return new RepresentationSerializer(new Options());
return new RepresentationSerializer($options ?? new Options());
}
}
Loading