@littlebearapps/platform-admin-sdk 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ import type {
14
14
  ErrorType,
15
15
  ErrorStatus,
16
16
  GitHubIssueType,
17
+ Priority,
17
18
  } from './lib/error-collector/types';
18
19
  import {
19
20
  shouldCapture,
@@ -907,101 +908,48 @@ async function computeFingerprintForLog(
907
908
  }
908
909
 
909
910
  /**
910
- * Process a single soft error log from a tail event
911
- * Called for each unique error in an invocation with multiple errors
911
+ * Shared issue creation and lifecycle handling for new and recurring errors.
912
+ * Both processSoftErrorLog() and processEvent() delegate here after computing
913
+ * fingerprints and occurrence records.
912
914
  */
913
- async function processSoftErrorLog(
914
- event: TailEvent,
915
- env: Env,
916
- github: GitHubClient,
917
- mapping: ScriptMapping,
918
- errorLog: { level: string; message: unknown[]; timestamp: number },
919
- dynamicPatterns: CompiledPattern[] = []
920
- ): Promise<void> {
921
- const errorType: ErrorType = 'soft_error';
922
-
923
- // Check rate limit
924
- const withinLimits = await checkRateLimit(env.PLATFORM_CACHE, event.scriptName);
925
- if (!withinLimits) {
926
- const coreMsg = extractCoreMessage(errorLog.message[0]);
927
- console.log(`Rate limited for script: ${event.scriptName} (error: ${coreMsg.slice(0, 50)})`);
928
- return;
929
- }
930
-
931
- // Compute fingerprint for this specific error log (now returns FingerprintResult)
932
- const fingerprintResult = await computeFingerprintForLog(event, errorType, errorLog, dynamicPatterns);
933
- const { fingerprint, category, dynamicPatternId } = fingerprintResult;
934
- const isTransient = category !== null;
935
-
936
- // Log dynamic pattern matches for observability and record evidence
937
- if (dynamicPatternId) {
938
- console.log(`Dynamic pattern match (soft error): ${category} (pattern: ${dynamicPatternId})`);
939
- // Record match evidence for human review context
940
- await recordPatternMatchEvidence(env.PLATFORM_DB, {
941
- patternId: dynamicPatternId,
942
- scriptName: event.scriptName,
943
- project: mapping.project,
944
- errorFingerprint: fingerprint,
945
- normalizedMessage: fingerprintResult.normalizedMessage ?? undefined,
946
- errorType: 'soft_error',
947
- priority: calculatePriority(errorType, mapping.tier, 1),
948
- });
949
- // Increment match_count so shadow evaluation has accurate stats
950
- await env.PLATFORM_DB.prepare(
951
- `UPDATE transient_pattern_suggestions SET match_count = match_count + 1, last_matched_at = unixepoch() WHERE id = ?`
952
- ).bind(dynamicPatternId).run();
953
- }
954
-
955
- // For transient errors, check if we already have an issue for today's window
956
- if (isTransient && category) {
957
- const existingIssue = await checkTransientErrorWindow(
958
- env.PLATFORM_CACHE,
959
- event.scriptName,
960
- category
961
- );
962
- if (existingIssue) {
963
- // Just update occurrence count in D1, don't create new issue
964
- await env.PLATFORM_DB.prepare(
965
- `
966
- UPDATE error_occurrences
967
- SET occurrence_count = occurrence_count + 1,
968
- last_seen_at = unixepoch(),
969
- updated_at = unixepoch()
970
- WHERE fingerprint = ?
971
- `
972
- )
973
- .bind(fingerprint)
974
- .run();
975
- console.log(
976
- `Transient soft error (${category}) for ${event.scriptName} - issue #${existingIssue} exists for today`
977
- );
978
- return;
979
- }
980
- }
981
-
982
- // Get or create occurrence
983
- const { isNew, occurrence } = await getOrCreateOccurrence(
984
- env.PLATFORM_DB,
985
- env.PLATFORM_CACHE,
986
- fingerprint,
987
- event.scriptName,
988
- mapping.project,
915
+ async function handleNewOrRecurringError(ctx: {
916
+ event: TailEvent;
917
+ env: Env;
918
+ github: GitHubClient;
919
+ mapping: ScriptMapping;
920
+ errorType: ErrorType;
921
+ fingerprint: string;
922
+ category: string | null;
923
+ isTransient: boolean;
924
+ occurrence: {
925
+ id: string;
926
+ occurrence_count: number;
927
+ github_issue_number?: number;
928
+ github_issue_url?: string;
929
+ status: ErrorStatus;
930
+ };
931
+ isNew: boolean;
932
+ priority: Priority;
933
+ title: string;
934
+ }): Promise<void> {
935
+ const {
936
+ event,
937
+ env,
938
+ github,
939
+ mapping,
989
940
  errorType,
990
- calculatePriority(errorType, mapping.tier, 1),
991
- mapping.repository
992
- );
993
-
994
- // Update context
995
- await updateOccurrenceContext(env.PLATFORM_DB, fingerprint, event);
996
-
997
- // Calculate priority with actual occurrence count
998
- const priority = calculatePriority(errorType, mapping.tier, occurrence.occurrence_count);
941
+ fingerprint,
942
+ category,
943
+ isTransient,
944
+ occurrence,
945
+ isNew,
946
+ priority,
947
+ title,
948
+ } = ctx;
949
+ const [owner, repo] = mapping.repository.split('/');
999
950
 
1000
- // If this is a new error, create a GitHub issue (with dedup check)
1001
951
  if (isNew) {
1002
952
  try {
1003
- const [owner, repo] = mapping.repository.split('/');
1004
-
1005
953
  // RACE CONDITION PREVENTION: Acquire lock before searching/creating
1006
954
  const lockAcquired = await acquireIssueLock(env.PLATFORM_CACHE, fingerprint);
1007
955
  if (!lockAcquired) {
@@ -1037,7 +985,6 @@ async function processSoftErrorLog(
1037
985
  );
1038
986
 
1039
987
  if (existingIssue.state === 'closed') {
1040
- // Reopen the issue
1041
988
  await github.updateIssue({
1042
989
  owner,
1043
990
  repo,
@@ -1050,7 +997,6 @@ async function processSoftErrorLog(
1050
997
 
1051
998
  await github.addComment(owner, repo, existingIssue.number, comment);
1052
999
 
1053
- // Update D1 with the found issue number
1054
1000
  await updateOccurrenceWithIssue(
1055
1001
  env.PLATFORM_DB,
1056
1002
  env.PLATFORM_CACHE,
@@ -1059,7 +1005,6 @@ async function processSoftErrorLog(
1059
1005
  `https://github.com/${owner}/${repo}/issues/${existingIssue.number}`
1060
1006
  );
1061
1007
 
1062
- // For transient errors, record the issue in the window cache
1063
1008
  if (isTransient && category) {
1064
1009
  await setTransientErrorWindow(
1065
1010
  env.PLATFORM_CACHE,
@@ -1069,12 +1014,10 @@ async function processSoftErrorLog(
1069
1014
  );
1070
1015
  }
1071
1016
 
1072
- return; // Don't create a new issue
1017
+ return;
1073
1018
  }
1074
1019
 
1075
- // No existing issue found - create new (original code)
1076
- const coreMsg = extractCoreMessage(errorLog.message[0]);
1077
- const title = `[${event.scriptName}] Error: ${coreMsg.slice(0, 60)}`.slice(0, 100);
1020
+ // No existing issue found - create new
1078
1021
  const body = formatIssueBody(
1079
1022
  event,
1080
1023
  errorType,
@@ -1085,7 +1028,6 @@ async function processSoftErrorLog(
1085
1028
  );
1086
1029
  const labels = getLabels(errorType, priority);
1087
1030
 
1088
- // Add transient label for transient errors
1089
1031
  if (isTransient) {
1090
1032
  labels.push('cf:transient');
1091
1033
  }
@@ -1101,10 +1043,9 @@ async function processSoftErrorLog(
1101
1043
  });
1102
1044
 
1103
1045
  console.log(
1104
- `Created issue #${issue.number} for ${event.scriptName} - ${coreMsg.slice(0, 30)}${isTransient ? ` (transient: ${category})` : ''}`
1046
+ `Created issue #${issue.number} for ${event.scriptName}${isTransient ? ` (transient: ${category})` : ''}`
1105
1047
  );
1106
1048
 
1107
- // Update occurrence with issue details
1108
1049
  await updateOccurrenceWithIssue(
1109
1050
  env.PLATFORM_DB,
1110
1051
  env.PLATFORM_CACHE,
@@ -1113,7 +1054,6 @@ async function processSoftErrorLog(
1113
1054
  issue.html_url
1114
1055
  );
1115
1056
 
1116
- // For transient errors, record the issue in the window cache
1117
1057
  if (isTransient && category) {
1118
1058
  await setTransientErrorWindow(env.PLATFORM_CACHE, event.scriptName, category, issue.number);
1119
1059
  }
@@ -1132,13 +1072,12 @@ async function processSoftErrorLog(
1132
1072
  priority,
1133
1073
  errorType,
1134
1074
  event.scriptName,
1135
- coreMsg,
1075
+ title,
1136
1076
  issue.number,
1137
1077
  issue.html_url,
1138
1078
  mapping.project
1139
1079
  );
1140
1080
  } finally {
1141
- // Always release lock
1142
1081
  await releaseIssueLock(env.PLATFORM_CACHE, fingerprint);
1143
1082
  }
1144
1083
  } catch (e) {
@@ -1146,12 +1085,10 @@ async function processSoftErrorLog(
1146
1085
  }
1147
1086
  } else if (occurrence.github_issue_number && occurrence.status === 'resolved') {
1148
1087
  // Error recurred after being resolved
1149
- // Skip regression logic for transient errors - they're expected to recur
1150
1088
  if (isTransient) {
1151
1089
  console.log(
1152
- `Transient soft error (${category}) recurred for ${event.scriptName} - not marking as regression`
1090
+ `Transient error (${category}) recurred for ${event.scriptName} - not marking as regression`
1153
1091
  );
1154
- // Just update to open status without regression label
1155
1092
  await env.PLATFORM_DB.prepare(
1156
1093
  `
1157
1094
  UPDATE error_occurrences
@@ -1169,9 +1106,6 @@ async function processSoftErrorLog(
1169
1106
 
1170
1107
  // Non-transient error: apply regression logic
1171
1108
  try {
1172
- const [owner, repo] = mapping.repository.split('/');
1173
-
1174
- // Check if issue is muted - if so, don't reopen or comment
1175
1109
  const muted = await isIssueMuted(github, owner, repo, occurrence.github_issue_number);
1176
1110
  if (muted) {
1177
1111
  console.log(`Issue #${occurrence.github_issue_number} is muted, skipping reopen`);
@@ -1196,7 +1130,6 @@ async function processSoftErrorLog(
1196
1130
 
1197
1131
  console.log(`Reopened issue #${occurrence.github_issue_number} as regression`);
1198
1132
 
1199
- // Update status in D1
1200
1133
  await env.PLATFORM_DB.prepare(
1201
1134
  `
1202
1135
  UPDATE error_occurrences
@@ -1214,17 +1147,14 @@ async function processSoftErrorLog(
1214
1147
  }
1215
1148
  } else if (occurrence.github_issue_number) {
1216
1149
  // Update existing issue with new occurrence count (every 10 occurrences)
1217
- if (occurrence.occurrence_count % 10 === 0) {
1218
- try {
1219
- const [owner, repo] = mapping.repository.split('/');
1220
-
1221
- // Check if issue is muted - if so, don't add comments
1222
- const muted = await isIssueMuted(github, owner, repo, occurrence.github_issue_number);
1223
- if (muted) {
1224
- console.log(`Issue #${occurrence.github_issue_number} is muted, skipping comment`);
1225
- return;
1226
- }
1150
+ try {
1151
+ const muted = await isIssueMuted(github, owner, repo, occurrence.github_issue_number);
1152
+ if (muted) {
1153
+ console.log(`Issue #${occurrence.github_issue_number} is muted, skipping comment`);
1154
+ return;
1155
+ }
1227
1156
 
1157
+ if (occurrence.occurrence_count % 10 === 0) {
1228
1158
  await github.addComment(
1229
1159
  owner,
1230
1160
  repo,
@@ -1232,13 +1162,124 @@ async function processSoftErrorLog(
1232
1162
  `📊 **Occurrence Update**\n\nThis error has now occurred **${occurrence.occurrence_count} times**.\n\n- **Last Seen**: ${new Date().toISOString()}\n- **Colo**: ${event.event?.request?.cf?.colo || 'unknown'}`
1233
1163
  );
1234
1164
  console.log(`Updated issue #${occurrence.github_issue_number} with occurrence count`);
1235
- } catch (e) {
1236
- console.error(`Failed to update issue: ${e}`);
1237
1165
  }
1166
+ } catch (e) {
1167
+ console.error(`Failed to update issue: ${e}`);
1238
1168
  }
1239
1169
  }
1240
1170
  }
1241
1171
 
1172
+ /**
1173
+ * Process a single soft error log from a tail event
1174
+ * Called for each unique error in an invocation with multiple errors
1175
+ */
1176
+ async function processSoftErrorLog(
1177
+ event: TailEvent,
1178
+ env: Env,
1179
+ github: GitHubClient,
1180
+ mapping: ScriptMapping,
1181
+ errorLog: { level: string; message: unknown[]; timestamp: number },
1182
+ dynamicPatterns: CompiledPattern[] = []
1183
+ ): Promise<void> {
1184
+ const errorType: ErrorType = 'soft_error';
1185
+
1186
+ // Check rate limit
1187
+ const withinLimits = await checkRateLimit(env.PLATFORM_CACHE, event.scriptName);
1188
+ if (!withinLimits) {
1189
+ const coreMsg = extractCoreMessage(errorLog.message[0]);
1190
+ console.log(`Rate limited for script: ${event.scriptName} (error: ${coreMsg.slice(0, 50)})`);
1191
+ return;
1192
+ }
1193
+
1194
+ // Compute fingerprint for this specific error log (now returns FingerprintResult)
1195
+ const fingerprintResult = await computeFingerprintForLog(event, errorType, errorLog, dynamicPatterns);
1196
+ const { fingerprint, category, dynamicPatternId } = fingerprintResult;
1197
+ const isTransient = category !== null;
1198
+
1199
+ // Log dynamic pattern matches for observability and record evidence
1200
+ if (dynamicPatternId) {
1201
+ console.log(`Dynamic pattern match (soft error): ${category} (pattern: ${dynamicPatternId})`);
1202
+ // Record match evidence for human review context
1203
+ await recordPatternMatchEvidence(env.PLATFORM_DB, {
1204
+ patternId: dynamicPatternId,
1205
+ scriptName: event.scriptName,
1206
+ project: mapping.project,
1207
+ errorFingerprint: fingerprint,
1208
+ normalizedMessage: fingerprintResult.normalizedMessage ?? undefined,
1209
+ errorType: 'soft_error',
1210
+ priority: calculatePriority(errorType, mapping.tier, 1),
1211
+ });
1212
+ // Increment match_count so shadow evaluation has accurate stats
1213
+ await env.PLATFORM_DB.prepare(
1214
+ `UPDATE transient_pattern_suggestions SET match_count = match_count + 1, last_matched_at = unixepoch() WHERE id = ?`
1215
+ ).bind(dynamicPatternId).run();
1216
+ }
1217
+
1218
+ // For transient errors, check if we already have an issue for today's window
1219
+ if (isTransient && category) {
1220
+ const existingIssue = await checkTransientErrorWindow(
1221
+ env.PLATFORM_CACHE,
1222
+ event.scriptName,
1223
+ category
1224
+ );
1225
+ if (existingIssue) {
1226
+ // Just update occurrence count in D1, don't create new issue
1227
+ await env.PLATFORM_DB.prepare(
1228
+ `
1229
+ UPDATE error_occurrences
1230
+ SET occurrence_count = occurrence_count + 1,
1231
+ last_seen_at = unixepoch(),
1232
+ updated_at = unixepoch()
1233
+ WHERE fingerprint = ?
1234
+ `
1235
+ )
1236
+ .bind(fingerprint)
1237
+ .run();
1238
+ console.log(
1239
+ `Transient soft error (${category}) for ${event.scriptName} - issue #${existingIssue} exists for today`
1240
+ );
1241
+ return;
1242
+ }
1243
+ }
1244
+
1245
+ // Get or create occurrence
1246
+ const { isNew, occurrence } = await getOrCreateOccurrence(
1247
+ env.PLATFORM_DB,
1248
+ env.PLATFORM_CACHE,
1249
+ fingerprint,
1250
+ event.scriptName,
1251
+ mapping.project,
1252
+ errorType,
1253
+ calculatePriority(errorType, mapping.tier, 1),
1254
+ mapping.repository
1255
+ );
1256
+
1257
+ // Update context
1258
+ await updateOccurrenceContext(env.PLATFORM_DB, fingerprint, event);
1259
+
1260
+ // Calculate priority with actual occurrence count
1261
+ const priority = calculatePriority(errorType, mapping.tier, occurrence.occurrence_count);
1262
+
1263
+ // Build title from the specific errorLog (important for multi-error processing)
1264
+ const coreMsg = extractCoreMessage(errorLog.message[0]);
1265
+ const title = `[${event.scriptName}] Error: ${coreMsg.slice(0, 60)}`.slice(0, 100);
1266
+
1267
+ await handleNewOrRecurringError({
1268
+ event,
1269
+ env,
1270
+ github,
1271
+ mapping,
1272
+ errorType,
1273
+ fingerprint,
1274
+ category,
1275
+ isTransient,
1276
+ occurrence,
1277
+ isNew,
1278
+ priority,
1279
+ title,
1280
+ });
1281
+ }
1282
+
1242
1283
  /**
1243
1284
  * Process a single tail event
1244
1285
  */
@@ -1404,247 +1445,23 @@ async function processEvent(
1404
1445
  // Calculate priority with actual occurrence count
1405
1446
  const priority = calculatePriority(errorType, mapping.tier, occurrence.occurrence_count);
1406
1447
 
1407
- // If this is a new error, create a GitHub issue (with dedup check)
1408
- if (isNew) {
1409
- try {
1410
- const [owner, repo] = mapping.repository.split('/');
1411
-
1412
- // RACE CONDITION PREVENTION: Acquire lock before searching/creating
1413
- const lockAcquired = await acquireIssueLock(env.PLATFORM_CACHE, fingerprint);
1414
- if (!lockAcquired) {
1415
- console.log(`Lock held by another worker for ${fingerprint}, skipping`);
1416
- return;
1417
- }
1418
-
1419
- try {
1420
- // DEDUP CHECK: Search GitHub for existing issue with this fingerprint
1421
- const existingIssue = await findExistingIssueByFingerprint(github, owner, repo, fingerprint);
1422
-
1423
- if (existingIssue) {
1424
- // Check if issue is muted/wontfix - don't reopen or create new
1425
- if (existingIssue.shouldSkip) {
1426
- console.log(`Issue #${existingIssue.number} is muted/wontfix, skipping`);
1427
- // Still link D1 record to prevent future searches
1428
- await updateOccurrenceWithIssue(
1429
- env.PLATFORM_DB,
1430
- env.PLATFORM_CACHE,
1431
- fingerprint,
1432
- existingIssue.number,
1433
- `https://github.com/${owner}/${repo}/issues/${existingIssue.number}`
1434
- );
1435
- return;
1436
- }
1437
-
1438
- // Found existing issue - update it instead of creating new
1439
- const comment = formatRecurrenceComment(
1440
- event,
1441
- errorType,
1442
- occurrence.occurrence_count,
1443
- existingIssue.state === 'closed'
1444
- );
1445
-
1446
- if (existingIssue.state === 'closed') {
1447
- // Reopen the issue
1448
- await github.updateIssue({
1449
- owner,
1450
- repo,
1451
- issue_number: existingIssue.number,
1452
- state: 'open',
1453
- });
1454
- await github.addLabels(owner, repo, existingIssue.number, ['cf:regression']);
1455
- console.log(`Reopened existing issue #${existingIssue.number} (dedup: ${fingerprint})`);
1456
- }
1457
-
1458
- await github.addComment(owner, repo, existingIssue.number, comment);
1459
-
1460
- // Update D1 with the found issue number
1461
- await updateOccurrenceWithIssue(
1462
- env.PLATFORM_DB,
1463
- env.PLATFORM_CACHE,
1464
- fingerprint,
1465
- existingIssue.number,
1466
- `https://github.com/${owner}/${repo}/issues/${existingIssue.number}`
1467
- );
1468
-
1469
- // For transient errors, record the issue in the window cache
1470
- if (isTransient && category) {
1471
- await setTransientErrorWindow(
1472
- env.PLATFORM_CACHE,
1473
- event.scriptName,
1474
- category,
1475
- existingIssue.number
1476
- );
1477
- }
1478
-
1479
- return; // Don't create a new issue
1480
- }
1481
-
1482
- // No existing issue found - create new (original code)
1483
- const title = formatErrorTitle(errorType, event, event.scriptName);
1484
- const body = formatIssueBody(
1485
- event,
1486
- errorType,
1487
- priority,
1488
- mapping,
1489
- fingerprint,
1490
- occurrence.occurrence_count
1491
- );
1492
- const labels = getLabels(errorType, priority);
1493
-
1494
- // Add transient label for transient errors
1495
- if (isTransient) {
1496
- labels.push('cf:transient');
1497
- }
1448
+ // Build title using the standard formatter (handles exception, soft_error, etc.)
1449
+ const title = formatErrorTitle(errorType, event, event.scriptName);
1498
1450
 
1499
- const issue = await github.createIssue({
1500
- owner,
1501
- repo,
1502
- title,
1503
- body,
1504
- labels,
1505
- type: getGitHubIssueType(errorType),
1506
- assignees: env.DEFAULT_ASSIGNEE ? [env.DEFAULT_ASSIGNEE] : [],
1507
- });
1508
-
1509
- console.log(
1510
- `Created issue #${issue.number} for ${event.scriptName}${isTransient ? ` (transient: ${category})` : ''}`
1511
- );
1512
-
1513
- // Update occurrence with issue details
1514
- await updateOccurrenceWithIssue(
1515
- env.PLATFORM_DB,
1516
- env.PLATFORM_CACHE,
1517
- fingerprint,
1518
- issue.number,
1519
- issue.html_url
1520
- );
1521
-
1522
- // For transient errors, record the issue in the window cache
1523
- if (isTransient && category) {
1524
- await setTransientErrorWindow(env.PLATFORM_CACHE, event.scriptName, category, issue.number);
1525
- }
1526
-
1527
- // Add to project board
1528
- try {
1529
- const issueDetails = await github.getIssue(owner, repo, issue.number);
1530
- await github.addToProject(issueDetails.node_id, env.GITHUB_PROJECT_ID);
1531
- console.log(`Added issue #${issue.number} to project board`);
1532
- } catch (e) {
1533
- console.error(`Failed to add to project board: ${e}`);
1534
- }
1535
-
1536
- // Create dashboard notification for P0-P2 errors
1537
- await createDashboardNotification(
1538
- env.NOTIFICATIONS_API,
1539
- priority,
1540
- errorType,
1541
- event.scriptName,
1542
- title,
1543
- issue.number,
1544
- issue.html_url,
1545
- mapping.project
1546
- );
1547
- } finally {
1548
- // Always release lock
1549
- await releaseIssueLock(env.PLATFORM_CACHE, fingerprint);
1550
- }
1551
- } catch (e) {
1552
- console.error(`Failed to create GitHub issue: ${e}`);
1553
- }
1554
- } else if (occurrence.github_issue_number && occurrence.status === 'resolved') {
1555
- // Error recurred after being resolved
1556
- // Skip regression logic for transient errors - they're expected to recur
1557
- if (isTransient) {
1558
- console.log(
1559
- `Transient error (${category}) recurred for ${event.scriptName} - not marking as regression`
1560
- );
1561
- // Just update to open status without regression label
1562
- await env.PLATFORM_DB.prepare(
1563
- `
1564
- UPDATE error_occurrences
1565
- SET status = 'open',
1566
- resolved_at = NULL,
1567
- resolved_by = NULL,
1568
- updated_at = unixepoch()
1569
- WHERE fingerprint = ?
1570
- `
1571
- )
1572
- .bind(fingerprint)
1573
- .run();
1574
- return;
1575
- }
1576
-
1577
- // Non-transient error: apply regression logic
1578
- try {
1579
- const [owner, repo] = mapping.repository.split('/');
1580
-
1581
- // Check if issue is muted - if so, don't reopen or comment
1582
- const muted = await isIssueMuted(github, owner, repo, occurrence.github_issue_number);
1583
- if (muted) {
1584
- console.log(`Issue #${occurrence.github_issue_number} is muted, skipping reopen`);
1585
- return;
1586
- }
1587
-
1588
- await github.updateIssue({
1589
- owner,
1590
- repo,
1591
- issue_number: occurrence.github_issue_number,
1592
- state: 'open',
1593
- });
1594
-
1595
- await github.addLabels(owner, repo, occurrence.github_issue_number, ['cf:regression']);
1596
-
1597
- await github.addComment(
1598
- owner,
1599
- repo,
1600
- occurrence.github_issue_number,
1601
- `⚠️ **Regression Detected**\n\nThis error has recurred after being marked as resolved.\n\n- **Occurrences**: ${occurrence.occurrence_count}\n- **Last Seen**: ${new Date().toISOString()}\n\nPlease investigate if the fix was incomplete.`
1602
- );
1603
-
1604
- console.log(`Reopened issue #${occurrence.github_issue_number} as regression`);
1605
-
1606
- // Update status in D1
1607
- await env.PLATFORM_DB.prepare(
1608
- `
1609
- UPDATE error_occurrences
1610
- SET status = 'open',
1611
- resolved_at = NULL,
1612
- resolved_by = NULL,
1613
- updated_at = unixepoch()
1614
- WHERE fingerprint = ?
1615
- `
1616
- )
1617
- .bind(fingerprint)
1618
- .run();
1619
- } catch (e) {
1620
- console.error(`Failed to reopen issue: ${e}`);
1621
- }
1622
- } else if (occurrence.github_issue_number) {
1623
- // Update existing issue with new occurrence count
1624
- try {
1625
- const [owner, repo] = mapping.repository.split('/');
1626
-
1627
- // Check if issue is muted - if so, don't add comments
1628
- const muted = await isIssueMuted(github, owner, repo, occurrence.github_issue_number);
1629
- if (muted) {
1630
- console.log(`Issue #${occurrence.github_issue_number} is muted, skipping comment`);
1631
- return;
1632
- }
1633
-
1634
- // Add a comment every 10 occurrences to avoid spam
1635
- if (occurrence.occurrence_count % 10 === 0) {
1636
- await github.addComment(
1637
- owner,
1638
- repo,
1639
- occurrence.github_issue_number,
1640
- `📊 **Occurrence Update**\n\nThis error has now occurred **${occurrence.occurrence_count} times**.\n\n- **Last Seen**: ${new Date().toISOString()}\n- **Colo**: ${event.event?.request?.cf?.colo || 'unknown'}`
1641
- );
1642
- console.log(`Updated issue #${occurrence.github_issue_number} with occurrence count`);
1643
- }
1644
- } catch (e) {
1645
- console.error(`Failed to update issue: ${e}`);
1646
- }
1647
- }
1451
+ await handleNewOrRecurringError({
1452
+ event,
1453
+ env,
1454
+ github,
1455
+ mapping,
1456
+ errorType,
1457
+ fingerprint,
1458
+ category,
1459
+ isTransient,
1460
+ occurrence,
1461
+ isNew,
1462
+ priority,
1463
+ title,
1464
+ });
1648
1465
  }
1649
1466
 
1650
1467
  /**