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
10 changes: 10 additions & 0 deletions src/FrameBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Sentry;

use Sentry\DataCollection\KeyValueDataFilter;
use Sentry\Serializer\RepresentationSerializerInterface;
use Sentry\Util\PrefixStripper;

Expand Down Expand Up @@ -201,6 +202,15 @@ private function getFunctionArguments(array $backtraceFrame): array
}
}

$dataCollection = $this->options->getDataCollection();

if ($dataCollection !== null) {
$argumentValues = KeyValueDataFilter::filterKeyValueData(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyValueDataFilter::filterKeyValueData() is a recursive function. But $argumentValues could include recursive arrays, which causes KeyValueDataFilter::filterKeyValueData() to recurse until the memory limit is exceeded.

For example:

$foo['recursion'] =& $foo;
\Sentry\DataCollection\KeyValueDataFilter::filterKeyValueData($foo, ['mode' => 'on', 'terms' => []]);

$argumentValues,
$dataCollection->getStackFrameVariables()
) ?? [];
}

foreach ($argumentValues as $argumentName => $argumentValue) {
$argumentValues[$argumentName] = $this->representationSerializer->representationSerialize($argumentValue);
}
Expand Down
6 changes: 5 additions & 1 deletion src/Integration/FrameContextifierIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ public function setupOnce(): void
return $event;
}

$maxContextLines = $client->getOptions()->getContextLines();
$options = $client->getOptions();
$dataCollection = $options->getDataCollection();
$maxContextLines = $dataCollection === null
? $options->getContextLines()
: $dataCollection->getFrameContextLines();
Comment on lines +50 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: When data_collection is enabled, setting context_lines: null is ignored, and context lines are still collected using the default value of 5.
Severity: LOW

Suggested Fix

The logic should respect context_lines: null even when data_collection is enabled. The check for $options->getContextLines() being null should take precedence. If it is null, then $maxContextLines should be set to null to disable context line collection, regardless of the data_collection settings.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/Integration/FrameContextifierIntegration.php#L50-L54

Potential issue: When a user configures `data_collection` and also sets `context_lines`
to `null` with the intention of disabling stack trace context lines, the setting is
ignored. The logic in `FrameContextifierIntegration` incorrectly prioritizes the
`data_collection` configuration for context lines. The method `getFrameContextLines()`
from `DataCollectionOptions` always returns an integer (defaulting to 5) and never
`null`. This prevents the `$maxContextLines === null` check from ever being true,
causing context lines to be collected against the user's explicit configuration.

Did we get this right? 👍 / 👎 to inform future reviews.

$integration = $client->getIntegration(self::class);

if ($integration === null || $maxContextLines === null) {
Expand Down
36 changes: 33 additions & 3 deletions tests/Integration/FrameContextifierIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,21 @@ final class FrameContextifierIntegrationTest extends TestCase
/**
* @dataProvider invokeDataProvider
*/
public function testInvoke(string $fixtureFilePath, int $lineNumber, int $contextLines, int $preContextCount, int $postContextCount): void
{
$options = new Options(['context_lines' => $contextLines]);
public function testInvoke(
string $fixtureFilePath,
int $lineNumber,
int $contextLines,
int $preContextCount,
int $postContextCount,
?int $dataCollectionContextLines = null
): void {
$options = ['context_lines' => $contextLines];

if ($dataCollectionContextLines !== null) {
$options['data_collection'] = ['frame_context_lines' => $dataCollectionContextLines];
}

$options = new Options($options);
$integration = new FrameContextifierIntegration();
$integration->setupOnce();

Expand Down Expand Up @@ -108,6 +120,24 @@ public static function invokeDataProvider(): \Generator
2,
5,
];

yield 'data collection context lines take precedence over legacy option' => [
realpath(__DIR__ . '/../Fixtures/code/LongFile.php'),
8,
1,
3,
3,
3,
];

yield 'data collection can omit surrounding context lines' => [
realpath(__DIR__ . '/../Fixtures/code/LongFile.php'),
8,
5,
0,
0,
0,
];
}

public function testInvokeLogsWarningMessageIfSourceCodeExcerptCannotBeRetrievedForFrame(): void
Expand Down
189 changes: 189 additions & 0 deletions tests/StacktraceBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,193 @@ public function testBuildFromBacktrace(): void
$this->assertSame(__FILE__, $frames[2]->getAbsoluteFilePath());
$this->assertSame($expectedLine, $frames[2]->getLine());
}

/**
* @dataProvider realExceptionStackFrameVariablesDataProvider
*
* @param array<string, mixed> $options
* @param array<string, array<string, mixed>> $expectedVariables
*/
public function testStackFrameVariablesFromRealException(array $options, array $expectedVariables): void
{
$previousIgnoreArgs = \ini_get('zend.exception_ignore_args');

try {
if ($previousIgnoreArgs !== false
&& (ini_set('zend.exception_ignore_args', '0') === false
|| \ini_get('zend.exception_ignore_args') !== '0')) {
$this->markTestSkipped('zend.exception_ignore_args cannot be disabled.');
}

$exception = self::createNestedException();
$sdkOptions = new Options($options);
$stacktraceBuilder = new StacktraceBuilder(
$sdkOptions,
new RepresentationSerializer($sdkOptions)
);
$frames = $stacktraceBuilder->buildFromException($exception)->getFrames();
$actualVariables = [];

foreach ($frames as $frame) {
$rawFunctionName = $frame->getRawFunctionName();

if ($rawFunctionName === null) {
continue;
}

$separatorPosition = strrpos($rawFunctionName, '::');
$methodName = $separatorPosition === false
? $rawFunctionName
: substr($rawFunctionName, $separatorPosition + 2);

if (\array_key_exists($methodName, $expectedVariables)) {
$actualVariables[$methodName] = $frame->getVars();
}
}

ksort($actualVariables);
ksort($expectedVariables);

$this->assertSame($expectedVariables, $actualVariables);
} finally {
if ($previousIgnoreArgs !== false) {
ini_set('zend.exception_ignore_args', $previousIgnoreArgs);
}
}
}

public static function realExceptionStackFrameVariablesDataProvider(): \Generator
{
yield 'legacy behavior is unchanged' => [
[],
[
'stackFrameInner' => [
'apiToken' => 'nested-secret',
'safeValue' => 'safe',
],
'stackFrameMiddle' => [
'metadata' => [
'api_token' => 'nested-secret',
'name' => 'alice',
],
],
'stackFrameOuter' => [
'requestId' => 'request-123',
'password' => 'secret',
],
],
];

yield 'default data collection filters mandatory sensitive values' => [
['data_collection' => []],
[
'stackFrameInner' => [
'apiToken' => '[Filtered]',
'safeValue' => 'safe',
],
'stackFrameMiddle' => [
'metadata' => [
'api_token' => '[Filtered]',
'name' => 'alice',
],
],
'stackFrameOuter' => [
'requestId' => 'request-123',
'password' => '[Filtered]',
],
],
];

yield 'collection can be disabled with boolean shorthand' => [
['data_collection' => ['stack_frame_variables' => false]],
[
'stackFrameInner' => [],
'stackFrameMiddle' => [],
'stackFrameOuter' => [],
],
];

yield 'allow list filters values not matching configured terms' => [
[
'data_collection' => [
'stack_frame_variables' => [
'mode' => 'allowList',
'terms' => ['request'],
],
],
],
[
'stackFrameInner' => [
'apiToken' => '[Filtered]',
'safeValue' => '[Filtered]',
],
'stackFrameMiddle' => [
'metadata' => '[Filtered]',
],
'stackFrameOuter' => [
'requestId' => 'request-123',
'password' => '[Filtered]',
],
],
];

yield 'deny list combines mandatory and custom terms' => [
[
'data_collection' => [
'stack_frame_variables' => [
'mode' => 'denyList',
'terms' => ['request'],
],
],
],
[
'stackFrameInner' => [
'apiToken' => '[Filtered]',
'safeValue' => 'safe',
],
'stackFrameMiddle' => [
'metadata' => [
'api_token' => '[Filtered]',
'name' => 'alice',
],
],
'stackFrameOuter' => [
'requestId' => '[Filtered]',
'password' => '[Filtered]',
],
],
];
}

private static function createNestedException(): \RuntimeException
{
try {
self::stackFrameOuter('request-123', 'secret');
} catch (\RuntimeException $exception) {
return $exception;
}

throw new \LogicException('Expected the nested stack frame fixture to throw.');
}

private static function stackFrameOuter(string $requestId, string $password): void
{
self::stackFrameMiddle([
'api_token' => 'nested-secret',
'name' => 'alice',
]);
}

/**
* @param array<string, string> $metadata
*/
private static function stackFrameMiddle(array $metadata): void
{
self::stackFrameInner($metadata['api_token'], 'safe');
}

private static function stackFrameInner(string $apiToken, string $safeValue): void
{
throw new \RuntimeException('Real nested stack frame fixture.');
}
}
Loading