Skip to content

Commit

Permalink
Merge pull request #4 from kodus/stack-trace-blacklist
Browse files Browse the repository at this point in the history
Stack trace filtering
  • Loading branch information
mindplay-dk authored Mar 6, 2019
2 parents a039d63 + eccfcfa commit e88a6ea
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 45 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ The constructor accepts two optional argument:

* `$root_path` - if specified, the project root-path will be removed from visible filenames in stack-traces.
* `$max_string_length` - specifies the maximum length at which reported PHP values will be truncated.
* `$filters` - specifies one or more filename patterns to be filtered from stack-traces.

In addition, the public `$error_levels` property lets you customize how PHP error-levels map to
Sentry severity-levels. The default configuration matches that of the official 2.0 client.
Expand Down
60 changes: 50 additions & 10 deletions src/Extensions/ExceptionReporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
*/
class ExceptionReporter implements SentryClientExtension
{
/**
* @var string placeholder for unavailable file-names
*/
const NO_FILE = "{no file}";

/**
* @var string root path (with trailing directory-separator)
*/
Expand All @@ -35,6 +40,11 @@ class ExceptionReporter implements SentryClientExtension
*/
protected $max_string_length;

/**
* @var string[] file-name patterns to filter from stack-traces
*/
protected $filters;

/**
* Severity of `ErrorException` mappings are identical to the official (2.0) client
* by default - you can override the error-level mappings via this public property.
Expand Down Expand Up @@ -66,16 +76,24 @@ class ExceptionReporter implements SentryClientExtension
/**
* The optional `$root_path`, if given, will be stripped from filenames.
*
* The optional `$filters` is an array of {@see \fnmatch()} patterns, which will be applied
* to relative paths of source-file references in stack-traces. You can use this to filter
* scripts that define/bootstrap sensitive values like passwords and hostnames, so that
* these lines will never show up in a stack-trace.
*
* @param string|null $root_path absolute project root-path (e.g. Composer root path; optional)
* @param int $max_string_length PHP values longer than this will be truncated
* @param string[] $filters Optional file-name patterns to filter from stack-traces
*/
public function __construct(?string $root_path = null, $max_string_length = 200)
public function __construct(?string $root_path = null, $max_string_length = 200, array $filters = [])
{
$this->root_path = $root_path
? rtrim($root_path, "/\\") . "/"
: null;

$this->max_string_length = $max_string_length;

$this->filters = $filters;
}

public function apply(Event $event, Throwable $exception, ?ServerRequestInterface $request): void
Expand Down Expand Up @@ -162,7 +180,7 @@ protected function createStackFrame(array $entry): StackFrame
{
$filename = isset($entry["file"])
? $entry["file"]
: "{no file}";
: self::NO_FILE;

$function = isset($entry["class"])
? $entry["class"] . @$entry["type"] . @$entry["function"]
Expand All @@ -174,22 +192,44 @@ protected function createStackFrame(array $entry): StackFrame

$frame = new StackFrame($filename, $function, $lineno);

if ($filename !== "{no file}") {
$this->loadContext($frame, $filename, $lineno, 5);
if ($this->root_path && strpos($filename, $this->root_path) !== -1) {
$frame->abs_path = $filename;
$frame->filename = substr($filename, strlen($this->root_path));
}

if ($this->root_path && strpos($filename, $this->root_path) !== -1) {
$frame->abs_path = $filename;
$frame->filename = substr($filename, strlen($this->root_path));
if ($this->isFiltered($filename)) {
$frame->context_line = "### FILTERED FILE ###";
} else {
if ($filename !== self::NO_FILE) {
$this->loadContext($frame, $filename, $lineno, 5);
}
}

if (isset($entry['args'])) {
$frame->vars = $this->extractVars($entry);
if (isset($entry['args'])) {
$frame->vars = $this->extractVars($entry);
}
}

return $frame;
}

/**
* @param string $filename absolute path to source-file
*
* @return bool true, if the given file matches any defined filter pattern
*
* @see $filters
*/
protected function isFiltered(string $filename): bool
{
foreach ($this->filters as $pattern) {
if (fnmatch($pattern, substr($filename, strlen($this->root_path)))) {
return true;
}
}

return false;
}

/**
* Attempts to load lines of source-code "context" from a PHP script to a {@see StackFrame} instance.
*
Expand Down
4 changes: 2 additions & 2 deletions test/test.fixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class MockSentryClient extends SentryClient
*/
public $time = 1538738714;

public function __construct(EventCapture $capture, ?array $extensions = null)
public function __construct(EventCapture $capture, ?array $extensions = null, ?array $filters = [])
{
$this->logger = new BreadcrumbLogger();

Expand All @@ -122,7 +122,7 @@ public function __construct(EventCapture $capture, ?array $extensions = null)
$extensions ?: [
new EnvironmentReporter(),
new RequestReporter(),
new ExceptionReporter(__DIR__),
new ExceptionReporter(__DIR__, 200, $filters),
new ClientSniffer(),
new ClientIPDetector(),
]
Expand Down
93 changes: 60 additions & 33 deletions test/test.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,19 @@ function () {
}
);

test(
"can capture Exception",
function () {
function test_exception_capture($use_filter = false) {
return function () use ($use_filter) {
$dsn = new MockDSN();

$capture = new MockDirectEventCapture($dsn);

$client = new MockSentryClient($capture);
$client = new MockSentryClient(
$capture,
null,
$use_filter
? ["*.fixtures.php"]
: []
);

$client->captureException(exception_with("ouch"));

Expand Down Expand Up @@ -176,37 +181,49 @@ function () {
eq($inner_frames[2]["lineno"], 34);
eq($inner_frames[3]["lineno"], 31, "can capture line-number of failed call-site");

eq(
$inner_frames[0]["context_line"],
' $fixture->outer($arg);',
"can capture context line"
);
$FILTERED = "### FILTERED FILE ###";

eq(
$inner_frames[0]["pre_context"],
[
'',
'function exception_with($arg): Exception {',
' $fixture = new TraceFixture();',
'',
' try {'
],
"can capture pre_context"
);
if ($use_filter) {
ok(! isset($inner_frames[0]["pre_context"]), "filtering removes pre_context");

eq(
$inner_frames[0]["post_context"],
[
' } catch (Exception $exception) {',
' return $exception;',
' }',
'}',
''
],
"can capture post_context"
);
eq($inner_frames[0]["context_line"], $FILTERED, "filtering removes context_line");

eq($inner_frames[0]["vars"], ['$arg' => '"ouch"'], "can capture arguments");
ok(! isset($inner_frames[0]["post_context"]), "filtering removes post_context");

ok(! isset($inner_frames[0]["vars"]), "filtering omits arguments");
} else {
eq(
$inner_frames[0]["pre_context"],
[
'',
'function exception_with($arg): Exception {',
' $fixture = new TraceFixture();',
'',
' try {'
],
"can capture pre_context"
);

eq(
$inner_frames[0]["context_line"],
' $fixture->outer($arg);',
"can capture context_line"
);

eq(
$inner_frames[0]["post_context"],
[
' } catch (Exception $exception) {',
' return $exception;',
' }',
'}',
''
],
"can capture post_context"
);

eq($inner_frames[0]["vars"], ['$arg' => '"ouch"'], "can capture arguments");
}

$outer = $body["exception"]["values"][1];

Expand All @@ -216,7 +233,17 @@ function () {
eq($outer_frames[1]["function"], TraceFixture::class . "->outer");

// echo json_encode($body, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
};
}

test(
"can capture Exception with vars and stack-traces",
test_exception_capture()
);

test(
"can capture Exception without vars and stack-traces from files matching a filter pattern",
test_exception_capture(true)
);

test(
Expand Down

0 comments on commit e88a6ea

Please sign in to comment.