86 lines
2.4 KiB
PHP
86 lines
2.4 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
class Fingerprinter
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Compute a fingerprint hash for grouping events into issues.
|
||
|
|
*
|
||
|
|
* If the payload contains a custom `fingerprint` array (Sentry SDK standard),
|
||
|
|
* that takes precedence. Otherwise we derive one from exception type, a
|
||
|
|
* sanitized message, and the top stacktrace frames.
|
||
|
|
*/
|
||
|
|
public function compute(array $payload): string
|
||
|
|
{
|
||
|
|
if (! empty($payload['fingerprint']) && is_array($payload['fingerprint'])) {
|
||
|
|
return $this->hash($payload['fingerprint']);
|
||
|
|
}
|
||
|
|
|
||
|
|
$parts = [];
|
||
|
|
|
||
|
|
$exception = $this->extractPrimaryException($payload);
|
||
|
|
|
||
|
|
if ($exception) {
|
||
|
|
$parts[] = $exception['type'] ?? '';
|
||
|
|
$parts[] = $this->sanitizeMessage($exception['value'] ?? '');
|
||
|
|
$parts = array_merge($parts, $this->topFrames($exception, 5));
|
||
|
|
} else {
|
||
|
|
$parts[] = $payload['message'] ?? $payload['logentry']['message'] ?? 'unknown';
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->hash($parts);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function extractPrimaryException(array $payload): ?array
|
||
|
|
{
|
||
|
|
$exceptions = $payload['exception']['values'] ?? [];
|
||
|
|
|
||
|
|
return end($exceptions) ?: null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Strip variable parts (numbers, UUIDs, hex strings) so that
|
||
|
|
* "User 42 not found" and "User 99 not found" group together.
|
||
|
|
*/
|
||
|
|
private function sanitizeMessage(string $message): string
|
||
|
|
{
|
||
|
|
$message = preg_replace(
|
||
|
|
'/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i',
|
||
|
|
'{uuid}',
|
||
|
|
$message,
|
||
|
|
);
|
||
|
|
|
||
|
|
$message = preg_replace('/\b[0-9a-f]{24,}\b/i', '{hash}', $message);
|
||
|
|
|
||
|
|
$message = preg_replace('/\b\d+\b/', '{n}', $message);
|
||
|
|
|
||
|
|
return $message;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return file+function strings from the top N stacktrace frames.
|
||
|
|
*/
|
||
|
|
private function topFrames(array $exception, int $limit): array
|
||
|
|
{
|
||
|
|
$frames = $exception['stacktrace']['frames'] ?? [];
|
||
|
|
|
||
|
|
$frames = array_reverse($frames);
|
||
|
|
$frames = array_slice($frames, 0, $limit);
|
||
|
|
|
||
|
|
$parts = [];
|
||
|
|
foreach ($frames as $frame) {
|
||
|
|
$file = $frame['filename'] ?? $frame['abs_path'] ?? '';
|
||
|
|
$function = $frame['function'] ?? '';
|
||
|
|
$parts[] = "{$file}:{$function}";
|
||
|
|
}
|
||
|
|
|
||
|
|
return $parts;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function hash(array $parts): string
|
||
|
|
{
|
||
|
|
return md5(implode("\x00", $parts));
|
||
|
|
}
|
||
|
|
}
|