@zintrust/trace 0.4.81 → 0.4.83

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 CHANGED
@@ -29,6 +29,7 @@ You can still import the package migrations manually if you prefer to keep them
29
29
  ```env
30
30
  TRACE_ENABLED=true
31
31
  TRACE_DB_CONNECTION=d1 # optional — omit to inherit DB_CONNECTION
32
+ TRACE_QUERY_CONNECTION=main # optional — app DB to observe for SQL traces
32
33
  TRACE_PRUNE_HOURS=24 # how long entries are kept (default: 24)
33
34
  TRACE_SLOW_QUERY_MS=100 # slow-query threshold in ms (default: 100)
34
35
  TRACE_LOG_LEVEL=info # minimum log level captured (default: info)
@@ -77,6 +78,7 @@ import type { TraceConfigOverrides } from '@zintrust/trace';
77
78
  export default {
78
79
  enabled: Env.getBool('TRACE_ENABLED', false),
79
80
  connection: Env.get('TRACE_DB_CONNECTION', '') || undefined,
81
+ observeConnection: Env.get('TRACE_QUERY_CONNECTION', '') || undefined,
80
82
  pruneAfterHours: Env.getInt('TRACE_PRUNE_HOURS', 24),
81
83
  slowQueryThreshold: Env.getInt('TRACE_SLOW_QUERY_MS', 100),
82
84
  logMinLevel: Env.get('TRACE_LOG_LEVEL', 'info') as TraceConfigOverrides['logMinLevel'],
@@ -102,6 +104,10 @@ export default {
102
104
 
103
105
  All include/exclude matching is contains-based, so a term like `report` matches `/reports/daily`, `monthly-report`, or any other trace content containing that fragment.
104
106
 
107
+ When trace storage lives on a different connection than your application data, keep `TRACE_DB_CONNECTION` pointed at the trace tables and set `TRACE_QUERY_CONNECTION` to the app connection you want SQL traces to observe. If you omit `TRACE_QUERY_CONNECTION`, trace automatically falls back to the main `DB_CONNECTION` whenever the storage connection is different.
108
+
109
+ When ZinTrust proxy runtimes also have trace enabled, proxy-executed SQL and SMTP operations are traced at the proxy boundary too. That means MySQL, PostgreSQL, SQL Server, and D1 proxy requests can still record the final SQL plus bound values, and the SMTP proxy can persist rendered mail text and HTML, even when the consumer application is talking to the proxy rather than executing the transport locally.
110
+
105
111
  ### 3. Mount the dashboard
106
112
 
107
113
  Register the dashboard explicitly in your route file when you want the UI. Restrict access with middleware — the trace does **not** apply auth automatically.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.77",
4
- "buildDate": "2026-04-08T09:26:55.306Z",
3
+ "version": "0.4.83",
4
+ "buildDate": "2026-04-08T17:13:19.081Z",
5
5
  "buildEnvironment": {
6
6
  "node": "v22.22.1",
7
7
  "platform": "darwin",
8
8
  "arch": "arm64"
9
9
  },
10
10
  "git": {
11
- "commit": "f37a0c05",
11
+ "commit": "3c792064",
12
12
  "branch": "release"
13
13
  },
14
14
  "package": {
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "files": {
24
24
  "build-manifest.json": {
25
- "size": 14438,
26
- "sha256": "967934d372f36722143e02cd66b98dae1904122d34754fa8267bf3133725e1b2"
25
+ "size": 14439,
26
+ "sha256": "dab00c1ade5c0b5fe2b10b5eeeae31dfd9a017f198794e18e7c2b69cfb5560d8"
27
27
  },
28
28
  "cli-register.d.ts": {
29
29
  "size": 255,
@@ -38,8 +38,8 @@
38
38
  "sha256": "b034cbef0c71fb868071363624ef7a9f8d7acc20f8be8c895dd5db5a75e81f37"
39
39
  },
40
40
  "config.js": {
41
- "size": 5862,
42
- "sha256": "2692029ea79d7454f920fcdd6466cd6ee443a7029f36597a213585ed869f9af7"
41
+ "size": 5961,
42
+ "sha256": "1402cf8ad7c850da99e429c8c937385b7b5cf401fa3714d972d4e6bd94f1663e"
43
43
  },
44
44
  "context.d.ts": {
45
45
  "size": 596,
@@ -70,16 +70,16 @@
70
70
  "sha256": "4862b41e0477f01afa0dbb446d4553b65c22ed774cd1e2db3489059ced392f94"
71
71
  },
72
72
  "dashboard/ui.js": {
73
- "size": 64493,
74
- "sha256": "061c304bad22631db4ad795457c0eb5064f060f534d5050e2e14c6a1c01a2c1b"
73
+ "size": 70368,
74
+ "sha256": "b9f67e7977a36b459ac8d37312547b2638fcab7fc9b6c3d0c072d02510fca62f"
75
75
  },
76
76
  "index.d.ts": {
77
- "size": 2370,
78
- "sha256": "4cfee97f40d9dd12f5ea37996bee548a5964ee2cbe45c4e844bf5a119d9470d3"
77
+ "size": 2470,
78
+ "sha256": "99c28d43f79dbb2b372bf6a8b611841c131f59f5066702b499915b874e9fa2b8"
79
79
  },
80
80
  "index.js": {
81
- "size": 3237,
82
- "sha256": "d610de7775e31196a8acca02425c407e64bfdb69977290443664d36eaa022426"
81
+ "size": 3255,
82
+ "sha256": "a293d38a9e56a2ab802f99143eb1eaebb18f854e610f29bc95878746ffe26326"
83
83
  },
84
84
  "migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
85
85
  "size": 304,
@@ -134,8 +134,8 @@
134
134
  "sha256": "71d366165dd36f1675aa253a76262b226fb6c62e5ab632746b8aea61c0c625fc"
135
135
  },
136
136
  "register.js": {
137
- "size": 10601,
138
- "sha256": "4e25cf1a5206578c0c1ad77febc258215d97d0468facc51d01a9259c0634757d"
137
+ "size": 12522,
138
+ "sha256": "66bccec7fac4d669702fa18e439788325d36ce17f228639005ffcde2cd968d70"
139
139
  },
140
140
  "storage/DebuggerStorage.d.ts": {
141
141
  "size": 517,
@@ -186,8 +186,8 @@
186
186
  "sha256": "d916e8e3abb1b1087f6b184851b0e6265e53380d7857b008e745d566aad15d44"
187
187
  },
188
188
  "types.d.ts": {
189
- "size": 7687,
190
- "sha256": "c1c5f5140d157d66355c65c774f055af2dccc470c9ee9fe403bbb96fbf4447d0"
189
+ "size": 8448,
190
+ "sha256": "de994120c04696e08afb428cff27a99fb2918f5c4277b8190b8fb3d36d139f0d"
191
191
  },
192
192
  "types.js": {
193
193
  "size": 696,
@@ -266,12 +266,12 @@
266
266
  "sha256": "879a739de9ce2c3c5b57bdad73eae2ce3de94ccdc7e666ca52a50b45b0bc9bfd"
267
267
  },
268
268
  "watchers/CacheWatcher.d.ts": {
269
- "size": 265,
270
- "sha256": "cd30b7e8cd9ccd90a125073198dd2ad098706ab6e545f6152f94ccaeb616fa52"
269
+ "size": 314,
270
+ "sha256": "19b5f6fe4f0fc8f3df6762f4f46d36f198c7c7d7da8d3d23e5c8f124462e8cf7"
271
271
  },
272
272
  "watchers/CacheWatcher.js": {
273
- "size": 1518,
274
- "sha256": "1dae7cddf7940d7e6d478429d846390c9a862e30bd72bc68182b85f92b425dd0"
273
+ "size": 1956,
274
+ "sha256": "e1e1b79e85e7553d856ff0209d912e27d7b4fe665579de856c60e04bb7519aa1"
275
275
  },
276
276
  "watchers/CommandWatcher.d.ts": {
277
277
  "size": 267,
@@ -298,12 +298,12 @@
298
298
  "sha256": "ff32b44b48b6e313d15ba4370a845faf74ff8b03504a6336c19f183ff91a90dc"
299
299
  },
300
300
  "watchers/ExceptionWatcher.d.ts": {
301
- "size": 144,
302
- "sha256": "d1b5f5668a2fa3069b5b9f9c8852b66e1ef6489e5d1f47381bde582d6e406960"
301
+ "size": 311,
302
+ "sha256": "0f58c50fd77704151399ca6cb6ec7890a9aef86afe28235951971f8cc9c1d600"
303
303
  },
304
304
  "watchers/ExceptionWatcher.js": {
305
- "size": 3451,
306
- "sha256": "e4fe3d1df1c7d547b740bdf993503f43b80b6cc55aa151fe0cdc0f59b4442491"
305
+ "size": 3691,
306
+ "sha256": "d2ddd55f14730b0404cea53c17a6cfd4bd65ff3c17c8b068cc284de109da36d1"
307
307
  },
308
308
  "watchers/GateWatcher.d.ts": {
309
309
  "size": 262,
@@ -314,20 +314,20 @@
314
314
  "sha256": "f318cdeec954ce0bba97be1dc11a6dff935b081e6b6a417c614be1934fa47f04"
315
315
  },
316
316
  "watchers/HttpClientWatcher.d.ts": {
317
- "size": 283,
318
- "sha256": "1b48661bff79b2f72464c978ad7e6dcf011197ca4a3014e64e99b70437b3e5a1"
317
+ "size": 333,
318
+ "sha256": "08ab7e213c489ecc4fdd3166d7a121b9a5220ff9cbae9841d4787d9a804e11ce"
319
319
  },
320
320
  "watchers/HttpClientWatcher.js": {
321
- "size": 1627,
322
- "sha256": "65e910145f4d24f06643a847a1d75bc60034a629a5da61097ba9324d1d1c7ddd"
321
+ "size": 2414,
322
+ "sha256": "817c74e7a89bcd0c53c0344d713b422e0c9e51ec65e7b6c97f0c486116dda7a5"
323
323
  },
324
324
  "watchers/HttpWatcher.d.ts": {
325
325
  "size": 96,
326
326
  "sha256": "ce9a95a670f755193fd74ce721dbfa4b30f20c879a6566ebb35229b3b2435429"
327
327
  },
328
328
  "watchers/HttpWatcher.js": {
329
- "size": 5557,
330
- "sha256": "9f86966178485e2aab46ce03785c7c512feb641dbf5d0dda22d51f3f259d374e"
329
+ "size": 5916,
330
+ "sha256": "9b3fed08fd11f8a2bfe1f667293af437b00f11dcc66bfb545b2f77925394e611"
331
331
  },
332
332
  "watchers/JobWatcher.d.ts": {
333
333
  "size": 441,
@@ -342,16 +342,16 @@
342
342
  "sha256": "f3ddc5f8b58c6c86ac6b464dd48e5a55e79ab2bf2e735feacffc7480e4ccc0c4"
343
343
  },
344
344
  "watchers/LogWatcher.js": {
345
- "size": 1768,
346
- "sha256": "a7e769d504d5528068e9349f28d703606d21404d3976f7cdb131cee417420ff6"
345
+ "size": 2026,
346
+ "sha256": "c5d2227cd76ce10162993ac31f474b2460cd41264c36f01b5130152f14a0ad21"
347
347
  },
348
348
  "watchers/MailWatcher.d.ts": {
349
- "size": 214,
350
- "sha256": "aa4a677b61f231cc29c314d3091dfecf45cac30f164f6ac5e99f0f3d1e714154"
349
+ "size": 244,
350
+ "sha256": "5031b96ef8e64a6d376576e8cddf1c2560f22432a78f1d2be55f7cea6bff4547"
351
351
  },
352
352
  "watchers/MailWatcher.js": {
353
- "size": 1214,
354
- "sha256": "2bc8cb264822107cfc29d0341559f517186190492e594cb7ab1942b6ae28ea56"
353
+ "size": 1655,
354
+ "sha256": "0eb4f43c27a0c76cf290bb6507e71dc595bff3f8ab66c25249d9ac6027b351c5"
355
355
  },
356
356
  "watchers/MiddlewareWatcher.d.ts": {
357
357
  "size": 259,
@@ -370,20 +370,20 @@
370
370
  "sha256": "de3d1e379c7b1289167fe0b1dbf2aa5a54137b841df17c8d397ee656d9ce37fd"
371
371
  },
372
372
  "watchers/NotificationWatcher.d.ts": {
373
- "size": 237,
374
- "sha256": "02fdf20e32ab8c93c10ce5c93a88b2349e73a30e9eb391413b8a401899d1053e"
373
+ "size": 274,
374
+ "sha256": "a1d918122c5db9a7f27fdf78c0c14a61f6e1213748ee6f9b06f976f33589dc33"
375
375
  },
376
376
  "watchers/NotificationWatcher.js": {
377
- "size": 1242,
378
- "sha256": "e72cc2fcc622f44b907560ca664a3f88a388ac444c7a0c290c00e95899b123a3"
377
+ "size": 1697,
378
+ "sha256": "b0ecc1df6a49dc8c5ffeb8dff0f1e3594ba016c4de3424dc8b5d7832e2f4cd11"
379
379
  },
380
380
  "watchers/QueryWatcher.d.ts": {
381
- "size": 97,
382
- "sha256": "6832a282b1658398264ede770d41c6aa86cb13625a3a87dac27fbaf7d2f7be6a"
381
+ "size": 240,
382
+ "sha256": "5d5046c65e5b683369c7709f1acd09b60aec3e7f44748fd1baeb35498836465b"
383
383
  },
384
384
  "watchers/QueryWatcher.js": {
385
- "size": 2642,
386
- "sha256": "76a7fdd42a340141810f541ed89845c303f5498db7bdcfa29d8d576cf36c8efb"
385
+ "size": 2935,
386
+ "sha256": "577c6fec0282d2290db5c4b6c606b9b6ecdd64209af2b09f3205a15bf656bbef"
387
387
  },
388
388
  "watchers/RedisWatcher.d.ts": {
389
389
  "size": 294,
package/dist/config.js CHANGED
@@ -76,9 +76,12 @@ const mergeWatchers = (base, override) => {
76
76
  const DEFAULTS = Object.freeze({
77
77
  enabled: false,
78
78
  connection: undefined,
79
+ observeConnection: undefined,
79
80
  pruneAfterHours: 24,
80
81
  ignoreRoutes: ['/trace', '/health', '/ping'],
81
82
  slowQueryThreshold: 100,
83
+ captureCachePayloads: false,
84
+ captureQueryBindings: true,
82
85
  logMinLevel: 'info',
83
86
  watchers: {},
84
87
  redaction: {
@@ -41,7 +41,7 @@ const encodeSvgDataUri = (svg) => {
41
41
  const compactSvg = svg.replaceAll(/>\s+</g, '><').trim();
42
42
  return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(compactSvg)}`;
43
43
  };
44
- const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
44
+ const DASHBOARD_DOCUMENT = String.raw `<!DOCTYPE html>
45
45
  <html lang="en">
46
46
  <head>
47
47
  <meta charset="UTF-8">
@@ -74,7 +74,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
74
74
  .tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
75
75
  .monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
76
76
  .duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
77
- .code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
77
+ .code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
78
78
  @media (max-width:1120px){.content-grid{grid-template-columns:1fr}}@media (max-width:920px){.layout{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line);padding:20px 16px 18px}.brand-row{padding:0 0 16px}.sidebar-status{margin:0 0 16px}.sidebar-group{padding:0}.main{padding:20px}}@media (max-width:640px){.stats-grid{grid-template-columns:1fr}.detail-card{padding:18px}.toolbar,.section-head,.pagination,.activity-list,.monitoring-wrap{padding-left:18px;padding-right:18px}.table-wrap{padding:0 8px 10px}.brand-row{align-items:stretch;gap:14px;padding:0 0 14px}.brand{width:100%;align-items:flex-start}.brand-copy{min-width:0}.brand-copy h1{font-size:1.18rem;line-height:1.12}.brand-copy p{font-size:.82rem;overflow-wrap:anywhere}.icon-button{align-self:flex-end}.sidebar-status{padding:12px}.nav-button{padding:11px 12px}.nav-title{font-size:.95rem}.nav-meta{font-size:.72rem}}@media (max-width:480px){.brand-row{flex-direction:column}.icon-button{align-self:flex-start}.nav-button{align-items:flex-start;flex-direction:column}.nav-meta{font-size:.7rem}}
79
79
  </style>
80
80
  </head>
@@ -196,6 +196,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
196
196
  .replace(/"/g, '&quot;')
197
197
  .replace(/'/g, '&#39;');
198
198
 
199
+ const looksLikeHtml = (value) => new RegExp('</?(?:html|body|div|table)\\b|<!doctype\\b', 'i').test(String(value || ''));
200
+
199
201
  const api = async (path, opts) => {
200
202
  const response = await fetch(API + path, opts);
201
203
  if (!response.ok) {
@@ -335,6 +337,28 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
335
337
  ].join('');
336
338
  };
337
339
 
340
+ const renderTextCard = (label, value) => {
341
+ const source = String(value ?? '');
342
+ return renderCodeCard(label, source, escapeHtml(source), 'language-text');
343
+ };
344
+
345
+ const renderHtmlPreview = (label, html) => {
346
+ const source = String(html ?? '');
347
+ const copyId = registerCopyPayload(source);
348
+ return [
349
+ '<section class="code-card">',
350
+ '<div class="code-toolbar">',
351
+ '<span class="code-label">' + escapeHtml(label) + '</span>',
352
+ '<button type="button" class="copy-button" data-action="copy-code" data-copy-id="' + escapeHtml(copyId) + '" title="Copy ' + escapeHtml(label) + '">',
353
+ COPY_ICON,
354
+ '</button>',
355
+ '</div>',
356
+ '<pre class="code-block language-html"><code>' + escapeHtml(source) + '</code></pre>',
357
+ '<div class="html-preview-wrap"><iframe class="html-preview" sandbox="allow-same-origin" srcdoc="' + escapeHtml(source) + '"></iframe></div>',
358
+ '</section>'
359
+ ].join('');
360
+ };
361
+
338
362
  const highlightJson = (value, label = 'JSON') => {
339
363
  const source = prettyJson(value);
340
364
  let output = '';
@@ -386,6 +410,14 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
386
410
 
387
411
  const detailJson = (value, label = 'JSON') => highlightJson(value ?? {}, label);
388
412
 
413
+ const renderPayload = (label, value) => {
414
+ if (value === undefined) return '<p class="trace-note">No ' + escapeHtml(label.toLowerCase()) + ' was captured.</p>';
415
+ if (typeof value === 'string') {
416
+ return looksLikeHtml(value) ? renderHtmlPreview(label, value) : renderTextCard(label, value);
417
+ }
418
+ return detailJson(value, label);
419
+ };
420
+
389
421
  const entrySummaryText = (entry) => {
390
422
  const content = entry && entry.content ? entry.content : {};
391
423
  if (entry.type === 'request') return [content.responseStatus || '', content.method || '', content.uri || ''].filter(Boolean).join(' ');
@@ -393,20 +425,20 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
393
425
  if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
394
426
  if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
395
427
  if (entry.type === 'job') return [content.name || '', content.status || 'queued'].filter(Boolean).join(' · ');
396
- if (entry.type === 'cache') return [content.operation || '', content.key || ''].filter(Boolean).join(' ');
428
+ if (entry.type === 'cache') return [content.operation || '', content.key || '', content.payloadLogged ? '' : '(payload off)'].filter(Boolean).join(' ');
397
429
  if (entry.type === 'schedule') return [content.name || '', content.status || 'ran'].filter(Boolean).join(' · ');
398
430
  if (entry.type === 'mail') return ['To ' + (content.to || 'unknown'), content.subject || 'No subject'].join(' · ');
399
431
  if (entry.type === 'auth') return [content.event || 'auth', content.userId ? '#' + content.userId : ''].filter(Boolean).join(' ');
400
432
  if (entry.type === 'event') return String(content.name || 'event');
401
433
  if (entry.type === 'model') return [content.action || '', content.model || ''].filter(Boolean).join(' ');
402
- if (entry.type === 'notification') return [content.notification || '', (content.channels || []).join(', ')].filter(Boolean).join(' -> ');
434
+ if (entry.type === 'notification') return [content.notification || '', content.message || (content.channels || []).join(', ')].filter(Boolean).join(' -> ');
403
435
  if (entry.type === 'redis') return String(content.command || 'redis');
404
436
  if (entry.type === 'gate') return [content.ability || '', content.result || ''].filter(Boolean).join(' · ');
405
437
  if (entry.type === 'middleware') return [content.name || '', content.event || ''].filter(Boolean).join(' · ');
406
438
  if (entry.type === 'command') return [content.name || '', content.exitCode !== undefined ? 'exit=' + content.exitCode : ''].filter(Boolean).join(' ');
407
439
  if (entry.type === 'batch') return [content.name || '', 'processed ' + (content.processed || 0) + '/' + (content.total || 0)].join(' · ');
408
440
  if (entry.type === 'view') return String(content.template || 'view');
409
- if (entry.type === 'client_request') return [content.method || '', content.url || ''].filter(Boolean).join(' ');
441
+ if (entry.type === 'client_request') return [content.method || '', content.url || '', content.responseStatus ? '[' + content.responseStatus + ']' : content.error ? '[failed]' : ''].filter(Boolean).join(' ');
410
442
  return JSON.stringify(content).slice(0, 160);
411
443
  };
412
444
 
@@ -442,6 +474,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
442
474
  { label: 'Connection', value: escapeHtml(content.connection || 'default') },
443
475
  { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) },
444
476
  { label: 'Slow', value: escapeHtml(content.slow ? 'Yes' : 'No') },
477
+ { label: 'Bindings', value: escapeHtml(content.bindingsIncluded === false ? 'Hidden' : 'Included') },
445
478
  { label: 'Hash', value: '<span class="mono">' + escapeHtml(content.hash || '') + '</span>' }
446
479
  ]),
447
480
  renderMetricBox('Runtime', [
@@ -449,7 +482,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
449
482
  { label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
450
483
  ]),
451
484
  '</div>',
452
- highlightSql(content.sql || '')
485
+ highlightSql(content.sql || ''),
486
+ content.bindingsIncluded === false ? '<p class="trace-note">SQL bindings were hidden for this entry.</p>' : (Array.isArray(content.bindings) ? detailJson(content.bindings, 'Bindings Json') : '')
453
487
  ].join('');
454
488
  }
455
489
 
@@ -493,7 +527,34 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
493
527
  renderMetricBox('Request', [
494
528
  { label: 'Method', value: escapeHtml(content.method || '') },
495
529
  { label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
496
- { label: 'Status', value: escapeHtml(content.responseStatus || '') },
530
+ { label: 'Status', value: escapeHtml(content.responseStatus || (content.error ? 'Failed' : 'Pending')) },
531
+ { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
532
+ ]),
533
+ renderMetricBox('Runtime', [
534
+ { label: 'Hostname', value: escapeHtml(content.hostname || '') },
535
+ { label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' },
536
+ { label: 'Error', value: escapeHtml(content.error || '-') }
537
+ ]),
538
+ '</div>',
539
+ '<div class="detail-stack">',
540
+ detailJson(content.requestHeaders || {}, 'Request Header Json'),
541
+ renderPayload('Request Body', content.requestBody),
542
+ detailJson(content.responseHeaders || {}, 'Response Header Json'),
543
+ renderPayload('Response Body', content.responseBody),
544
+ '</div>'
545
+ ].join('');
546
+ }
547
+
548
+ if (entry.type === 'cache') {
549
+ return [
550
+ '<div class="detail-grid">',
551
+ renderMetricBox('Cache', [
552
+ { label: 'Operation', value: escapeHtml(content.operation || '') },
553
+ { label: 'Key', value: '<span class="mono">' + escapeHtml(content.key || '') + '</span>' },
554
+ { label: 'Store', value: escapeHtml(content.store || 'default') },
555
+ { label: 'Hit', value: escapeHtml(content.hit === undefined ? '-' : (content.hit ? 'Yes' : 'No')) },
556
+ { label: 'Payload', value: escapeHtml(content.payloadLogged ? 'Captured' : 'Disabled') },
557
+ { label: 'TTL', value: escapeHtml(content.ttl === undefined ? '-' : String(content.ttl)) },
497
558
  { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
498
559
  ]),
499
560
  renderMetricBox('Runtime', [
@@ -501,7 +562,41 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
501
562
  { label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
502
563
  ]),
503
564
  '</div>',
504
- detailJson(content.requestHeaders || {})
565
+ content.payloadLogged ? renderPayload('Cache Payload', content.payload) : '<p class="trace-note">Cache payload logging is disabled. Set TRACE_CACHE_PAYLOADS=true to include values.</p>'
566
+ ].join('');
567
+ }
568
+
569
+ if (entry.type === 'mail') {
570
+ return [
571
+ '<div class="detail-grid">',
572
+ renderMetricBox('Mail', [
573
+ { label: 'To', value: escapeHtml(content.to || '') },
574
+ { label: 'Subject', value: escapeHtml(content.subject || '') },
575
+ { label: 'Template', value: escapeHtml(content.template || '-') },
576
+ { label: 'Hostname', value: escapeHtml(content.hostname || '') }
577
+ ]),
578
+ '</div>',
579
+ '<div class="detail-stack">',
580
+ renderPayload('Mail Text', content.text),
581
+ renderPayload('Mail Html', content.html),
582
+ '</div>'
583
+ ].join('');
584
+ }
585
+
586
+ if (entry.type === 'notification') {
587
+ return [
588
+ '<div class="detail-grid">',
589
+ renderMetricBox('Notification', [
590
+ { label: 'Notification', value: escapeHtml(content.notification || '') },
591
+ { label: 'Channels', value: escapeHtml((content.channels || []).join(', ') || '-') },
592
+ { label: 'Recipient', value: escapeHtml(content.notifiable || '-') },
593
+ { label: 'Hostname', value: escapeHtml(content.hostname || '') }
594
+ ]),
595
+ '</div>',
596
+ '<div class="detail-stack">',
597
+ renderPayload('Message', content.message),
598
+ content.payload === undefined ? '<p class="trace-note">No additional notification payload was captured.</p>' : detailJson(content.payload, 'Notification Payload Json'),
599
+ '</div>'
505
600
  ].join('');
506
601
  }
507
602
 
package/dist/register.js CHANGED
@@ -64,6 +64,17 @@ const resolveTraceConnectionName = (env, configuredConnection) => {
64
64
  }
65
65
  return resolveDefaultConnection();
66
66
  };
67
+ const resolveObservedConnectionName = (env, configuredObservedConnection, storageConnectionName) => {
68
+ if (typeof configuredObservedConnection === 'string' &&
69
+ configuredObservedConnection.trim() !== '') {
70
+ return resolveTraceConnectionName(env, configuredObservedConnection);
71
+ }
72
+ const defaultConnectionName = resolveTraceConnectionName(env, undefined);
73
+ if (storageConnectionName !== defaultConnectionName) {
74
+ return defaultConnectionName;
75
+ }
76
+ return storageConnectionName;
77
+ };
67
78
  const isObjectValue = (value) => {
68
79
  return typeof value === 'object' && value !== null && !Array.isArray(value);
69
80
  };
@@ -90,6 +101,16 @@ const parseEnvList = (rawValue) => {
90
101
  .map((entry) => entry.trim())
91
102
  .filter((entry) => entry !== '');
92
103
  };
104
+ const parseEnvBool = (rawValue) => {
105
+ const value = rawValue.trim().toLowerCase();
106
+ if (value === '')
107
+ return undefined;
108
+ if (['1', 'true', 'yes', 'on'].includes(value))
109
+ return true;
110
+ if (['0', 'false', 'no', 'off'].includes(value))
111
+ return false;
112
+ return undefined;
113
+ };
93
114
  const resolveTraceStartupOverrides = (core) => {
94
115
  const traceConfigFile = core.StartupConfigFile?.Trace;
95
116
  if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '')
@@ -136,14 +157,18 @@ if (!traceAlreadyInitialized && Env) {
136
157
  const enabled = startupOverrides?.enabled === true || Env.getBool('TRACE_ENABLED', false);
137
158
  if (enabled) {
138
159
  const connectionRaw = Env.get('TRACE_DB_CONNECTION', '').trim();
160
+ const observeConnectionRaw = Env.get('TRACE_QUERY_CONNECTION', '').trim();
139
161
  const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
140
162
  const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
141
163
  const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
164
+ const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
165
+ const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
142
166
  const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
143
167
  const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
144
168
  const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
145
169
  const redactionQuery = parseEnvList(Env.get('TRACE_REDACT_QUERY', ''));
146
170
  const connection = connectionRaw === '' ? startupOverrides?.connection : connectionRaw;
171
+ const observeConnection = observeConnectionRaw === '' ? startupOverrides?.observeConnection : observeConnectionRaw;
147
172
  const pruneAfterHours = pruneAfterHoursRaw === ''
148
173
  ? startupOverrides?.pruneAfterHours
149
174
  : Number.parseInt(pruneAfterHoursRaw, 10);
@@ -151,6 +176,8 @@ if (!traceAlreadyInitialized && Env) {
151
176
  ? startupOverrides?.slowQueryThreshold
152
177
  : Number.parseInt(slowQueryThresholdRaw, 10);
153
178
  const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw);
179
+ const captureCachePayloads = parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
180
+ const captureQueryBindings = parseEnvBool(captureQueryBindingsRaw) ?? startupOverrides?.captureQueryBindings;
154
181
  const redaction = buildTraceRedactionOverrides({
155
182
  startupOverrides,
156
183
  redactionBody,
@@ -162,19 +189,24 @@ if (!traceAlreadyInitialized && Env) {
162
189
  ...startupOverrides,
163
190
  enabled,
164
191
  connection,
192
+ observeConnection,
165
193
  ...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
166
194
  ? { pruneAfterHours }
167
195
  : {}),
168
196
  ...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
169
197
  ? { slowQueryThreshold }
170
198
  : {}),
199
+ ...(typeof captureCachePayloads === 'boolean' ? { captureCachePayloads } : {}),
200
+ ...(typeof captureQueryBindings === 'boolean' ? { captureQueryBindings } : {}),
171
201
  logMinLevel,
172
202
  ...(redaction === undefined ? {} : { redaction }),
173
203
  });
174
204
  const resolvedConnectionName = resolveTraceConnectionName(Env, config.connection);
175
- const db = core.useDatabase?.(undefined, resolvedConnectionName);
176
- if (db) {
177
- const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(db), config), config.redaction), {
205
+ const resolvedObservedConnectionName = resolveObservedConnectionName(Env, config.observeConnection, resolvedConnectionName);
206
+ const storageDb = core.useDatabase?.(undefined, resolvedConnectionName);
207
+ const observedDb = core.useDatabase?.(undefined, resolvedObservedConnectionName);
208
+ if (storageDb && observedDb) {
209
+ const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config), config.redaction), {
178
210
  connectionName: resolvedConnectionName,
179
211
  logger: core.Logger,
180
212
  });
@@ -203,7 +235,7 @@ if (!traceAlreadyInitialized && Env) {
203
235
  import('./watchers/ViewWatcher.js'),
204
236
  import('./watchers/HttpClientWatcher.js'),
205
237
  ]);
206
- const watcherArgs = { storage, config, db };
238
+ const watcherArgs = { storage, config, db: observedDb };
207
239
  HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
208
240
  QueryWatcher.register(watcherArgs);
209
241
  LogWatcher.register(watcherArgs);
package/dist/types.d.ts CHANGED
@@ -43,6 +43,9 @@ export interface RequestContent {
43
43
  export interface QueryContent {
44
44
  connection: string;
45
45
  sql: string;
46
+ statement?: string;
47
+ bindings?: unknown[];
48
+ bindingsIncluded?: boolean;
46
49
  time: number;
47
50
  duration: number;
48
51
  slow: boolean;
@@ -91,6 +94,10 @@ export interface CacheContent {
91
94
  operation: 'get' | 'set' | 'delete' | 'clear' | 'has';
92
95
  key: string;
93
96
  hit?: boolean;
97
+ store?: string;
98
+ payload?: unknown;
99
+ payloadLogged?: boolean;
100
+ ttl?: number;
94
101
  duration: number;
95
102
  hostname: string;
96
103
  }
@@ -106,6 +113,8 @@ export interface MailContent {
106
113
  to: string;
107
114
  subject: string;
108
115
  template?: string;
116
+ text?: string;
117
+ html?: string;
109
118
  hostname: string;
110
119
  }
111
120
  export interface AuthContent {
@@ -130,6 +139,8 @@ export interface NotificationContent {
130
139
  channels: string[];
131
140
  notifiable?: string;
132
141
  notification: string;
142
+ message?: string;
143
+ payload?: unknown;
133
144
  hostname: string;
134
145
  }
135
146
  export interface RedisContent {
@@ -181,10 +192,25 @@ export interface ClientRequestContent {
181
192
  method: string;
182
193
  url: string;
183
194
  requestHeaders: Record<string, string>;
184
- responseStatus: number;
195
+ requestBody?: unknown;
196
+ responseStatus?: number;
197
+ responseHeaders?: Record<string, string>;
198
+ responseBody?: unknown;
199
+ error?: string;
185
200
  duration: number;
186
201
  hostname: string;
187
202
  }
203
+ export interface ClientRequestTraceInput {
204
+ method: string;
205
+ url: string;
206
+ requestHeaders: Record<string, string>;
207
+ responseStatus?: number;
208
+ duration: number;
209
+ requestBody?: unknown;
210
+ responseHeaders?: Record<string, string>;
211
+ responseBody?: unknown;
212
+ error?: string;
213
+ }
188
214
  export interface ITraceEntry<T = unknown> {
189
215
  uuid: string;
190
216
  batchId: string;
@@ -277,9 +303,12 @@ export type WatcherToggles = {
277
303
  export interface ITraceConfig {
278
304
  enabled: boolean;
279
305
  connection?: string;
306
+ observeConnection?: string;
280
307
  pruneAfterHours: number;
281
308
  ignoreRoutes: string[];
282
309
  slowQueryThreshold: number;
310
+ captureCachePayloads: boolean;
311
+ captureQueryBindings: boolean;
283
312
  logMinLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
284
313
  watchers: WatcherToggles;
285
314
  redaction: RedactionConfig;
@@ -1,5 +1,5 @@
1
1
  import type { CacheContent, ITraceWatcher } from '../types';
2
- declare const emit: (operation: CacheContent["operation"], key: string, duration: number, hit?: boolean) => void;
2
+ declare const emit: (operation: CacheContent["operation"], key: string, duration: number, hit?: boolean, payload?: unknown, store?: string, ttl?: number) => void;
3
3
  export declare const CacheWatcher: ITraceWatcher & {
4
4
  emit: typeof emit;
5
5
  };