Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhanced variable scrubbing and context vars #37

Merged
merged 2 commits into from
Oct 25, 2024
Merged
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
5 changes: 5 additions & 0 deletions Classes/Command/SentryCommandController.php
Original file line number Diff line number Diff line change
@@ -45,6 +45,11 @@ public function showScopeCommand(): void

$this->outputLine();

$this->outputLine('Scope Extra:');
\Neos\Flow\var_dump($this->scopeProvider->collectContexts());

$this->outputLine();

$this->outputLine('Scope Release:');
\Neos\Flow\var_dump($this->scopeProvider->collectRelease());

53 changes: 51 additions & 2 deletions Classes/Integration/NetlogixIntegration.php
Original file line number Diff line number Diff line change
@@ -106,16 +106,17 @@ function ($exception) {
private static function rewriteStacktraceAndFlagInApp(Stacktrace $stacktrace): Stacktrace
{
$frames = array_map(function ($frame) {
$functionName = self::replaceProxyClassName($frame->getFunctionName());
$classPathAndFilename = self::getOriginalClassPathAndFilename($frame->getFile());
return new Frame(
self::replaceProxyClassName($frame->getFunctionName()),
$functionName,
$classPathAndFilename,
$frame->getLine(),
self::replaceProxyClassName($frame->getRawFunctionName()),
$frame->getAbsoluteFilePath()
? Files::concatenatePaths([FLOW_PATH_ROOT, trim($classPathAndFilename, '/')])
: null,
$frame->getVars(),
self::scrubVariablesFromFrame((string)$functionName, $frame->getVars()),
self::isInApp($classPathAndFilename)
);
}, $stacktrace->getFrames());
@@ -159,6 +160,51 @@ private static function isInApp(string $path): bool
return true;
}

private static function scrubVariablesFromFrame(string $traceFunction, array $frameVariables): array
{
if (!$frameVariables) {
return $frameVariables;
}
assert(is_array($frameVariables));

$config = Bootstrap::$staticObjectManager
->get(ConfigurationManager::class)
->getConfiguration(
ConfigurationManager::CONFIGURATION_TYPE_SETTINGS,
'Netlogix.Sentry.variableScrubbing'
) ?? [];

$scrubbing = (bool)($config['scrubbing'] ?? false);
if (!$scrubbing) {
return $frameVariables;
}

$keep = $config['keepFromScrubbing'] ?? [];
if (!$keep) {
return [];
}

$result = [];
$traceFunction = str_replace('_Original::', '::', $traceFunction);
foreach ($keep as $keepConfig) {
try {
['className' => $className, 'methodName' => $methodName, 'arguments' => $arguments] = $keepConfig;
$configFunction = $className . '::' . $methodName;
if ($configFunction !== $traceFunction) {
continue;
}
foreach ($arguments as $argumentName) {
$result[$argumentName] = $frameVariables[$argumentName] ?? '👻';
}

} catch (\Exception $e) {
}

}

return $result;
}

private static function configureScopeForEvent(Event $event, EventHint $hint): void
{
try {
@@ -170,6 +216,9 @@ private static function configureScopeForEvent(Event $event, EventHint $hint): v
$configureEvent = function () use ($event, $scopeProvider) {
$event->setEnvironment($scopeProvider->collectEnvironment());
$event->setExtra($scopeProvider->collectExtra());
foreach ($scopeProvider->collectContexts() as $key => $value) {
$event->setContext($key, $value);
}
$event->setRelease($scopeProvider->collectRelease());
$event->setTags($scopeProvider->collectTags());
$userData = $scopeProvider->collectUser();
14 changes: 14 additions & 0 deletions Classes/Scope/Context/ContextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);

namespace Netlogix\Sentry\Scope\Context;

interface ContextProvider
{

/**
* @return array<string, mixed>
*/
public function getContexts(): array;

}
161 changes: 161 additions & 0 deletions Classes/Scope/Extra/VariablesFromStackProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php

declare(strict_types=1);

namespace Netlogix\Sentry\Scope\Extra;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Reflection\MethodReflection;
use Neos\Utility\ObjectAccess;
use Netlogix\Sentry\Scope\ScopeProvider;
use Sentry\SentrySdk;
use Sentry\Serializer\RepresentationSerializer;
use Throwable;
use Traversable;

use function array_combine;
use function array_filter;
use function class_exists;
use function is_string;
use function iterator_to_array;
use function json_encode;
use function method_exists;

/**
* @Flow\Scope("singleton")
*/
final class VariablesFromStackProvider implements ExtraProvider
{
private const FUNCTION_PATTERN = '%s::%s()';

/**
* @var ScopeProvider
* @Flow\Inject
*/
protected $scopeProvider;

/**
* @var array
* @Flow\InjectConfiguration(package="Netlogix.Sentry", path="variableScrubbing.contextDetails")
*/
protected array $settings = [];

public function getExtra(): array
{
$result = iterator_to_array($this->collectDataFromTraversables(), false);
if ($result) {
return ['Method Arguments' => $result];
} else {
return [];
}
}

private function collectDataFromTraversables(): Traversable
{
$throwable = $this->scopeProvider->getCurrentThrowable();
while ($throwable instanceof Throwable) {
yield from $this->collectDataFromTraces($throwable);
$throwable = $throwable->getPrevious();
}
}

private function collectDataFromTraces(Throwable $throwable): Traversable
{
$traces = $throwable->getTrace();
foreach ($traces as $trace) {
yield from $this->collectDataFromTrace($trace);
}
}

private function collectDataFromTrace(array $trace): Traversable
{
$traceFunction = self::callablePattern($trace['class'] ?? '', $trace['function'] ?? '');

$settings = iterator_to_array($this->getSettings(), false);
foreach ($settings as ['className' => $className, 'methodName' => $methodName, 'argumentPaths' => $argumentPaths]) {
$configFunction = self::callablePattern($className, $methodName);
if ($traceFunction !== $configFunction) {
continue;
}
$values = [];
foreach ($argumentPaths as $argumentPathName => $argumentPathLookup) {
try {
$values[$argumentPathName] = $this->representationSerialize(
ObjectAccess::getPropertyPath($trace['args'], $argumentPathLookup)
);
} catch (Throwable $t) {
$values[$argumentPathName] = '👻';
}
}
yield [$configFunction => $values];
}
}

private function representationSerialize($value)
{
static $representationSerialize;

if (!$representationSerialize) {
$client = SentrySdk::getCurrentHub()->getClient();
if ($client) {
$serializer = new RepresentationSerializer($client->getOptions());
$representationSerialize = function($value) use ($serializer) {
return $serializer->representationSerialize($value);
};
} else {
$representationSerialize = function($value) {
return json_encode($value);
};
}
}

return $representationSerialize($value);
}

private function getSettings(): Traversable
{
foreach ($this->settings as $config) {
$className = $config['className'] ?? null;
if (!$className || !class_exists($className)) {
continue;
}

$methodName = $config['methodName'] ?? null;
if (!$methodName || !method_exists($className, $methodName)) {
continue;
}

if (!is_array($config['arguments'])) {
continue;
}

$argumentPaths = array_filter($config['arguments'] ?? [], function ($argumentPath) {
return is_string($argumentPath) && $argumentPath;
});
$argumentPaths = array_combine($argumentPaths, $argumentPaths);

$reflection = new MethodReflection($className, $methodName);
foreach ($reflection->getParameters() as $parameter) {
$search = sprintf('/^%s./', $parameter->getName());
$replace = sprintf('%d.', $parameter->getPosition());
$argumentPaths = preg_replace($search, $replace, $argumentPaths);
}

yield [
'className' => $className,
'methodName' => $methodName,
'argumentPaths' => $argumentPaths
];
yield [
'className' => $className . '_Original',
'methodName' => $methodName,
'argumentPaths' => $argumentPaths
];
}
}

private function callablePattern(string $className, string $methodName): string
{
return sprintf(self::FUNCTION_PATTERN, $className, $methodName);
}
}
17 changes: 17 additions & 0 deletions Classes/Scope/ScopeProvider.php
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Utility\PositionalArraySorter;
use Netlogix\Sentry\Exception\InvalidProviderType;
use Netlogix\Sentry\Scope\Context\ContextProvider;
use Netlogix\Sentry\Scope\Environment\EnvironmentProvider;
use Netlogix\Sentry\Scope\Extra\ExtraProvider;
use Netlogix\Sentry\Scope\Release\ReleaseProvider;
@@ -24,13 +25,15 @@ class ScopeProvider

private const SCOPE_ENVIRONMENT = 'environment';
private const SCOPE_EXTRA = 'extra';
private const SCOPE_CONTEXTS = 'contexts';
private const SCOPE_RELEASE = 'release';
private const SCOPE_TAGS = 'tags';
private const SCOPE_USER = 'user';

private const SCOPE_TYPE_MAPPING = [
self::SCOPE_ENVIRONMENT => EnvironmentProvider::class,
self::SCOPE_EXTRA => ExtraProvider::class,
self::SCOPE_CONTEXTS => ContextProvider::class,
self::SCOPE_RELEASE => ReleaseProvider::class,
self::SCOPE_TAGS => TagProvider::class,
self::SCOPE_USER => UserProvider::class,
@@ -52,6 +55,7 @@ class ScopeProvider
protected $providers = [
self::SCOPE_ENVIRONMENT => [],
self::SCOPE_EXTRA => [],
self::SCOPE_CONTEXTS => [],
self::SCOPE_RELEASE => [],
self::SCOPE_TAGS => [],
self::SCOPE_USER => [],
@@ -98,6 +102,19 @@ public function collectExtra(): array
return $extra;
}

public function collectContexts(): array
{
$contexts = [];

foreach ($this->providers[self::SCOPE_CONTEXTS] as $provider) {
assert($provider instanceof ContextProvider);

$extra = array_merge_recursive($extra, $provider->getContexts());
}

return $contexts;
}

public function collectRelease(): ?string
{
$release = null;
1 change: 1 addition & 0 deletions Configuration/Settings.Providers.yaml
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ Netlogix:

extra:
'Netlogix\Sentry\Scope\Extra\ReferenceCodeProvider': true
'Netlogix\Sentry\Scope\Extra\VariablesFromStackProvider': true

release:
# See Configuration/Settings.Release.yaml for settings
38 changes: 38 additions & 0 deletions Configuration/Settings.VariableScrubbing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Netlogix:
Sentry:

variableScrubbing:

scrubbing: true

keepFromScrubbing:

'Neos\ContentRepository\Search\Indexer\NodeIndexingManager::indexNode()':
className: 'Neos\ContentRepository\Search\Indexer\NodeIndexingManager'
methodName: 'indexNode'
arguments:
- 'node'

'Flowpack\ElasticSearch\ContentRepositoryAdaptor\Indexer\NodeIndexer::indexNode()':
className: 'Flowpack\ElasticSearch\ContentRepositoryAdaptor\Indexer\NodeIndexer'
methodName: 'indexNode'
arguments:
- 'node'

contextDetails:

'Neos\ContentRepository\Search\Indexer\NodeIndexingManager::indexNode()':
className: 'Neos\ContentRepository\Search\Indexer\NodeIndexingManager'
methodName: 'indexNode'
arguments:
- 'node.path'
- 'node.identifier'
- 'node.name'

'Flowpack\ElasticSearch\ContentRepositoryAdaptor\Indexer\NodeIndexer::indexNode()':
className: 'Flowpack\ElasticSearch\ContentRepositoryAdaptor\Indexer\NodeIndexer'
methodName: 'indexNode'
arguments:
- 'node.path'
- 'node.identifier'
- 'node.name'
Loading