@zintrust/trace 0.4.76 → 0.4.77

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.
Files changed (45) hide show
  1. package/README.md +101 -15
  2. package/dist/build-manifest.json +78 -38
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +123 -4
  5. package/dist/dashboard/ui.js +80 -23
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +5 -0
  8. package/dist/migrations/20260331000001_create_zin_trace_entries_table.js +1 -1
  9. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
  10. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
  11. package/dist/migrations/index.js +2 -1
  12. package/dist/register.js +107 -9
  13. package/dist/storage/TraceContentRedaction.d.ts +4 -0
  14. package/dist/storage/TraceContentRedaction.js +33 -0
  15. package/dist/storage/TraceEntryFiltering.d.ts +4 -0
  16. package/dist/storage/TraceEntryFiltering.js +13 -0
  17. package/dist/storage/TraceStorage.js +35 -5
  18. package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
  19. package/dist/storage/TraceWriteDiagnostics.js +98 -0
  20. package/dist/types.d.ts +37 -20
  21. package/dist/utils/entryFilter.d.ts +4 -0
  22. package/dist/utils/entryFilter.js +95 -0
  23. package/dist/utils/redact.d.ts +1 -0
  24. package/dist/utils/redact.js +43 -9
  25. package/dist/watchers/CommandWatcher.js +1 -1
  26. package/dist/watchers/HttpClientWatcher.js +1 -1
  27. package/dist/watchers/HttpWatcher.js +104 -20
  28. package/dist/watchers/LogWatcher.js +1 -0
  29. package/package.json +3 -3
  30. package/src/config.ts +152 -5
  31. package/src/dashboard/routes.ts +6 -2
  32. package/src/dashboard/ui.ts +80 -23
  33. package/src/index.ts +7 -0
  34. package/src/register.ts +137 -10
  35. package/src/storage/TraceContentRedaction.ts +44 -0
  36. package/src/storage/TraceEntryFiltering.ts +14 -0
  37. package/src/storage/TraceStorage.ts +52 -5
  38. package/src/storage/TraceWriteDiagnostics.ts +174 -0
  39. package/src/types.ts +40 -20
  40. package/src/utils/entryFilter.ts +108 -0
  41. package/src/utils/redact.ts +57 -9
  42. package/src/watchers/CommandWatcher.ts +1 -1
  43. package/src/watchers/HttpClientWatcher.ts +1 -1
  44. package/src/watchers/HttpWatcher.ts +132 -21
  45. package/src/watchers/LogWatcher.ts +27 -27
package/src/config.ts CHANGED
@@ -1,7 +1,107 @@
1
1
  /**
2
2
  * TraceConfig — defaults and merge helper for @zintrust/trace
3
3
  */
4
- import type { ITraceConfig, TraceConfigOverrides } from './types';
4
+ import type {
5
+ ITraceConfig,
6
+ TraceConfigOverrides,
7
+ TraceFilterRule,
8
+ TraceRequestWatcherConfig,
9
+ TraceWatcherToggle,
10
+ } from './types';
11
+
12
+ const mergeStringLists = (base: string[], override?: string[]): string[] => {
13
+ const merged = new Set<string>();
14
+
15
+ for (const value of [...base, ...(override ?? [])]) {
16
+ if (typeof value !== 'string') continue;
17
+ const normalized = value.trim();
18
+ if (normalized !== '') merged.add(normalized);
19
+ }
20
+
21
+ return [...merged];
22
+ };
23
+
24
+ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ };
27
+
28
+ const mergeFilterRule = (
29
+ base?: TraceFilterRule,
30
+ override?: TraceFilterRule
31
+ ): TraceFilterRule | undefined => {
32
+ const include = mergeStringLists(base?.include ?? [], override?.include);
33
+ const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
34
+
35
+ if (include.length === 0 && exclude.length === 0) return undefined;
36
+
37
+ return Object.freeze({
38
+ ...(include.length > 0 ? { include } : {}),
39
+ ...(exclude.length > 0 ? { exclude } : {}),
40
+ });
41
+ };
42
+
43
+ const mergeWatcherToggle = (
44
+ base?: TraceWatcherToggle,
45
+ override?: TraceWatcherToggle
46
+ ): TraceWatcherToggle | undefined => {
47
+ if (override === undefined) return base;
48
+ if (override === false || override === true) return override;
49
+
50
+ const baseRule = isObjectValue(base) ? base : undefined;
51
+ return mergeFilterRule(baseRule, override);
52
+ };
53
+
54
+ const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'] as const;
55
+
56
+ const mergeRequestWatcherToggle = (
57
+ base?: ITraceConfig['watchers']['request'],
58
+ override?: ITraceConfig['watchers']['request']
59
+ ): ITraceConfig['watchers']['request'] | undefined => {
60
+ if (override === undefined) return base;
61
+ if (override === false || override === true) return override;
62
+
63
+ const baseConfig = isObjectValue(base) ? base : undefined;
64
+ const merged: TraceRequestWatcherConfig = mergeFilterRule(baseConfig, override) ?? {};
65
+
66
+ for (const key of REQUEST_METHOD_KEYS) {
67
+ const rule = mergeFilterRule(baseConfig?.[key], override[key]);
68
+ if (rule !== undefined) merged[key] = rule;
69
+ }
70
+
71
+ return merged;
72
+ };
73
+
74
+ const mergeWatchers = (
75
+ base: ITraceConfig['watchers'],
76
+ override?: TraceConfigOverrides['watchers']
77
+ ): ITraceConfig['watchers'] => {
78
+ if (override === undefined) return { ...base };
79
+
80
+ return {
81
+ ...base,
82
+ ...override,
83
+ request: mergeRequestWatcherToggle(base.request, override.request),
84
+ query: mergeWatcherToggle(base.query, override.query),
85
+ exception: mergeWatcherToggle(base.exception, override.exception),
86
+ log: mergeWatcherToggle(base.log, override.log),
87
+ job: mergeWatcherToggle(base.job, override.job),
88
+ cache: mergeWatcherToggle(base.cache, override.cache),
89
+ schedule: mergeWatcherToggle(base.schedule, override.schedule),
90
+ mail: mergeWatcherToggle(base.mail, override.mail),
91
+ auth: mergeWatcherToggle(base.auth, override.auth),
92
+ event: mergeWatcherToggle(base.event, override.event),
93
+ model: mergeWatcherToggle(base.model, override.model),
94
+ notification: mergeWatcherToggle(base.notification, override.notification),
95
+ redis: mergeWatcherToggle(base.redis, override.redis),
96
+ gate: mergeWatcherToggle(base.gate, override.gate),
97
+ middleware: mergeWatcherToggle(base.middleware, override.middleware),
98
+ command: mergeWatcherToggle(base.command, override.command),
99
+ batch: mergeWatcherToggle(base.batch, override.batch),
100
+ dump: mergeWatcherToggle(base.dump, override.dump),
101
+ view: mergeWatcherToggle(base.view, override.view),
102
+ clientRequest: mergeWatcherToggle(base.clientRequest, override.clientRequest),
103
+ };
104
+ };
5
105
 
6
106
  const DEFAULTS: ITraceConfig = Object.freeze({
7
107
  enabled: false,
@@ -12,6 +112,37 @@ const DEFAULTS: ITraceConfig = Object.freeze({
12
112
  logMinLevel: 'info',
13
113
  watchers: {},
14
114
  redaction: {
115
+ keys: [
116
+ 'password',
117
+ 'pass',
118
+ 'passwd',
119
+ 'token',
120
+ 'accessToken',
121
+ 'access_token',
122
+ 'refreshToken',
123
+ 'refresh_token',
124
+ 'secret',
125
+ 'secretKey',
126
+ 'secret_key',
127
+ 'apiKey',
128
+ 'api_key',
129
+ 'auth',
130
+ 'authToken',
131
+ 'auth_token',
132
+ 'authorization',
133
+ 'cookie',
134
+ 'session',
135
+ 'sessionId',
136
+ 'session_id',
137
+ 'card',
138
+ 'cardNumber',
139
+ 'card_number',
140
+ 'cardToken',
141
+ 'card_token',
142
+ 'cvv',
143
+ 'cvc',
144
+ 'pan',
145
+ ],
15
146
  headers: ['authorization', 'cookie', 'x-api-key', 'x-auth-token'],
16
147
  body: ['password', 'token', 'secret', 'apiKey', 'api_key', 'jwt', 'bearer'],
17
148
  query: [],
@@ -20,7 +151,20 @@ const DEFAULTS: ITraceConfig = Object.freeze({
20
151
 
21
152
  const isWatcherEnabled = (config: ITraceConfig, key: keyof ITraceConfig['watchers']): boolean => {
22
153
  const override = config.watchers[key];
23
- return override !== false; // undefined = enabled by default; explicit false = disabled
154
+ if (override === false) return false;
155
+ if (isObjectValue(override) && override.enabled === false) return false;
156
+ return true; // undefined = enabled by default; explicit false = disabled
157
+ };
158
+
159
+ const getRedactionFields = (
160
+ config: ITraceConfig,
161
+ key: keyof ITraceConfig['redaction']
162
+ ): string[] => {
163
+ if (key === 'keys') {
164
+ return mergeStringLists([], config.redaction.keys);
165
+ }
166
+
167
+ return mergeStringLists(config.redaction.keys, config.redaction[key]);
24
168
  };
25
169
 
26
170
  export const TraceConfig = Object.freeze({
@@ -33,14 +177,17 @@ export const TraceConfig = Object.freeze({
33
177
  return Object.freeze({
34
178
  ...DEFAULTS,
35
179
  ...overrides,
36
- watchers: { ...DEFAULTS.watchers, ...(overrides.watchers ?? {}) },
180
+ watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
37
181
  redaction: {
38
- ...DEFAULTS.redaction,
39
- ...(overrides.redaction ?? {}),
182
+ keys: mergeStringLists(DEFAULTS.redaction.keys, overrides.redaction?.keys),
183
+ headers: mergeStringLists(DEFAULTS.redaction.headers, overrides.redaction?.headers),
184
+ body: mergeStringLists(DEFAULTS.redaction.body, overrides.redaction?.body),
185
+ query: mergeStringLists(DEFAULTS.redaction.query, overrides.redaction?.query),
40
186
  },
41
187
  ignoreRoutes: overrides.ignoreRoutes ?? DEFAULTS.ignoreRoutes,
42
188
  });
43
189
  },
44
190
 
191
+ getRedactionFields,
45
192
  isWatcherEnabled,
46
193
  });
@@ -20,6 +20,10 @@ import {
20
20
  } from './handlers';
21
21
  import { buildDashboardHtml } from './ui';
22
22
 
23
+ type HtmlResponse = {
24
+ html(body: string): void;
25
+ };
26
+
23
27
  export type TraceDashboardOptions = {
24
28
  /** Base path for the dashboard, e.g. '/trace'. Defaults to '/trace'. */
25
29
  basePath?: string;
@@ -61,7 +65,7 @@ export const registerTraceRoutes = (
61
65
  Router.get(
62
66
  router,
63
67
  base,
64
- (_req, res) => {
68
+ (_req: unknown, res: HtmlResponse) => {
65
69
  res.html(buildDashboardHtml(base, appConfig.name));
66
70
  },
67
71
  routeOptions
@@ -70,7 +74,7 @@ export const registerTraceRoutes = (
70
74
  Router.get(
71
75
  router,
72
76
  `${base}/*`,
73
- (_req, res) => {
77
+ (_req: unknown, res: HtmlResponse) => {
74
78
  res.html(buildDashboardHtml(base, appConfig.name));
75
79
  },
76
80
  routeOptions
@@ -77,7 +77,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
77
77
  .section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:22px 24px 16px}.section-head h3{margin:0;font-size:1.04rem}.section-head p{margin:6px 0 0;color:var(--muted);font-size:.92rem}.toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 18px}.control,.toolbar input,.toolbar select{height:44px;border-radius:13px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);padding:0 14px;min-width:0}.toolbar input,.toolbar select{flex:1 1 180px}.toolbar input::placeholder{color:var(--muted)}.btn{height:44px;border:none;border-radius:13px;padding:0 16px;cursor:pointer;font-weight:800}.btn-primary{background:linear-gradient(135deg,var(--accent-strong),var(--accent));color:#fff}.btn-danger{background:rgba(239,68,68,.12);color:var(--danger);border:1px solid rgba(239,68,68,.18)}.btn-ghost{background:var(--surface-soft);color:var(--text);border:1px solid var(--line)}
78
78
  .table-wrap{overflow:auto;padding:0 12px 12px}table{width:100%;border-collapse:separate;border-spacing:0;min-width:880px}th{padding:14px;color:var(--muted);font-size:.74rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase;text-align:left;border-bottom:1px solid var(--line)}td{padding:15px 14px;border-bottom:1px solid var(--line);vertical-align:top}.row-button{cursor:pointer}.row-button:hover td{background:rgba(56,189,248,.05)}.summary{font-size:.93rem;font-weight:700;line-height:1.4;color:var(--text)}.summary-sub{margin-top:6px;color:var(--muted);font-size:.82rem;line-height:1.4}.mono{font-family:var(--mono)}.empty{padding:44px 24px;color:var(--muted);line-height:1.65;text-align:center}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 24px 24px;color:var(--muted);flex-wrap:wrap}.pagination-controls{display:flex;gap:8px}.pagination button{height:40px;min-width:92px;padding:0 14px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:not-allowed}
79
79
  .activity-list{list-style:none;margin:0;padding:0 24px 24px}.activity-item{padding:14px 0;border-top:1px solid var(--line)}.activity-item:first-child{border-top:none}.activity-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.activity-time{color:var(--muted);font-size:.85rem}.activity-summary{margin-top:8px;color:var(--text);line-height:1.48}.back-link{display:inline-flex;align-items:center;gap:8px;margin:0 0 14px;color:var(--accent);font-weight:800;cursor:pointer}.detail-card{padding:24px}.detail-meta{display:flex;flex-wrap:wrap;gap:10px;margin:14px 0 20px;color:var(--muted);font-size:.9rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.detail-stack{display:grid;gap:16px;margin-top:18px}.detail-box{padding:16px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.detail-box h4{margin:0 0 10px;font-size:.92rem}.detail-box dl{margin:0;display:grid;gap:8px}.detail-box dt{font-size:.76rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.detail-box dd{margin:0;color:var(--text);line-height:1.45}.trace-tabs{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0 16px}.trace-tab{border:none;border-radius:12px;padding:10px 12px;background:transparent;color:var(--muted);cursor:pointer;box-shadow:inset 0 0 0 1px var(--line);font-weight:800}.trace-tab.active{background:rgba(56,189,248,.12);color:var(--text);box-shadow:inset 0 0 0 1px rgba(56,189,248,.28)}.trace-panel{display:grid;gap:14px}.trace-item{padding:18px;border-radius:16px;background:var(--surface-soft);border:1px solid var(--line)}.trace-item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.trace-item-summary{margin-top:10px;display:grid;gap:10px}.trace-note{color:var(--muted);line-height:1.6}
80
- .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)}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-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-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}
80
+ .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}
81
81
  .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}
82
82
  .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}
83
83
  .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}
@@ -146,15 +146,27 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
146
146
  monitoring: { title: 'Monitoring tags', subtitle: 'Pinned tags for trace pivots.' }
147
147
  };
148
148
 
149
- let state = {
150
- page: 'overview',
151
- entriesPage: 1,
152
- entriesFilter: { type: '', tag: '', batchId: '' },
153
- detail: null,
154
- detailBatch: null,
155
- detailTab: 'summary'
149
+ const createInitialState = () => {
150
+ const search = new URLSearchParams(window.location.search);
151
+ const page = search.get('page');
152
+ const entriesPage = Number.parseInt(search.get('entriesPage') || '1', 10);
153
+
154
+ return {
155
+ page: page && Object.prototype.hasOwnProperty.call(PAGE_COPY, page) ? page : 'overview',
156
+ entriesPage: Number.isFinite(entriesPage) && entriesPage > 0 ? entriesPage : 1,
157
+ entriesFilter: {
158
+ type: search.get('type') || '',
159
+ tag: search.get('tag') || '',
160
+ batchId: search.get('batchId') || ''
161
+ },
162
+ detail: null,
163
+ detailBatch: null,
164
+ detailTab: 'summary'
165
+ };
156
166
  };
157
167
 
168
+ let state = createInitialState();
169
+
158
170
  let copySequence = 0;
159
171
  const copyPayloads = new Map();
160
172
 
@@ -199,7 +211,19 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
199
211
  return response.json();
200
212
  };
201
213
 
202
- const typeClass = (type) => 'type-pill pill-' + String(type || '').replace(/_/g, '-');
214
+ const requestMethodClass = (entry) => {
215
+ if (!entry || entry.type !== 'request') return '';
216
+ const method = String(entry.content && entry.content.method || '').toUpperCase();
217
+ if (method === 'GET') return ' method-get';
218
+ if (method === 'POST') return ' method-post';
219
+ return ' method-other';
220
+ };
221
+
222
+ const typeClass = (entryOrType, maybeEntry) => {
223
+ const entry = maybeEntry || (typeof entryOrType === 'object' ? entryOrType : null);
224
+ const type = entry && entry.type ? entry.type : entryOrType;
225
+ return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
226
+ };
203
227
 
204
228
  const timeSince = (value) => {
205
229
  const createdAt = Number(value);
@@ -251,11 +275,34 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
251
275
  return '<span class="duration-chip ' + tone + '" title="' + toneLabel + '">' + escapeHtml(formatDuration(duration)) + '</span>';
252
276
  };
253
277
 
278
+ const buildEntriesHref = (params) => {
279
+ const search = new URLSearchParams();
280
+ search.set('page', 'entries');
281
+ if (params.type) search.set('type', String(params.type));
282
+ if (params.tag) search.set('tag', String(params.tag));
283
+ if (params.batchId) search.set('batchId', String(params.batchId));
284
+ return BASE + '?' + search.toString();
285
+ };
286
+
254
287
  const tagsHtml = (tags) => (tags || []).map((tag) => {
255
288
  const css = tag === 'failed' ? 'tag failed' : tag === 'slow' ? 'tag slow' : 'tag';
256
- return '<button type="button" class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '">' + escapeHtml(tag) + '</button>';
289
+ const href = buildEntriesHref({ tag });
290
+ return '<a class="' + css + '" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(href) + '">' + escapeHtml(tag) + '</a>';
257
291
  }).join('');
258
292
 
293
+ const syncUrl = () => {
294
+ const search = new URLSearchParams();
295
+ if (state.page !== 'overview') search.set('page', state.page);
296
+ if (state.page === 'entries' || state.entriesFilter.type || state.entriesFilter.tag || state.entriesFilter.batchId) {
297
+ if (state.entriesFilter.type) search.set('type', state.entriesFilter.type);
298
+ if (state.entriesFilter.tag) search.set('tag', state.entriesFilter.tag);
299
+ if (state.entriesFilter.batchId) search.set('batchId', state.entriesFilter.batchId);
300
+ if (state.entriesPage > 1) search.set('entriesPage', String(state.entriesPage));
301
+ }
302
+ const nextUrl = search.toString() === '' ? BASE : BASE + '?' + search.toString();
303
+ window.history.replaceState(null, '', nextUrl);
304
+ };
305
+
259
306
  const batchSnippet = (batchId) => {
260
307
  const raw = String(batchId || '');
261
308
  return raw === '' ? '-' : escapeHtml(raw.slice(0, 8));
@@ -475,7 +522,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
475
522
  '<section class="trace-item">',
476
523
  '<div class="trace-item-head">',
477
524
  '<div>',
478
- '<span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span>',
525
+ '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>',
479
526
  '</div>',
480
527
  '<div class="activity-head">' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div>',
481
528
  '</div>',
@@ -501,10 +548,11 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
501
548
  { id: 'logs', label: 'Logs', count: batchEntriesByType('log').length },
502
549
  { id: 'exceptions', label: 'Exceptions', count: batchEntriesByType('exception').length },
503
550
  { id: 'http', label: 'HTTP', count: batchEntriesByType('client_request').length },
504
- { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type)).length }
551
+ { id: 'cache', label: 'Cache', count: batchEntriesByType('cache').length },
552
+ { id: 'other', label: 'Other', count: batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type)).length }
505
553
  ];
506
554
  const currentTab = traceTabs.some((tab) => tab.id === state.detailTab) ? state.detailTab : 'summary';
507
- const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request'].includes(item.type));
555
+ const otherEntries = batchEntries().filter((item) => !['request','query','log','exception','client_request','cache'].includes(item.type));
508
556
  const panels = {
509
557
  summary: [
510
558
  '<div class="detail-grid">',
@@ -527,18 +575,19 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
527
575
  ].join(''),
528
576
  payload: detailJson(content.payload || {}),
529
577
  headers: '<div class="detail-stack">' + detailJson(content.headers || {}) + detailJson(content.responseHeaders || {}) + '</div>',
530
- response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div><p class="trace-note">Response body capture is not wired yet. Status and headers are available.</p>' + detailJson(content.responseHeaders || {}) + '</div>',
578
+ response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody)) + detailJson(content.responseHeaders || {}) + '</div>',
531
579
  queries: renderTraceItems(batchEntriesByType('query')),
532
580
  logs: renderTraceItems(batchEntriesByType('log')),
533
581
  exceptions: renderTraceItems(batchEntriesByType('exception')),
534
582
  http: renderTraceItems(batchEntriesByType('client_request')),
583
+ cache: renderTraceItems(batchEntriesByType('cache')),
535
584
  other: renderTraceItems(otherEntries)
536
585
  };
537
586
 
538
587
  main.innerHTML = [
539
588
  '<span class="back-link" data-action="close-detail"><- Back to entries</span>',
540
589
  '<section class="panel detail-card">',
541
- '<div><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
590
+ '<div><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
542
591
  '<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
543
592
  '<div class="trace-tabs">',
544
593
  traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
@@ -564,10 +613,10 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
564
613
  const recentRows = recent.data || [];
565
614
  const recentTable = recentRows.length === 0
566
615
  ? '<div class="empty">No trace entries recorded.</div>'
567
- : '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
616
+ : '<div class="table-wrap"><table><thead><tr><th>Type</th><th>Summary</th><th>Tags</th><th>Duration</th><th>Happened</th></tr></thead><tbody>' + recentRows.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('') + '</tbody></table></div>';
568
617
  const activityList = recentRows.length === 0
569
618
  ? '<div class="empty">No recent activity.</div>'
570
- : '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
619
+ : '<ul class="activity-list">' + recentRows.slice(0, 5).map((entry) => '<li class="activity-item"><div class="activity-head"><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span>' + durationHtml(entry) + '<span class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</span></div><div class="activity-summary">' + escapeHtml(entrySummaryText(entry)) + '</div></li>').join('') + '</ul>';
571
620
 
572
621
  main.innerHTML = [
573
622
  statsCardsHtml(stats),
@@ -611,7 +660,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
611
660
  const total = Number(response.total || 0);
612
661
  const perPage = Number(response.perPage || 50);
613
662
  const totalPages = Math.max(1, Math.ceil(total / perPage));
614
- const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="mono">' + batchSnippet(entry.batchId) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
663
+ const rows = data.map((entry) => '<tr class="row-button" data-action="show-detail" data-uuid="' + escapeHtml(entry.uuid) + '"><td><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span></td><td>' + entrySummaryHtml(entry) + '</td><td>' + tagsHtml(entry.tags) + '</td><td>' + durationHtml(entry) + '</td><td class="mono">' + batchSnippet(entry.batchId) + '</td><td class="activity-time">' + escapeHtml(timeSince(entry.createdAt)) + '</td></tr>').join('');
615
664
 
616
665
  main.innerHTML = [
617
666
  '<section class="panel">',
@@ -647,7 +696,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
647
696
  main.innerHTML = [
648
697
  '<span class="back-link" data-action="close-detail"><- Back to entries</span>',
649
698
  '<section class="panel detail-card">',
650
- '<div><span class="' + typeClass(entry.type) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
699
+ '<div><span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags) + '</div>',
651
700
  '<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
652
701
  '<div class="detail-stack">',
653
702
  renderEntryBody(entry),
@@ -663,13 +712,14 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
663
712
  const tags = result.tags || [];
664
713
  main.innerHTML = [
665
714
  '<section class="panel">',
666
- '<div class="section-head"><div><h3>Monitoring tags</h3><p>Pinned tags for quick filtering.</p></div></div>',
715
+ '<div class="section-head"><div><h3>Monitoring tags</h3><p>Save the tags you filter by often so they stay one click away.</p></div></div>',
667
716
  '<div class="monitoring-wrap">',
668
717
  '<div class="tag-list">',
669
- tags.length === 0 ? '<span class="helper-text">No tags monitored.</span>' : tags.map((tag) => '<span class="tag-item"><button type="button" class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '">' + escapeHtml(tag) + '</button><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
718
+ tags.length === 0 ? '<span class="helper-text">No tags monitored yet. Add tags like auth, checkout, queue:emails, or nightly-sync to pin them here.</span>' : tags.map((tag) => '<span class="tag-item"><a class="tag mono" data-action="filter-tag" data-tag="' + escapeHtml(tag) + '" href="' + escapeHtml(buildEntriesHref({ tag })) + '">' + escapeHtml(tag) + '</a><button type="button" class="tag-remove" data-action="remove-tag" data-tag="' + escapeHtml(tag) + '">x</button></span>').join(''),
670
719
  '</div>',
720
+ '<p class="helper-text">Click a saved tag to filter the entries list by that exact tag. Because each tag is a real link, you can also open it in a new tab. Removing a saved tag only removes the shortcut here; it does not delete any trace entries.</p>',
671
721
  '<div class="toolbar" style="padding:0;margin-top:8px">',
672
- '<input id="new-tag" class="control" type="text" placeholder="Add tag">',
722
+ '<input id="new-tag" class="control" type="text" placeholder="Add tag, for example checkout">',
673
723
  '<button type="button" class="btn btn-primary" data-action="add-tag">Add tag</button>',
674
724
  '</div>',
675
725
  '</div>',
@@ -694,6 +744,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
694
744
  button.classList.toggle('active', button.getAttribute('data-type') === activeShortcut);
695
745
  });
696
746
 
747
+ syncUrl();
748
+
697
749
  if (state.page === 'overview') await renderOverview(main);
698
750
  if (state.page === 'entries') await renderEntries(main);
699
751
  if (state.page === 'monitoring') await renderMonitoring(main);
@@ -828,7 +880,12 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
828
880
  const action = target.getAttribute('data-action');
829
881
  if (action === 'go-page') { setPage(String(target.getAttribute('data-page') || 'overview')); return; }
830
882
  if (action === 'type-shortcut') { setTypeShortcut(String(target.getAttribute('data-type') || '')); return; }
831
- if (action === 'filter-tag') { filterByTag(String(target.getAttribute('data-tag') || '')); return; }
883
+ if (action === 'filter-tag') {
884
+ if (target instanceof HTMLAnchorElement && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)) return;
885
+ event.preventDefault();
886
+ filterByTag(String(target.getAttribute('data-tag') || ''));
887
+ return;
888
+ }
832
889
  if (action === 'detail-tab') { state = { ...state, detailTab: String(target.getAttribute('data-tab') || 'summary') }; render(); return; }
833
890
  if (action === 'clear-all') { clearAll(); return; }
834
891
  if (action === 'show-detail') { showDetail(String(target.getAttribute('data-uuid') || '')); return; }
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  * import '@zintrust/trace/register';
7
7
  */
8
8
 
9
+ import { ExceptionWatcher as ExceptionWatcherApi } from './watchers/ExceptionWatcher';
10
+
9
11
  // ---------------------------------------------------------------------------
10
12
  // Config
11
13
  // ---------------------------------------------------------------------------
@@ -16,6 +18,7 @@ export { TraceConfig } from './config';
16
18
  // ---------------------------------------------------------------------------
17
19
  export { TraceStorage } from './storage';
18
20
  export type { ITraceStorage } from './storage';
21
+ export { TraceContentRedaction } from './storage/TraceContentRedaction';
19
22
 
20
23
  // ---------------------------------------------------------------------------
21
24
  // Context
@@ -52,6 +55,10 @@ export { RedisWatcher } from './watchers/RedisWatcher';
52
55
  export { ScheduleWatcher } from './watchers/ScheduleWatcher';
53
56
  export { ViewWatcher } from './watchers/ViewWatcher';
54
57
 
58
+ export const captureTraceException = (error: unknown): void => {
59
+ ExceptionWatcherApi.capture(error);
60
+ };
61
+
55
62
  // ---------------------------------------------------------------------------
56
63
  // Types
57
64
  // ---------------------------------------------------------------------------