thatcher 1.0.41 → 1.0.43
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 +27 -0
- package/package.json +2 -5
- package/src/app/api/audit/dashboard/route.js +2 -2
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +2 -2
- package/src/app/api/audit/permissions/route.js +3 -3
- package/src/app/api/audit/permissions/stats/route.js +2 -2
- package/src/app/api/audit/route.js +2 -3
- package/src/app/api/audit/stats/route.js +2 -2
- package/src/app/api/auth/google/callback/route.js +1 -1
- package/src/app/api/auth/google/route.js +1 -1
- package/src/app/api/auth/login/route.js +1 -1
- package/src/app/api/auth/logout/route.js +1 -1
- package/src/app/api/auth/me/route.js +1 -1
- package/src/app/api/auth/mwr-bridge/route.js +1 -1
- package/src/app/api/auth/password-reset/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/debug/[[...path]]/route.js +2 -2
- package/src/app/api/debug/config/route.js +1 -1
- package/src/app/api/debug/hooks/route.js +1 -1
- package/src/app/api/debug/plugins/route.js +1 -1
- package/src/app/api/debug/sqlite/route.js +1 -1
- package/src/app/api/debug/sync/route.js +1 -1
- package/src/app/api/debug/workflow/route.js +1 -1
- package/src/app/api/domains/[domain]/route.js +1 -2
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/batch/route.js +1 -1
- package/src/app/api/email/allocate/route.js +1 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/email/send/route.js +1 -1
- package/src/app/api/email/unallocated/route.js +1 -1
- package/src/app/api/files/[id]/route.js +1 -1
- package/src/app/api/health/route.js +1 -2
- package/src/app/api/metrics/route.js +1 -2
- package/src/engine.js +3 -3
- package/src/engine.server.js +3 -3
- package/src/index.js +35 -10
- package/src/lib/action-factory.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/api.js +3 -4
- package/src/lib/audit-logger-enhanced.js +1 -1
- package/src/lib/auth-middleware.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/{busybase-adapter.js → busybase/adapter.js} +3 -3
- package/src/lib/{busybase-audit-reads.js → busybase/audit-reads.js} +1 -1
- package/src/lib/{busybase-audit.js → busybase/audit.js} +2 -2
- package/src/lib/{busybase-lucia-adapter.js → busybase/lucia-adapter.js} +1 -1
- package/src/lib/{busybase-store.js → busybase/store.js} +3 -3
- package/src/lib/config-generator-engine.js +13 -1
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +4 -4
- package/src/lib/email-sender.js +1 -1
- package/src/lib/errors/index.js +3 -0
- package/src/lib/{error-recovery.js → errors/recovery.js} +120 -4
- package/src/lib/{error-handler.js → errors/types.js} +1 -1
- package/src/lib/errors/wrap.js +225 -0
- package/src/lib/events-engine.js +1 -1
- package/src/lib/hot-reload/index.js +34 -0
- package/src/lib/index.js +5 -5
- package/src/lib/keyed-cache.js +112 -0
- package/src/lib/monitor.js +285 -0
- package/src/lib/next-shim.js +294 -0
- package/src/lib/observability-init.js +213 -0
- package/src/lib/perf.js +468 -0
- package/src/lib/query-cache.js +40 -46
- package/src/lib/realtime-server.js +18 -0
- package/src/lib/render-cache.js +14 -28
- package/src/lib/{request-tracing.js → request-trace.js} +57 -2
- package/src/lib/response-formatter.js +18 -0
- package/src/lib/route-helpers.js +1 -1
- package/src/lib/state-protocol.js +13 -0
- package/src/lib/state-transport-client.js +6 -0
- package/src/lib/state-transport-reconnect.js +5 -0
- package/src/lib/state-transport-server.js +8 -0
- package/src/lib/utils.js +1 -1
- package/src/lib/{validate.js → validation/entity-validators.js} +9 -8
- package/src/lib/validation/index.js +1 -1
- package/src/lib/validation/security-validators.js +1 -1
- package/src/lib/validation-middleware.js +2 -2
- package/src/lib/workflow-engine.js +23 -3
- package/src/lib/xstate-workflow-engine.js +18 -2
- package/src/services/collaborator-role.service.js +2 -2
- package/src/services/notification-engine.js +5 -5
- package/src/services/permission.service.js +1 -1
- package/src/ui/format-helpers.js +1 -4
- package/src/ui/highlight-threading-renderer.js +1 -1
- package/src/ui/page-handler-admin.js +6 -5
- package/src/ui/page-handler-helpers.js +2 -2
- package/src/ui/page-handler-reviews.js +4 -4
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +2 -2
- package/src/ui/render-helpers.js +1 -1
- package/src/ui/renderer.js +1 -1
- package/src/ui/{review-comparison-renderer.js → review/comparison.js} +1 -1
- package/src/ui/{review-detail-renderer.js → review/detail-renderer.js} +3 -3
- package/src/ui/review/index.js +32 -0
- package/src/ui/{review-mwr-renderer.js → review/mwr.js} +1 -1
- package/src/ui/{review-renderer.js → review/renderer.js} +1 -1
- package/src/ui/{settings-renderer.js → settings/home.js} +7 -34
- package/src/ui/{settings-renderer-advanced.js → settings/review.js} +117 -56
- package/src/ui/settings/shared.js +45 -0
- package/src/ui/{settings-renderer-teams.js → settings/teams.js} +1 -2
- package/src/ui/settings/templates.js +116 -0
- package/src/lib/api-error-wrapper.js +0 -125
- package/src/lib/db-monitor.js +0 -127
- package/src/lib/error-resilience.js +0 -132
- package/src/lib/monitoring-init.js +0 -67
- package/src/lib/next-compat.js +0 -94
- package/src/lib/next-polyfills.js +0 -135
- package/src/lib/observability-bootstrap.js +0 -116
- package/src/lib/perf-monitor.js +0 -94
- package/src/lib/perf-profiler.js +0 -233
- package/src/lib/query-perf.js +0 -117
- package/src/lib/request-tracker.js +0 -43
- package/src/lib/resource-monitor.js +0 -120
- package/src/lib/validators.js +0 -67
- package/src/lib/with-error-handler.js +0 -31
- package/src/ui/settings-renderer-advanced2.js +0 -159
- /package/src/ui/{review-detail-panels.js → review/detail-panels.js} +0 -0
- /package/src/ui/{review-detail-script.js → review/detail-script.js} +0 -0
- /package/src/ui/{review-widgets.js → review/widgets.js} +0 -0
- /package/src/ui/{review-zone-nav.js → review/zone-nav.js} +0 -0
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
|
|
3
|
-
const requestContext = new AsyncLocalStorage();
|
|
4
|
-
|
|
5
|
-
export class NextResponse {
|
|
6
|
-
constructor(body, init = {}) {
|
|
7
|
-
this.body = body;
|
|
8
|
-
this.status = init.status || 200;
|
|
9
|
-
this.headers = new Map(Object.entries(init.headers || {}));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async json() {
|
|
13
|
-
return this.body;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
static json(body, init = {}) {
|
|
17
|
-
return new NextResponse(body, init);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
static redirect(url, status = 307) {
|
|
21
|
-
return new NextResponse(null, {
|
|
22
|
-
status,
|
|
23
|
-
headers: { Location: url }
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
let fallbackRequest = null;
|
|
29
|
-
let fallbackResponse = null;
|
|
30
|
-
|
|
31
|
-
export function setCurrentRequest(req) {
|
|
32
|
-
fallbackRequest = req;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function setCurrentResponse(res) {
|
|
36
|
-
fallbackResponse = res;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function runWithContext(req, res, fn) {
|
|
40
|
-
return requestContext.run({ req, res }, fn);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getRequest() {
|
|
44
|
-
const store = requestContext.getStore();
|
|
45
|
-
return store?.req || fallbackRequest;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getResponse() {
|
|
49
|
-
const store = requestContext.getStore();
|
|
50
|
-
return store?.res || fallbackResponse;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function cookies() {
|
|
54
|
-
if (typeof window !== 'undefined') {
|
|
55
|
-
throw new Error('cookies() should only be called on the server side');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const req = getRequest();
|
|
59
|
-
const res = getResponse();
|
|
60
|
-
const cookieHeader = req?.headers?.cookie || '';
|
|
61
|
-
const cookieMap = {};
|
|
62
|
-
|
|
63
|
-
if (cookieHeader) {
|
|
64
|
-
cookieHeader.split(';').forEach(cookie => {
|
|
65
|
-
const [name, value] = cookie.split('=').map(s => s.trim());
|
|
66
|
-
if (name) cookieMap[name] = decodeURIComponent(value || '');
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
get: (name) => {
|
|
72
|
-
const value = cookieMap[name];
|
|
73
|
-
return value ? { name, value } : undefined;
|
|
74
|
-
},
|
|
75
|
-
set: (name, value, options = {}) => {
|
|
76
|
-
if (!res) return;
|
|
77
|
-
let setCookieValue = `${name}=${encodeURIComponent(value)}`;
|
|
78
|
-
if (options.path) setCookieValue += `; Path=${options.path}`;
|
|
79
|
-
if (options.maxAge) setCookieValue += `; Max-Age=${options.maxAge}`;
|
|
80
|
-
if (options.expires) setCookieValue += `; Expires=${options.expires}`;
|
|
81
|
-
if (options.secure) setCookieValue += '; Secure';
|
|
82
|
-
if (options.httpOnly) setCookieValue += '; HttpOnly';
|
|
83
|
-
if (options.sameSite) setCookieValue += `; SameSite=${options.sameSite}`;
|
|
84
|
-
const existing = res.getHeader('Set-Cookie') || [];
|
|
85
|
-
const setCookies = Array.isArray(existing) ? existing : [existing];
|
|
86
|
-
res.setHeader('Set-Cookie', [...setCookies, setCookieValue]);
|
|
87
|
-
},
|
|
88
|
-
delete: (name) => {
|
|
89
|
-
if (!res) return;
|
|
90
|
-
const setCookieValue = `${name}=; Path=/; Max-Age=0`;
|
|
91
|
-
const existing = res.getHeader('Set-Cookie') || [];
|
|
92
|
-
const setCookies = Array.isArray(existing) ? existing : [existing];
|
|
93
|
-
res.setHeader('Set-Cookie', [...setCookies, setCookieValue]);
|
|
94
|
-
},
|
|
95
|
-
getAll: () => {
|
|
96
|
-
return Object.entries(cookieMap).map(([name, value]) => ({ name, value }));
|
|
97
|
-
},
|
|
98
|
-
has: (name) => {
|
|
99
|
-
return name in cookieMap;
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function headers() {
|
|
105
|
-
return {
|
|
106
|
-
get: (name) => null,
|
|
107
|
-
getSetCookie: () => [],
|
|
108
|
-
has: (name) => false,
|
|
109
|
-
entries: () => [],
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function revalidatePath() {}
|
|
114
|
-
|
|
115
|
-
export function revalidateTag() {}
|
|
116
|
-
|
|
117
|
-
export function redirect(path, status = 302) {
|
|
118
|
-
if (typeof window !== 'undefined') {
|
|
119
|
-
window.location.href = path;
|
|
120
|
-
} else {
|
|
121
|
-
const error = new Error(`Redirect to ${path}`);
|
|
122
|
-
error.type = 'redirect';
|
|
123
|
-
error.location = path;
|
|
124
|
-
error.status = status;
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function notFound() {
|
|
130
|
-
if (typeof window !== 'undefined') {
|
|
131
|
-
window.location.href = '/404';
|
|
132
|
-
} else {
|
|
133
|
-
throw new Error('notFound()');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { tracer } from './tracing.js';
|
|
2
|
-
import { perfProfiler } from './perf-profiler.js';
|
|
3
|
-
import { debugRegistry } from './debug-registry.js';
|
|
4
|
-
import { createExportSink } from './export-sink.js';
|
|
5
|
-
import { withHookTracing } from './request-tracing.js';
|
|
6
|
-
import { hookEngine } from './hook-engine.js';
|
|
7
|
-
import { createLogger } from './logger.js';
|
|
8
|
-
|
|
9
|
-
const logger = createLogger('[ObservabilityBootstrap]');
|
|
10
|
-
|
|
11
|
-
export async function bootstrapObservability(options = {}) {
|
|
12
|
-
const results = {
|
|
13
|
-
tracing: false,
|
|
14
|
-
profiler: false,
|
|
15
|
-
registry: false,
|
|
16
|
-
export: false,
|
|
17
|
-
hookTracing: false,
|
|
18
|
-
errors: [],
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
debugRegistry.expose('system', {
|
|
23
|
-
pid: () => process.pid,
|
|
24
|
-
uptime: () => process.uptime(),
|
|
25
|
-
memory: () => process.memoryUsage(),
|
|
26
|
-
cpuUsage: () => process.cpuUsage(),
|
|
27
|
-
version: () => process.version,
|
|
28
|
-
platform: () => process.platform,
|
|
29
|
-
}, 'System information');
|
|
30
|
-
|
|
31
|
-
results.registry = true;
|
|
32
|
-
logger.info('Debug registry initialized');
|
|
33
|
-
} catch (error) {
|
|
34
|
-
results.errors.push({ subsystem: 'registry', error: error.message });
|
|
35
|
-
logger.error('Registry bootstrap failed', { error: error.message });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
if (globalThis.__debug__) {
|
|
40
|
-
globalThis.__debug__.expose('tracing', {
|
|
41
|
-
activeSpans: () => tracer.getActiveSpans(),
|
|
42
|
-
recentTraces: (limit) => tracer.getRecentTraces(limit),
|
|
43
|
-
traceById: (id) => tracer.getTraceById(id),
|
|
44
|
-
stats: () => tracer.getStats(),
|
|
45
|
-
currentTraceId: () => tracer.getCurrentTraceId(),
|
|
46
|
-
}, 'Tracing Core');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
results.tracing = true;
|
|
50
|
-
logger.info('Tracing initialized');
|
|
51
|
-
} catch (error) {
|
|
52
|
-
results.errors.push({ subsystem: 'tracing', error: error.message });
|
|
53
|
-
logger.error('Tracing bootstrap failed', { error: error.message });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
if (globalThis.__debug__) {
|
|
58
|
-
globalThis.__debug__.expose('perf', {
|
|
59
|
-
stats: (op) => op ? perfProfiler.getStats(op) : perfProfiler.getAllStats(),
|
|
60
|
-
slow: () => perfProfiler.getSlowOperations(),
|
|
61
|
-
regressions: () => perfProfiler.detectRegressions(),
|
|
62
|
-
alerts: () => perfProfiler.getAlerts(),
|
|
63
|
-
thresholds: () => Object.fromEntries(perfProfiler._thresholds),
|
|
64
|
-
}, 'Performance Profiler');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
results.profiler = true;
|
|
68
|
-
logger.info('Performance profiler initialized');
|
|
69
|
-
} catch (error) {
|
|
70
|
-
results.errors.push({ subsystem: 'profiler', error: error.message });
|
|
71
|
-
logger.error('Profiler bootstrap failed', { error: error.message });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const exportTarget = options.exportTarget || process.env.OBSERVABILITY_EXPORT_TARGET;
|
|
76
|
-
if (exportTarget) {
|
|
77
|
-
await createExportSink({
|
|
78
|
-
target: exportTarget,
|
|
79
|
-
url: options.exportUrl || process.env.OBSERVABILITY_EXPORT_URL,
|
|
80
|
-
batchSize: options.batchSize,
|
|
81
|
-
batchIntervalMs: options.batchIntervalMs,
|
|
82
|
-
});
|
|
83
|
-
results.export = true;
|
|
84
|
-
logger.info('Export sink initialized', { target: exportTarget });
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
results.errors.push({ subsystem: 'export', error: error.message });
|
|
88
|
-
logger.error('Export sink bootstrap failed', { error: error.message });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
withHookTracing(hookEngine);
|
|
93
|
-
results.hookTracing = true;
|
|
94
|
-
logger.info('Hook tracing initialized');
|
|
95
|
-
} catch (error) {
|
|
96
|
-
results.errors.push({ subsystem: 'hookTracing', error: error.message });
|
|
97
|
-
logger.error('Hook tracing bootstrap failed', { error: error.message });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return results;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export async function shutdownObservability() {
|
|
104
|
-
const { getExportSink } = await import('./export-sink.js');
|
|
105
|
-
const sink = getExportSink();
|
|
106
|
-
if (sink) {
|
|
107
|
-
await sink.close();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
tracer.clear();
|
|
111
|
-
perfProfiler.clear();
|
|
112
|
-
|
|
113
|
-
logger.info('Observability shutdown complete');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export default bootstrapObservability;
|
package/src/lib/perf-monitor.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { createLogger } from './logger.js';
|
|
2
|
-
|
|
3
|
-
const log = createLogger('[Perf]');
|
|
4
|
-
const metrics = new Map()
|
|
5
|
-
const thresholds = { render: 100, query: 50, api: 200, total: 500 }
|
|
6
|
-
let enabled = process.env.PERF_MONITOR !== 'false'
|
|
7
|
-
|
|
8
|
-
export function startTimer(key) {
|
|
9
|
-
if (!enabled) return null
|
|
10
|
-
const start = process.hrtime.bigint()
|
|
11
|
-
return { key, start }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function endTimer(timer) {
|
|
15
|
-
if (!enabled || !timer) return 0
|
|
16
|
-
const end = process.hrtime.bigint()
|
|
17
|
-
const ms = Number(end - timer.start) / 1000000
|
|
18
|
-
recordMetric(timer.key, ms)
|
|
19
|
-
return ms
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function recordMetric(key, value) {
|
|
23
|
-
if (!enabled) return
|
|
24
|
-
if (!metrics.has(key)) metrics.set(key, [])
|
|
25
|
-
const arr = metrics.get(key)
|
|
26
|
-
arr.push({ value, ts: Date.now() })
|
|
27
|
-
if (arr.length > 1000) arr.shift()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function getMetrics(key) {
|
|
31
|
-
if (!key) return Object.fromEntries(metrics.entries())
|
|
32
|
-
return metrics.get(key) || []
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function getStats(key) {
|
|
36
|
-
const data = metrics.get(key) || []
|
|
37
|
-
if (data.length === 0) return null
|
|
38
|
-
const values = data.map(d => d.value)
|
|
39
|
-
values.sort((a, b) => a - b)
|
|
40
|
-
return {
|
|
41
|
-
count: values.length,
|
|
42
|
-
min: values[0],
|
|
43
|
-
max: values[values.length - 1],
|
|
44
|
-
avg: values.reduce((a, b) => a + b, 0) / values.length,
|
|
45
|
-
p50: values[Math.floor(values.length * 0.5)],
|
|
46
|
-
p95: values[Math.floor(values.length * 0.95)],
|
|
47
|
-
p99: values[Math.floor(values.length * 0.99)]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function checkThreshold(key, value) {
|
|
52
|
-
if (!enabled) return true
|
|
53
|
-
const limit = thresholds[key]
|
|
54
|
-
if (limit && value > limit) {
|
|
55
|
-
log.warn(`${key} exceeded threshold: ${value.toFixed(2)}ms > ${limit}ms`)
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function setThreshold(key, ms) {
|
|
62
|
-
thresholds[key] = ms
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function clearMetrics() {
|
|
66
|
-
metrics.clear()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function enable() {
|
|
70
|
-
enabled = true
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function disable() {
|
|
74
|
-
enabled = false
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function measure(key, fn) {
|
|
78
|
-
const timer = startTimer(key)
|
|
79
|
-
try {
|
|
80
|
-
const result = fn()
|
|
81
|
-
if (result instanceof Promise) {
|
|
82
|
-
return result.finally(() => {
|
|
83
|
-
const ms = endTimer(timer)
|
|
84
|
-
checkThreshold(key, ms)
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
const ms = endTimer(timer)
|
|
88
|
-
checkThreshold(key, ms)
|
|
89
|
-
return result
|
|
90
|
-
} catch (err) {
|
|
91
|
-
endTimer(timer)
|
|
92
|
-
throw err
|
|
93
|
-
}
|
|
94
|
-
}
|
package/src/lib/perf-profiler.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { createLogger } from './logger.js';
|
|
2
|
-
|
|
3
|
-
const logger = createLogger('[PerfProfiler]');
|
|
4
|
-
|
|
5
|
-
const _profiles = new Map();
|
|
6
|
-
const _thresholds = new Map();
|
|
7
|
-
const _alerts = [];
|
|
8
|
-
const MAX_ALERTS = 100;
|
|
9
|
-
const BASELINE_WINDOW_MS = 3600000;
|
|
10
|
-
|
|
11
|
-
const DEFAULT_THRESHOLDS = {
|
|
12
|
-
'http.request': 500,
|
|
13
|
-
'db.query': 100,
|
|
14
|
-
'db.write': 200,
|
|
15
|
-
'hook.execute': 50,
|
|
16
|
-
'workflow.transition': 100,
|
|
17
|
-
'formula.evaluate': 50,
|
|
18
|
-
'auth.login': 300,
|
|
19
|
-
'render.page': 200,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export class PerfProfiler {
|
|
23
|
-
constructor() {
|
|
24
|
-
this._profiles = _profiles;
|
|
25
|
-
this._thresholds = new Map(Object.entries(DEFAULT_THRESHOLDS));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
setThreshold(operation, thresholdMs) {
|
|
29
|
-
this._thresholds.set(operation, thresholdMs);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
getThreshold(operation) {
|
|
33
|
-
return this._thresholds.get(operation) || 100;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
record(operation, durationMs, metadata = {}) {
|
|
37
|
-
let profile = _profiles.get(operation);
|
|
38
|
-
if (!profile) {
|
|
39
|
-
profile = {
|
|
40
|
-
operation,
|
|
41
|
-
count: 0,
|
|
42
|
-
totalMs: 0,
|
|
43
|
-
minMs: Infinity,
|
|
44
|
-
maxMs: 0,
|
|
45
|
-
durations: [],
|
|
46
|
-
slowCount: 0,
|
|
47
|
-
lastRecorded: null,
|
|
48
|
-
metadata: {},
|
|
49
|
-
};
|
|
50
|
-
_profiles.set(operation, profile);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
profile.count++;
|
|
54
|
-
profile.totalMs += durationMs;
|
|
55
|
-
profile.minMs = Math.min(profile.minMs, durationMs);
|
|
56
|
-
profile.maxMs = Math.max(profile.maxMs, durationMs);
|
|
57
|
-
profile.lastRecorded = Date.now();
|
|
58
|
-
|
|
59
|
-
profile.durations.push({ durationMs, timestamp: Date.now(), ...metadata });
|
|
60
|
-
while (profile.durations.length > 10000) {
|
|
61
|
-
profile.durations.shift();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const threshold = this.getThreshold(operation);
|
|
65
|
-
if (durationMs > threshold) {
|
|
66
|
-
profile.slowCount++;
|
|
67
|
-
this._maybeAlert(operation, durationMs, threshold, metadata);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return profile;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async measure(operation, fn, metadata = {}) {
|
|
74
|
-
const start = performance.now();
|
|
75
|
-
try {
|
|
76
|
-
const result = await fn();
|
|
77
|
-
const duration = performance.now() - start;
|
|
78
|
-
this.record(operation, duration, metadata);
|
|
79
|
-
return result;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const duration = performance.now() - start;
|
|
82
|
-
this.record(operation, duration, { ...metadata, error: error.message });
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
measureSync(operation, fn, metadata = {}) {
|
|
88
|
-
const start = performance.now();
|
|
89
|
-
try {
|
|
90
|
-
const result = fn();
|
|
91
|
-
const duration = performance.now() - start;
|
|
92
|
-
this.record(operation, duration, metadata);
|
|
93
|
-
return result;
|
|
94
|
-
} catch (error) {
|
|
95
|
-
const duration = performance.now() - start;
|
|
96
|
-
this.record(operation, duration, { ...metadata, error: error.message });
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getStats(operation) {
|
|
102
|
-
const profile = _profiles.get(operation);
|
|
103
|
-
if (!profile) return null;
|
|
104
|
-
|
|
105
|
-
const durations = profile.durations.map(d => d.durationMs).sort((a, b) => a - b);
|
|
106
|
-
const count = durations.length;
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
operation: profile.operation,
|
|
110
|
-
count: profile.count,
|
|
111
|
-
totalMs: profile.totalMs,
|
|
112
|
-
avgMs: profile.count > 0 ? profile.totalMs / profile.count : 0,
|
|
113
|
-
minMs: profile.minMs === Infinity ? 0 : profile.minMs,
|
|
114
|
-
maxMs: profile.maxMs,
|
|
115
|
-
p50: this._percentile(durations, 50),
|
|
116
|
-
p95: this._percentile(durations, 95),
|
|
117
|
-
p99: this._percentile(durations, 99),
|
|
118
|
-
slowCount: profile.slowCount,
|
|
119
|
-
threshold: this.getThreshold(operation),
|
|
120
|
-
lastRecorded: profile.lastRecorded,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getAllStats() {
|
|
125
|
-
return Array.from(_profiles.keys()).map(key => this.getStats(key));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
getSlowOperations() {
|
|
129
|
-
return this.getAllStats()
|
|
130
|
-
.filter(s => s.slowCount > 0)
|
|
131
|
-
.sort((a, b) => b.slowCount - a.slowCount);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
detectRegressions(windowMs = 300000) {
|
|
135
|
-
const regressions = [];
|
|
136
|
-
const now = Date.now();
|
|
137
|
-
|
|
138
|
-
for (const [operation, profile] of _profiles.entries()) {
|
|
139
|
-
const recent = profile.durations.filter(d => now - d.timestamp < windowMs);
|
|
140
|
-
const older = profile.durations.filter(d => now - d.timestamp >= windowMs && now - d.timestamp < windowMs * 2);
|
|
141
|
-
|
|
142
|
-
if (recent.length < 10 || older.length < 10) continue;
|
|
143
|
-
|
|
144
|
-
const recentAvg = recent.reduce((sum, d) => sum + d.durationMs, 0) / recent.length;
|
|
145
|
-
const olderAvg = older.reduce((sum, d) => sum + d.durationMs, 0) / older.length;
|
|
146
|
-
|
|
147
|
-
if (olderAvg > 0 && recentAvg > olderAvg * 1.5) {
|
|
148
|
-
regressions.push({
|
|
149
|
-
operation,
|
|
150
|
-
recentAvg: Math.round(recentAvg * 100) / 100,
|
|
151
|
-
olderAvg: Math.round(olderAvg * 100) / 100,
|
|
152
|
-
regressionPercent: Math.round((recentAvg / olderAvg - 1) * 100),
|
|
153
|
-
sampleSize: recent.length,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return regressions;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
getAlerts() {
|
|
162
|
-
return [..._alerts];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
clearAlerts() {
|
|
166
|
-
_alerts.length = 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
clear() {
|
|
170
|
-
_profiles.clear();
|
|
171
|
-
_alerts.length = 0;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
_percentile(sorted, p) {
|
|
175
|
-
if (sorted.length === 0) return 0;
|
|
176
|
-
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
177
|
-
return sorted[Math.max(0, index)];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
_maybeAlert(operation, durationMs, thresholdMs, metadata) {
|
|
181
|
-
const alertKey = `${operation}:slow`;
|
|
182
|
-
const existing = _alerts.find(a => a.key === alertKey && Date.now() - a.timestamp < 60000);
|
|
183
|
-
if (existing) return;
|
|
184
|
-
|
|
185
|
-
const alert = {
|
|
186
|
-
key: alertKey,
|
|
187
|
-
operation,
|
|
188
|
-
type: 'slow_operation',
|
|
189
|
-
durationMs: Math.round(durationMs * 100) / 100,
|
|
190
|
-
thresholdMs,
|
|
191
|
-
severity: durationMs > thresholdMs * 5 ? 'critical' : durationMs > thresholdMs * 2 ? 'warning' : 'info',
|
|
192
|
-
timestamp: Date.now(),
|
|
193
|
-
metadata,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
_alerts.push(alert);
|
|
197
|
-
while (_alerts.length > MAX_ALERTS) {
|
|
198
|
-
_alerts.shift();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
logger.warn('Slow operation detected', alert);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export const perfProfiler = new PerfProfiler();
|
|
206
|
-
|
|
207
|
-
export function createPerfProfiler() {
|
|
208
|
-
return new PerfProfiler();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function getPerfProfiler() {
|
|
212
|
-
return perfProfiler;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export async function measurePerf(operation, fn, metadata = {}) {
|
|
216
|
-
return perfProfiler.measure(operation, fn, metadata);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function measurePerfSync(operation, fn, metadata = {}) {
|
|
220
|
-
return perfProfiler.measureSync(operation, fn, metadata);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export default PerfProfiler;
|
|
224
|
-
|
|
225
|
-
if (globalThis.__debug__) {
|
|
226
|
-
globalThis.__debug__.expose('perf', {
|
|
227
|
-
stats: (op) => op ? perfProfiler.getStats(op) : perfProfiler.getAllStats(),
|
|
228
|
-
slow: () => perfProfiler.getSlowOperations(),
|
|
229
|
-
regressions: () => perfProfiler.detectRegressions(),
|
|
230
|
-
alerts: () => perfProfiler.getAlerts(),
|
|
231
|
-
thresholds: () => Object.fromEntries(perfProfiler._thresholds),
|
|
232
|
-
}, 'Performance Profiler');
|
|
233
|
-
}
|
package/src/lib/query-perf.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
const queryMetrics = new Map();
|
|
2
|
-
const slowQueries = [];
|
|
3
|
-
const MAX_SLOW_QUERIES = 100;
|
|
4
|
-
const SLOW_QUERY_THRESHOLD = 100;
|
|
5
|
-
|
|
6
|
-
export const trackQuery = (sql, duration, params = []) => {
|
|
7
|
-
const key = sql.substring(0, 200);
|
|
8
|
-
|
|
9
|
-
if (!queryMetrics.has(key)) {
|
|
10
|
-
queryMetrics.set(key, {
|
|
11
|
-
sql: key,
|
|
12
|
-
count: 0,
|
|
13
|
-
totalTime: 0,
|
|
14
|
-
minTime: Infinity,
|
|
15
|
-
maxTime: 0,
|
|
16
|
-
times: []
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const metric = queryMetrics.get(key);
|
|
21
|
-
metric.count++;
|
|
22
|
-
metric.totalTime += duration;
|
|
23
|
-
metric.minTime = Math.min(metric.minTime, duration);
|
|
24
|
-
metric.maxTime = Math.max(metric.maxTime, duration);
|
|
25
|
-
metric.times.push(duration);
|
|
26
|
-
|
|
27
|
-
if (metric.times.length > 1000) metric.times.shift();
|
|
28
|
-
|
|
29
|
-
if (duration >= SLOW_QUERY_THRESHOLD) {
|
|
30
|
-
slowQueries.push({
|
|
31
|
-
sql: key,
|
|
32
|
-
duration,
|
|
33
|
-
params: params.slice(0, 10),
|
|
34
|
-
timestamp: Date.now()
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
if (slowQueries.length > MAX_SLOW_QUERIES) slowQueries.shift();
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const percentile = (arr, p) => {
|
|
42
|
-
if (arr.length === 0) return 0;
|
|
43
|
-
const sorted = [...arr].sort((a, b) => a - b);
|
|
44
|
-
const index = Math.ceil(sorted.length * p / 100) - 1;
|
|
45
|
-
return sorted[Math.max(0, index)];
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const getMetrics = () => {
|
|
49
|
-
const metrics = [];
|
|
50
|
-
|
|
51
|
-
for (const [, metric] of queryMetrics) {
|
|
52
|
-
const avgTime = metric.count > 0 ? metric.totalTime / metric.count : 0;
|
|
53
|
-
const p50 = percentile(metric.times, 50);
|
|
54
|
-
const p95 = percentile(metric.times, 95);
|
|
55
|
-
const p99 = percentile(metric.times, 99);
|
|
56
|
-
|
|
57
|
-
metrics.push({
|
|
58
|
-
sql: metric.sql,
|
|
59
|
-
count: metric.count,
|
|
60
|
-
avgTime: avgTime.toFixed(2),
|
|
61
|
-
minTime: metric.minTime === Infinity ? 0 : metric.minTime.toFixed(2),
|
|
62
|
-
maxTime: metric.maxTime.toFixed(2),
|
|
63
|
-
p50: p50.toFixed(2),
|
|
64
|
-
p95: p95.toFixed(2),
|
|
65
|
-
p99: p99.toFixed(2),
|
|
66
|
-
totalTime: metric.totalTime.toFixed(2)
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return metrics.sort((a, b) => parseFloat(b.totalTime) - parseFloat(a.totalTime));
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export const getSlowQueries = () => {
|
|
74
|
-
return slowQueries.slice().reverse();
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const getSummary = () => {
|
|
78
|
-
const metrics = getMetrics();
|
|
79
|
-
const totalQueries = metrics.reduce((sum, m) => sum + m.count, 0);
|
|
80
|
-
const totalTime = metrics.reduce((sum, m) => sum + parseFloat(m.totalTime), 0);
|
|
81
|
-
const allTimes = [];
|
|
82
|
-
|
|
83
|
-
for (const [, metric] of queryMetrics) {
|
|
84
|
-
allTimes.push(...metric.times);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
totalQueries,
|
|
89
|
-
uniqueQueries: metrics.length,
|
|
90
|
-
totalTime: totalTime.toFixed(2),
|
|
91
|
-
avgTime: totalQueries > 0 ? (totalTime / totalQueries).toFixed(2) : '0',
|
|
92
|
-
p50: percentile(allTimes, 50).toFixed(2),
|
|
93
|
-
p95: percentile(allTimes, 95).toFixed(2),
|
|
94
|
-
p99: percentile(allTimes, 99).toFixed(2),
|
|
95
|
-
slowQueries: slowQueries.length,
|
|
96
|
-
slowQueryThreshold: SLOW_QUERY_THRESHOLD
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export const clearMetrics = () => {
|
|
101
|
-
queryMetrics.clear();
|
|
102
|
-
slowQueries.length = 0;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export const withPerfTracking = (db, sql, params, executor) => {
|
|
106
|
-
const start = performance.now();
|
|
107
|
-
try {
|
|
108
|
-
const result = executor();
|
|
109
|
-
const duration = performance.now() - start;
|
|
110
|
-
trackQuery(sql, duration, params);
|
|
111
|
-
return result;
|
|
112
|
-
} catch (e) {
|
|
113
|
-
const duration = performance.now() - start;
|
|
114
|
-
trackQuery(sql, duration, params);
|
|
115
|
-
throw e;
|
|
116
|
-
}
|
|
117
|
-
};
|