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
98 changes: 82 additions & 16 deletions src/DataCollection/DataCollectionOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
* }
*
Expand Down Expand Up @@ -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,
];
Expand Down Expand Up @@ -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<string, mixed> $queryParams
* @param array<string, mixed> $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]);
}

/**
Expand All @@ -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<string, mixed> $stackFrameVariables
*
* @phpstan-param bool|array{mode?: 'off'|'denyList'|'allowList', terms?: string[]} $stackFrameVariables
*/
public function setStackFrameVariables($stackFrameVariables): self
{
return $this->updateOptions(['stack_frame_variables' => $stackFrameVariables]);
}
Expand Down Expand Up @@ -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) === [];
});
Expand All @@ -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<string, mixed> $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;
Comment thread
Litarnus marked this conversation as resolved.
}

/**
* @param array<string, mixed> $override
*/
Expand Down
2 changes: 1 addition & 1 deletion src/DataCollection/KeyValueDataFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]';
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@
* response?: array{mode?: "off"|"denyList"|"allowList", terms?: array<string>},
* },
* http_bodies?: array<"incomingRequest"|"outgoingRequest"|"incomingResponse"|"outgoingResponse">,
* query_params?: array{mode?: "off"|"denyList"|"allowList", terms?: array<string>},
* url_query_params?: array{mode?: "off"|"denyList"|"allowList", terms?: array<string>},
* 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<string>},
* frame_context_lines?: int,
* },
* default_integrations?: bool,
Expand Down
57 changes: 55 additions & 2 deletions tests/DataCollection/DataCollectionOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}

Expand Down Expand Up @@ -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']);
}
}
21 changes: 18 additions & 3 deletions tests/DataCollection/KeyValueDataFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions tests/OptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down