From 0b902d7c19017446fc4733f1d3845fc06c911161 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Fri, 29 May 2026 06:02:27 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20issue=20reporter=20=E2=80=94=20local=20F?= =?UTF-8?q?P=20cache=20prevents=20duplicate=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _getFpCachePath(), _loadFpCache(), _saveFpCache() helpers - Check data/reported_issue_fps.json before GitHub Search API (falls back to /tmp/ when data/ is not writable) - Save new issue number to cache immediately after creation - Apply 30-minute comment throttle per fingerprint - Fall back to GitHub Search on first run / cache miss Fixes root cause of ~50 duplicate issues (#134 duplicates #135-#183) caused by GitHub Search API indexing delay. --- api/index.php | 73 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/api/index.php b/api/index.php index 649e9aa..eca9b6a 100644 --- a/api/index.php +++ b/api/index.php @@ -9931,9 +9931,41 @@ function checkUpdate(): void { ]); } +/** + * Return path to the local fingerprint deduplication cache. + * Falls back to /tmp when data/ is not writable (e.g. fresh install with wrong perms). + */ +function _getFpCachePath(): string { + $primary = __DIR__ . '/../data/reported_issue_fps.json'; + return is_writable(dirname($primary)) ? $primary : (sys_get_temp_dir() . '/evershelf_fps.json'); +} + +/** Load & prune (> 30 days) the local FP cache. */ +function _loadFpCache(): array { + $path = _getFpCachePath(); + if (!file_exists($path)) return []; + $data = @json_decode(@file_get_contents($path), true) ?: []; + $cutoff = time() - 30 * 86400; + return array_filter($data, fn($v) => ($v['ts'] ?? 0) > $cutoff); +} + +/** Persist the local FP cache. */ +function _saveFpCache(array $cache): void { + @file_put_contents(_getFpCachePath(), json_encode($cache), LOCK_EX); +} + /** * Create a GitHub issue, or add a comment to an existing open issue with the * same fingerprint. Uses the REST API v3 directly (no library needed). + * + * Deduplication strategy (two-layer): + * 1. Local file cache (data/reported_issue_fps.json or /tmp fallback) — checked + * first to avoid the GitHub Search API indexing delay that caused duplicate + * issues to be created in rapid succession. + * 2. GitHub Search API — used only on first occurrence (cache miss) as backup. + * + * Comment throttle: at most one recurrence comment per 30 minutes per fingerprint, + * to avoid flooding an issue when an error fires on every request. */ function _createOrCommentGithubIssue( string $token, string $repo, @@ -9944,13 +9976,27 @@ function _createOrCommentGithubIssue( $fp = _errorFingerprint($source, $type, $message); EverLog::debug('_createOrCommentGithubIssue', ['fp' => $fp, 'type' => $type]); - // ── 1. Search for an existing open issue with this fingerprint ───────── - $searchQuery = urlencode("repo:$repo is:issue is:open label:auto-report \"fp:$fp\" in:body"); - $searchResult = _githubRequest($token, 'GET', "https://api.github.com/search/issues?q=$searchQuery&per_page=1"); - + // ── 1. Check local cache (fast, avoids Search API indexing lag) ──────── + $fpCache = _loadFpCache(); $existingIssueNumber = null; - if (isset($searchResult['body']['items']) && count($searchResult['body']['items']) > 0) { - $existingIssueNumber = $searchResult['body']['items'][0]['number'] ?? null; + if (isset($fpCache[$fp])) { + $existingIssueNumber = $fpCache[$fp]['issue']; + // Comment throttle: skip if we already commented within the last 30 min + $lastComment = $fpCache[$fp]['last_comment'] ?? 0; + if (time() - $lastComment < 1800) { + EverLog::debug('_createOrCommentGithubIssue: throttled', ['fp' => $fp]); + return; + } + } else { + // ── 2. Fall back to GitHub Search (handles first run / cache cleared) ─ + $searchQuery = urlencode("repo:$repo is:issue is:open label:auto-report \"fp:$fp\" in:body"); + $searchResult = _githubRequest($token, 'GET', "https://api.github.com/search/issues?q=$searchQuery&per_page=1"); + if (!empty($searchResult['body']['items'][0]['number'])) { + $existingIssueNumber = (int)$searchResult['body']['items'][0]['number']; + // Populate local cache with what we found + $fpCache[$fp] = ['issue' => $existingIssueNumber, 'ts' => time(), 'last_comment' => 0]; + _saveFpCache($fpCache); + } } // ── Build the common details block ───────────────────────────────────── @@ -9965,7 +10011,7 @@ function _createOrCommentGithubIssue( $verMd = $version ? "\n**Version:** `$version`" : ''; if ($existingIssueNumber) { - // ── 2a. Post a comment to the existing issue ────────────────────── + // ── 3a. Post a comment to the existing issue ────────────────────── $body = "### 🔁 Recurrence — $ts\n" . "**Source:** `$source` | **Type:** `$type`\n" . $urlMd . $uaMd . $verMd . "\n" @@ -9975,8 +10021,11 @@ function _createOrCommentGithubIssue( "https://api.github.com/repos/$repo/issues/$existingIssueNumber/comments", ['body' => $body] ); + // Update throttle timestamp + $fpCache[$fp]['last_comment'] = time(); + _saveFpCache($fpCache); } else { - // ── 2b. Create a new issue ──────────────────────────────────────── + // ── 3b. Create a new issue ──────────────────────────────────────── // Determine labels from source $labelMap = [ 'pwa' => 'js-error', @@ -10004,7 +10053,7 @@ function _createOrCommentGithubIssue( . "\n" . "_This issue was created automatically by EverShelf's error reporter. fp:`{$fp}`_"; - _githubRequest($token, 'POST', + $newIssueRes = _githubRequest($token, 'POST', "https://api.github.com/repos/$repo/issues", [ 'title' => $title, @@ -10012,6 +10061,12 @@ function _createOrCommentGithubIssue( 'labels' => ['auto-report', $typeLabel], ] ); + // Save to local cache immediately to prevent duplicates on rapid recurrences + $newNum = $newIssueRes['body']['number'] ?? null; + if ($newNum) { + $fpCache[$fp] = ['issue' => (int)$newNum, 'ts' => time(), 'last_comment' => time()]; + _saveFpCache($fpCache); + } } }