binja 0.5.3 → 0.6.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/dist/debug/collector.d.ts +43 -0
- package/dist/debug/collector.d.ts.map +1 -1
- package/dist/debug/index.d.ts +2 -1
- package/dist/debug/index.d.ts.map +1 -1
- package/dist/debug/index.js +278 -1
- package/dist/debug/integrations.d.ts +104 -0
- package/dist/debug/integrations.d.ts.map +1 -0
- package/dist/index.js +121 -1
- package/package.json +1 -1
|
@@ -18,6 +18,34 @@ export interface ContextValue {
|
|
|
18
18
|
expandable: boolean;
|
|
19
19
|
children?: Record<string, ContextValue>;
|
|
20
20
|
}
|
|
21
|
+
export interface QueryInfo {
|
|
22
|
+
/** SQL query or operation name */
|
|
23
|
+
sql: string;
|
|
24
|
+
/** Query parameters */
|
|
25
|
+
params?: any[];
|
|
26
|
+
/** Duration in milliseconds */
|
|
27
|
+
duration: number;
|
|
28
|
+
/** Number of rows returned */
|
|
29
|
+
rows?: number;
|
|
30
|
+
/** Source ORM (drizzle, prisma, etc.) */
|
|
31
|
+
source?: string;
|
|
32
|
+
/** Timestamp when query was executed */
|
|
33
|
+
timestamp: number;
|
|
34
|
+
/** Whether this query was detected as part of N+1 */
|
|
35
|
+
isN1?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface QueryStats {
|
|
38
|
+
/** Total number of queries */
|
|
39
|
+
count: number;
|
|
40
|
+
/** Total duration of all queries */
|
|
41
|
+
totalDuration: number;
|
|
42
|
+
/** Number of slow queries (> 100ms) */
|
|
43
|
+
slowCount: number;
|
|
44
|
+
/** Number of N+1 queries detected */
|
|
45
|
+
n1Count: number;
|
|
46
|
+
/** Queries grouped by similar SQL for N+1 detection */
|
|
47
|
+
queryCounts: Map<string, number>;
|
|
48
|
+
}
|
|
21
49
|
export interface DebugData {
|
|
22
50
|
startTime: number;
|
|
23
51
|
endTime?: number;
|
|
@@ -35,6 +63,8 @@ export interface DebugData {
|
|
|
35
63
|
testsUsed: Map<string, number>;
|
|
36
64
|
cacheHits: number;
|
|
37
65
|
cacheMisses: number;
|
|
66
|
+
queries: QueryInfo[];
|
|
67
|
+
queryStats: QueryStats;
|
|
38
68
|
warnings: string[];
|
|
39
69
|
}
|
|
40
70
|
export declare class DebugCollector {
|
|
@@ -59,6 +89,19 @@ export declare class DebugCollector {
|
|
|
59
89
|
recordCacheHit(): void;
|
|
60
90
|
recordCacheMiss(): void;
|
|
61
91
|
addWarning(message: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Record a database query for telemetry
|
|
94
|
+
* @param query Query information
|
|
95
|
+
*/
|
|
96
|
+
recordQuery(query: Omit<QueryInfo, 'timestamp' | 'isN1'>): void;
|
|
97
|
+
/**
|
|
98
|
+
* Normalize SQL query for comparison (removes specific values)
|
|
99
|
+
*/
|
|
100
|
+
private normalizeQuery;
|
|
101
|
+
/**
|
|
102
|
+
* Get query statistics
|
|
103
|
+
*/
|
|
104
|
+
getQueryStats(): QueryStats;
|
|
62
105
|
getData(): DebugData;
|
|
63
106
|
getSummary(): {
|
|
64
107
|
totalTime: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/debug/collector.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAA;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,SAAS;IAExB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,aAAa,EAAE,YAAY,EAAE,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,IAAI,EAAE,SAAS,GAAG,KAAK,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAGhB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IAG7C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAG9B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IAGnB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAW;;
|
|
1
|
+
{"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/debug/collector.ts"],"names":[],"mappings":"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"}
|
package/dist/debug/index.d.ts
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
import type { Environment } from '../index';
|
|
14
14
|
import { PanelOptions } from './panel';
|
|
15
15
|
export { DebugCollector, startDebugCollection, endDebugCollection, getDebugCollector } from './collector';
|
|
16
|
-
export type { DebugData, ContextValue } from './collector';
|
|
16
|
+
export type { DebugData, ContextValue, QueryInfo, QueryStats } from './collector';
|
|
17
17
|
export { generateDebugPanel } from './panel';
|
|
18
18
|
export type { PanelOptions } from './panel';
|
|
19
|
+
export { recordQuery, createPrismaMiddleware, setupPrismaLogging, createDrizzleLogger, wrapDrizzleQuery, wrapBunSQL, wrapQuery, createQueryWrapper, } from './integrations';
|
|
19
20
|
export interface DebugRenderOptions {
|
|
20
21
|
/** Panel options */
|
|
21
22
|
panel?: PanelOptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/debug/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,OAAO,EAAsB,YAAY,EAAE,MAAM,SAAS,CAAA;AAG1D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACzG,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/debug/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAE3C,OAAO,EAAsB,YAAY,EAAE,MAAM,SAAS,CAAA;AAG1D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACzG,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAC5C,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAG3C,OAAO,EACL,WAAW,EACX,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,SAAS,EACT,kBAAkB,GACnB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,WAAW,kBAAkB;IACjC,oBAAoB;IACpB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,EACjC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB;yBAEvD,MAAM,YAAW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAQ,OAAO,CAAC,MAAM,CAAC;yBAG3D,MAAM,YAAW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAQ,OAAO,CAAC,MAAM,CAAC;EAIzF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB;IAE9E;;OAEG;aAEa,GAAG,GAAG,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC;IAyBjD;;OAEG;gBAEO,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,MAAM,IAAI;EA2BjD"}
|
package/dist/debug/index.js
CHANGED
|
@@ -14,6 +14,14 @@ class DebugCollector {
|
|
|
14
14
|
testsUsed: new Map,
|
|
15
15
|
cacheHits: 0,
|
|
16
16
|
cacheMisses: 0,
|
|
17
|
+
queries: [],
|
|
18
|
+
queryStats: {
|
|
19
|
+
count: 0,
|
|
20
|
+
totalDuration: 0,
|
|
21
|
+
slowCount: 0,
|
|
22
|
+
n1Count: 0,
|
|
23
|
+
queryCounts: new Map
|
|
24
|
+
},
|
|
17
25
|
warnings: []
|
|
18
26
|
};
|
|
19
27
|
}
|
|
@@ -154,6 +162,33 @@ class DebugCollector {
|
|
|
154
162
|
addWarning(message) {
|
|
155
163
|
this.data.warnings.push(message);
|
|
156
164
|
}
|
|
165
|
+
recordQuery(query) {
|
|
166
|
+
const normalizedSql = this.normalizeQuery(query.sql);
|
|
167
|
+
const currentCount = this.data.queryStats.queryCounts.get(normalizedSql) || 0;
|
|
168
|
+
this.data.queryStats.queryCounts.set(normalizedSql, currentCount + 1);
|
|
169
|
+
const isN1 = currentCount >= 2;
|
|
170
|
+
const queryInfo = {
|
|
171
|
+
...query,
|
|
172
|
+
timestamp: performance.now(),
|
|
173
|
+
isN1
|
|
174
|
+
};
|
|
175
|
+
this.data.queries.push(queryInfo);
|
|
176
|
+
this.data.queryStats.count++;
|
|
177
|
+
this.data.queryStats.totalDuration += query.duration;
|
|
178
|
+
if (query.duration > 100) {
|
|
179
|
+
this.data.queryStats.slowCount++;
|
|
180
|
+
}
|
|
181
|
+
if (isN1 && currentCount === 2) {
|
|
182
|
+
this.data.queryStats.n1Count++;
|
|
183
|
+
this.addWarning(`N+1 query detected: ${normalizedSql.slice(0, 50)}...`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
normalizeQuery(sql) {
|
|
187
|
+
return sql.replace(/\s+/g, " ").replace(/= \?/g, "= ?").replace(/= \$\d+/g, "= ?").replace(/= '\w+'/g, "= '?'").replace(/= \d+/g, "= ?").replace(/IN \([^)]+\)/gi, "IN (?)").trim();
|
|
188
|
+
}
|
|
189
|
+
getQueryStats() {
|
|
190
|
+
return this.data.queryStats;
|
|
191
|
+
}
|
|
157
192
|
getData() {
|
|
158
193
|
return this.data;
|
|
159
194
|
}
|
|
@@ -315,6 +350,25 @@ function generateStyles(id, c, opts) {
|
|
|
315
350
|
#${id} .dbg-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(234,179,8,0.1); border-radius: 6px; border-left: 3px solid ${c.warning}; }
|
|
316
351
|
#${id} .dbg-warning-icon { color: ${c.warning}; flex-shrink: 0; margin-top: 1px; }
|
|
317
352
|
#${id} .dbg-warning-text { color: ${c.text}; font-size: 12px; }
|
|
353
|
+
#${id} .dbg-queries { display: flex; flex-direction: column; gap: 8px; }
|
|
354
|
+
#${id} .dbg-query { background: ${c.bg}; border-radius: 6px; overflow: hidden; }
|
|
355
|
+
#${id} .dbg-query.n1 { border-left: 3px solid ${c.error}; }
|
|
356
|
+
#${id} .dbg-query.slow { border-left: 3px solid ${c.warning}; }
|
|
357
|
+
#${id} .dbg-query-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; gap: 8px; }
|
|
358
|
+
#${id} .dbg-query-sql { flex: 1; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; color: ${c.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
359
|
+
#${id} .dbg-query-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
360
|
+
#${id} .dbg-query-time { font-family: 'SF Mono', Monaco, monospace; font-size: 10px; padding: 2px 6px; border-radius: 3px; background: ${c.bgTertiary}; }
|
|
361
|
+
#${id} .dbg-query-time.slow { background: rgba(234,179,8,0.15); color: ${c.warning}; }
|
|
362
|
+
#${id} .dbg-query-rows { font-size: 10px; color: ${c.textMuted}; }
|
|
363
|
+
#${id} .dbg-query-source { font-size: 9px; padding: 2px 5px; border-radius: 3px; background: rgba(59,130,246,0.1); color: ${c.accent}; text-transform: uppercase; }
|
|
364
|
+
#${id} .dbg-query-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600; }
|
|
365
|
+
#${id} .dbg-query-badge.n1 { background: rgba(239,68,68,0.15); color: ${c.error}; }
|
|
366
|
+
#${id} .dbg-query-stats { display: flex; gap: 12px; margin-bottom: 12px; padding: 10px; background: ${c.bg}; border-radius: 6px; }
|
|
367
|
+
#${id} .dbg-query-stat { flex: 1; text-align: center; }
|
|
368
|
+
#${id} .dbg-query-stat-num { font-size: 18px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; color: ${c.text}; }
|
|
369
|
+
#${id} .dbg-query-stat-num.warning { color: ${c.warning}; }
|
|
370
|
+
#${id} .dbg-query-stat-num.error { color: ${c.error}; }
|
|
371
|
+
#${id} .dbg-query-stat-label { font-size: 10px; color: ${c.textMuted}; margin-top: 2px; }
|
|
318
372
|
`;
|
|
319
373
|
}
|
|
320
374
|
function getPosition(pos) {
|
|
@@ -339,7 +393,8 @@ var icons = {
|
|
|
339
393
|
filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
|
|
340
394
|
cache: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1010 10H12V2z"/><path d="M12 2a10 10 0 00-8.66 15"/></svg>`,
|
|
341
395
|
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
342
|
-
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg
|
|
396
|
+
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg>`,
|
|
397
|
+
database: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`
|
|
343
398
|
};
|
|
344
399
|
function generateToggle(id, data, c) {
|
|
345
400
|
const time = (data.totalTime || 0).toFixed(1);
|
|
@@ -368,6 +423,7 @@ function generatePanel(id, data, c, opts) {
|
|
|
368
423
|
${generateTemplatesSection(data)}
|
|
369
424
|
${generateContextSection(data)}
|
|
370
425
|
${generateFiltersSection(data)}
|
|
426
|
+
${generateQueriesSection(data)}
|
|
371
427
|
${generateCacheSection(data)}
|
|
372
428
|
${generateWarningsSection(data)}
|
|
373
429
|
</div>
|
|
@@ -531,6 +587,70 @@ function generateWarningsSection(data) {
|
|
|
531
587
|
</div>
|
|
532
588
|
</div>`;
|
|
533
589
|
}
|
|
590
|
+
function generateQueriesSection(data) {
|
|
591
|
+
if (data.queries.length === 0)
|
|
592
|
+
return "";
|
|
593
|
+
const stats = data.queryStats;
|
|
594
|
+
const hasIssues = stats.slowCount > 0 || stats.n1Count > 0;
|
|
595
|
+
const statsHtml = `
|
|
596
|
+
<div class="dbg-query-stats">
|
|
597
|
+
<div class="dbg-query-stat">
|
|
598
|
+
<div class="dbg-query-stat-num">${stats.count}</div>
|
|
599
|
+
<div class="dbg-query-stat-label">Queries</div>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="dbg-query-stat">
|
|
602
|
+
<div class="dbg-query-stat-num">${stats.totalDuration.toFixed(1)}ms</div>
|
|
603
|
+
<div class="dbg-query-stat-label">Total Time</div>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="dbg-query-stat">
|
|
606
|
+
<div class="dbg-query-stat-num ${stats.slowCount > 0 ? "warning" : ""}">${stats.slowCount}</div>
|
|
607
|
+
<div class="dbg-query-stat-label">Slow (>100ms)</div>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="dbg-query-stat">
|
|
610
|
+
<div class="dbg-query-stat-num ${stats.n1Count > 0 ? "error" : ""}">${stats.n1Count}</div>
|
|
611
|
+
<div class="dbg-query-stat-label">N+1</div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>`;
|
|
614
|
+
const queries = data.queries.map((q) => {
|
|
615
|
+
const isSlow = q.duration > 100;
|
|
616
|
+
const classes = [
|
|
617
|
+
"dbg-query",
|
|
618
|
+
q.isN1 ? "n1" : "",
|
|
619
|
+
isSlow ? "slow" : ""
|
|
620
|
+
].filter(Boolean).join(" ");
|
|
621
|
+
const badges = [];
|
|
622
|
+
if (q.isN1) {
|
|
623
|
+
badges.push('<span class="dbg-query-badge n1">N+1</span>');
|
|
624
|
+
}
|
|
625
|
+
const rowsText = q.rows !== undefined ? `<span class="dbg-query-rows">${q.rows} rows</span>` : "";
|
|
626
|
+
const sourceText = q.source ? `<span class="dbg-query-source">${escapeHtml(q.source)}</span>` : "";
|
|
627
|
+
return `
|
|
628
|
+
<div class="${classes}">
|
|
629
|
+
<div class="dbg-query-header">
|
|
630
|
+
<span class="dbg-query-sql" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>
|
|
631
|
+
<div class="dbg-query-meta">
|
|
632
|
+
${badges.join("")}
|
|
633
|
+
${sourceText}
|
|
634
|
+
${rowsText}
|
|
635
|
+
<span class="dbg-query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
</div>`;
|
|
639
|
+
}).join("");
|
|
640
|
+
const metaColor = hasIssues ? 'style="color:#ef4444"' : "";
|
|
641
|
+
return `
|
|
642
|
+
<div class="dbg-section ${hasIssues ? "open" : ""}">
|
|
643
|
+
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
644
|
+
<span class="dbg-section-title">${icons.database} Queries</span>
|
|
645
|
+
<span class="dbg-section-meta" ${metaColor}>${stats.count} (${stats.totalDuration.toFixed(1)}ms)</span>
|
|
646
|
+
<span class="dbg-chevron">${icons.chevron}</span>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="dbg-section-content">
|
|
649
|
+
${statsHtml}
|
|
650
|
+
<div class="dbg-queries">${queries}</div>
|
|
651
|
+
</div>
|
|
652
|
+
</div>`;
|
|
653
|
+
}
|
|
534
654
|
function generateScript(id) {
|
|
535
655
|
return `
|
|
536
656
|
(function(){
|
|
@@ -568,6 +688,155 @@ function generateScript(id) {
|
|
|
568
688
|
function escapeHtml(str) {
|
|
569
689
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
570
690
|
}
|
|
691
|
+
// src/debug/integrations.ts
|
|
692
|
+
function recordQuery(query) {
|
|
693
|
+
const collector = getDebugCollector();
|
|
694
|
+
if (collector) {
|
|
695
|
+
collector.recordQuery(query);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function createPrismaMiddleware() {
|
|
699
|
+
return async (params, next) => {
|
|
700
|
+
const collector = getDebugCollector();
|
|
701
|
+
const start = performance.now();
|
|
702
|
+
const result = await next(params);
|
|
703
|
+
const duration = performance.now() - start;
|
|
704
|
+
if (collector) {
|
|
705
|
+
const sql = `${params.action} ${params.model}`;
|
|
706
|
+
collector.recordQuery({
|
|
707
|
+
sql,
|
|
708
|
+
params: params.args ? [JSON.stringify(params.args)] : undefined,
|
|
709
|
+
duration,
|
|
710
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
711
|
+
source: "prisma"
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return result;
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function setupPrismaLogging(prisma) {
|
|
718
|
+
prisma.$on("query", (e) => {
|
|
719
|
+
const collector = getDebugCollector();
|
|
720
|
+
if (collector) {
|
|
721
|
+
collector.recordQuery({
|
|
722
|
+
sql: e.query,
|
|
723
|
+
params: e.params ? JSON.parse(e.params) : undefined,
|
|
724
|
+
duration: e.duration,
|
|
725
|
+
source: "prisma"
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
function createDrizzleLogger() {
|
|
731
|
+
return {
|
|
732
|
+
logQuery(query, params) {
|
|
733
|
+
const collector = getDebugCollector();
|
|
734
|
+
if (collector) {
|
|
735
|
+
collector.recordQuery({
|
|
736
|
+
sql: query,
|
|
737
|
+
params,
|
|
738
|
+
duration: 0,
|
|
739
|
+
source: "drizzle"
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
async function wrapDrizzleQuery(query, sql) {
|
|
746
|
+
const collector = getDebugCollector();
|
|
747
|
+
const start = performance.now();
|
|
748
|
+
const result = await query;
|
|
749
|
+
const duration = performance.now() - start;
|
|
750
|
+
if (collector) {
|
|
751
|
+
collector.recordQuery({
|
|
752
|
+
sql: sql || "Drizzle Query",
|
|
753
|
+
duration,
|
|
754
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
755
|
+
source: "drizzle"
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
function wrapBunSQL(db) {
|
|
761
|
+
const handler = {
|
|
762
|
+
get(target, prop) {
|
|
763
|
+
const value = target[prop];
|
|
764
|
+
if (typeof value === "function") {
|
|
765
|
+
if (prop === "query" || prop === "run" || prop === "prepare") {
|
|
766
|
+
return function(...args) {
|
|
767
|
+
const collector = getDebugCollector();
|
|
768
|
+
const sql = typeof args[0] === "string" ? args[0] : "SQL Query";
|
|
769
|
+
const start = performance.now();
|
|
770
|
+
const result = value.apply(target, args);
|
|
771
|
+
if (result && typeof result.all === "function") {
|
|
772
|
+
return wrapPreparedStatement(result, sql);
|
|
773
|
+
}
|
|
774
|
+
const duration = performance.now() - start;
|
|
775
|
+
if (collector) {
|
|
776
|
+
collector.recordQuery({
|
|
777
|
+
sql,
|
|
778
|
+
params: args.slice(1),
|
|
779
|
+
duration,
|
|
780
|
+
rows: Array.isArray(result) ? result.length : undefined,
|
|
781
|
+
source: "bun:sqlite"
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return value;
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
return new Proxy(db, handler);
|
|
792
|
+
}
|
|
793
|
+
function wrapPreparedStatement(stmt, sql) {
|
|
794
|
+
const handler = {
|
|
795
|
+
get(target, prop) {
|
|
796
|
+
const value = target[prop];
|
|
797
|
+
if (typeof value === "function" && (prop === "all" || prop === "get" || prop === "run")) {
|
|
798
|
+
return function(...args) {
|
|
799
|
+
const collector = getDebugCollector();
|
|
800
|
+
const start = performance.now();
|
|
801
|
+
const result = value.apply(target, args);
|
|
802
|
+
const duration = performance.now() - start;
|
|
803
|
+
if (collector) {
|
|
804
|
+
collector.recordQuery({
|
|
805
|
+
sql,
|
|
806
|
+
params: args,
|
|
807
|
+
duration,
|
|
808
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
809
|
+
source: "bun:sqlite"
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return result;
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
return new Proxy(stmt, handler);
|
|
819
|
+
}
|
|
820
|
+
async function wrapQuery(sql, queryFn, source = "sql") {
|
|
821
|
+
const collector = getDebugCollector();
|
|
822
|
+
const start = performance.now();
|
|
823
|
+
const result = await queryFn();
|
|
824
|
+
const duration = performance.now() - start;
|
|
825
|
+
if (collector) {
|
|
826
|
+
collector.recordQuery({
|
|
827
|
+
sql,
|
|
828
|
+
duration,
|
|
829
|
+
rows: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
830
|
+
source
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
function createQueryWrapper(source) {
|
|
836
|
+
return (sql, queryFn) => {
|
|
837
|
+
return wrapQuery(sql, queryFn, source);
|
|
838
|
+
};
|
|
839
|
+
}
|
|
571
840
|
|
|
572
841
|
// src/debug/index.ts
|
|
573
842
|
async function renderWithDebug(env, templateName, context = {}, options = {}) {
|
|
@@ -666,13 +935,21 @@ function debugMiddleware(env, options = {}) {
|
|
|
666
935
|
};
|
|
667
936
|
}
|
|
668
937
|
export {
|
|
938
|
+
wrapQuery,
|
|
939
|
+
wrapDrizzleQuery,
|
|
940
|
+
wrapBunSQL,
|
|
669
941
|
startDebugCollection,
|
|
942
|
+
setupPrismaLogging,
|
|
670
943
|
renderWithDebug,
|
|
671
944
|
renderStringWithDebug,
|
|
945
|
+
recordQuery,
|
|
672
946
|
getDebugCollector,
|
|
673
947
|
generateDebugPanel,
|
|
674
948
|
endDebugCollection,
|
|
675
949
|
debugMiddleware,
|
|
950
|
+
createQueryWrapper,
|
|
951
|
+
createPrismaMiddleware,
|
|
952
|
+
createDrizzleLogger,
|
|
676
953
|
createDebugRenderer,
|
|
677
954
|
DebugCollector
|
|
678
955
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORM Integrations for Query Telemetry
|
|
3
|
+
*
|
|
4
|
+
* Provides helpers for Prisma, Drizzle, and Bun SQL
|
|
5
|
+
*/
|
|
6
|
+
import type { QueryInfo } from './collector';
|
|
7
|
+
type QueryInput = Omit<QueryInfo, 'timestamp' | 'isN1'>;
|
|
8
|
+
/**
|
|
9
|
+
* Record a query manually (works with any database)
|
|
10
|
+
*/
|
|
11
|
+
export declare function recordQuery(query: QueryInput): void;
|
|
12
|
+
/**
|
|
13
|
+
* Create Prisma middleware for query telemetry
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { PrismaClient } from '@prisma/client'
|
|
18
|
+
* import { createPrismaMiddleware } from 'binja/debug'
|
|
19
|
+
*
|
|
20
|
+
* const prisma = new PrismaClient()
|
|
21
|
+
* prisma.$use(createPrismaMiddleware())
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function createPrismaMiddleware(): (params: any, next: (params: any) => Promise<any>) => Promise<any>;
|
|
25
|
+
/**
|
|
26
|
+
* Setup Prisma query event logging
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { PrismaClient } from '@prisma/client'
|
|
31
|
+
* import { setupPrismaLogging } from 'binja/debug'
|
|
32
|
+
*
|
|
33
|
+
* const prisma = new PrismaClient({ log: [{ emit: 'event', level: 'query' }] })
|
|
34
|
+
* setupPrismaLogging(prisma)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function setupPrismaLogging(prisma: any): void;
|
|
38
|
+
/**
|
|
39
|
+
* Create Drizzle logger for query telemetry
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import { drizzle } from 'drizzle-orm/...'
|
|
44
|
+
* import { createDrizzleLogger } from 'binja/debug'
|
|
45
|
+
*
|
|
46
|
+
* const db = drizzle(client, { logger: createDrizzleLogger() })
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function createDrizzleLogger(): {
|
|
50
|
+
logQuery(query: string, params: unknown[]): void;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Wrap a Drizzle query for accurate timing
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { wrapDrizzleQuery } from 'binja/debug'
|
|
58
|
+
*
|
|
59
|
+
* const users = await wrapDrizzleQuery(
|
|
60
|
+
* db.select().from(usersTable).where(eq(usersTable.id, 1))
|
|
61
|
+
* )
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function wrapDrizzleQuery<T>(query: Promise<T>, sql?: string): Promise<T>;
|
|
65
|
+
/**
|
|
66
|
+
* Wrap Bun SQL database for query telemetry
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* import { Database } from 'bun:sqlite'
|
|
71
|
+
* import { wrapBunSQL } from 'binja/debug'
|
|
72
|
+
*
|
|
73
|
+
* const db = wrapBunSQL(new Database('mydb.sqlite'))
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function wrapBunSQL<T extends object>(db: T): T;
|
|
77
|
+
/**
|
|
78
|
+
* Wrap any async query function for timing
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { wrapQuery } from 'binja/debug'
|
|
83
|
+
*
|
|
84
|
+
* const result = await wrapQuery(
|
|
85
|
+
* 'SELECT * FROM users WHERE id = ?',
|
|
86
|
+
* () => db.query('SELECT * FROM users WHERE id = ?', [1])
|
|
87
|
+
* )
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export declare function wrapQuery<T>(sql: string, queryFn: () => Promise<T>, source?: string): Promise<T>;
|
|
91
|
+
/**
|
|
92
|
+
* Create a query wrapper for any database client
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* import { createQueryWrapper } from 'binja/debug'
|
|
97
|
+
*
|
|
98
|
+
* const query = createQueryWrapper('mysql')
|
|
99
|
+
* const users = await query('SELECT * FROM users', () => mysql.query('SELECT * FROM users'))
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function createQueryWrapper(source: string): <T>(sql: string, queryFn: () => Promise<T>) => Promise<T>;
|
|
103
|
+
export {};
|
|
104
|
+
//# sourceMappingURL=integrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrations.d.ts","sourceRoot":"","sources":["../../src/debug/integrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,KAAK,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC,CAAA;AAEvD;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAKnD;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,KACtB,QAAQ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,kBAqB/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAYpD;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB;oBAEf,MAAM,UAAU,OAAO,EAAE,GAAG,IAAI;EAcnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,EACjB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,CAAC,CAAC,CAkBZ;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CA0CrD;AA0CD;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,MAAM,SAAQ,GACb,OAAO,CAAC,CAAC,CAAC,CAkBZ;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,IACvC,CAAC,EAAE,KAAK,MAAM,EAAE,SAAS,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAG/D"}
|
package/dist/index.js
CHANGED
|
@@ -5081,6 +5081,14 @@ class DebugCollector {
|
|
|
5081
5081
|
testsUsed: new Map,
|
|
5082
5082
|
cacheHits: 0,
|
|
5083
5083
|
cacheMisses: 0,
|
|
5084
|
+
queries: [],
|
|
5085
|
+
queryStats: {
|
|
5086
|
+
count: 0,
|
|
5087
|
+
totalDuration: 0,
|
|
5088
|
+
slowCount: 0,
|
|
5089
|
+
n1Count: 0,
|
|
5090
|
+
queryCounts: new Map
|
|
5091
|
+
},
|
|
5084
5092
|
warnings: []
|
|
5085
5093
|
};
|
|
5086
5094
|
}
|
|
@@ -5221,6 +5229,33 @@ class DebugCollector {
|
|
|
5221
5229
|
addWarning(message) {
|
|
5222
5230
|
this.data.warnings.push(message);
|
|
5223
5231
|
}
|
|
5232
|
+
recordQuery(query) {
|
|
5233
|
+
const normalizedSql = this.normalizeQuery(query.sql);
|
|
5234
|
+
const currentCount = this.data.queryStats.queryCounts.get(normalizedSql) || 0;
|
|
5235
|
+
this.data.queryStats.queryCounts.set(normalizedSql, currentCount + 1);
|
|
5236
|
+
const isN1 = currentCount >= 2;
|
|
5237
|
+
const queryInfo = {
|
|
5238
|
+
...query,
|
|
5239
|
+
timestamp: performance.now(),
|
|
5240
|
+
isN1
|
|
5241
|
+
};
|
|
5242
|
+
this.data.queries.push(queryInfo);
|
|
5243
|
+
this.data.queryStats.count++;
|
|
5244
|
+
this.data.queryStats.totalDuration += query.duration;
|
|
5245
|
+
if (query.duration > 100) {
|
|
5246
|
+
this.data.queryStats.slowCount++;
|
|
5247
|
+
}
|
|
5248
|
+
if (isN1 && currentCount === 2) {
|
|
5249
|
+
this.data.queryStats.n1Count++;
|
|
5250
|
+
this.addWarning(`N+1 query detected: ${normalizedSql.slice(0, 50)}...`);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
normalizeQuery(sql) {
|
|
5254
|
+
return sql.replace(/\s+/g, " ").replace(/= \?/g, "= ?").replace(/= \$\d+/g, "= ?").replace(/= '\w+'/g, "= '?'").replace(/= \d+/g, "= ?").replace(/IN \([^)]+\)/gi, "IN (?)").trim();
|
|
5255
|
+
}
|
|
5256
|
+
getQueryStats() {
|
|
5257
|
+
return this.data.queryStats;
|
|
5258
|
+
}
|
|
5224
5259
|
getData() {
|
|
5225
5260
|
return this.data;
|
|
5226
5261
|
}
|
|
@@ -5379,6 +5414,25 @@ function generateStyles(id, c2, opts) {
|
|
|
5379
5414
|
#${id} .dbg-warning { display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; background: rgba(234,179,8,0.1); border-radius: 6px; border-left: 3px solid ${c2.warning}; }
|
|
5380
5415
|
#${id} .dbg-warning-icon { color: ${c2.warning}; flex-shrink: 0; margin-top: 1px; }
|
|
5381
5416
|
#${id} .dbg-warning-text { color: ${c2.text}; font-size: 12px; }
|
|
5417
|
+
#${id} .dbg-queries { display: flex; flex-direction: column; gap: 8px; }
|
|
5418
|
+
#${id} .dbg-query { background: ${c2.bg}; border-radius: 6px; overflow: hidden; }
|
|
5419
|
+
#${id} .dbg-query.n1 { border-left: 3px solid ${c2.error}; }
|
|
5420
|
+
#${id} .dbg-query.slow { border-left: 3px solid ${c2.warning}; }
|
|
5421
|
+
#${id} .dbg-query-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; gap: 8px; }
|
|
5422
|
+
#${id} .dbg-query-sql { flex: 1; font-family: 'SF Mono', Monaco, monospace; font-size: 11px; color: ${c2.text}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
5423
|
+
#${id} .dbg-query-meta { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
5424
|
+
#${id} .dbg-query-time { font-family: 'SF Mono', Monaco, monospace; font-size: 10px; padding: 2px 6px; border-radius: 3px; background: ${c2.bgTertiary}; }
|
|
5425
|
+
#${id} .dbg-query-time.slow { background: rgba(234,179,8,0.15); color: ${c2.warning}; }
|
|
5426
|
+
#${id} .dbg-query-rows { font-size: 10px; color: ${c2.textMuted}; }
|
|
5427
|
+
#${id} .dbg-query-source { font-size: 9px; padding: 2px 5px; border-radius: 3px; background: rgba(59,130,246,0.1); color: ${c2.accent}; text-transform: uppercase; }
|
|
5428
|
+
#${id} .dbg-query-badge { font-size: 9px; padding: 2px 5px; border-radius: 3px; font-weight: 600; }
|
|
5429
|
+
#${id} .dbg-query-badge.n1 { background: rgba(239,68,68,0.15); color: ${c2.error}; }
|
|
5430
|
+
#${id} .dbg-query-stats { display: flex; gap: 12px; margin-bottom: 12px; padding: 10px; background: ${c2.bg}; border-radius: 6px; }
|
|
5431
|
+
#${id} .dbg-query-stat { flex: 1; text-align: center; }
|
|
5432
|
+
#${id} .dbg-query-stat-num { font-size: 18px; font-weight: 600; font-family: 'SF Mono', Monaco, monospace; color: ${c2.text}; }
|
|
5433
|
+
#${id} .dbg-query-stat-num.warning { color: ${c2.warning}; }
|
|
5434
|
+
#${id} .dbg-query-stat-num.error { color: ${c2.error}; }
|
|
5435
|
+
#${id} .dbg-query-stat-label { font-size: 10px; color: ${c2.textMuted}; margin-top: 2px; }
|
|
5382
5436
|
`;
|
|
5383
5437
|
}
|
|
5384
5438
|
function getPosition(pos) {
|
|
@@ -5403,7 +5457,8 @@ var icons = {
|
|
|
5403
5457
|
filter: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`,
|
|
5404
5458
|
cache: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1010 10H12V2z"/><path d="M12 2a10 10 0 00-8.66 15"/></svg>`,
|
|
5405
5459
|
warning: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
|
|
5406
|
-
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg
|
|
5460
|
+
file: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/></svg>`,
|
|
5461
|
+
database: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>`
|
|
5407
5462
|
};
|
|
5408
5463
|
function generateToggle(id, data, c2) {
|
|
5409
5464
|
const time2 = (data.totalTime || 0).toFixed(1);
|
|
@@ -5432,6 +5487,7 @@ function generatePanel(id, data, c2, opts) {
|
|
|
5432
5487
|
${generateTemplatesSection(data)}
|
|
5433
5488
|
${generateContextSection(data)}
|
|
5434
5489
|
${generateFiltersSection(data)}
|
|
5490
|
+
${generateQueriesSection(data)}
|
|
5435
5491
|
${generateCacheSection(data)}
|
|
5436
5492
|
${generateWarningsSection(data)}
|
|
5437
5493
|
</div>
|
|
@@ -5595,6 +5651,70 @@ function generateWarningsSection(data) {
|
|
|
5595
5651
|
</div>
|
|
5596
5652
|
</div>`;
|
|
5597
5653
|
}
|
|
5654
|
+
function generateQueriesSection(data) {
|
|
5655
|
+
if (data.queries.length === 0)
|
|
5656
|
+
return "";
|
|
5657
|
+
const stats = data.queryStats;
|
|
5658
|
+
const hasIssues = stats.slowCount > 0 || stats.n1Count > 0;
|
|
5659
|
+
const statsHtml = `
|
|
5660
|
+
<div class="dbg-query-stats">
|
|
5661
|
+
<div class="dbg-query-stat">
|
|
5662
|
+
<div class="dbg-query-stat-num">${stats.count}</div>
|
|
5663
|
+
<div class="dbg-query-stat-label">Queries</div>
|
|
5664
|
+
</div>
|
|
5665
|
+
<div class="dbg-query-stat">
|
|
5666
|
+
<div class="dbg-query-stat-num">${stats.totalDuration.toFixed(1)}ms</div>
|
|
5667
|
+
<div class="dbg-query-stat-label">Total Time</div>
|
|
5668
|
+
</div>
|
|
5669
|
+
<div class="dbg-query-stat">
|
|
5670
|
+
<div class="dbg-query-stat-num ${stats.slowCount > 0 ? "warning" : ""}">${stats.slowCount}</div>
|
|
5671
|
+
<div class="dbg-query-stat-label">Slow (>100ms)</div>
|
|
5672
|
+
</div>
|
|
5673
|
+
<div class="dbg-query-stat">
|
|
5674
|
+
<div class="dbg-query-stat-num ${stats.n1Count > 0 ? "error" : ""}">${stats.n1Count}</div>
|
|
5675
|
+
<div class="dbg-query-stat-label">N+1</div>
|
|
5676
|
+
</div>
|
|
5677
|
+
</div>`;
|
|
5678
|
+
const queries = data.queries.map((q) => {
|
|
5679
|
+
const isSlow = q.duration > 100;
|
|
5680
|
+
const classes = [
|
|
5681
|
+
"dbg-query",
|
|
5682
|
+
q.isN1 ? "n1" : "",
|
|
5683
|
+
isSlow ? "slow" : ""
|
|
5684
|
+
].filter(Boolean).join(" ");
|
|
5685
|
+
const badges = [];
|
|
5686
|
+
if (q.isN1) {
|
|
5687
|
+
badges.push('<span class="dbg-query-badge n1">N+1</span>');
|
|
5688
|
+
}
|
|
5689
|
+
const rowsText = q.rows !== undefined ? `<span class="dbg-query-rows">${q.rows} rows</span>` : "";
|
|
5690
|
+
const sourceText = q.source ? `<span class="dbg-query-source">${escapeHtml(q.source)}</span>` : "";
|
|
5691
|
+
return `
|
|
5692
|
+
<div class="${classes}">
|
|
5693
|
+
<div class="dbg-query-header">
|
|
5694
|
+
<span class="dbg-query-sql" title="${escapeHtml(q.sql)}">${escapeHtml(q.sql)}</span>
|
|
5695
|
+
<div class="dbg-query-meta">
|
|
5696
|
+
${badges.join("")}
|
|
5697
|
+
${sourceText}
|
|
5698
|
+
${rowsText}
|
|
5699
|
+
<span class="dbg-query-time ${isSlow ? "slow" : ""}">${q.duration.toFixed(1)}ms</span>
|
|
5700
|
+
</div>
|
|
5701
|
+
</div>
|
|
5702
|
+
</div>`;
|
|
5703
|
+
}).join("");
|
|
5704
|
+
const metaColor = hasIssues ? 'style="color:#ef4444"' : "";
|
|
5705
|
+
return `
|
|
5706
|
+
<div class="dbg-section ${hasIssues ? "open" : ""}">
|
|
5707
|
+
<div class="dbg-section-header" onclick="this.parentElement.classList.toggle('open')">
|
|
5708
|
+
<span class="dbg-section-title">${icons.database} Queries</span>
|
|
5709
|
+
<span class="dbg-section-meta" ${metaColor}>${stats.count} (${stats.totalDuration.toFixed(1)}ms)</span>
|
|
5710
|
+
<span class="dbg-chevron">${icons.chevron}</span>
|
|
5711
|
+
</div>
|
|
5712
|
+
<div class="dbg-section-content">
|
|
5713
|
+
${statsHtml}
|
|
5714
|
+
<div class="dbg-queries">${queries}</div>
|
|
5715
|
+
</div>
|
|
5716
|
+
</div>`;
|
|
5717
|
+
}
|
|
5598
5718
|
function generateScript(id) {
|
|
5599
5719
|
return `
|
|
5600
5720
|
(function(){
|