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)); } }