Skip to content
Draft
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
71 changes: 71 additions & 0 deletions Classes/CaptureLedger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);

namespace Flownative\Sentry;

/*
* This file is part of the Flownative.Sentry package.
*
* (c) Robert Lemke, Flownative GmbH - www.flownative.com
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Throwable;
use WeakMap;

/**
* Per-process ledger of throwables that already produced an accepted Sentry
* event. Prevents duplicate events from log-then-rethrow patterns (e.g.
* Flow's Doctrine Query wrapper or job queue wrappers): a throwable — or a
* wrapper carrying it anywhere in its getPrevious() chain — is only
* captured once.
*
* Backed by a WeakMap, so entries vanish with the throwable; retried jobs
* construct new throwable instances and are captured per attempt.
*/
final class CaptureLedger
{
private WeakMap $capturedThrowables;

public function __construct()
{
$this->capturedThrowables = new WeakMap();
}

public function hasBeenCaptured(Throwable $throwable): bool
{
$seenObjectIds = [];
for ($current = $throwable; $current !== null; $current = $current->getPrevious()) {
$objectId = spl_object_id($current);
if (isset($seenObjectIds[$objectId])) {
break;
}
$seenObjectIds[$objectId] = true;
if (isset($this->capturedThrowables[$current])) {
return true;
}
}
return false;
}

/**
* Only call this after a capture was ACCEPTED (not excluded, not dropped
* by a scrubber without replacement) — a rejected capture must not
* suppress a later legitimate one.
*/
public function remember(Throwable $throwable): void
{
$seenObjectIds = [];
for ($current = $throwable; $current !== null; $current = $current->getPrevious()) {
$objectId = spl_object_id($current);
if (isset($seenObjectIds[$objectId])) {
break;
}
$seenObjectIds[$objectId] = true;
$this->capturedThrowables[$current] = true;
}
}
}
18 changes: 18 additions & 0 deletions Classes/Exception/InvalidConfigurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Flownative\Sentry\Exception;

/*
* This file is part of the Flownative.Sentry package.
*
* (c) Robert Lemke, Flownative GmbH - www.flownative.com
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

class InvalidConfigurationException extends \RuntimeException
{
}
39 changes: 39 additions & 0 deletions Classes/IntegrationFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);

namespace Flownative\Sentry;

/*
* This file is part of the Flownative.Sentry package.
*
* (c) Robert Lemke, Flownative GmbH - www.flownative.com
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Closure;

/**
* Builds the callable for the SDK's "integrations" option. Only the callable
* form can remove a default integration — an "integrations" array is merged
* with the defaults by the SDK and cannot unregister anything.
*/
final class IntegrationFilter
{
/**
* @param string[] $excludedIntegrationClassNames
*/
public static function excluding(array $excludedIntegrationClassNames): Closure
{
return static function (array $integrations) use ($excludedIntegrationClassNames): array {
return array_values(array_filter(
$integrations,
static function ($integration) use ($excludedIntegrationClassNames): bool {
return !in_array(get_class($integration), $excludedIntegrationClassNames, true);
}
));
};
}
}
38 changes: 38 additions & 0 deletions Classes/Scrubbing/EventScrubberInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

namespace Flownative\Sentry\Scrubbing;

/*
* This file is part of the Flownative.Sentry package.
*
* (c) Robert Lemke, Flownative GmbH - www.flownative.com
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Sentry\Event;
use Sentry\EventHint;

/**
* A scrubber removes or redacts data from an event before it is sent to
* Sentry. Scrubbers are registered explicitly via the
* Flownative.Sentry.scrubbers settings and run for error events,
* transactions and check-ins, after the configured exception excludes.
*
* Implementations are constructed with the "options" array of their
* registry entry: __construct(array $options = []).
*
* The scrub() implementation may mutate the given event, replace it, or
* return null to discard it. If a scrubber throws, the event is discarded
* and a synthetic failure event is sent instead (fail-closed, fail-loud).
*
* The EventHint may contain the original throwable including unscrubbed
* data — it must never be copied into the event or logged.
*/
interface EventScrubberInterface
{
public function scrub(Event $event, ?EventHint $hint): ?Event;
}
89 changes: 89 additions & 0 deletions Classes/Scrubbing/FrameVarsScrubber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);

namespace Flownative\Sentry\Scrubbing;

/*
* This file is part of the Flownative.Sentry package.
*
* (c) Robert Lemke, Flownative GmbH - www.flownative.com
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Sentry\Event;
use Sentry\EventHint;
use Sentry\Frame;
use Sentry\Stacktrace;

/**
* Rebuilds all stack traces of an event with frames stripped of their
* variables (captured function arguments), absolute file paths and source
* context. Function names, cleaned file paths and line numbers remain, so
* traces stay debuggable.
*/
final class FrameVarsScrubber implements EventScrubberInterface
{
public function scrub(Event $event, ?EventHint $hint): ?Event
{
foreach ($event->getExceptions() as $exceptionDataBag) {
$stacktrace = $exceptionDataBag->getStacktrace();
if ($stacktrace !== null) {
$exceptionDataBag->setStacktrace($this->rebuildStacktrace($stacktrace));
}
}

$eventStacktrace = $event->getStacktrace();
if ($eventStacktrace !== null) {
$event->setStacktrace($this->rebuildStacktrace($eventStacktrace));
}

return $event;
}

private function rebuildStacktrace(Stacktrace $stacktrace): Stacktrace
{
$strippedFrames = [];
foreach ($stacktrace->getFrames() as $frame) {
$strippedFrames[] = new Frame(
$this->stripAnonymousClassPaths($frame->getFunctionName()),
$this->relativizeFilePath($frame->getFile()),
$frame->getLine(),
$this->stripAnonymousClassPaths($frame->getRawFunctionName()),
null,
[],
$frame->isInApp()
);
}
return new Stacktrace($strippedFrames);
}

/**
* Frame::getFile() regularly carries the absolute path for files outside
* the Flow proxy cache; local usernames and server layout must not leave
* the system.
*/
private function relativizeFilePath(string $file): string
{
if (!str_starts_with($file, '/')) {
return $file;
}
if (defined('FLOW_PATH_ROOT') && str_starts_with($file, FLOW_PATH_ROOT)) {
return substr($file, strlen(FLOW_PATH_ROOT));
}
return '…/' . basename($file);
}

/**
* Anonymous class names embed file paths (class@anonymous/path/file.php:12).
*/
private function stripAnonymousClassPaths(?string $functionName): ?string
{
if ($functionName === null || !str_contains($functionName, '@anonymous')) {
return $functionName;
}
return preg_replace('/@anonymous[^:]*(?::\d+)?(\$\w+)?/', '@anonymous', $functionName) ?? '[anonymous]';
}
}
Loading