adonisjs-server-stats 1.6.5 → 1.6.11
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 +1 -0
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/dashboard-api.d.ts +2 -1
- package/dist/core/dashboard-api.d.ts.map +1 -1
- package/dist/core/dashboard-data-controller.d.ts +2 -0
- package/dist/core/dashboard-data-controller.d.ts.map +1 -1
- package/dist/core/debug-data-controller.d.ts +1 -0
- package/dist/core/debug-data-controller.d.ts.map +1 -1
- package/dist/core/history-buffer.d.ts.map +1 -1
- package/dist/core/index.js +404 -361
- package/dist/core/server-stats-controller.d.ts +1 -0
- package/dist/core/server-stats-controller.d.ts.map +1 -1
- package/dist/core/sparkline.d.ts.map +1 -1
- package/dist/react/core/api-client.d.ts.map +1 -1
- package/dist/react/core/dashboard-api.d.ts +2 -1
- package/dist/react/core/dashboard-api.d.ts.map +1 -1
- package/dist/react/core/dashboard-data-controller.d.ts +2 -0
- package/dist/react/core/dashboard-data-controller.d.ts.map +1 -1
- package/dist/react/core/debug-data-controller.d.ts +1 -0
- package/dist/react/core/debug-data-controller.d.ts.map +1 -1
- package/dist/react/core/history-buffer.d.ts.map +1 -1
- package/dist/react/core/server-stats-controller.d.ts +1 -0
- package/dist/react/core/server-stats-controller.d.ts.map +1 -1
- package/dist/react/core/sparkline.d.ts.map +1 -1
- package/dist/src/collectors/app_collector.d.ts.map +1 -1
- package/dist/src/collectors/db_pool_collector.d.ts.map +1 -1
- package/dist/src/collectors/redis_collector.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.d.ts +3 -1
- package/dist/src/controller/debug_controller.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.js +25 -20
- package/dist/src/dashboard/chart_aggregator.js +42 -41
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_controller.js +7 -5
- package/dist/src/dashboard/dashboard_store.d.ts +61 -19
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_store.js +677 -474
- package/dist/src/dashboard/integrations/config_inspector.d.ts +4 -0
- package/dist/src/dashboard/integrations/config_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/integrations/config_inspector.js +16 -2
- package/dist/src/dashboard/migrator.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.js +30 -4
- package/dist/src/data/data_access.d.ts.map +1 -1
- package/dist/src/data/data_access.js +26 -6
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +17 -7
- package/dist/src/debug/email_collector.d.ts +2 -0
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +17 -13
- package/dist/src/debug/event_collector.d.ts +7 -1
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +46 -17
- package/dist/src/debug/query_collector.d.ts +12 -0
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +35 -5
- package/dist/src/debug/ring_buffer.d.ts +14 -0
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/ring_buffer.js +48 -2
- package/dist/src/debug/trace_collector.d.ts +1 -0
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +4 -1
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +5 -1
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client/debug-panel.js +1 -1
- package/dist/src/edge/client/stats-bar.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
- package/dist/src/edge/client-vue/debug-panel.js +2 -2
- package/dist/src/edge/client-vue/stats-bar.js +3 -3
- package/dist/src/engine/request_metrics.d.ts.map +1 -1
- package/dist/src/engine/request_metrics.js +33 -3
- package/dist/src/log_stream/log_stream_provider.js +1 -1
- package/dist/src/log_stream/log_stream_service.d.ts +1 -0
- package/dist/src/log_stream/log_stream_service.d.ts.map +1 -1
- package/dist/src/log_stream/log_stream_service.js +13 -3
- package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +17 -31
- package/dist/src/stubs/config.stub +3 -0
- package/dist/src/types.d.ts +12 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/logger.d.ts +7 -5
- package/dist/src/utils/logger.d.ts.map +1 -1
- package/dist/src/utils/logger.js +27 -5
- package/package.json +6 -2
|
@@ -20,6 +20,10 @@ export interface SanitizedEnvVars {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare class ConfigInspector {
|
|
22
22
|
private app;
|
|
23
|
+
private cachedConfig;
|
|
24
|
+
private cachedEnv;
|
|
25
|
+
private cacheTimestamp;
|
|
26
|
+
private static readonly CACHE_TTL_MS;
|
|
23
27
|
constructor(app: ApplicationService);
|
|
24
28
|
/**
|
|
25
29
|
* Get the full application config with sensitive values redacted.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config_inspector.d.ts","sourceRoot":"","sources":["../../../../src/dashboard/integrations/config_inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAM9D,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,CAAA;CAC5C;AA4DD;;;;;GAKG;AACH,qBAAa,eAAe;
|
|
1
|
+
{"version":3,"file":"config_inspector.d.ts","sourceRoot":"","sources":["../../../../src/dashboard/integrations/config_inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAM9D,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,CAAA;CAC5C;AA4DD;;;;;GAKG;AACH,qBAAa,eAAe;IAMd,OAAO,CAAC,GAAG;IALvB,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,cAAc,CAAY;IAClC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAEzB,GAAG,EAAE,kBAAkB;IAE3C;;OAEG;IACH,SAAS,IAAI,eAAe;IAiB5B;;OAEG;IACH,UAAU,IAAI,gBAAgB;CAwB/B"}
|
|
@@ -58,6 +58,10 @@ function redact(value) {
|
|
|
58
58
|
*/
|
|
59
59
|
export class ConfigInspector {
|
|
60
60
|
app;
|
|
61
|
+
cachedConfig = null;
|
|
62
|
+
cachedEnv = null;
|
|
63
|
+
cacheTimestamp = 0;
|
|
64
|
+
static CACHE_TTL_MS = 30_000; // 30 seconds
|
|
61
65
|
constructor(app) {
|
|
62
66
|
this.app = app;
|
|
63
67
|
}
|
|
@@ -65,9 +69,14 @@ export class ConfigInspector {
|
|
|
65
69
|
* Get the full application config with sensitive values redacted.
|
|
66
70
|
*/
|
|
67
71
|
getConfig() {
|
|
72
|
+
if (this.cachedConfig && Date.now() - this.cacheTimestamp < ConfigInspector.CACHE_TTL_MS) {
|
|
73
|
+
return this.cachedConfig;
|
|
74
|
+
}
|
|
68
75
|
try {
|
|
69
76
|
const raw = this.app.config?.all?.() ?? {};
|
|
70
|
-
|
|
77
|
+
this.cachedConfig = { config: sanitizeObject(raw) };
|
|
78
|
+
this.cacheTimestamp = Date.now();
|
|
79
|
+
return this.cachedConfig;
|
|
71
80
|
}
|
|
72
81
|
catch {
|
|
73
82
|
return { config: {} };
|
|
@@ -77,6 +86,9 @@ export class ConfigInspector {
|
|
|
77
86
|
* Get environment variables with sensitive values redacted.
|
|
78
87
|
*/
|
|
79
88
|
getEnvVars() {
|
|
89
|
+
if (this.cachedEnv && Date.now() - this.cacheTimestamp < ConfigInspector.CACHE_TTL_MS) {
|
|
90
|
+
return this.cachedEnv;
|
|
91
|
+
}
|
|
80
92
|
try {
|
|
81
93
|
const env = {};
|
|
82
94
|
const sorted = Object.keys(process.env).sort();
|
|
@@ -91,7 +103,9 @@ export class ConfigInspector {
|
|
|
91
103
|
env[key] = value;
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
|
-
|
|
106
|
+
this.cachedEnv = { env };
|
|
107
|
+
this.cacheTimestamp = Date.now();
|
|
108
|
+
return this.cachedEnv;
|
|
95
109
|
}
|
|
96
110
|
catch {
|
|
97
111
|
return { env: {} };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../../../src/dashboard/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAahC;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../../../src/dashboard/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAahC;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAuKzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDxF"}
|
|
@@ -34,6 +34,8 @@ export async function autoMigrate(db) {
|
|
|
34
34
|
`);
|
|
35
35
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_created ON server_stats_requests(created_at)`);
|
|
36
36
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_url ON server_stats_requests(url)`);
|
|
37
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_duration ON server_stats_requests(duration)`);
|
|
38
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_requests_status ON server_stats_requests(status_code)`);
|
|
37
39
|
await yieldToEventLoop();
|
|
38
40
|
// -- server_stats_queries ---------------------------------------------------
|
|
39
41
|
await db.raw(`
|
|
@@ -54,6 +56,7 @@ export async function autoMigrate(db) {
|
|
|
54
56
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_created ON server_stats_queries(created_at)`);
|
|
55
57
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_normalized ON server_stats_queries(sql_normalized)`);
|
|
56
58
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_request ON server_stats_queries(request_id)`);
|
|
59
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_queries_duration ON server_stats_queries(duration)`);
|
|
57
60
|
await yieldToEventLoop();
|
|
58
61
|
// -- server_stats_events ----------------------------------------------------
|
|
59
62
|
await db.raw(`
|
|
@@ -66,6 +69,7 @@ export async function autoMigrate(db) {
|
|
|
66
69
|
)
|
|
67
70
|
`);
|
|
68
71
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_events_created ON server_stats_events(created_at)`);
|
|
72
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_events_name ON server_stats_events(event_name)`);
|
|
69
73
|
await yieldToEventLoop();
|
|
70
74
|
// -- server_stats_emails ----------------------------------------------------
|
|
71
75
|
await db.raw(`
|
|
@@ -118,6 +122,7 @@ export async function autoMigrate(db) {
|
|
|
118
122
|
)
|
|
119
123
|
`);
|
|
120
124
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_traces_created ON server_stats_traces(created_at)`);
|
|
125
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_traces_request ON server_stats_traces(request_id)`);
|
|
121
126
|
await yieldToEventLoop();
|
|
122
127
|
// -- server_stats_metrics ---------------------------------------------------
|
|
123
128
|
await db.raw(`
|
|
@@ -134,6 +139,7 @@ export async function autoMigrate(db) {
|
|
|
134
139
|
)
|
|
135
140
|
`);
|
|
136
141
|
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_metrics_bucket ON server_stats_metrics(bucket)`);
|
|
142
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_metrics_created ON server_stats_metrics(created_at)`);
|
|
137
143
|
await yieldToEventLoop();
|
|
138
144
|
// -- server_stats_saved_filters ---------------------------------------------
|
|
139
145
|
await db.raw(`
|
|
@@ -145,6 +151,7 @@ export async function autoMigrate(db) {
|
|
|
145
151
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
146
152
|
)
|
|
147
153
|
`);
|
|
154
|
+
await db.raw(`CREATE INDEX IF NOT EXISTS idx_ss_filters_section ON server_stats_saved_filters(section)`);
|
|
148
155
|
}
|
|
149
156
|
/**
|
|
150
157
|
* Delete records older than `retentionDays` from all tables.
|
|
@@ -163,15 +170,34 @@ export async function runRetentionCleanup(db, retentionDays) {
|
|
|
163
170
|
const days = Math.max(1, Math.floor(retentionDays));
|
|
164
171
|
const cutoff = `datetime('now', '-${days} days')`;
|
|
165
172
|
try {
|
|
173
|
+
// Batch deletes to avoid blocking the event loop for large tables.
|
|
174
|
+
// Each batch deletes up to 1000 rows, yielding between batches.
|
|
175
|
+
const batchDelete = async (table) => {
|
|
176
|
+
let deleted;
|
|
177
|
+
do {
|
|
178
|
+
const result = await db.raw(`DELETE FROM ${table} WHERE rowid IN (SELECT rowid FROM ${table} WHERE created_at < ${cutoff} LIMIT 1000)`);
|
|
179
|
+
deleted = result ?? 0;
|
|
180
|
+
// SQLite returns the number of changes via better-sqlite3's .run().changes
|
|
181
|
+
// but through Knex.raw() the exact shape varies — loop until no matches
|
|
182
|
+
const remaining = await db.raw(`SELECT COUNT(*) as cnt FROM ${table} WHERE created_at < ${cutoff} LIMIT 1`);
|
|
183
|
+
const cnt = remaining?.[0]?.cnt ?? 0;
|
|
184
|
+
if (cnt === 0)
|
|
185
|
+
break;
|
|
186
|
+
await yieldToEventLoop();
|
|
187
|
+
} while (true);
|
|
188
|
+
};
|
|
166
189
|
// Cascade deletes queries, events, traces via FK ON DELETE CASCADE
|
|
167
|
-
await
|
|
190
|
+
await batchDelete('server_stats_requests');
|
|
168
191
|
await yieldToEventLoop();
|
|
169
192
|
// Standalone tables
|
|
170
|
-
await
|
|
193
|
+
await batchDelete('server_stats_logs');
|
|
171
194
|
await yieldToEventLoop();
|
|
172
|
-
await
|
|
195
|
+
await batchDelete('server_stats_emails');
|
|
173
196
|
await yieldToEventLoop();
|
|
174
|
-
await
|
|
197
|
+
await batchDelete('server_stats_metrics');
|
|
198
|
+
await yieldToEventLoop();
|
|
199
|
+
// Reclaim space and update query planner statistics
|
|
200
|
+
await db.raw('PRAGMA optimize');
|
|
175
201
|
}
|
|
176
202
|
catch (err) {
|
|
177
203
|
// Log but don't throw — retention cleanup failure shouldn't block init
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data_access.d.ts","sourceRoot":"","sources":["../../../src/data/data_access.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAMf,MAAM,iCAAiC,CAAA;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EAEX,WAAW,EACX,WAAW,EACZ,MAAM,mBAAmB,CAAA;AAM1B,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAEjC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AA6ED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAC,CAAQ;gBAGtB,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,EACrE,OAAO,CAAC,EAAE,MAAM;IAQlB,+DAA+D;IAC/D,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,wEAAwE;IACxE,OAAO,KAAK,cAAc,GAEzB;IAMK,UAAU,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB/E,eAAe,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAQrF,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB9E;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"data_access.d.ts","sourceRoot":"","sources":["../../../src/data/data_access.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EAMf,MAAM,iCAAiC,CAAA;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EAEX,WAAW,EACX,WAAW,EACZ,MAAM,mBAAmB,CAAA;AAM1B,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAEjC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AA6ED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,OAAO,CAAC,CAAQ;gBAGtB,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,cAAc,GAAG,IAAI,GAAG,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,EACrE,OAAO,CAAC,EAAE,MAAM;IAQlB,+DAA+D;IAC/D,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED,wEAAwE;IACxE,OAAO,KAAK,cAAc,GAEzB;IAMK,UAAU,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB/E,eAAe,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAQrF,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;IAsB9E;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAqCjE;;;;;OAKG;IACG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAYrF;;;;;OAKG;IACG,SAAS,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAsCjE;;;;OAIG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAazF;;;;;OAKG;IACH,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC;IA8BxD;;;;;;OAMG;IACG,OAAO,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAyB/D;;;;;OAKG;YACW,WAAW;CAoC1B"}
|
|
@@ -146,8 +146,21 @@ export class DataAccess {
|
|
|
146
146
|
return fromDashboardResult(result);
|
|
147
147
|
}
|
|
148
148
|
const emails = this.debugStore.emails.getEmails();
|
|
149
|
-
// Strip html/text from list response
|
|
150
|
-
|
|
149
|
+
// Strip html/text from list response — build lightweight objects
|
|
150
|
+
// without object-spread to avoid copying large HTML bodies
|
|
151
|
+
const stripped = emails.map((e) => ({
|
|
152
|
+
id: e.id,
|
|
153
|
+
from: e.from,
|
|
154
|
+
to: e.to,
|
|
155
|
+
cc: e.cc,
|
|
156
|
+
bcc: e.bcc,
|
|
157
|
+
subject: e.subject,
|
|
158
|
+
mailer: e.mailer,
|
|
159
|
+
status: e.status,
|
|
160
|
+
messageId: e.messageId,
|
|
161
|
+
attachmentCount: e.attachmentCount,
|
|
162
|
+
timestamp: e.timestamp,
|
|
163
|
+
}));
|
|
151
164
|
return wrapArray(stripped, opts, (e, term) => {
|
|
152
165
|
return (e.from.toLowerCase().includes(term) ||
|
|
153
166
|
e.to.toLowerCase().includes(term) ||
|
|
@@ -193,10 +206,17 @@ export class DataAccess {
|
|
|
193
206
|
return { data: [], meta: { total: 0, page: 1, perPage: opts.perPage ?? 50, lastPage: 1 } };
|
|
194
207
|
}
|
|
195
208
|
const traces = this.debugStore.traces.getTraces();
|
|
196
|
-
// Strip spans from list view, add warningCount
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
// Strip spans from list view, add warningCount — build lightweight
|
|
210
|
+
// objects without spread to avoid copying large span arrays
|
|
211
|
+
const list = traces.map((t) => ({
|
|
212
|
+
id: t.id,
|
|
213
|
+
method: t.method,
|
|
214
|
+
url: t.url,
|
|
215
|
+
statusCode: t.statusCode,
|
|
216
|
+
totalDuration: t.totalDuration,
|
|
217
|
+
spanCount: t.spanCount,
|
|
218
|
+
warningCount: t.warnings.length,
|
|
219
|
+
timestamp: t.timestamp,
|
|
200
220
|
}));
|
|
201
221
|
return wrapArray(list, opts, (t, term) => {
|
|
202
222
|
return t.method.toLowerCase().includes(term) || t.url.toLowerCase().includes(term);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAA;AAEnE;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAA;gBAE1B,MAAM,EAAE,gBAAgB;IAQpC;;;OAGG;IACH,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAOpD,gEAAgE;IAChE,cAAc,IAAI;QAChB,OAAO,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACzC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KACzC;IASK,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7D,IAAI,IAAI,IAAI;IAOZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"debug_store.d.ts","sourceRoot":"","sources":["../../../src/debug/debug_store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAA;AAEnE;;;GAGG;AACH,qBAAa,UAAU;IACrB,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAA;gBAE1B,MAAM,EAAE,gBAAgB;IAQpC;;;OAGG;IACH,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAOpD,gEAAgE;IAChE,cAAc,IAAI;QAChB,OAAO,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACzC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;QACxC,MAAM,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KACzC;IASK,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAa7D,IAAI,IAAI,IAAI;IAOZ,kEAAkE;IAC5D,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCjD,uDAAuD;IACjD,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAqCpD"}
|
|
@@ -62,15 +62,25 @@ export class DebugStore {
|
|
|
62
62
|
}
|
|
63
63
|
/** Serialize all collector data to a JSON file (atomic write). */
|
|
64
64
|
async saveToDisk(filePath) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
65
|
+
// Build JSON incrementally to avoid a single massive JSON.stringify
|
|
66
|
+
// that blocks the event loop when buffers are large.
|
|
67
|
+
const parts = ['{'];
|
|
68
|
+
const queries = this.queries.getQueries();
|
|
69
|
+
parts.push(`"queries":${JSON.stringify(queries)},`);
|
|
70
|
+
// Yield between each collector's serialization to let the event loop breathe
|
|
71
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
72
|
+
const events = this.events.getEvents();
|
|
73
|
+
parts.push(`"events":${JSON.stringify(events)},`);
|
|
74
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
75
|
+
const emails = this.emails.getEmails();
|
|
76
|
+
parts.push(`"emails":${JSON.stringify(emails)}`);
|
|
70
77
|
if (this.traces) {
|
|
71
|
-
|
|
78
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
79
|
+
const traces = this.traces.getTraces();
|
|
80
|
+
parts.push(`,"traces":${JSON.stringify(traces)}`);
|
|
72
81
|
}
|
|
73
|
-
|
|
82
|
+
parts.push('}');
|
|
83
|
+
const json = parts.join('');
|
|
74
84
|
const tmpPath = filePath + '.tmp';
|
|
75
85
|
await mkdir(dirname(filePath), { recursive: true });
|
|
76
86
|
await writeFile(tmpPath, json, 'utf-8');
|
|
@@ -9,6 +9,7 @@ import type { EmailRecord, Emitter } from './types.js';
|
|
|
9
9
|
* - `queued:mail:error` — queued email failed
|
|
10
10
|
*/
|
|
11
11
|
export declare class EmailCollector {
|
|
12
|
+
private static readonly MAX_HTML_SIZE;
|
|
12
13
|
private buffer;
|
|
13
14
|
private emitter;
|
|
14
15
|
private handlers;
|
|
@@ -25,6 +26,7 @@ export declare class EmailCollector {
|
|
|
25
26
|
};
|
|
26
27
|
clear(): void;
|
|
27
28
|
private buildRecord;
|
|
29
|
+
private capSize;
|
|
28
30
|
/** Register a callback that fires whenever a new email is recorded. */
|
|
29
31
|
onNewItem(cb: ((item: EmailRecord) => void) | null): void;
|
|
30
32
|
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/email_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAA8B,MAAM,YAAY,CAAA;AAElF;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAA6D;gBAEjE,SAAS,GAAE,MAAY;IAI7B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"email_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/email_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAA8B,MAAM,YAAY,CAAA;AAElF;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAS;IAC9C,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAA6D;gBAEjE,SAAS,GAAE,MAAY;IAI7B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAuD5C,IAAI,IAAI,IAAI;IAUZ,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKvC,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,WAAW;IAsBnB,OAAO,CAAC,OAAO;IAMf,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
|
|
@@ -10,6 +10,7 @@ import { RingBuffer } from './ring_buffer.js';
|
|
|
10
10
|
* - `queued:mail:error` — queued email failed
|
|
11
11
|
*/
|
|
12
12
|
export class EmailCollector {
|
|
13
|
+
static MAX_HTML_SIZE = 50_000; // 50 KB cap per email body
|
|
13
14
|
buffer;
|
|
14
15
|
emitter = null;
|
|
15
16
|
handlers = [];
|
|
@@ -29,15 +30,12 @@ export class EmailCollector {
|
|
|
29
30
|
const msg = data?.message || data;
|
|
30
31
|
const to = extractAddresses(msg?.to);
|
|
31
32
|
const subject = msg?.subject || '';
|
|
32
|
-
// Try to find the matching 'sending' record and update it
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
rec.messageId = data?.response?.messageId || data?.messageId || null;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
33
|
+
// Try to find the matching 'sending' record and update it (no buffer copy)
|
|
34
|
+
const match = this.buffer.findFromEnd((rec) => rec.status === 'sending' && rec.to === to && rec.subject === subject);
|
|
35
|
+
if (match) {
|
|
36
|
+
match.status = 'sent';
|
|
37
|
+
match.messageId = data?.response?.messageId || data?.messageId || null;
|
|
38
|
+
return;
|
|
41
39
|
}
|
|
42
40
|
// No matching 'sending' record — insert a new 'sent' record
|
|
43
41
|
const record = this.buildRecord(msg, 'sent', data);
|
|
@@ -80,8 +78,7 @@ export class EmailCollector {
|
|
|
80
78
|
return this.buffer.latest(n);
|
|
81
79
|
}
|
|
82
80
|
getEmailHtml(id) {
|
|
83
|
-
const
|
|
84
|
-
const record = all.find((r) => r.id === id);
|
|
81
|
+
const record = this.buffer.findFromEnd((r) => r.id === id);
|
|
85
82
|
return record?.html ?? null;
|
|
86
83
|
}
|
|
87
84
|
getTotalCount() {
|
|
@@ -101,8 +98,8 @@ export class EmailCollector {
|
|
|
101
98
|
cc: extractAddresses(msg?.cc) || null,
|
|
102
99
|
bcc: extractAddresses(msg?.bcc) || null,
|
|
103
100
|
subject: msg?.subject || '(no subject)',
|
|
104
|
-
html: msg?.html
|
|
105
|
-
text: msg?.text
|
|
101
|
+
html: this.capSize(msg?.html),
|
|
102
|
+
text: this.capSize(msg?.text),
|
|
106
103
|
mailer: data?.mailerName || data?.mailer || 'unknown',
|
|
107
104
|
status,
|
|
108
105
|
messageId: null,
|
|
@@ -110,6 +107,13 @@ export class EmailCollector {
|
|
|
110
107
|
timestamp: Date.now(),
|
|
111
108
|
};
|
|
112
109
|
}
|
|
110
|
+
capSize(value) {
|
|
111
|
+
if (!value)
|
|
112
|
+
return null;
|
|
113
|
+
if (value.length <= EmailCollector.MAX_HTML_SIZE)
|
|
114
|
+
return value;
|
|
115
|
+
return value.slice(0, EmailCollector.MAX_HTML_SIZE) + '\n<!-- truncated -->';
|
|
116
|
+
}
|
|
113
117
|
/** Register a callback that fires whenever a new email is recorded. */
|
|
114
118
|
onNewItem(cb) {
|
|
115
119
|
this.buffer.onPush(cb);
|
|
@@ -10,8 +10,14 @@ export declare class EventCollector {
|
|
|
10
10
|
constructor(maxEvents?: number);
|
|
11
11
|
start(emitter: Emitter): void;
|
|
12
12
|
stop(): void;
|
|
13
|
+
/** Reusable WeakSet to avoid GC churn on every event. */
|
|
14
|
+
private circulars;
|
|
13
15
|
private summarizeData;
|
|
14
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Recursively limit object depth to prevent deeply-nested payloads
|
|
18
|
+
* from causing expensive serialization.
|
|
19
|
+
*/
|
|
20
|
+
private limitDepth;
|
|
15
21
|
getEvents(): EventRecord[];
|
|
16
22
|
getLatest(n?: number): EventRecord[];
|
|
17
23
|
getTotalCount(): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/event_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEtD;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,OAAO,CAAuB;gBAE1B,SAAS,GAAE,MAAY;IAInC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAkC7B,IAAI,IAAI,IAAI;IAQZ,OAAO,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"event_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/event_collector.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEtD;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,OAAO,CAAuB;gBAE1B,SAAS,GAAE,MAAY;IAInC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAkC7B,IAAI,IAAI,IAAI;IAQZ,yDAAyD;IACzD,OAAO,CAAC,SAAS,CAAyB;IAE1C,OAAO,CAAC,aAAa;IAsBrB;;;OAGG;IACH,OAAO,CAAC,UAAU;IAkClB,SAAS,IAAI,WAAW,EAAE;IAI1B,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
|
|
@@ -46,15 +46,22 @@ export class EventCollector {
|
|
|
46
46
|
this.originalEmit = null;
|
|
47
47
|
this.emitter = null;
|
|
48
48
|
}
|
|
49
|
+
/** Reusable WeakSet to avoid GC churn on every event. */
|
|
50
|
+
circulars = new WeakSet();
|
|
49
51
|
summarizeData(data) {
|
|
50
52
|
if (data === undefined || data === null)
|
|
51
53
|
return null;
|
|
52
54
|
try {
|
|
53
55
|
if (typeof data === 'string')
|
|
54
|
-
return data;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
return data.length > 4096 ? data.slice(0, 4096) + '...' : data;
|
|
57
|
+
if (typeof data !== 'object')
|
|
58
|
+
return String(data);
|
|
59
|
+
// Reuse the WeakSet across calls to avoid per-event allocation
|
|
60
|
+
this.circulars = new WeakSet();
|
|
61
|
+
const limited = this.limitDepth(data, 3, this.circulars);
|
|
62
|
+
// Compact JSON (no indent) to reduce string size and serialization time
|
|
63
|
+
const result = JSON.stringify(limited) ?? '';
|
|
64
|
+
return result.length > 4096 ? result.slice(0, 4096) + '...' : result;
|
|
58
65
|
}
|
|
59
66
|
catch {
|
|
60
67
|
if (typeof data === 'object' && data !== null) {
|
|
@@ -65,20 +72,42 @@ export class EventCollector {
|
|
|
65
72
|
return typeof data;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (typeof value === 'function')
|
|
77
|
-
return `[Function: ${value.name || 'anonymous'}]`;
|
|
78
|
-
if (typeof value === 'bigint')
|
|
79
|
-
return value.toString();
|
|
75
|
+
/**
|
|
76
|
+
* Recursively limit object depth to prevent deeply-nested payloads
|
|
77
|
+
* from causing expensive serialization.
|
|
78
|
+
*/
|
|
79
|
+
limitDepth(value, maxDepth, seen) {
|
|
80
|
+
if (maxDepth <= 0)
|
|
81
|
+
return '[...]';
|
|
82
|
+
if (value === null || value === undefined)
|
|
80
83
|
return value;
|
|
81
|
-
|
|
84
|
+
if (typeof value === 'function')
|
|
85
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
86
|
+
if (typeof value === 'bigint')
|
|
87
|
+
return value.toString();
|
|
88
|
+
if (typeof value !== 'object')
|
|
89
|
+
return value;
|
|
90
|
+
if (seen.has(value))
|
|
91
|
+
return '[Circular]';
|
|
92
|
+
seen.add(value);
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
const take = Math.min(value.length, 20);
|
|
95
|
+
const arr = new Array(take);
|
|
96
|
+
for (let i = 0; i < take; i++) {
|
|
97
|
+
arr[i] = this.limitDepth(value[i], maxDepth - 1, seen);
|
|
98
|
+
}
|
|
99
|
+
return arr;
|
|
100
|
+
}
|
|
101
|
+
const keys = Object.keys(value);
|
|
102
|
+
const result = {};
|
|
103
|
+
const limit = Math.min(keys.length, 50);
|
|
104
|
+
for (let i = 0; i < limit; i++) {
|
|
105
|
+
result[keys[i]] = this.limitDepth(value[keys[i]], maxDepth - 1, seen);
|
|
106
|
+
}
|
|
107
|
+
if (keys.length > 50) {
|
|
108
|
+
result['...'] = `(${keys.length - 50} more keys)`;
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
82
111
|
}
|
|
83
112
|
getEvents() {
|
|
84
113
|
return this.buffer.toArray();
|
|
@@ -10,11 +10,23 @@ export declare class QueryCollector {
|
|
|
10
10
|
private slowThresholdMs;
|
|
11
11
|
private emitter;
|
|
12
12
|
private handler;
|
|
13
|
+
private cachedSummary;
|
|
14
|
+
private summaryComputedAt;
|
|
13
15
|
constructor(maxQueries?: number, slowThresholdMs?: number);
|
|
14
16
|
start(emitter: Emitter): Promise<void>;
|
|
15
17
|
stop(): void;
|
|
16
18
|
getQueries(): QueryRecord[];
|
|
19
|
+
/**
|
|
20
|
+
* Get only queries with id > lastId.
|
|
21
|
+
* Uses collectFromEnd for O(K) performance where K = number of new items,
|
|
22
|
+
* instead of O(N) full buffer copy + filter.
|
|
23
|
+
*/
|
|
24
|
+
getQueriesSince(lastId: number): QueryRecord[];
|
|
17
25
|
getLatest(n?: number): QueryRecord[];
|
|
26
|
+
/**
|
|
27
|
+
* Cached for 1s to avoid 4 full O(N) passes over the 500-item buffer
|
|
28
|
+
* on every 3-second auto-refresh from the debug panel.
|
|
29
|
+
*/
|
|
18
30
|
getSummary(): {
|
|
19
31
|
total: number;
|
|
20
32
|
slow: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/query_collector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAA;AAEpE;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAA8C;
|
|
1
|
+
{"version":3,"file":"query_collector.d.ts","sourceRoot":"","sources":["../../../src/debug/query_collector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAA;AAEpE;;;;;GAKG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,aAAa,CAKN;IACf,OAAO,CAAC,iBAAiB,CAAY;gBAEzB,UAAU,GAAE,MAAY,EAAE,eAAe,GAAE,MAAY;IAK7D,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAmC5C,IAAI,IAAI,IAAI;IAQZ,UAAU,IAAI,WAAW,EAAE;IAI3B;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE;IAK9C,SAAS,CAAC,CAAC,GAAE,MAAY,GAAG,WAAW,EAAE;IAIzC;;;OAGG;IACH,UAAU;eA7ED,MAAM;cACP,MAAM;oBACA,MAAM;qBACL,MAAM;;IA4GrB,aAAa,IAAI,MAAM;IAIvB,aAAa,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAIjD,KAAK,IAAI,IAAI;IAIb,uEAAuE;IACvE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAIzD,0EAA0E;IAC1E,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,IAAI;CAK1C"}
|
|
@@ -12,6 +12,8 @@ export class QueryCollector {
|
|
|
12
12
|
slowThresholdMs;
|
|
13
13
|
emitter = null;
|
|
14
14
|
handler = null;
|
|
15
|
+
cachedSummary = null;
|
|
16
|
+
summaryComputedAt = 0;
|
|
15
17
|
constructor(maxQueries = 500, slowThresholdMs = 100) {
|
|
16
18
|
this.buffer = new RingBuffer(maxQueries);
|
|
17
19
|
this.slowThresholdMs = slowThresholdMs;
|
|
@@ -57,25 +59,53 @@ export class QueryCollector {
|
|
|
57
59
|
getQueries() {
|
|
58
60
|
return this.buffer.toArray();
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Get only queries with id > lastId.
|
|
64
|
+
* Uses collectFromEnd for O(K) performance where K = number of new items,
|
|
65
|
+
* instead of O(N) full buffer copy + filter.
|
|
66
|
+
*/
|
|
67
|
+
getQueriesSince(lastId) {
|
|
68
|
+
if (lastId <= 0)
|
|
69
|
+
return this.buffer.toArray();
|
|
70
|
+
return this.buffer.collectFromEnd((q) => q.id > lastId);
|
|
71
|
+
}
|
|
60
72
|
getLatest(n = 100) {
|
|
61
73
|
return this.buffer.latest(n);
|
|
62
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Cached for 1s to avoid 4 full O(N) passes over the 500-item buffer
|
|
77
|
+
* on every 3-second auto-refresh from the debug panel.
|
|
78
|
+
*/
|
|
63
79
|
getSummary() {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (this.cachedSummary && now - this.summaryComputedAt < 1000) {
|
|
82
|
+
return this.cachedSummary;
|
|
83
|
+
}
|
|
84
|
+
// Single pass over the buffer to compute all metrics at once
|
|
64
85
|
const queries = this.buffer.toArray();
|
|
65
86
|
const total = queries.length;
|
|
66
|
-
|
|
87
|
+
let slow = 0;
|
|
88
|
+
let totalDuration = 0;
|
|
67
89
|
const sqlCounts = new Map();
|
|
68
90
|
for (const q of queries) {
|
|
91
|
+
if (q.duration > this.slowThresholdMs)
|
|
92
|
+
slow++;
|
|
93
|
+
totalDuration += q.duration;
|
|
69
94
|
sqlCounts.set(q.sql, (sqlCounts.get(q.sql) || 0) + 1);
|
|
70
95
|
}
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
96
|
+
let duplicates = 0;
|
|
97
|
+
for (const count of sqlCounts.values()) {
|
|
98
|
+
if (count > 1)
|
|
99
|
+
duplicates++;
|
|
100
|
+
}
|
|
101
|
+
this.cachedSummary = {
|
|
74
102
|
total,
|
|
75
103
|
slow,
|
|
76
104
|
duplicates,
|
|
77
|
-
avgDuration: round(
|
|
105
|
+
avgDuration: total > 0 ? round(totalDuration / total) : 0,
|
|
78
106
|
};
|
|
107
|
+
this.summaryComputedAt = now;
|
|
108
|
+
return this.cachedSummary;
|
|
79
109
|
}
|
|
80
110
|
getTotalCount() {
|
|
81
111
|
return this.buffer.size();
|
|
@@ -21,6 +21,20 @@ export declare class RingBuffer<T> {
|
|
|
21
21
|
size(): number;
|
|
22
22
|
getCapacity(): number;
|
|
23
23
|
clear(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Find a single item by predicate, searching from newest to oldest.
|
|
26
|
+
* Returns immediately on first match without copying the buffer.
|
|
27
|
+
*/
|
|
28
|
+
findFromEnd(predicate: (item: T) => boolean): T | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Collect items from the end of the buffer while the predicate holds.
|
|
31
|
+
* Iterates from newest to oldest and stops at the first non-match.
|
|
32
|
+
* Returns items in insertion order (oldest first).
|
|
33
|
+
*
|
|
34
|
+
* Useful for efficiently getting "items since ID X" without copying the
|
|
35
|
+
* entire buffer, since IDs are monotonically increasing.
|
|
36
|
+
*/
|
|
37
|
+
collectFromEnd(predicate: (item: T) => boolean): T[];
|
|
24
38
|
/** Bulk-load items (e.g. from disk). Pushes each in order, respecting capacity. */
|
|
25
39
|
load(items: T[]): void;
|
|
26
40
|
/** Restore the auto-increment counter (e.g. after loading persisted data). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ring_buffer.d.ts","sourceRoot":"","sources":["../../../src/debug/ring_buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,CAAC;IAOX,OAAO,CAAC,QAAQ;IAN5B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,YAAY,CAAmC;gBAEnC,QAAQ,EAAE,MAAM;IAIpC,oEAAoE;IACpE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAI5C,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IASnB,2DAA2D;IAC3D,OAAO,IAAI,CAAC,EAAE;IAcd,sDAAsD;IACtD,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;
|
|
1
|
+
{"version":3,"file":"ring_buffer.d.ts","sourceRoot":"","sources":["../../../src/debug/ring_buffer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAU,CAAC,CAAC;IAOX,OAAO,CAAC,QAAQ;IAN5B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,YAAY,CAAmC;gBAEnC,QAAQ,EAAE,MAAM;IAIpC,oEAAoE;IACpE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI;IAI5C,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IASnB,2DAA2D;IAC3D,OAAO,IAAI,CAAC,EAAE;IAcd,sDAAsD;IACtD,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;IActB,SAAS,IAAI,MAAM;IAInB,IAAI,IAAI,MAAM;IAId,WAAW,IAAI,MAAM;IAIrB,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,WAAW,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,SAAS;IAc3D;;;;;;;OAOG;IACH,cAAc,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,EAAE;IAgBpD,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAMtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAG5B"}
|