fix: issue reporter — local FP cache prevents duplicate issues

- 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.
This commit is contained in:
dadaloop82
2026-05-29 06:02:27 +00:00
parent 1637cc1020
commit 0b902d7c19
+63 -8
View File
@@ -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 ─────────
// ── 1. Check local cache (fast, avoids Search API indexing lag) ────────
$fpCache = _loadFpCache();
$existingIssueNumber = 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");
$existingIssueNumber = null;
if (isset($searchResult['body']['items']) && count($searchResult['body']['items']) > 0) {
$existingIssueNumber = $searchResult['body']['items'][0]['number'] ?? null;
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(
. "<!-- auto-report fp:$fp -->\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);
}
}
}