diff --git a/src/DataCollection/DataCollectionOptions.php b/src/DataCollection/DataCollectionOptions.php index 3902ec614..62f60f4b3 100644 --- a/src/DataCollection/DataCollectionOptions.php +++ b/src/DataCollection/DataCollectionOptions.php @@ -15,9 +15,11 @@ * cookies: KeyValueCollectionBehavior, * http_headers: HttpHeaders, * http_bodies: string[], - * query_params: KeyValueCollectionBehavior, + * url_query_params: KeyValueCollectionBehavior, * gen_ai: GenAi, - * stack_frame_variables: bool, + * database_query_data: bool, + * queues: bool, + * stack_frame_variables: KeyValueCollectionBehavior, * frame_context_lines: int * } * @@ -59,11 +61,13 @@ final class DataCollectionOptions implements \ArrayAccess 'response' => self::COLLECTION_DEFAULT, ], 'http_bodies' => self::HTTP_BODY_TYPES, - 'query_params' => self::COLLECTION_DEFAULT, + 'url_query_params' => self::COLLECTION_DEFAULT, 'gen_ai' => [ 'inputs' => true, 'outputs' => true, ], + 'database_query_data' => true, + 'queues' => true, 'stack_frame_variables' => true, 'frame_context_lines' => 5, ]; @@ -163,19 +167,19 @@ public function setHttpBodies(array $httpBodies): self /** * @phpstan-return KeyValueCollectionBehavior */ - public function getQueryParams(): array + public function getUrlQueryParams(): array { - return $this->options['query_params']; + return $this->options['url_query_params']; } /** - * @param array $queryParams + * @param array $urlQueryParams * - * @phpstan-param array{mode?: 'off'|'denyList'|'allowList', terms?: string[]} $queryParams + * @phpstan-param array{mode?: 'off'|'denyList'|'allowList', terms?: string[]} $urlQueryParams */ - public function setQueryParams(array $queryParams): self + public function setUrlQueryParams(array $urlQueryParams): self { - return $this->updateOptions(['query_params' => $queryParams]); + return $this->updateOptions(['url_query_params' => $urlQueryParams]); } /** @@ -196,12 +200,45 @@ public function setGenAi(array $genAi): self return $this->updateOptions(['gen_ai' => $genAi]); } - public function shouldCollectStackFrameVariables(): bool + public function shouldCollectDatabaseQueryData(): bool + { + return $this->options['database_query_data']; + } + + public function setDatabaseQueryData(bool $databaseQueryData): self + { + return $this->updateOptions(['database_query_data' => $databaseQueryData]); + } + + public function shouldCollectQueues(): bool + { + return $this->options['queues']; + } + + public function setQueues(bool $queues): self + { + return $this->updateOptions(['queues' => $queues]); + } + + /** + * @phpstan-return KeyValueCollectionBehavior + */ + public function getStackFrameVariables(): array { return $this->options['stack_frame_variables']; } - public function setStackFrameVariables(bool $stackFrameVariables): self + public function shouldCollectStackFrameVariables(): bool + { + return $this->options['stack_frame_variables']['mode'] !== 'off'; + } + + /** + * @param bool|array $stackFrameVariables + * + * @phpstan-param bool|array{mode?: 'off'|'denyList'|'allowList', terms?: string[]} $stackFrameVariables + */ + public function setStackFrameVariables($stackFrameVariables): self { return $this->updateOptions(['stack_frame_variables' => $stackFrameVariables]); } @@ -290,19 +327,24 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('http_headers.response.mode', 'string'); $resolver->setAllowedTypes('http_headers.response.terms', 'string[]'); $resolver->setAllowedTypes('http_bodies', 'string[]'); - $resolver->setAllowedTypes('query_params', 'array'); - $resolver->setAllowedTypes('query_params.mode', 'string'); - $resolver->setAllowedTypes('query_params.terms', 'string[]'); + $resolver->setAllowedTypes('url_query_params', 'array'); + $resolver->setAllowedTypes('url_query_params.mode', 'string'); + $resolver->setAllowedTypes('url_query_params.terms', 'string[]'); $resolver->setAllowedTypes('gen_ai', 'array'); $resolver->setAllowedTypes('gen_ai.inputs', 'bool'); $resolver->setAllowedTypes('gen_ai.outputs', 'bool'); - $resolver->setAllowedTypes('stack_frame_variables', 'bool'); + $resolver->setAllowedTypes('database_query_data', 'bool'); + $resolver->setAllowedTypes('queues', 'bool'); + $resolver->setAllowedTypes('stack_frame_variables', ['bool', 'array']); + $resolver->setAllowedTypes('stack_frame_variables.mode', 'string'); + $resolver->setAllowedTypes('stack_frame_variables.terms', 'string[]'); $resolver->setAllowedTypes('frame_context_lines', 'int'); $resolver->setAllowedValues('cookies.mode', self::COLLECTION_MODES); $resolver->setAllowedValues('http_headers.request.mode', self::COLLECTION_MODES); $resolver->setAllowedValues('http_headers.response.mode', self::COLLECTION_MODES); - $resolver->setAllowedValues('query_params.mode', self::COLLECTION_MODES); + $resolver->setAllowedValues('url_query_params.mode', self::COLLECTION_MODES); + $resolver->setAllowedValues('stack_frame_variables.mode', self::COLLECTION_MODES); $resolver->setAllowedValues('http_bodies', static function (array $value): bool { return array_diff($value, self::HTTP_BODY_TYPES) === []; }); @@ -320,9 +362,33 @@ private function configureOptions(OptionsResolver $resolver): void return $value; }); + $resolver->setNormalizer( + 'stack_frame_variables', + \Closure::fromCallable([$this, 'normalizeStackFrameVariables']) + ); $resolver->setDefaults(self::DEFAULTS); } + /** + * @param bool|array $value + * + * @phpstan-param bool|array{mode?: 'off'|'denyList'|'allowList', terms?: string[]} $value + * + * @phpstan-return array{mode?: 'off'|'denyList'|'allowList', terms?: string[]} + */ + private function normalizeStackFrameVariables($value): array + { + if ($value === true) { + return self::COLLECTION_DEFAULT; + } + + if ($value === false) { + return ['mode' => 'off', 'terms' => []]; + } + + return $value; + } + /** * @param array $override */ diff --git a/src/DataCollection/KeyValueDataFilter.php b/src/DataCollection/KeyValueDataFilter.php index c90c6d5ab..e29efe9d0 100644 --- a/src/DataCollection/KeyValueDataFilter.php +++ b/src/DataCollection/KeyValueDataFilter.php @@ -118,7 +118,7 @@ public static function filterQueryString(string $queryString, array $behavior): $encodedKey = $separatorPosition === false ? $part : substr($part, 0, $separatorPosition); $key = urldecode($encodedKey); - if (self::shouldFilterValue($key, $behavior)) { + if ($separatorPosition !== false && self::shouldFilterValue($key, $behavior)) { $parts[$index] = $encodedKey . '=[Filtered]'; } } diff --git a/src/functions.php b/src/functions.php index e5e0e58a7..66e76cffb 100644 --- a/src/functions.php +++ b/src/functions.php @@ -39,9 +39,11 @@ * response?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, * }, * http_bodies?: array<"incomingRequest"|"outgoingRequest"|"incomingResponse"|"outgoingResponse">, - * query_params?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, + * url_query_params?: array{mode?: "off"|"denyList"|"allowList", terms?: array}, * gen_ai?: array{inputs?: bool, outputs?: bool}, - * stack_frame_variables?: bool, + * database_query_data?: bool, + * queues?: bool, + * stack_frame_variables?: bool|array{mode?: "off"|"denyList"|"allowList", terms?: array}, * frame_context_lines?: int, * }, * default_integrations?: bool, diff --git a/tests/DataCollection/DataCollectionOptionsTest.php b/tests/DataCollection/DataCollectionOptionsTest.php index 1df694aa2..1ff747466 100644 --- a/tests/DataCollection/DataCollectionOptionsTest.php +++ b/tests/DataCollection/DataCollectionOptionsTest.php @@ -21,8 +21,11 @@ public function testDefaults(): void 'response' => $collectionDefault, ], $options->getHttpHeaders()); $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $options->getHttpBodies()); - $this->assertSame($collectionDefault, $options->getQueryParams()); + $this->assertSame($collectionDefault, $options->getUrlQueryParams()); $this->assertSame(['inputs' => true, 'outputs' => true], $options->getGenAi()); + $this->assertTrue($options->shouldCollectDatabaseQueryData()); + $this->assertTrue($options->shouldCollectQueues()); + $this->assertSame($collectionDefault, $options->getStackFrameVariables()); $this->assertTrue($options->shouldCollectStackFrameVariables()); $this->assertSame(5, $options->getFrameContextLines()); } @@ -59,26 +62,68 @@ public function testNullHttpBodiesUsesDefault(): void $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $options->getHttpBodies()); } + public function testStackFrameVariablesSupportsBooleanAndKeyValueCollectionBehavior(): void + { + $options = new DataCollectionOptions([ + 'stack_frame_variables' => [ + 'mode' => 'allowList', + 'terms' => ['request_id'], + ], + ]); + + $this->assertSame([ + 'mode' => 'allowList', + 'terms' => ['request_id'], + ], $options->getStackFrameVariables()); + $this->assertTrue($options->shouldCollectStackFrameVariables()); + + $options->setStackFrameVariables(['terms' => ['trace_id']]); + $this->assertSame([ + 'mode' => 'allowList', + 'terms' => ['trace_id'], + ], $options->getStackFrameVariables()); + + $options->setStackFrameVariables(false); + $this->assertSame(['mode' => 'off', 'terms' => []], $options->getStackFrameVariables()); + $this->assertFalse($options->shouldCollectStackFrameVariables()); + + $options->setStackFrameVariables(true); + $this->assertSame(['mode' => 'denyList', 'terms' => []], $options->getStackFrameVariables()); + $this->assertTrue($options->shouldCollectStackFrameVariables()); + + $options->setStackFrameVariables(['mode' => 'off']); + $this->assertSame(['mode' => 'off', 'terms' => []], $options->getStackFrameVariables()); + $this->assertFalse($options->shouldCollectStackFrameVariables()); + } + public function testInvalidValuesUseDefaultsAndSettersKeepCurrentValues(): void { $options = new DataCollectionOptions([ 'cookies' => ['mode' => 'invalid', 'terms' => [42]], 'http_bodies' => ['invalid'], 'gen_ai' => ['inputs' => 'invalid'], + 'database_query_data' => 'invalid', + 'queues' => 'invalid', + 'stack_frame_variables' => ['mode' => 'invalid'], 'frame_context_lines' => -1, ]); $this->assertSame(['mode' => 'denyList', 'terms' => []], $options->getCookies()); $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $options->getHttpBodies()); $this->assertSame(['inputs' => true, 'outputs' => true], $options->getGenAi()); + $this->assertTrue($options->shouldCollectDatabaseQueryData()); + $this->assertTrue($options->shouldCollectQueues()); + $this->assertSame(['mode' => 'denyList', 'terms' => []], $options->getStackFrameVariables()); $this->assertSame(5, $options->getFrameContextLines()); $options->setCookies(['mode' => 'allowList'])->setCookies(['mode' => 'invalid']); $options->setHttpBodies(['incomingRequest'])->setHttpBodies(['invalid']); + $options->setStackFrameVariables(['mode' => 'allowList'])->setStackFrameVariables(['terms' => [42]]); $options->setFrameContextLines(2)->setFrameContextLines(-1); $this->assertSame('allowList', $options->getCookies()['mode']); $this->assertSame(['incomingRequest'], $options->getHttpBodies()); + $this->assertSame(['mode' => 'allowList', 'terms' => []], $options->getStackFrameVariables()); $this->assertSame(2, $options->getFrameContextLines()); } @@ -125,11 +170,19 @@ public function testArrayAccessUnsetRestoresDefault(): void $options = new DataCollectionOptions([ 'user_info' => false, 'http_bodies' => [], + 'stack_frame_variables' => false, ]); - unset($options['user_info'], $options['http_bodies'], $options['unknown'], $options[0]); + unset( + $options['user_info'], + $options['http_bodies'], + $options['stack_frame_variables'], + $options['unknown'], + $options[0] + ); $this->assertTrue($options['user_info']); $this->assertSame(DataCollectionOptions::HTTP_BODY_TYPES, $options['http_bodies']); + $this->assertSame(['mode' => 'denyList', 'terms' => []], $options['stack_frame_variables']); } } diff --git a/tests/DataCollection/KeyValueDataFilterTest.php b/tests/DataCollection/KeyValueDataFilterTest.php index 2662102cb..bf2752873 100644 --- a/tests/DataCollection/KeyValueDataFilterTest.php +++ b/tests/DataCollection/KeyValueDataFilterTest.php @@ -166,13 +166,28 @@ public function testFilterQueryStringAppliesMandatoryAndCustomDenyListTerms(): v $this->assertSame('token=[Filtered]&page=[Filtered]&flag', $filtered); } - public function testFilterQueryStringDecodesKeysBeforeMatching(): void + public function testFilterQueryStringDecodesKeysBeforeMatchingAndPreservesEncoding(): void { $behavior = ['mode' => 'denyList', 'terms' => []]; - $filtered = KeyValueDataFilter::filterQueryString('api%5Ftoken=secret&page=1', $behavior); + $filtered = KeyValueDataFilter::filterQueryString( + 'api%5Ftoken=secret&q=a%20b%26c&encoded%20field=encoded%2Bvalue', + $behavior + ); - $this->assertSame('api%5Ftoken=[Filtered]&page=1', $filtered); + $this->assertSame( + 'api%5Ftoken=[Filtered]&q=a%20b%26c&encoded%20field=encoded%2Bvalue', + $filtered + ); + } + + public function testFilterQueryStringPreservesValuelessParameters(): void + { + $behavior = ['mode' => 'denyList', 'terms' => []]; + + $filtered = KeyValueDataFilter::filterQueryString('token&token=&flag', $behavior); + + $this->assertSame('token&token=[Filtered]&flag', $filtered); } public function testFilterQueryStringDoesNotTreatCookieNamesAsCookieHeaders(): void diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index fada65cbe..b886b0df0 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -689,14 +689,25 @@ public function testDataCollectionOptionNormalizesNestedArray(): void 'http_headers' => [ 'request' => ['mode' => 'off'], ], + 'url_query_params' => ['terms' => ['private']], 'gen_ai' => ['outputs' => false], + 'database_query_data' => false, + 'queues' => false, + 'stack_frame_variables' => ['mode' => 'allowList', 'terms' => ['request_id']], ], ]))->getDataCollection(); $this->assertFalse($dataCollection->shouldCollectUserInfo()); $this->assertSame('off', $dataCollection->getHttpHeaders()['request']['mode']); $this->assertSame('denyList', $dataCollection->getHttpHeaders()['response']['mode']); + $this->assertSame(['mode' => 'denyList', 'terms' => ['private']], $dataCollection->getUrlQueryParams()); $this->assertSame(['inputs' => true, 'outputs' => false], $dataCollection->getGenAi()); + $this->assertFalse($dataCollection->shouldCollectDatabaseQueryData()); + $this->assertFalse($dataCollection->shouldCollectQueues()); + $this->assertSame([ + 'mode' => 'allowList', + 'terms' => ['request_id'], + ], $dataCollection->getStackFrameVariables()); } public function testDataCollectionOptionPreservesObjectIdentityAndCanBeUpdatedThroughGetter(): void