From fd4ba99c82fe170b5a1fbd2add67c151d97bc833 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 11:03:59 +0200 Subject: [PATCH 1/9] feat(pii): add data collection options --- src/DataCollection/DataCollectionOptions.php | 266 +++++++++++++++++++ src/Options.php | 185 +++++++++++++ src/functions.php | 13 + tests/OptionsTest.php | 109 ++++++++ 4 files changed, 573 insertions(+) create mode 100644 src/DataCollection/DataCollectionOptions.php diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php new file mode 100644 index 000000000..16a993af9 --- /dev/null +++ b/src/DataCollection/DataCollectionOptions.php @@ -0,0 +1,266 @@ + + * + * @phpstan-var array{ + * user_info: bool, + * cookies: array{mode: string, terms: string[]}, + * http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}, + * http_bodies: string[], + * query_params: array{mode: string, terms: string[]}, + * gen_ai: array{inputs: bool, outputs: bool}, + * stack_frame_variables: bool, + * frame_context_lines: int + * } + */ + private $options; + + /** + * @param array $options + * + * @phpstan-param array{ + * user_info: bool, + * cookies: array{mode: string, terms: string[]}, + * http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}, + * http_bodies: string[], + * query_params: array{mode: string, terms: string[]}, + * gen_ai: array{inputs: bool, outputs: bool}, + * stack_frame_variables: bool, + * frame_context_lines: int + * } $options + */ + public function __construct(array $options) + { + $this->options = $options; + } + + public static function default(): self + { + return new self([ + 'user_info' => true, + 'cookies' => self::getDefaultKeyValueCollection(), + 'http_headers' => [ + 'request' => self::getDefaultKeyValueCollection(), + 'response' => self::getDefaultKeyValueCollection(), + ], + 'http_bodies' => self::HTTP_BODY_TYPES, + 'query_params' => self::getDefaultKeyValueCollection(), + 'gen_ai' => [ + 'inputs' => true, + 'outputs' => true, + ], + 'stack_frame_variables' => true, + 'frame_context_lines' => 5, + ]); + } + + /** + * @return array{mode: string, terms: string[]} + */ + public static function getDefaultKeyValueCollection(): array + { + return [ + 'mode' => 'denyList', + 'terms' => [], + ]; + } + + public function shouldCollectUserInfo(): bool + { + return $this->options['user_info']; + } + + public function setUserInfo(bool $userInfo): self + { + $this->options['user_info'] = $userInfo; + + return $this; + } + + /** + * @return array{mode: string, terms: string[]} + */ + public function getCookies(): array + { + return $this->options['cookies']; + } + + /** + * @param array{mode: string, terms: string[]} $cookies + */ + public function setCookies(array $cookies): self + { + $this->options['cookies'] = $cookies; + + return $this; + } + + /** + * @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} + */ + public function getHttpHeaders(): array + { + return $this->options['http_headers']; + } + + /** + * @param array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} $httpHeaders + */ + public function setHttpHeaders(array $httpHeaders): self + { + $this->options['http_headers'] = $httpHeaders; + + return $this; + } + + /** + * @return string[] + */ + public function getHttpBodies(): array + { + return $this->options['http_bodies']; + } + + /** + * @param string[] $httpBodies + */ + public function setHttpBodies(array $httpBodies): self + { + $this->options['http_bodies'] = $httpBodies; + + return $this; + } + + /** + * @return array{mode: string, terms: string[]} + */ + public function getQueryParams(): array + { + return $this->options['query_params']; + } + + /** + * @param array{mode: string, terms: string[]} $queryParams + */ + public function setQueryParams(array $queryParams): self + { + $this->options['query_params'] = $queryParams; + + return $this; + } + + /** + * @return array{inputs: bool, outputs: bool} + */ + public function getGenAi(): array + { + return $this->options['gen_ai']; + } + + /** + * @param array{inputs: bool, outputs: bool} $genAi + */ + public function setGenAi(array $genAi): self + { + $this->options['gen_ai'] = $genAi; + + return $this; + } + + public function shouldCollectStackFrameVariables(): bool + { + return $this->options['stack_frame_variables']; + } + + public function setStackFrameVariables(bool $stackFrameVariables): self + { + $this->options['stack_frame_variables'] = $stackFrameVariables; + + return $this; + } + + public function getFrameContextLines(): int + { + return $this->options['frame_context_lines']; + } + + public function setFrameContextLines(int $frameContextLines): self + { + $this->options['frame_context_lines'] = $frameContextLines; + + return $this; + } + + /** + * @return array + * + * @phpstan-return array{ + * user_info: bool, + * cookies: array{mode: string, terms: string[]}, + * http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}, + * http_bodies: string[], + * query_params: array{mode: string, terms: string[]}, + * gen_ai: array{inputs: bool, outputs: bool}, + * stack_frame_variables: bool, + * frame_context_lines: int + * } + */ + public function toArray(): array + { + return $this->options; + } +} diff --git a/src/Options.php b/src/Options.php index 7e8afbfc6..e6a0279da 100644 --- a/src/Options.php +++ b/src/Options.php @@ -6,6 +6,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use Sentry\DataCollection\DataCollectionOptions; use Sentry\HttpClient\HttpClientInterface; use Sentry\Integration\ErrorListenerIntegration; use Sentry\Integration\IntegrationInterface; @@ -405,6 +406,23 @@ public function setContextLines(?int $contextLines): self return $this; } + public function getDataCollection(): DataCollectionOptions + { + /** @var DataCollectionOptions $dataCollection */ + $dataCollection = $this->options['data_collection']; + + return $dataCollection; + } + + public function setDataCollection(DataCollectionOptions $dataCollection): self + { + $options = array_merge($this->options, ['data_collection' => $dataCollection]); + + $this->options = $this->resolver->resolve($options); + + return $this; + } + /** * Gets the environment. */ @@ -1397,6 +1415,8 @@ public function setTracesSampler(?callable $sampler): self { $options = array_merge($this->options, ['traces_sampler' => $sampler]); + // resolve produces and we expect + // @mago-ignore analysis:property-type-coercion $this->options = $this->resolver->resolve($options); return $this; @@ -1493,6 +1513,7 @@ private function configureOptions(OptionsResolver $resolver): void 'capture_silenced_errors' => false, 'max_request_body_size' => 'medium', 'class_serializers' => [], + 'data_collection' => DataCollectionOptions::default(), ]); $resolver->setAllowedTypes('prefixes', 'string[]'); @@ -1549,6 +1570,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('capture_silenced_errors', 'bool'); $resolver->setAllowedTypes('max_request_body_size', 'string'); $resolver->setAllowedTypes('class_serializers', 'array'); + $resolver->setAllowedTypes('data_collection', ['array', DataCollectionOptions::class]); $resolver->setAllowedValues('max_request_body_size', ['none', 'never', 'small', 'medium', 'always']); $resolver->setAllowedValues('dsn', \Closure::fromCallable([$this, 'validateDsnOption'])); @@ -1559,6 +1581,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedValues('metric_flush_threshold', \Closure::fromCallable([$this, 'validateMetricFlushThresholdOption'])); $resolver->setNormalizer('dsn', \Closure::fromCallable([$this, 'normalizeDsnOption'])); + $resolver->setNormalizer('data_collection', \Closure::fromCallable([$this, 'normalizeDataCollectionOption'])); $resolver->setNormalizer('prefixes', function (SymfonyOptions $options, array $value) { return array_map([$this, 'normalizeAbsolutePath'], $value); @@ -1620,6 +1643,168 @@ private function normalizeSpotlightUrl(SymfonyOptions $options, string $url): st return $url; } + /** + * @param mixed $value + */ + private function normalizeDataCollectionOption(SymfonyOptions $options, $value): DataCollectionOptions + { + if ($value instanceof DataCollectionOptions) { + return $value; + } + + /** @var array $value */ + $resolvedOptions = $this->resolveDataCollectionOptions($value); + + return new DataCollectionOptions([ + 'user_info' => $resolvedOptions['user_info'], + 'cookies' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['cookies']), + 'http_headers' => $this->normalizeHttpHeadersDataCollectionOptions($resolvedOptions['http_headers']), + 'http_bodies' => $resolvedOptions['http_bodies'], + 'query_params' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['query_params']), + 'gen_ai' => $this->normalizeGenAiDataCollectionOptions($resolvedOptions['gen_ai']), + 'stack_frame_variables' => $resolvedOptions['stack_frame_variables'], + 'frame_context_lines' => $resolvedOptions['frame_context_lines'], + ]); + } + + /** + * @param array $value + * + * @return array + * + * @phpstan-return array{ + * user_info: bool, + * cookies: array, + * http_headers: array, + * http_bodies: string[], + * query_params: array, + * gen_ai: array, + * stack_frame_variables: bool, + * frame_context_lines: int + * } + */ + private function resolveDataCollectionOptions(array $value): array + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'user_info' => true, + 'cookies' => [], + 'http_headers' => [], + 'http_bodies' => DataCollectionOptions::HTTP_BODY_TYPES, + 'query_params' => [], + 'gen_ai' => [], + 'stack_frame_variables' => true, + 'frame_context_lines' => 5, + ]); + $resolver->setAllowedTypes('user_info', 'bool'); + $resolver->setAllowedTypes('cookies', 'array'); + $resolver->setAllowedTypes('http_headers', 'array'); + $resolver->setAllowedTypes('http_bodies', 'string[]'); + $resolver->setAllowedTypes('query_params', 'array'); + $resolver->setAllowedTypes('gen_ai', 'array'); + $resolver->setAllowedTypes('stack_frame_variables', 'bool'); + $resolver->setAllowedTypes('frame_context_lines', 'int'); + $resolver->setAllowedValues('http_bodies', static function (array $value): bool { + /** @var string[] $value */ + return \count(array_diff($value, DataCollectionOptions::HTTP_BODY_TYPES)) === 0; + }); + $resolver->setAllowedValues('frame_context_lines', static function (int $value): bool { + return $value >= 0; + }); + + /** @var array{ + * user_info: bool, + * cookies: array, + * http_headers: array, + * http_bodies: string[], + * query_params: array, + * gen_ai: array, + * stack_frame_variables: bool, + * frame_context_lines: int + * } $resolvedOptions + */ + $resolvedOptions = $resolver->resolve($value); + + return $resolvedOptions; + } + + /** + * @param array $value + * + * @return array{mode: string, terms: string[]} + */ + private function normalizeKeyValueDataCollectionOptions(array $value): array + { + $resolver = new OptionsResolver(); + $resolver->setDefaults(DataCollectionOptions::getDefaultKeyValueCollection()); + $resolver->setAllowedTypes('mode', 'string'); + $resolver->setAllowedTypes('terms', 'string[]'); + $resolver->setAllowedValues('mode', [ + 'off', + 'denyList', + 'allowList', + ]); + + /** @var array{mode: string, terms: string[]} $resolvedOptions */ + $resolvedOptions = $resolver->resolve($value); + + return $resolvedOptions; + } + + /** + * @param array $value + * + * @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} + */ + private function normalizeHttpHeadersDataCollectionOptions(array $value): array + { + if (!isset($value['request']) && !isset($value['response'])) { + $headers = $this->normalizeKeyValueDataCollectionOptions($value); + + return [ + 'request' => $headers, + 'response' => $headers, + ]; + } + + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'request' => [], + 'response' => [], + ]); + $resolver->setAllowedTypes('request', 'array'); + $resolver->setAllowedTypes('response', 'array'); + + /** @var array{request: array, response: array} $resolvedOptions */ + $resolvedOptions = $resolver->resolve($value); + + return [ + 'request' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['request']), + 'response' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['response']), + ]; + } + + /** + * @param array $value + * + * @return array{inputs: bool, outputs: bool} + */ + private function normalizeGenAiDataCollectionOptions(array $value): array + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'inputs' => true, + 'outputs' => true, + ]); + $resolver->setAllowedTypes('inputs', 'bool'); + $resolver->setAllowedTypes('outputs', 'bool'); + + /** @var array{inputs: bool, outputs: bool} $resolvedOptions */ + $resolvedOptions = $resolver->resolve($value); + + return $resolvedOptions; + } + /** * Normalizes the DSN option by parsing the host, public and secret keys and * an optional path. diff --git a/src/functions.php b/src/functions.php index ecbc8a349..405371963 100644 --- a/src/functions.php +++ b/src/functions.php @@ -31,6 +31,19 @@ * before_send_transaction?: callable, * capture_silenced_errors?: bool, * context_lines?: int|null, + * data_collection?: DataCollection\DataCollectionOptions|array{ + * user_info?: bool, + * cookies?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * http_headers?: array{mode?: "off"|"denyList"|"allowList", terms?: array}|array{ + * request?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * response?: array{mode?: "off"|"denyList"|"allowList", terms?: array} + * }, + * http_bodies?: array<"incomingRequest"|"outgoingRequest"|"incomingResponse"|"outgoingResponse">, + * query_params?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * gen_ai?: array{inputs?: bool, outputs?: bool}, + * stack_frame_variables?: bool, + * frame_context_lines?: int + * }, * default_integrations?: bool, * dsn?: string|bool|Dsn|null, * enable_logs?: bool, diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index 9b48eb8bd..972996fb8 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -6,12 +6,14 @@ use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; +use Sentry\DataCollection\DataCollectionOptions; use Sentry\Dsn; use Sentry\HttpClient\HttpClient; use Sentry\Options; use Sentry\Serializer\PayloadSerializer; use Sentry\Transport\HttpTransport; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; final class OptionsTest extends TestCase { @@ -191,6 +193,13 @@ static function (): void {}, 'setContextLines', ]; + yield [ + 'data_collection', + DataCollectionOptions::default()->setUserInfo(false), + 'getDataCollection', + 'setDataCollection', + ]; + yield [ 'environment', 'foo', @@ -676,6 +685,106 @@ public static function contextLinesOptionValidatesInputValueDataProvider(): \Gen ]; } + public function testDataCollectionOptionDefaults(): void + { + $dataCollection = (new Options())->getDataCollection(); + + $this->assertTrue($dataCollection->shouldCollectUserInfo()); + $this->assertSame('denyList', $dataCollection->getCookies()['mode']); + $this->assertSame([], $dataCollection->getCookies()['terms']); + $this->assertSame('denyList', $dataCollection->getHttpHeaders()['request']['mode']); + $this->assertSame('denyList', $dataCollection->getHttpHeaders()['response']['mode']); + $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $dataCollection->getHttpBodies()); + $this->assertSame('denyList', $dataCollection->getQueryParams()['mode']); + $this->assertTrue($dataCollection->getGenAi()['inputs']); + $this->assertTrue($dataCollection->getGenAi()['outputs']); + $this->assertTrue($dataCollection->shouldCollectStackFrameVariables()); + $this->assertSame(5, $dataCollection->getFrameContextLines()); + } + + public function testDataCollectionOptionNormalizesArrayConfiguration(): void + { + $dataCollection = (new Options([ + 'data_collection' => [ + 'user_info' => false, + 'cookies' => [ + 'mode' => 'allowList', + 'terms' => ['session_id'], + ], + 'http_headers' => [ + 'request' => [ + 'mode' => 'off', + 'terms' => ['authorization'], + ], + 'response' => [ + 'mode' => 'denyList', + 'terms' => ['set-cookie'], + ], + ], + 'http_bodies' => ['incomingRequest'], + 'query_params' => [ + 'mode' => 'off', + ], + 'gen_ai' => [ + 'inputs' => false, + ], + 'stack_frame_variables' => false, + 'frame_context_lines' => 0, + ], + ]))->getDataCollection(); + + $this->assertFalse($dataCollection->shouldCollectUserInfo()); + $this->assertSame('allowList', $dataCollection->getCookies()['mode']); + $this->assertSame(['session_id'], $dataCollection->getCookies()['terms']); + $this->assertSame('off', $dataCollection->getHttpHeaders()['request']['mode']); + $this->assertSame(['authorization'], $dataCollection->getHttpHeaders()['request']['terms']); + $this->assertSame('denyList', $dataCollection->getHttpHeaders()['response']['mode']); + $this->assertSame(['set-cookie'], $dataCollection->getHttpHeaders()['response']['terms']); + $this->assertSame(['incomingRequest'], $dataCollection->getHttpBodies()); + $this->assertSame('off', $dataCollection->getQueryParams()['mode']); + $this->assertFalse($dataCollection->getGenAi()['inputs']); + $this->assertTrue($dataCollection->getGenAi()['outputs']); + $this->assertFalse($dataCollection->shouldCollectStackFrameVariables()); + $this->assertSame(0, $dataCollection->getFrameContextLines()); + } + + public function testDataCollectionOptionAppliesSingleHttpHeadersConfigurationToBothDirections(): void + { + $dataCollection = (new Options([ + 'data_collection' => [ + 'http_headers' => [ + 'mode' => 'allowList', + 'terms' => ['x-request-id'], + ], + ], + ]))->getDataCollection(); + + $this->assertSame('allowList', $dataCollection->getHttpHeaders()['request']['mode']); + $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()['request']['terms']); + $this->assertSame('allowList', $dataCollection->getHttpHeaders()['response']['mode']); + $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()['response']['terms']); + } + + public function testDataCollectionOptionAcceptsConfigurationObjects(): void + { + $dataCollection = DataCollectionOptions::default() + ->setUserInfo(false) + ->setCookies(['mode' => 'off', 'terms' => []]) + ->setHttpHeaders([ + 'request' => ['mode' => 'allowList', 'terms' => ['x-request-id']], + 'response' => ['mode' => 'denyList', 'terms' => []], + ]) + ->setHttpBodies([]) + ->setQueryParams(['mode' => 'denyList', 'terms' => ['token']]) + ->setGenAi(['inputs' => false, 'outputs' => false]) + ->setStackFrameVariables(false) + ->setFrameContextLines(1); + + $options = new Options(['data_collection' => $dataCollection]); + + $this->assertSame($dataCollection, $options->getDataCollection()); + } + /** * @dataProvider logFlushThresholdOptionIsValidatedCorrectlyDataProvider */ From 80ce7d2714c8527e345a700b5e97da53ac716151 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 11:19:56 +0200 Subject: [PATCH 2/9] cleanup --- src/DataCollection/DataCollectionOptions.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php index 16a993af9..549b4469a 100644 --- a/src/DataCollection/DataCollectionOptions.php +++ b/src/DataCollection/DataCollectionOptions.php @@ -4,9 +4,6 @@ namespace Sentry\DataCollection; -/** - * Configuration for data automatically collected by the SDK. - */ final class DataCollectionOptions { /** @@ -19,9 +16,6 @@ final class DataCollectionOptions 'outgoingResponse', ]; - /** - * Terms used to identify sensitive key-value data which must be filtered. - */ public const SENSITIVE_DEFAULTS = [ 'auth', 'token', @@ -42,9 +36,6 @@ final class DataCollectionOptions 'identity', ]; - /** - * Additional deny terms users may opt into for user-identifying values. - */ public const EXTENDED_DENY_TERMS = [ 'forwarded', '-ip', From dc79754cd878ae2e75e2b1d5b95e70151a3d417d Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 11:24:22 +0200 Subject: [PATCH 3/9] CS --- tests/OptionsTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index 972996fb8..cc9a10719 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -13,7 +13,6 @@ use Sentry\Serializer\PayloadSerializer; use Sentry\Transport\HttpTransport; use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; -use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; final class OptionsTest extends TestCase { From ee3346463447b745e4a0c2a114407a93817c811c Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 13:08:16 +0200 Subject: [PATCH 4/9] update http header setter to use same behaviour as constructor --- src/DataCollection/DataCollectionOptions.php | 4 +- .../DataCollectionOptionsNormalizer.php | 94 +++++++++++++++++++ src/Options.php | 86 +---------------- .../DataCollectionOptionsNormalizerTest.php | 48 ++++++++++ 4 files changed, 149 insertions(+), 83 deletions(-) create mode 100644 src/DataCollection/DataCollectionOptionsNormalizer.php create mode 100644 tests/DataCollection/DataCollectionOptionsNormalizerTest.php diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php index 549b4469a..ec9180dae 100644 --- a/src/DataCollection/DataCollectionOptions.php +++ b/src/DataCollection/DataCollectionOptions.php @@ -149,11 +149,11 @@ public function getHttpHeaders(): array } /** - * @param array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} $httpHeaders + * @param array{mode?: string, terms?: string[]}|array{request?: array{mode?: string, terms?: string[]}, response?: array{mode?: string, terms?: string[]}} $httpHeaders */ public function setHttpHeaders(array $httpHeaders): self { - $this->options['http_headers'] = $httpHeaders; + $this->options['http_headers'] = DataCollectionOptionsNormalizer::normalizeHttpHeaders($httpHeaders); return $this; } diff --git a/src/DataCollection/DataCollectionOptionsNormalizer.php b/src/DataCollection/DataCollectionOptionsNormalizer.php new file mode 100644 index 000000000..83bdc948b --- /dev/null +++ b/src/DataCollection/DataCollectionOptionsNormalizer.php @@ -0,0 +1,94 @@ + $value + * + * @return array{mode: string, terms: string[]} + */ + public static function normalizeKeyValueCollection(array $value): array + { + $resolver = new OptionsResolver(); + $resolver->setDefaults(DataCollectionOptions::getDefaultKeyValueCollection()); + $resolver->setAllowedTypes('mode', 'string'); + $resolver->setAllowedTypes('terms', 'string[]'); + $resolver->setAllowedValues('mode', [ + 'off', + 'denyList', + 'allowList', + ]); + + /** @var array{mode: string, terms: string[]} $resolvedOptions */ + $resolvedOptions = $resolver->resolve($value); + + return $resolvedOptions; + } + + /** + * @param array $value + * + * @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} + */ + public static function normalizeHttpHeaders(array $value): array + { + if (!isset($value['request']) && !isset($value['response'])) { + $headers = self::normalizeKeyValueCollection($value); + + return [ + 'request' => $headers, + 'response' => $headers, + ]; + } + + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'request' => [], + 'response' => [], + ]); + $resolver->setAllowedTypes('request', 'array'); + $resolver->setAllowedTypes('response', 'array'); + + /** @var array{request: array, response: array} $resolvedOptions */ + $resolvedOptions = $resolver->resolve($value); + + return [ + 'request' => self::normalizeKeyValueCollection($resolvedOptions['request']), + 'response' => self::normalizeKeyValueCollection($resolvedOptions['response']), + ]; + } + + /** + * @param array $value + * + * @return array{inputs: bool, outputs: bool} + */ + public static function normalizeGenAi(array $value): array + { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'inputs' => true, + 'outputs' => true, + ]); + $resolver->setAllowedTypes('inputs', 'bool'); + $resolver->setAllowedTypes('outputs', 'bool'); + + /** @var array{inputs: bool, outputs: bool} $resolvedOptions */ + $resolvedOptions = $resolver->resolve($value); + + return $resolvedOptions; + } +} diff --git a/src/Options.php b/src/Options.php index e6a0279da..72cd25e24 100644 --- a/src/Options.php +++ b/src/Options.php @@ -7,6 +7,7 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Sentry\DataCollection\DataCollectionOptions; +use Sentry\DataCollection\DataCollectionOptionsNormalizer; use Sentry\HttpClient\HttpClientInterface; use Sentry\Integration\ErrorListenerIntegration; use Sentry\Integration\IntegrationInterface; @@ -1657,11 +1658,11 @@ private function normalizeDataCollectionOption(SymfonyOptions $options, $value): return new DataCollectionOptions([ 'user_info' => $resolvedOptions['user_info'], - 'cookies' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['cookies']), - 'http_headers' => $this->normalizeHttpHeadersDataCollectionOptions($resolvedOptions['http_headers']), + 'cookies' => DataCollectionOptionsNormalizer::normalizeKeyValueCollection($resolvedOptions['cookies']), + 'http_headers' => DataCollectionOptionsNormalizer::normalizeHttpHeaders($resolvedOptions['http_headers']), 'http_bodies' => $resolvedOptions['http_bodies'], - 'query_params' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['query_params']), - 'gen_ai' => $this->normalizeGenAiDataCollectionOptions($resolvedOptions['gen_ai']), + 'query_params' => DataCollectionOptionsNormalizer::normalizeKeyValueCollection($resolvedOptions['query_params']), + 'gen_ai' => DataCollectionOptionsNormalizer::normalizeGenAi($resolvedOptions['gen_ai']), 'stack_frame_variables' => $resolvedOptions['stack_frame_variables'], 'frame_context_lines' => $resolvedOptions['frame_context_lines'], ]); @@ -1728,83 +1729,6 @@ private function resolveDataCollectionOptions(array $value): array return $resolvedOptions; } - /** - * @param array $value - * - * @return array{mode: string, terms: string[]} - */ - private function normalizeKeyValueDataCollectionOptions(array $value): array - { - $resolver = new OptionsResolver(); - $resolver->setDefaults(DataCollectionOptions::getDefaultKeyValueCollection()); - $resolver->setAllowedTypes('mode', 'string'); - $resolver->setAllowedTypes('terms', 'string[]'); - $resolver->setAllowedValues('mode', [ - 'off', - 'denyList', - 'allowList', - ]); - - /** @var array{mode: string, terms: string[]} $resolvedOptions */ - $resolvedOptions = $resolver->resolve($value); - - return $resolvedOptions; - } - - /** - * @param array $value - * - * @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} - */ - private function normalizeHttpHeadersDataCollectionOptions(array $value): array - { - if (!isset($value['request']) && !isset($value['response'])) { - $headers = $this->normalizeKeyValueDataCollectionOptions($value); - - return [ - 'request' => $headers, - 'response' => $headers, - ]; - } - - $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'request' => [], - 'response' => [], - ]); - $resolver->setAllowedTypes('request', 'array'); - $resolver->setAllowedTypes('response', 'array'); - - /** @var array{request: array, response: array} $resolvedOptions */ - $resolvedOptions = $resolver->resolve($value); - - return [ - 'request' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['request']), - 'response' => $this->normalizeKeyValueDataCollectionOptions($resolvedOptions['response']), - ]; - } - - /** - * @param array $value - * - * @return array{inputs: bool, outputs: bool} - */ - private function normalizeGenAiDataCollectionOptions(array $value): array - { - $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'inputs' => true, - 'outputs' => true, - ]); - $resolver->setAllowedTypes('inputs', 'bool'); - $resolver->setAllowedTypes('outputs', 'bool'); - - /** @var array{inputs: bool, outputs: bool} $resolvedOptions */ - $resolvedOptions = $resolver->resolve($value); - - return $resolvedOptions; - } - /** * Normalizes the DSN option by parsing the host, public and secret keys and * an optional path. diff --git a/tests/DataCollection/DataCollectionOptionsNormalizerTest.php b/tests/DataCollection/DataCollectionOptionsNormalizerTest.php new file mode 100644 index 000000000..40f860b3f --- /dev/null +++ b/tests/DataCollection/DataCollectionOptionsNormalizerTest.php @@ -0,0 +1,48 @@ + 'allowList', + 'terms' => ['x-request-id'], + ]); + + $this->assertSame('allowList', $httpHeaders['request']['mode']); + $this->assertSame(['x-request-id'], $httpHeaders['request']['terms']); + $this->assertSame('allowList', $httpHeaders['response']['mode']); + $this->assertSame(['x-request-id'], $httpHeaders['response']['terms']); + } + + public function testNormalizeHttpHeadersMergesDirectionDefaults(): void + { + $httpHeaders = DataCollectionOptionsNormalizer::normalizeHttpHeaders([ + 'request' => [ + 'mode' => 'off', + ], + ]); + + $this->assertSame('off', $httpHeaders['request']['mode']); + $this->assertSame([], $httpHeaders['request']['terms']); + $this->assertSame('denyList', $httpHeaders['response']['mode']); + $this->assertSame([], $httpHeaders['response']['terms']); + } + + public function testNormalizeGenAiMergesDefaults(): void + { + $genAi = DataCollectionOptionsNormalizer::normalizeGenAi([ + 'inputs' => false, + ]); + + $this->assertFalse($genAi['inputs']); + $this->assertTrue($genAi['outputs']); + } +} From 143df867f6740a907185ba001213e0df162711e8 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 17:53:24 +0200 Subject: [PATCH 5/9] update data structure --- src/DataCollection/DataCollectionOptions.php | 230 +++++++++++------- .../DataCollectionOptionsNormalizer.php | 94 ------- src/DataCollection/GenAi.php | 77 ++++++ src/DataCollection/HttpHeaders.php | 120 +++++++++ .../KeyValueCollectionBehavior.php | 114 +++++++++ src/Options.php | 75 +----- src/functions.php | 12 +- .../DataCollectionOptionsNormalizerTest.php | 48 ---- .../DataCollectionOptionsTest.php | 105 ++++++++ tests/OptionsTest.php | 40 ++- 10 files changed, 574 insertions(+), 341 deletions(-) delete mode 100644 src/DataCollection/DataCollectionOptionsNormalizer.php create mode 100644 src/DataCollection/GenAi.php create mode 100644 src/DataCollection/HttpHeaders.php create mode 100644 src/DataCollection/KeyValueCollectionBehavior.php delete mode 100644 tests/DataCollection/DataCollectionOptionsNormalizerTest.php create mode 100644 tests/DataCollection/DataCollectionOptionsTest.php diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php index ec9180dae..cc3fce5d8 100644 --- a/src/DataCollection/DataCollectionOptions.php +++ b/src/DataCollection/DataCollectionOptions.php @@ -4,6 +4,24 @@ namespace Sentry\DataCollection; +use Symfony\Component\OptionsResolver\Options as SymfonyOptions; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @phpstan-import-type KeyValueCollection from KeyValueCollectionBehavior + * + * @phpstan-type HttpHeadersOption KeyValueCollection|array{request?: KeyValueCollection|KeyValueCollectionBehavior, response?: KeyValueCollection|KeyValueCollectionBehavior} + * @phpstan-type ResolvedDataCollectionOptions array{ + * user_info: bool, + * cookies: KeyValueCollectionBehavior, + * http_headers: HttpHeaders, + * http_bodies: string[], + * query_params: KeyValueCollectionBehavior, + * gen_ai: GenAi, + * stack_frame_variables: bool, + * frame_context_lines: int + * } + */ final class DataCollectionOptions { /** @@ -47,67 +65,29 @@ final class DataCollectionOptions /** * @var array * - * @phpstan-var array{ - * user_info: bool, - * cookies: array{mode: string, terms: string[]}, - * http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}, - * http_bodies: string[], - * query_params: array{mode: string, terms: string[]}, - * gen_ai: array{inputs: bool, outputs: bool}, - * stack_frame_variables: bool, - * frame_context_lines: int - * } + * @phpstan-var ResolvedDataCollectionOptions */ private $options; + /** + * @var OptionsResolver + */ + private $resolver; + /** * @param array $options - * - * @phpstan-param array{ - * user_info: bool, - * cookies: array{mode: string, terms: string[]}, - * http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}, - * http_bodies: string[], - * query_params: array{mode: string, terms: string[]}, - * gen_ai: array{inputs: bool, outputs: bool}, - * stack_frame_variables: bool, - * frame_context_lines: int - * } $options */ - public function __construct(array $options) + public function __construct(array $options = []) { - $this->options = $options; - } + $this->resolver = new OptionsResolver(); + $this->configureOptions($this->resolver); - public static function default(): self - { - return new self([ - 'user_info' => true, - 'cookies' => self::getDefaultKeyValueCollection(), - 'http_headers' => [ - 'request' => self::getDefaultKeyValueCollection(), - 'response' => self::getDefaultKeyValueCollection(), - ], - 'http_bodies' => self::HTTP_BODY_TYPES, - 'query_params' => self::getDefaultKeyValueCollection(), - 'gen_ai' => [ - 'inputs' => true, - 'outputs' => true, - ], - 'stack_frame_variables' => true, - 'frame_context_lines' => 5, - ]); + $this->options = $this->resolveOptions($options); } - /** - * @return array{mode: string, terms: string[]} - */ - public static function getDefaultKeyValueCollection(): array + public static function default(): self { - return [ - 'mode' => 'denyList', - 'terms' => [], - ]; + return new self(); } public function shouldCollectUserInfo(): bool @@ -117,43 +97,39 @@ public function shouldCollectUserInfo(): bool public function setUserInfo(bool $userInfo): self { - $this->options['user_info'] = $userInfo; + $this->options = $this->resolveOptions(array_merge($this->options, ['user_info' => $userInfo])); return $this; } - /** - * @return array{mode: string, terms: string[]} - */ - public function getCookies(): array + public function getCookies(): KeyValueCollectionBehavior { return $this->options['cookies']; } /** - * @param array{mode: string, terms: string[]} $cookies + * @param KeyValueCollection|KeyValueCollectionBehavior $cookies */ - public function setCookies(array $cookies): self + public function setCookies($cookies): self { - $this->options['cookies'] = $cookies; + $this->options = $this->resolveOptions(array_merge($this->options, ['cookies' => $cookies])); return $this; } - /** - * @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} - */ - public function getHttpHeaders(): array + public function getHttpHeaders(): HttpHeaders { return $this->options['http_headers']; } /** - * @param array{mode?: string, terms?: string[]}|array{request?: array{mode?: string, terms?: string[]}, response?: array{mode?: string, terms?: string[]}} $httpHeaders + * @param array|HttpHeaders $httpHeaders + * + * @phpstan-param HttpHeadersOption|HttpHeaders $httpHeaders */ - public function setHttpHeaders(array $httpHeaders): self + public function setHttpHeaders($httpHeaders): self { - $this->options['http_headers'] = DataCollectionOptionsNormalizer::normalizeHttpHeaders($httpHeaders); + $this->options = $this->resolveOptions(array_merge($this->options, ['http_headers' => $httpHeaders])); return $this; } @@ -167,37 +143,31 @@ public function getHttpBodies(): array } /** - * @param string[] $httpBodies + * @param string[]|null $httpBodies */ - public function setHttpBodies(array $httpBodies): self + public function setHttpBodies(?array $httpBodies): self { - $this->options['http_bodies'] = $httpBodies; + $this->options = $this->resolveOptions(array_merge($this->options, ['http_bodies' => $httpBodies])); return $this; } - /** - * @return array{mode: string, terms: string[]} - */ - public function getQueryParams(): array + public function getQueryParams(): KeyValueCollectionBehavior { return $this->options['query_params']; } /** - * @param array{mode: string, terms: string[]} $queryParams + * @param KeyValueCollection|KeyValueCollectionBehavior $queryParams */ - public function setQueryParams(array $queryParams): self + public function setQueryParams($queryParams): self { - $this->options['query_params'] = $queryParams; + $this->options = $this->resolveOptions(array_merge($this->options, ['query_params' => $queryParams])); return $this; } - /** - * @return array{inputs: bool, outputs: bool} - */ - public function getGenAi(): array + public function getGenAi(): GenAi { return $this->options['gen_ai']; } @@ -207,7 +177,7 @@ public function getGenAi(): array */ public function setGenAi(array $genAi): self { - $this->options['gen_ai'] = $genAi; + $this->options = $this->resolveOptions(array_merge($this->options, ['gen_ai' => $genAi])); return $this; } @@ -219,7 +189,7 @@ public function shouldCollectStackFrameVariables(): bool public function setStackFrameVariables(bool $stackFrameVariables): self { - $this->options['stack_frame_variables'] = $stackFrameVariables; + $this->options = $this->resolveOptions(array_merge($this->options, ['stack_frame_variables' => $stackFrameVariables])); return $this; } @@ -231,27 +201,99 @@ public function getFrameContextLines(): int public function setFrameContextLines(int $frameContextLines): self { - $this->options['frame_context_lines'] = $frameContextLines; + $this->options = $this->resolveOptions(array_merge($this->options, ['frame_context_lines' => $frameContextLines])); return $this; } + private function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'user_info' => true, + 'cookies' => new KeyValueCollectionBehavior(), + 'http_headers' => new HttpHeaders(), + 'http_bodies' => self::HTTP_BODY_TYPES, + 'query_params' => new KeyValueCollectionBehavior(), + 'gen_ai' => new GenAi(), + 'stack_frame_variables' => true, + 'frame_context_lines' => 5, + ]); + $resolver->setAllowedTypes('user_info', 'bool'); + $resolver->setAllowedTypes('cookies', ['array', KeyValueCollectionBehavior::class]); + $resolver->setAllowedTypes('http_headers', ['array', HttpHeaders::class]); + $resolver->setAllowedTypes('http_bodies', ['null', 'string[]']); + $resolver->setAllowedTypes('query_params', ['array', KeyValueCollectionBehavior::class]); + $resolver->setAllowedTypes('gen_ai', ['array', GenAi::class]); + $resolver->setAllowedTypes('stack_frame_variables', 'bool'); + $resolver->setAllowedTypes('frame_context_lines', 'int'); + $resolver->setAllowedValues('http_bodies', static function (?array $value): bool { + if ($value === null) { + return true; + } + + /** @var string[] $value */ + return \count(array_diff($value, self::HTTP_BODY_TYPES)) === 0; + }); + $resolver->setAllowedValues('frame_context_lines', static function (int $value): bool { + return $value >= 0; + }); + $resolver->setNormalizer('cookies', \Closure::fromCallable([self::class, 'normalizeKeyValueCollection'])); + $resolver->setNormalizer('http_headers', \Closure::fromCallable([self::class, 'normalizeHttpHeaders'])); + $resolver->setNormalizer('http_bodies', static function (SymfonyOptions $options, ?array $value): array { + return $value ?? self::HTTP_BODY_TYPES; + }); + $resolver->setNormalizer('query_params', \Closure::fromCallable([self::class, 'normalizeKeyValueCollection'])); + $resolver->setNormalizer('gen_ai', \Closure::fromCallable([self::class, 'normalizeGenAi'])); + } + /** + * @param array $options + * * @return array * - * @phpstan-return array{ - * user_info: bool, - * cookies: array{mode: string, terms: string[]}, - * http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}, - * http_bodies: string[], - * query_params: array{mode: string, terms: string[]}, - * gen_ai: array{inputs: bool, outputs: bool}, - * stack_frame_variables: bool, - * frame_context_lines: int - * } + * @phpstan-return ResolvedDataCollectionOptions + */ + private function resolveOptions(array $options): array + { + /** @var ResolvedDataCollectionOptions $resolvedOptions */ + $resolvedOptions = $this->resolver->resolve($options); + + return $resolvedOptions; + } + + /** + * @param array|HttpHeaders $value */ - public function toArray(): array + private static function normalizeHttpHeaders(SymfonyOptions $options, $value): HttpHeaders { - return $this->options; + if ($value instanceof HttpHeaders) { + return $value; + } + + return new HttpHeaders($value); + } + + /** + * @param array|KeyValueCollectionBehavior $value + */ + private static function normalizeKeyValueCollection(SymfonyOptions $options, $value): KeyValueCollectionBehavior + { + if ($value instanceof KeyValueCollectionBehavior) { + return $value; + } + + return new KeyValueCollectionBehavior($value); + } + + /** + * @param array|GenAi $value + */ + private static function normalizeGenAi(SymfonyOptions $options, $value): GenAi + { + if ($value instanceof GenAi) { + return $value; + } + + return new GenAi($value); } } diff --git a/src/DataCollection/DataCollectionOptionsNormalizer.php b/src/DataCollection/DataCollectionOptionsNormalizer.php deleted file mode 100644 index 83bdc948b..000000000 --- a/src/DataCollection/DataCollectionOptionsNormalizer.php +++ /dev/null @@ -1,94 +0,0 @@ - $value - * - * @return array{mode: string, terms: string[]} - */ - public static function normalizeKeyValueCollection(array $value): array - { - $resolver = new OptionsResolver(); - $resolver->setDefaults(DataCollectionOptions::getDefaultKeyValueCollection()); - $resolver->setAllowedTypes('mode', 'string'); - $resolver->setAllowedTypes('terms', 'string[]'); - $resolver->setAllowedValues('mode', [ - 'off', - 'denyList', - 'allowList', - ]); - - /** @var array{mode: string, terms: string[]} $resolvedOptions */ - $resolvedOptions = $resolver->resolve($value); - - return $resolvedOptions; - } - - /** - * @param array $value - * - * @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}} - */ - public static function normalizeHttpHeaders(array $value): array - { - if (!isset($value['request']) && !isset($value['response'])) { - $headers = self::normalizeKeyValueCollection($value); - - return [ - 'request' => $headers, - 'response' => $headers, - ]; - } - - $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'request' => [], - 'response' => [], - ]); - $resolver->setAllowedTypes('request', 'array'); - $resolver->setAllowedTypes('response', 'array'); - - /** @var array{request: array, response: array} $resolvedOptions */ - $resolvedOptions = $resolver->resolve($value); - - return [ - 'request' => self::normalizeKeyValueCollection($resolvedOptions['request']), - 'response' => self::normalizeKeyValueCollection($resolvedOptions['response']), - ]; - } - - /** - * @param array $value - * - * @return array{inputs: bool, outputs: bool} - */ - public static function normalizeGenAi(array $value): array - { - $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'inputs' => true, - 'outputs' => true, - ]); - $resolver->setAllowedTypes('inputs', 'bool'); - $resolver->setAllowedTypes('outputs', 'bool'); - - /** @var array{inputs: bool, outputs: bool} $resolvedOptions */ - $resolvedOptions = $resolver->resolve($value); - - return $resolvedOptions; - } -} diff --git a/src/DataCollection/GenAi.php b/src/DataCollection/GenAi.php new file mode 100644 index 000000000..36b1e9124 --- /dev/null +++ b/src/DataCollection/GenAi.php @@ -0,0 +1,77 @@ + $options + */ + public function __construct(array $options = []) + { + /** @var array{inputs: bool, outputs: bool} $opts */ + $opts = self::getResolverInstance()->resolve($options); + $this->inputs = $opts['inputs']; + $this->outputs = $opts['outputs']; + } + + public function setInputs(bool $value): self + { + $this->inputs = $value; + + return $this; + } + + public function getInputs(): bool + { + return $this->inputs; + } + + public function setOutputs(bool $outputs): self + { + $this->outputs = $outputs; + + return $this; + } + + public function getOutputs(): bool + { + return $this->outputs; + } + + private static function getResolverInstance(): OptionsResolver + { + if (self::$resolver === null) { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'inputs' => true, + 'outputs' => true, + ]); + $resolver->setAllowedTypes('inputs', 'bool'); + $resolver->setAllowedTypes('outputs', 'bool'); + + self::$resolver = $resolver; + } + + return self::$resolver; + } +} diff --git a/src/DataCollection/HttpHeaders.php b/src/DataCollection/HttpHeaders.php new file mode 100644 index 000000000..f5679cc23 --- /dev/null +++ b/src/DataCollection/HttpHeaders.php @@ -0,0 +1,120 @@ +update($options); + } + + /** + * @param mixed[] $options + */ + public function update(array $options = []): self + { + $resolver = self::getResolverInstance(); + + if (!isset($options['request']) && !isset($options['response'])) { + $options = [ + 'request' => $options, + 'response' => $options, + ]; + } + + /** @var array{request: array|KeyValueCollectionBehavior, response: array|KeyValueCollectionBehavior} $options */ + $options = $resolver->resolve($options); + + $request = $options['request']; + if ($request instanceof KeyValueCollectionBehavior) { + $this->request = $request; + } else { + $this->request = new KeyValueCollectionBehavior($request); + } + + $response = $options['response']; + if ($response instanceof KeyValueCollectionBehavior) { + $this->response = $response; + } else { + $this->response = new KeyValueCollectionBehavior($response); + } + + return $this; + } + + public function getRequest(): KeyValueCollectionBehavior + { + return $this->request; + } + + /** + * @param mixed $value + */ + public function setRequest($value): self + { + return $this->update([ + 'request' => $value, + 'response' => $this->response, + ]); + } + + public function getResponse(): KeyValueCollectionBehavior + { + return $this->response; + } + + /** + * @param mixed $value + */ + public function setResponse($value): self + { + return $this->update([ + 'request' => $this->request, + 'response' => $value, + ]); + } + + private static function getResolverInstance(): OptionsResolver + { + if (self::$resolver === null) { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'request' => [], + 'response' => [], + ]); + $resolver->setAllowedTypes('request', ['array', KeyValueCollectionBehavior::class]); + $resolver->setAllowedTypes('response', ['array', KeyValueCollectionBehavior::class]); + self::$resolver = $resolver; + } + + return self::$resolver; + } +} diff --git a/src/DataCollection/KeyValueCollectionBehavior.php b/src/DataCollection/KeyValueCollectionBehavior.php new file mode 100644 index 000000000..b015c3251 --- /dev/null +++ b/src/DataCollection/KeyValueCollectionBehavior.php @@ -0,0 +1,114 @@ + + * + * @phpstan-var KeyValueCollection + */ + private $options; + + /** + * @param mixed[] $options + */ + public function __construct(array $options = []) + { + $this->update($options); + } + + /** + * @param mixed[] $options + */ + public function update(array $options): self + { + $this->options = self::resolveOptions($options); + + return $this; + } + + public function getMode(): string + { + return $this->options['mode']; + } + + public function setMode(string $mode): self + { + $options = array_merge($this->options, ['mode' => $mode]); + + $this->options = self::resolveOptions($options); + + return $this; + } + + /** + * @return string[] + */ + public function getTerms(): array + { + return $this->options['terms']; + } + + /** + * @param string[] $terms + */ + public function setTerms(array $terms): self + { + $options = array_merge($this->options, ['terms' => $terms]); + + $this->options = self::resolveOptions($options); + + return $this; + } + + /** + * @param mixed[] $options + * + * @return array + * + * @phpstan-return KeyValueCollection + */ + private static function resolveOptions(array $options): array + { + /** @var KeyValueCollection $resolvedOptions */ + $resolvedOptions = self::getResolverInstance()->resolve($options); + + return $resolvedOptions; + } + + private static function getResolverInstance(): OptionsResolver + { + if (self::$resolver === null) { + $resolver = new OptionsResolver(); + $resolver->setDefaults([ + 'mode' => 'denyList', + 'terms' => [], + ]); + $resolver->setAllowedTypes('mode', 'string'); + $resolver->setAllowedTypes('terms', 'string[]'); + $resolver->setAllowedValues('mode', [ + 'off', + 'denyList', + 'allowList', + ]); + + self::$resolver = $resolver; + } + + return self::$resolver; + } +} diff --git a/src/Options.php b/src/Options.php index 72cd25e24..6c2940d43 100644 --- a/src/Options.php +++ b/src/Options.php @@ -7,7 +7,6 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Sentry\DataCollection\DataCollectionOptions; -use Sentry\DataCollection\DataCollectionOptionsNormalizer; use Sentry\HttpClient\HttpClientInterface; use Sentry\Integration\ErrorListenerIntegration; use Sentry\Integration\IntegrationInterface; @@ -1654,79 +1653,7 @@ private function normalizeDataCollectionOption(SymfonyOptions $options, $value): } /** @var array $value */ - $resolvedOptions = $this->resolveDataCollectionOptions($value); - - return new DataCollectionOptions([ - 'user_info' => $resolvedOptions['user_info'], - 'cookies' => DataCollectionOptionsNormalizer::normalizeKeyValueCollection($resolvedOptions['cookies']), - 'http_headers' => DataCollectionOptionsNormalizer::normalizeHttpHeaders($resolvedOptions['http_headers']), - 'http_bodies' => $resolvedOptions['http_bodies'], - 'query_params' => DataCollectionOptionsNormalizer::normalizeKeyValueCollection($resolvedOptions['query_params']), - 'gen_ai' => DataCollectionOptionsNormalizer::normalizeGenAi($resolvedOptions['gen_ai']), - 'stack_frame_variables' => $resolvedOptions['stack_frame_variables'], - 'frame_context_lines' => $resolvedOptions['frame_context_lines'], - ]); - } - - /** - * @param array $value - * - * @return array - * - * @phpstan-return array{ - * user_info: bool, - * cookies: array, - * http_headers: array, - * http_bodies: string[], - * query_params: array, - * gen_ai: array, - * stack_frame_variables: bool, - * frame_context_lines: int - * } - */ - private function resolveDataCollectionOptions(array $value): array - { - $resolver = new OptionsResolver(); - $resolver->setDefaults([ - 'user_info' => true, - 'cookies' => [], - 'http_headers' => [], - 'http_bodies' => DataCollectionOptions::HTTP_BODY_TYPES, - 'query_params' => [], - 'gen_ai' => [], - 'stack_frame_variables' => true, - 'frame_context_lines' => 5, - ]); - $resolver->setAllowedTypes('user_info', 'bool'); - $resolver->setAllowedTypes('cookies', 'array'); - $resolver->setAllowedTypes('http_headers', 'array'); - $resolver->setAllowedTypes('http_bodies', 'string[]'); - $resolver->setAllowedTypes('query_params', 'array'); - $resolver->setAllowedTypes('gen_ai', 'array'); - $resolver->setAllowedTypes('stack_frame_variables', 'bool'); - $resolver->setAllowedTypes('frame_context_lines', 'int'); - $resolver->setAllowedValues('http_bodies', static function (array $value): bool { - /** @var string[] $value */ - return \count(array_diff($value, DataCollectionOptions::HTTP_BODY_TYPES)) === 0; - }); - $resolver->setAllowedValues('frame_context_lines', static function (int $value): bool { - return $value >= 0; - }); - - /** @var array{ - * user_info: bool, - * cookies: array, - * http_headers: array, - * http_bodies: string[], - * query_params: array, - * gen_ai: array, - * stack_frame_variables: bool, - * frame_context_lines: int - * } $resolvedOptions - */ - $resolvedOptions = $resolver->resolve($value); - - return $resolvedOptions; + return new DataCollectionOptions($value); } /** diff --git a/src/functions.php b/src/functions.php index 405371963..9e61467ae 100644 --- a/src/functions.php +++ b/src/functions.php @@ -33,13 +33,13 @@ * context_lines?: int|null, * data_collection?: DataCollection\DataCollectionOptions|array{ * user_info?: bool, - * cookies?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, - * http_headers?: array{mode?: "off"|"denyList"|"allowList", terms?: array}|array{ - * request?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, - * response?: array{mode?: "off"|"denyList"|"allowList", terms?: array} + * cookies?: DataCollection\KeyValueCollectionBehavior|array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * http_headers?: DataCollection\HttpHeaders|array{mode?: "off"|"denyList"|"allowList", terms?: array}|array{ + * request?: DataCollection\KeyValueCollectionBehavior|array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * response?: DataCollection\KeyValueCollectionBehavior|array{mode?: "off"|"denyList"|"allowList", terms?: array} * }, - * http_bodies?: array<"incomingRequest"|"outgoingRequest"|"incomingResponse"|"outgoingResponse">, - * query_params?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * http_bodies?: array<"incomingRequest"|"outgoingRequest"|"incomingResponse"|"outgoingResponse">|null, + * query_params?: DataCollection\KeyValueCollectionBehavior|array{mode?: "off"|"denyList"|"allowList", terms?: array}, * gen_ai?: array{inputs?: bool, outputs?: bool}, * stack_frame_variables?: bool, * frame_context_lines?: int diff --git a/tests/DataCollection/DataCollectionOptionsNormalizerTest.php b/tests/DataCollection/DataCollectionOptionsNormalizerTest.php deleted file mode 100644 index 40f860b3f..000000000 --- a/tests/DataCollection/DataCollectionOptionsNormalizerTest.php +++ /dev/null @@ -1,48 +0,0 @@ - 'allowList', - 'terms' => ['x-request-id'], - ]); - - $this->assertSame('allowList', $httpHeaders['request']['mode']); - $this->assertSame(['x-request-id'], $httpHeaders['request']['terms']); - $this->assertSame('allowList', $httpHeaders['response']['mode']); - $this->assertSame(['x-request-id'], $httpHeaders['response']['terms']); - } - - public function testNormalizeHttpHeadersMergesDirectionDefaults(): void - { - $httpHeaders = DataCollectionOptionsNormalizer::normalizeHttpHeaders([ - 'request' => [ - 'mode' => 'off', - ], - ]); - - $this->assertSame('off', $httpHeaders['request']['mode']); - $this->assertSame([], $httpHeaders['request']['terms']); - $this->assertSame('denyList', $httpHeaders['response']['mode']); - $this->assertSame([], $httpHeaders['response']['terms']); - } - - public function testNormalizeGenAiMergesDefaults(): void - { - $genAi = DataCollectionOptionsNormalizer::normalizeGenAi([ - 'inputs' => false, - ]); - - $this->assertFalse($genAi['inputs']); - $this->assertTrue($genAi['outputs']); - } -} diff --git a/tests/DataCollection/DataCollectionOptionsTest.php b/tests/DataCollection/DataCollectionOptionsTest.php new file mode 100644 index 000000000..e6d53ef91 --- /dev/null +++ b/tests/DataCollection/DataCollectionOptionsTest.php @@ -0,0 +1,105 @@ +assertTrue($dataCollection->shouldCollectUserInfo()); + $this->assertSame('denyList', $dataCollection->getCookies()->getMode()); + $this->assertSame([], $dataCollection->getCookies()->getTerms()); + $this->assertSame('denyList', $dataCollection->getHttpHeaders()->getRequest()->getMode()); + $this->assertSame('denyList', $dataCollection->getHttpHeaders()->getResponse()->getMode()); + $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $dataCollection->getHttpBodies()); + $this->assertSame('denyList', $dataCollection->getQueryParams()->getMode()); + $this->assertTrue($dataCollection->getGenAi()->getInputs()); + $this->assertTrue($dataCollection->getGenAi()->getOutputs()); + $this->assertTrue($dataCollection->shouldCollectStackFrameVariables()); + $this->assertSame(5, $dataCollection->getFrameContextLines()); + } + + public function testSetHttpHeadersUsesConstructorNormalization(): void + { + $httpHeaders = [ + 'mode' => 'allowList', + 'terms' => ['x-request-id'], + ]; + $fromConstructor = new DataCollectionOptions([ + 'http_headers' => $httpHeaders, + ]); + $fromSetter = (new DataCollectionOptions())->setHttpHeaders($httpHeaders); + + $this->assertSame($fromConstructor->getHttpHeaders()->getRequest()->getMode(), $fromSetter->getHttpHeaders()->getRequest()->getMode()); + $this->assertSame($fromConstructor->getHttpHeaders()->getRequest()->getTerms(), $fromSetter->getHttpHeaders()->getRequest()->getTerms()); + $this->assertSame($fromConstructor->getHttpHeaders()->getResponse()->getMode(), $fromSetter->getHttpHeaders()->getResponse()->getMode()); + $this->assertSame($fromConstructor->getHttpHeaders()->getResponse()->getTerms(), $fromSetter->getHttpHeaders()->getResponse()->getTerms()); + } + + public function testHttpHeadersConfigurationAppliesToBothRequestAndResponse(): void + { + $dataCollection = new DataCollectionOptions([ + 'http_headers' => [ + 'mode' => 'allowList', + 'terms' => ['x-request-id'], + ], + ]); + + $this->assertSame('allowList', $dataCollection->getHttpHeaders()->getRequest()->getMode()); + $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()->getRequest()->getTerms()); + $this->assertSame('allowList', $dataCollection->getHttpHeaders()->getResponse()->getMode()); + $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()->getResponse()->getTerms()); + } + + public function testNullHttpBodiesUsesDefaultBodyTypes(): void + { + $dataCollection = new DataCollectionOptions([ + 'http_bodies' => null, + ]); + + $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $dataCollection->getHttpBodies()); + } + + /** + * @dataProvider invalidOptionsDataProvider + * + * @param array $options + */ + public function testValidatesOptions(array $options): void + { + $this->expectException(InvalidOptionsException::class); + + new DataCollectionOptions($options); + } + + public static function invalidOptionsDataProvider(): \Generator + { + yield 'invalid http body type' => [ + [ + 'http_bodies' => ['invalid'], + ], + ]; + + yield 'negative frame context lines' => [ + [ + 'frame_context_lines' => -1, + ], + ]; + + yield 'invalid key-value mode' => [ + [ + 'cookies' => [ + 'mode' => 'invalid', + ], + ], + ]; + } +} diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index cc9a10719..8b3289fe1 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -684,21 +684,11 @@ public static function contextLinesOptionValidatesInputValueDataProvider(): \Gen ]; } - public function testDataCollectionOptionDefaults(): void + public function testDataCollectionOptionDefaultsToResolvedDataCollectionOptions(): void { $dataCollection = (new Options())->getDataCollection(); - $this->assertTrue($dataCollection->shouldCollectUserInfo()); - $this->assertSame('denyList', $dataCollection->getCookies()['mode']); - $this->assertSame([], $dataCollection->getCookies()['terms']); - $this->assertSame('denyList', $dataCollection->getHttpHeaders()['request']['mode']); - $this->assertSame('denyList', $dataCollection->getHttpHeaders()['response']['mode']); - $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $dataCollection->getHttpBodies()); - $this->assertSame('denyList', $dataCollection->getQueryParams()['mode']); - $this->assertTrue($dataCollection->getGenAi()['inputs']); - $this->assertTrue($dataCollection->getGenAi()['outputs']); - $this->assertTrue($dataCollection->shouldCollectStackFrameVariables()); - $this->assertSame(5, $dataCollection->getFrameContextLines()); + $this->assertInstanceOf(DataCollectionOptions::class, $dataCollection); } public function testDataCollectionOptionNormalizesArrayConfiguration(): void @@ -733,16 +723,16 @@ public function testDataCollectionOptionNormalizesArrayConfiguration(): void ]))->getDataCollection(); $this->assertFalse($dataCollection->shouldCollectUserInfo()); - $this->assertSame('allowList', $dataCollection->getCookies()['mode']); - $this->assertSame(['session_id'], $dataCollection->getCookies()['terms']); - $this->assertSame('off', $dataCollection->getHttpHeaders()['request']['mode']); - $this->assertSame(['authorization'], $dataCollection->getHttpHeaders()['request']['terms']); - $this->assertSame('denyList', $dataCollection->getHttpHeaders()['response']['mode']); - $this->assertSame(['set-cookie'], $dataCollection->getHttpHeaders()['response']['terms']); + $this->assertSame('allowList', $dataCollection->getCookies()->getMode()); + $this->assertSame(['session_id'], $dataCollection->getCookies()->getTerms()); + $this->assertSame('off', $dataCollection->getHttpHeaders()->getRequest()->getMode()); + $this->assertSame(['authorization'], $dataCollection->getHttpHeaders()->getRequest()->getTerms()); + $this->assertSame('denyList', $dataCollection->getHttpHeaders()->getResponse()->getMode()); + $this->assertSame(['set-cookie'], $dataCollection->getHttpHeaders()->getResponse()->getTerms()); $this->assertSame(['incomingRequest'], $dataCollection->getHttpBodies()); - $this->assertSame('off', $dataCollection->getQueryParams()['mode']); - $this->assertFalse($dataCollection->getGenAi()['inputs']); - $this->assertTrue($dataCollection->getGenAi()['outputs']); + $this->assertSame('off', $dataCollection->getQueryParams()->getMode()); + $this->assertFalse($dataCollection->getGenAi()->getInputs()); + $this->assertTrue($dataCollection->getGenAi()->getOutputs()); $this->assertFalse($dataCollection->shouldCollectStackFrameVariables()); $this->assertSame(0, $dataCollection->getFrameContextLines()); } @@ -758,10 +748,10 @@ public function testDataCollectionOptionAppliesSingleHttpHeadersConfigurationToB ], ]))->getDataCollection(); - $this->assertSame('allowList', $dataCollection->getHttpHeaders()['request']['mode']); - $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()['request']['terms']); - $this->assertSame('allowList', $dataCollection->getHttpHeaders()['response']['mode']); - $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()['response']['terms']); + $this->assertSame('allowList', $dataCollection->getHttpHeaders()->getRequest()->getMode()); + $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()->getRequest()->getTerms()); + $this->assertSame('allowList', $dataCollection->getHttpHeaders()->getResponse()->getMode()); + $this->assertSame(['x-request-id'], $dataCollection->getHttpHeaders()->getResponse()->getTerms()); } public function testDataCollectionOptionAcceptsConfigurationObjects(): void From de7ca0d488f548305353190a98a2fd41f2c83fe1 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 18:17:41 +0200 Subject: [PATCH 6/9] change isset to array_key_exists --- src/DataCollection/HttpHeaders.php | 2 +- tests/DataCollection/HttpHeadersTest.php | 46 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/DataCollection/HttpHeadersTest.php diff --git a/src/DataCollection/HttpHeaders.php b/src/DataCollection/HttpHeaders.php index f5679cc23..8bbe29e6a 100644 --- a/src/DataCollection/HttpHeaders.php +++ b/src/DataCollection/HttpHeaders.php @@ -43,7 +43,7 @@ public function update(array $options = []): self { $resolver = self::getResolverInstance(); - if (!isset($options['request']) && !isset($options['response'])) { + if (!\array_key_exists('request', $options) && !\array_key_exists('response', $options)) { $options = [ 'request' => $options, 'response' => $options, diff --git a/tests/DataCollection/HttpHeadersTest.php b/tests/DataCollection/HttpHeadersTest.php new file mode 100644 index 000000000..3aafcbedd --- /dev/null +++ b/tests/DataCollection/HttpHeadersTest.php @@ -0,0 +1,46 @@ +assertSame('denyList', $httpHeaders->getRequest()->getMode()); + $this->assertSame([], $httpHeaders->getRequest()->getTerms()); + $this->assertSame('denyList', $httpHeaders->getResponse()->getMode()); + $this->assertSame([], $httpHeaders->getResponse()->getTerms()); + } + + public function testConfigurationAppliesToBothRequestAndResponse(): void + { + $httpHeaders = new HttpHeaders([ + 'mode' => 'allowList', + 'terms' => ['x-request-id'], + ]); + + $this->assertSame('allowList', $httpHeaders->getRequest()->getMode()); + $this->assertSame(['x-request-id'], $httpHeaders->getRequest()->getTerms()); + $this->assertSame('allowList', $httpHeaders->getResponse()->getMode()); + $this->assertSame(['x-request-id'], $httpHeaders->getResponse()->getTerms()); + } + + public function testRequestAndResponseIsNull(): void + { + $this->expectException(InvalidOptionsException::class); + $this->expectExceptionMessage('The option "request" with value null is expected to be of type "array" or "Sentry\DataCollection\KeyValueCollectionBehavior", but is of type "null"'); + + new HttpHeaders([ + 'request' => null, + 'response' => null, + ]); + } +} From 3b372de1418bcf216133e84c639d29b9dd2b6b5a Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 18:22:53 +0200 Subject: [PATCH 7/9] fix --- tests/DataCollection/HttpHeadersTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/DataCollection/HttpHeadersTest.php b/tests/DataCollection/HttpHeadersTest.php index 3aafcbedd..62794d5ce 100644 --- a/tests/DataCollection/HttpHeadersTest.php +++ b/tests/DataCollection/HttpHeadersTest.php @@ -36,7 +36,6 @@ public function testConfigurationAppliesToBothRequestAndResponse(): void public function testRequestAndResponseIsNull(): void { $this->expectException(InvalidOptionsException::class); - $this->expectExceptionMessage('The option "request" with value null is expected to be of type "array" or "Sentry\DataCollection\KeyValueCollectionBehavior", but is of type "null"'); new HttpHeaders([ 'request' => null, From 99f2cb77a3ac01765837e08c85de641845bfdb0f Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 18:24:44 +0200 Subject: [PATCH 8/9] fix --- src/DataCollection/DataCollectionOptions.php | 40 ++++++-------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php index cc3fce5d8..466632de2 100644 --- a/src/DataCollection/DataCollectionOptions.php +++ b/src/DataCollection/DataCollectionOptions.php @@ -97,9 +97,7 @@ public function shouldCollectUserInfo(): bool public function setUserInfo(bool $userInfo): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['user_info' => $userInfo])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['user_info' => $userInfo])); } public function getCookies(): KeyValueCollectionBehavior @@ -112,9 +110,7 @@ public function getCookies(): KeyValueCollectionBehavior */ public function setCookies($cookies): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['cookies' => $cookies])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['cookies' => $cookies])); } public function getHttpHeaders(): HttpHeaders @@ -129,9 +125,7 @@ public function getHttpHeaders(): HttpHeaders */ public function setHttpHeaders($httpHeaders): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['http_headers' => $httpHeaders])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['http_headers' => $httpHeaders])); } /** @@ -147,9 +141,7 @@ public function getHttpBodies(): array */ public function setHttpBodies(?array $httpBodies): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['http_bodies' => $httpBodies])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['http_bodies' => $httpBodies])); } public function getQueryParams(): KeyValueCollectionBehavior @@ -162,9 +154,7 @@ public function getQueryParams(): KeyValueCollectionBehavior */ public function setQueryParams($queryParams): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['query_params' => $queryParams])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['query_params' => $queryParams])); } public function getGenAi(): GenAi @@ -177,9 +167,7 @@ public function getGenAi(): GenAi */ public function setGenAi(array $genAi): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['gen_ai' => $genAi])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['gen_ai' => $genAi])); } public function shouldCollectStackFrameVariables(): bool @@ -189,9 +177,7 @@ public function shouldCollectStackFrameVariables(): bool public function setStackFrameVariables(bool $stackFrameVariables): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['stack_frame_variables' => $stackFrameVariables])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['stack_frame_variables' => $stackFrameVariables])); } public function getFrameContextLines(): int @@ -201,9 +187,7 @@ public function getFrameContextLines(): int public function setFrameContextLines(int $frameContextLines): self { - $this->options = $this->resolveOptions(array_merge($this->options, ['frame_context_lines' => $frameContextLines])); - - return $this; + return $this->resolveOptions(array_merge($this->options, ['frame_context_lines' => $frameContextLines])); } private function configureOptions(OptionsResolver $resolver): void @@ -249,16 +233,16 @@ private function configureOptions(OptionsResolver $resolver): void /** * @param array $options * - * @return array - * * @phpstan-return ResolvedDataCollectionOptions */ - private function resolveOptions(array $options): array + private function resolveOptions(array $options): self { /** @var ResolvedDataCollectionOptions $resolvedOptions */ $resolvedOptions = $this->resolver->resolve($options); - return $resolvedOptions; + $this->options = $resolvedOptions; + + return $this; } /** From ab460c62b220fccf79cc901d79f68bdf03dbbd76 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 30 Jun 2026 18:27:21 +0200 Subject: [PATCH 9/9] fix --- src/DataCollection/DataCollectionOptions.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php index 466632de2..5661b28e7 100644 --- a/src/DataCollection/DataCollectionOptions.php +++ b/src/DataCollection/DataCollectionOptions.php @@ -82,7 +82,7 @@ public function __construct(array $options = []) $this->resolver = new OptionsResolver(); $this->configureOptions($this->resolver); - $this->options = $this->resolveOptions($options); + $this->resolveOptions($options); } public static function default(): self @@ -232,8 +232,6 @@ private function configureOptions(OptionsResolver $resolver): void /** * @param array $options - * - * @phpstan-return ResolvedDataCollectionOptions */ private function resolveOptions(array $options): self {