@littlebearapps/create-platform 1.0.0 → 1.1.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 +98 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +36 -6
- package/dist/prompts.d.ts +14 -2
- package/dist/prompts.js +29 -7
- package/dist/templates.js +78 -0
- package/package.json +3 -2
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Clustering for Pattern Discovery
|
|
3
|
+
*
|
|
4
|
+
* Groups similar unclassified errors to reduce AI API costs
|
|
5
|
+
* and improve suggestion quality.
|
|
6
|
+
*
|
|
7
|
+
* @module workers/lib/pattern-discovery/clustering
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
11
|
+
import type { UnclassifiedError, ErrorCluster } from './types';
|
|
12
|
+
import type { Logger } from '@littlebearapps/platform-sdk';
|
|
13
|
+
|
|
14
|
+
/** Minimum occurrences to consider for clustering */
|
|
15
|
+
const MIN_OCCURRENCE_COUNT = 3;
|
|
16
|
+
|
|
17
|
+
/** Maximum clusters to process per run */
|
|
18
|
+
const MAX_CLUSTERS_PER_RUN = 20;
|
|
19
|
+
|
|
20
|
+
/** Maximum samples per cluster to send to AI */
|
|
21
|
+
export const MAX_SAMPLES_PER_CLUSTER = 5;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Query unclassified errors from D1
|
|
25
|
+
* Returns errors without a category that have occurred multiple times
|
|
26
|
+
*
|
|
27
|
+
* Uses COALESCE to check both last_exception_message and normalized_message,
|
|
28
|
+
* allowing pattern discovery to work for:
|
|
29
|
+
* - Exceptions (have last_exception_message)
|
|
30
|
+
* - Soft errors (have normalized_message from console.error logs)
|
|
31
|
+
* - Workflow failures (have normalized_message from logs)
|
|
32
|
+
*/
|
|
33
|
+
export async function queryUnclassifiedErrors(
|
|
34
|
+
db: D1Database,
|
|
35
|
+
log: Logger
|
|
36
|
+
): Promise<UnclassifiedError[]> {
|
|
37
|
+
try {
|
|
38
|
+
// Query high-frequency errors that haven't been resolved
|
|
39
|
+
// Uses COALESCE to get message from either exception or normalized logs
|
|
40
|
+
// Errors with transient categories (tracked in fingerprint) are already handled
|
|
41
|
+
const result = await db
|
|
42
|
+
.prepare(
|
|
43
|
+
`
|
|
44
|
+
SELECT
|
|
45
|
+
fingerprint,
|
|
46
|
+
script_name as scriptName,
|
|
47
|
+
COALESCE(last_exception_message, normalized_message, '') as normalizedMessage,
|
|
48
|
+
occurrence_count as occurrenceCount,
|
|
49
|
+
last_seen_at as lastSeenAt
|
|
50
|
+
FROM error_occurrences
|
|
51
|
+
WHERE status = 'open'
|
|
52
|
+
AND occurrence_count >= ?
|
|
53
|
+
AND error_category IS NULL
|
|
54
|
+
AND (
|
|
55
|
+
(last_exception_message IS NOT NULL AND last_exception_message != '')
|
|
56
|
+
OR (normalized_message IS NOT NULL AND normalized_message != '')
|
|
57
|
+
)
|
|
58
|
+
ORDER BY occurrence_count DESC
|
|
59
|
+
LIMIT 500
|
|
60
|
+
`
|
|
61
|
+
)
|
|
62
|
+
.bind(MIN_OCCURRENCE_COUNT)
|
|
63
|
+
.all<UnclassifiedError>();
|
|
64
|
+
|
|
65
|
+
log.info('Queried unclassified errors', {
|
|
66
|
+
count: result.results.length,
|
|
67
|
+
minOccurrences: MIN_OCCURRENCE_COUNT,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return result.results;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
log.error('Failed to query unclassified errors', error);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Simple hash function for clustering
|
|
79
|
+
* Normalizes message and creates a stable hash for grouping
|
|
80
|
+
*/
|
|
81
|
+
function hashMessage(message: string): string {
|
|
82
|
+
// Further normalize: lowercase, collapse whitespace, remove numbers
|
|
83
|
+
const normalized = message
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.replace(/\s+/g, ' ')
|
|
86
|
+
.replace(/\d+/g, 'N')
|
|
87
|
+
.replace(/[a-f0-9]{8,}/gi, 'HASH') // Remove hex strings
|
|
88
|
+
.trim();
|
|
89
|
+
|
|
90
|
+
// Simple string hash
|
|
91
|
+
let hash = 0;
|
|
92
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
93
|
+
const char = normalized.charCodeAt(i);
|
|
94
|
+
hash = (hash << 5) - hash + char;
|
|
95
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
96
|
+
}
|
|
97
|
+
return hash.toString(16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Cluster errors by normalized message similarity
|
|
102
|
+
* Uses exact match on further-normalized messages as first pass
|
|
103
|
+
*/
|
|
104
|
+
export function clusterErrors(errors: UnclassifiedError[]): Map<string, UnclassifiedError[]> {
|
|
105
|
+
const clusters = new Map<string, UnclassifiedError[]>();
|
|
106
|
+
|
|
107
|
+
for (const error of errors) {
|
|
108
|
+
const hash = hashMessage(error.normalizedMessage);
|
|
109
|
+
const existing = clusters.get(hash) || [];
|
|
110
|
+
existing.push(error);
|
|
111
|
+
clusters.set(hash, existing);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return clusters;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert clustered errors to ErrorCluster objects
|
|
119
|
+
* Filters to clusters with sufficient volume
|
|
120
|
+
*/
|
|
121
|
+
export function buildClusterObjects(
|
|
122
|
+
clusteredErrors: Map<string, UnclassifiedError[]>
|
|
123
|
+
): ErrorCluster[] {
|
|
124
|
+
const clusters: ErrorCluster[] = [];
|
|
125
|
+
|
|
126
|
+
for (const [hash, errors] of clusteredErrors) {
|
|
127
|
+
// Sum occurrences across all errors in cluster
|
|
128
|
+
const totalOccurrences = errors.reduce((sum, e) => sum + e.occurrenceCount, 0);
|
|
129
|
+
|
|
130
|
+
// Skip small clusters
|
|
131
|
+
if (totalOccurrences < MIN_OCCURRENCE_COUNT * 2) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Find representative message (most common or first)
|
|
136
|
+
const representative = errors.reduce((best, e) =>
|
|
137
|
+
e.occurrenceCount > best.occurrenceCount ? e : best
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Collect unique scripts
|
|
141
|
+
const scripts = [...new Set(errors.map((e) => e.scriptName))];
|
|
142
|
+
|
|
143
|
+
// Find time range
|
|
144
|
+
const firstSeen = Math.min(...errors.map((e) => e.lastSeenAt));
|
|
145
|
+
const lastSeen = Math.max(...errors.map((e) => e.lastSeenAt));
|
|
146
|
+
|
|
147
|
+
clusters.push({
|
|
148
|
+
id: `cluster-${hash}-${Date.now()}`,
|
|
149
|
+
clusterHash: hash,
|
|
150
|
+
representativeMessage: representative.normalizedMessage,
|
|
151
|
+
occurrenceCount: totalOccurrences,
|
|
152
|
+
uniqueFingerprints: errors.length,
|
|
153
|
+
firstSeenAt: firstSeen,
|
|
154
|
+
lastSeenAt: lastSeen,
|
|
155
|
+
scripts,
|
|
156
|
+
status: 'pending',
|
|
157
|
+
suggestionId: null,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Sort by occurrence count descending
|
|
162
|
+
clusters.sort((a, b) => b.occurrenceCount - a.occurrenceCount);
|
|
163
|
+
|
|
164
|
+
// Limit to max clusters per run
|
|
165
|
+
return clusters.slice(0, MAX_CLUSTERS_PER_RUN);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get sample messages for a cluster
|
|
170
|
+
* Returns diverse samples for AI analysis
|
|
171
|
+
*/
|
|
172
|
+
export function getSampleMessages(
|
|
173
|
+
errors: UnclassifiedError[],
|
|
174
|
+
maxSamples: number = MAX_SAMPLES_PER_CLUSTER
|
|
175
|
+
): string[] {
|
|
176
|
+
// Get unique messages (some may be duplicates with different fingerprints)
|
|
177
|
+
const uniqueMessages = [...new Set(errors.map((e) => e.normalizedMessage))];
|
|
178
|
+
|
|
179
|
+
// Return up to maxSamples
|
|
180
|
+
return uniqueMessages.slice(0, maxSamples);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Store clusters in D1 for tracking
|
|
185
|
+
*/
|
|
186
|
+
export async function storeClusters(
|
|
187
|
+
db: D1Database,
|
|
188
|
+
clusters: ErrorCluster[],
|
|
189
|
+
log: Logger
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
for (const cluster of clusters) {
|
|
192
|
+
try {
|
|
193
|
+
await db
|
|
194
|
+
.prepare(
|
|
195
|
+
`
|
|
196
|
+
INSERT INTO error_clusters (
|
|
197
|
+
id, cluster_hash, representative_message,
|
|
198
|
+
occurrence_count, unique_fingerprints,
|
|
199
|
+
first_seen_at, last_seen_at, scripts, status
|
|
200
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
201
|
+
ON CONFLICT (cluster_hash) DO UPDATE SET
|
|
202
|
+
occurrence_count = excluded.occurrence_count,
|
|
203
|
+
unique_fingerprints = excluded.unique_fingerprints,
|
|
204
|
+
last_seen_at = excluded.last_seen_at,
|
|
205
|
+
scripts = excluded.scripts,
|
|
206
|
+
updated_at = unixepoch()
|
|
207
|
+
`
|
|
208
|
+
)
|
|
209
|
+
.bind(
|
|
210
|
+
cluster.id,
|
|
211
|
+
cluster.clusterHash,
|
|
212
|
+
cluster.representativeMessage.slice(0, 500),
|
|
213
|
+
cluster.occurrenceCount,
|
|
214
|
+
cluster.uniqueFingerprints,
|
|
215
|
+
cluster.firstSeenAt,
|
|
216
|
+
cluster.lastSeenAt,
|
|
217
|
+
JSON.stringify(cluster.scripts),
|
|
218
|
+
cluster.status
|
|
219
|
+
)
|
|
220
|
+
.run();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
log.warn('Failed to store cluster', error, { clusterId: cluster.id });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
log.info('Stored clusters', { count: clusters.length });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get pending clusters for AI analysis
|
|
231
|
+
*/
|
|
232
|
+
export async function getPendingClusters(
|
|
233
|
+
db: D1Database,
|
|
234
|
+
limit: number = 10
|
|
235
|
+
): Promise<ErrorCluster[]> {
|
|
236
|
+
const result = await db
|
|
237
|
+
.prepare(
|
|
238
|
+
`
|
|
239
|
+
SELECT
|
|
240
|
+
id, cluster_hash as clusterHash, representative_message as representativeMessage,
|
|
241
|
+
occurrence_count as occurrenceCount, unique_fingerprints as uniqueFingerprints,
|
|
242
|
+
first_seen_at as firstSeenAt, last_seen_at as lastSeenAt,
|
|
243
|
+
scripts, status, suggestion_id as suggestionId
|
|
244
|
+
FROM error_clusters
|
|
245
|
+
WHERE status = 'pending'
|
|
246
|
+
ORDER BY occurrence_count DESC
|
|
247
|
+
LIMIT ?
|
|
248
|
+
`
|
|
249
|
+
)
|
|
250
|
+
.bind(limit)
|
|
251
|
+
.all<ErrorCluster & { scripts: string }>();
|
|
252
|
+
|
|
253
|
+
return result.results.map((r) => ({
|
|
254
|
+
...r,
|
|
255
|
+
scripts: JSON.parse(r.scripts || '[]'),
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Update cluster status
|
|
261
|
+
*/
|
|
262
|
+
export async function updateClusterStatus(
|
|
263
|
+
db: D1Database,
|
|
264
|
+
clusterId: string,
|
|
265
|
+
status: ErrorCluster['status'],
|
|
266
|
+
suggestionId?: string
|
|
267
|
+
): Promise<void> {
|
|
268
|
+
await db
|
|
269
|
+
.prepare(
|
|
270
|
+
`
|
|
271
|
+
UPDATE error_clusters
|
|
272
|
+
SET status = ?, suggestion_id = ?, updated_at = unixepoch()
|
|
273
|
+
WHERE id = ?
|
|
274
|
+
`
|
|
275
|
+
)
|
|
276
|
+
.bind(status, suggestionId || null, clusterId)
|
|
277
|
+
.run();
|
|
278
|
+
}
|