$sourceType, 'issue_category' => $issueCategory, 'issue_subtype' => $issueSubtype, 'environment_tags' => (function () use ($title) { // Extract each [bracketed] tag, then keep the known environment ones. // (explode('][') leaves brackets stuck to the first/last tag, so e.g. // "[production] ..." never matched and the env tag was dropped from the // dedup hash — letting prod and staging issues collide onto one ticket.) preg_match_all('/\[([^\]]+)\]/', $title, $m); return array_values(array_filter( $m[1], fn($tag) => in_array($tag, ['production', 'development', 'staging', 'single-node', 'cluster-wide'], true) )); })(), ]; // Manual tickets should be unique by title (so different software installs don't collide) if ($sourceType === 'manual') { $stableComponents['title'] = $title; } // Include hostname for node-specific issues if (!$isClusterWide) { $stableComponents['hostname'] = $hostname; } // Include drive identifier for drive-specific tickets. // Use serial when available (stable across reboots/reshuffles); fall back to // device path for tickets created before serial was added to the payload. if ($isDriveTicket) { $stableComponents['drive'] = $serial ?? ($deviceMatches[0] ?? ''); } sort($stableComponents['environment_tags']); return $stableComponents; } /** SHA-256 dedup hash of the payload's stable components. */ public static function generate(array $data): string { return hash('sha256', json_encode(self::components($data), JSON_UNESCAPED_SLASHES)); } }