@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.
- package/README.md +140 -45
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +4 -1
- package/package.json +13 -3
- package/templates/full/workers/lib/pattern-discovery/index.ts +13 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +1 -0
- package/templates/shared/docs/kv-key-patterns.md +101 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +1 -0
- package/templates/shared/migrations/seed.sql.hbs +2 -2
- package/templates/shared/scripts/sync-config.ts +59 -5
- package/templates/shared/workers/lib/platform-settings.ts +16 -1
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +11 -5
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +23 -8
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +8 -5
- package/templates/standard/workers/error-collector.ts +179 -362
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +48 -8
- package/templates/standard/workers/platform-sentinel.ts +4 -4
|
@@ -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
|
-
*
|
|
911
|
-
*
|
|
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
|
|
914
|
-
event: TailEvent
|
|
915
|
-
env: Env
|
|
916
|
-
github: GitHubClient
|
|
917
|
-
mapping: ScriptMapping
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
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;
|
|
1017
|
+
return;
|
|
1073
1018
|
}
|
|
1074
1019
|
|
|
1075
|
-
// No existing issue found - create new
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
-
//
|
|
1408
|
-
|
|
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
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
/**
|