Skip to content

Commit

Permalink
Merge pull request #37 from netlogix/feature/frame-scrubbing-and-cont…
Browse files Browse the repository at this point in the history
…ext-variables

feat: Enhanced variable scrubbing and context vars
  • Loading branch information
stephanschuler authored Oct 25, 2024
2 parents 54931f4 + 6c8c3ef commit 5bc037c
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 2 deletions.
5 changes: 5 additions & 0 deletions Classes/Command/SentryCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
53 changes: 51 additions & 2 deletions Classes/Integration/NetlogixIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand Down
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
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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 => [],
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions Configuration/Settings.Providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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

0 comments on commit 5bc037c

Please sign in to comment.