@zintrust/trace 0.9.2 → 0.9.4

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.
@@ -0,0 +1,182 @@
1
+ import type { IDatabase } from '@zintrust/core';
2
+
3
+ type TraceErrorFactory = {
4
+ createConfigError?(message: string, details?: unknown): Error;
5
+ };
6
+
7
+ type TraceErrorApi = {
8
+ ErrorFactory?: TraceErrorFactory;
9
+ };
10
+
11
+ type TraceEnvApi = {
12
+ get(key: string, fallback: string): string;
13
+ };
14
+
15
+ type GlobalTraceConnectionState = {
16
+ __zintrust_system_trace_connection_name__?: string;
17
+ __zintrust_system_trace_plugin_requested__?: boolean;
18
+ };
19
+
20
+ export const TRACE_REQUIRED_TABLES = [
21
+ 'zin_trace_entries',
22
+ 'zin_trace_entries_tags',
23
+ 'zin_trace_monitoring',
24
+ ] as const;
25
+
26
+ const createFallbackTraceConfigError = (message: string, details?: unknown): Error => {
27
+ const error = new globalThis.Error(message) as Error & {
28
+ code?: string;
29
+ details?: unknown;
30
+ name?: string;
31
+ statusCode?: number;
32
+ };
33
+ error.name = 'ConfigError';
34
+ error.code = 'CONFIG_ERROR';
35
+ error.statusCode = 500;
36
+ error.details = details;
37
+ return error;
38
+ };
39
+
40
+ export const createTraceConfigError = (
41
+ coreApi: TraceErrorApi,
42
+ message: string,
43
+ details?: unknown
44
+ ): Error => {
45
+ if (coreApi.ErrorFactory?.createConfigError !== undefined) {
46
+ return coreApi.ErrorFactory.createConfigError(message, details);
47
+ }
48
+
49
+ return createFallbackTraceConfigError(message, details);
50
+ };
51
+
52
+ export const getRuntimeTraceConnectionName = (): string | undefined => {
53
+ const runtimeConnection = (
54
+ globalThis as GlobalTraceConnectionState
55
+ ).__zintrust_system_trace_connection_name__?.trim();
56
+
57
+ return runtimeConnection === undefined || runtimeConnection === ''
58
+ ? undefined
59
+ : runtimeConnection;
60
+ };
61
+
62
+ export const resolveDashboardTraceConnectionName = (
63
+ coreApi: TraceErrorApi,
64
+ input: {
65
+ explicitConnectionName?: string;
66
+ configuredConnectionName?: string;
67
+ }
68
+ ): string => {
69
+ const explicitConnection = input.explicitConnectionName?.trim();
70
+ if (explicitConnection !== undefined && explicitConnection !== '') {
71
+ return explicitConnection;
72
+ }
73
+
74
+ const runtimeConnection = getRuntimeTraceConnectionName();
75
+ if (runtimeConnection !== undefined) {
76
+ return runtimeConnection;
77
+ }
78
+
79
+ const configuredConnection = input.configuredConnectionName?.trim();
80
+ if (configuredConnection !== undefined && configuredConnection !== '') {
81
+ return configuredConnection;
82
+ }
83
+
84
+ throw createTraceConfigError(coreApi, 'Trace dashboard connection is not configured.', {
85
+ envKey: 'TRACE_DB_CONNECTION',
86
+ hint: 'Import @zintrust/trace/register before mounting the dashboard, pass connectionName explicitly, or set TRACE_DB_CONNECTION to the trace storage connection.',
87
+ });
88
+ };
89
+
90
+ export const resolveTraceConnectionName = (
91
+ env: Pick<TraceEnvApi, 'get'> | undefined,
92
+ configuredConnection?: string
93
+ ): string => {
94
+ const resolveDefaultConnection = (): string => {
95
+ const defaultConnection = env?.get('DB_CONNECTION', '').trim() ?? '';
96
+ if (defaultConnection === '' || defaultConnection === 'default') return 'default';
97
+ return defaultConnection;
98
+ };
99
+
100
+ const explicitConnection = configuredConnection?.trim();
101
+ if (explicitConnection !== undefined && explicitConnection !== '') {
102
+ return explicitConnection === 'default' ? resolveDefaultConnection() : explicitConnection;
103
+ }
104
+
105
+ return resolveDefaultConnection();
106
+ };
107
+
108
+ export const resolveObservedConnectionName = (
109
+ env: Pick<TraceEnvApi, 'get'> | undefined,
110
+ configuredObservedConnection: string | undefined,
111
+ storageConnectionName: string
112
+ ): string => {
113
+ if (
114
+ typeof configuredObservedConnection === 'string' &&
115
+ configuredObservedConnection.trim() !== ''
116
+ ) {
117
+ return resolveTraceConnectionName(env, configuredObservedConnection);
118
+ }
119
+
120
+ const defaultConnectionName = resolveTraceConnectionName(env);
121
+ if (storageConnectionName !== defaultConnectionName) {
122
+ return defaultConnectionName;
123
+ }
124
+
125
+ return storageConnectionName;
126
+ };
127
+
128
+ export function assertTraceConnectionResolved(
129
+ coreApi: TraceErrorApi,
130
+ db: IDatabase | undefined,
131
+ params: { connectionName: string; envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION' }
132
+ ): asserts db is IDatabase {
133
+ if (db !== undefined) {
134
+ return;
135
+ }
136
+
137
+ const pluginRequested =
138
+ (globalThis as GlobalTraceConnectionState).__zintrust_system_trace_plugin_requested__ === true;
139
+ let hint =
140
+ 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.';
141
+
142
+ if (params.envKey === 'TRACE_DB_CONNECTION') {
143
+ hint = pluginRequested
144
+ ? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
145
+ : 'If this module is being imported from zintrust.plugins.*, switch that import to @zintrust/trace/plugin so trace registration runs after database runtime registration. Otherwise configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.';
146
+ }
147
+
148
+ throw createTraceConfigError(
149
+ coreApi,
150
+ `Trace connection "${params.connectionName}" could not be resolved.`,
151
+ {
152
+ connectionName: params.connectionName,
153
+ envKey: params.envKey,
154
+ hint,
155
+ }
156
+ );
157
+ }
158
+
159
+ export const assertTraceStorageReady = async (
160
+ coreApi: TraceErrorApi,
161
+ db: IDatabase,
162
+ connectionName: string,
163
+ operation = 'Trace storage connection'
164
+ ): Promise<void> => {
165
+ try {
166
+ await Promise.all(
167
+ TRACE_REQUIRED_TABLES.map(async (table) => {
168
+ await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
169
+ })
170
+ );
171
+ } catch (error) {
172
+ throw createTraceConfigError(
173
+ coreApi,
174
+ `${operation} "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`,
175
+ {
176
+ connectionName,
177
+ error,
178
+ requiredTables: [...TRACE_REQUIRED_TABLES],
179
+ }
180
+ );
181
+ }
182
+ };
package/src/config.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  TraceConfigOverrides,
9
9
  TraceContentDispatchConfig,
10
10
  TraceFilterRule,
11
+ TraceProxyConfig,
11
12
  TraceRequestWatcherConfig,
12
13
  TraceWatcherToggle,
13
14
  } from './types';
@@ -253,10 +254,31 @@ const mergeContentDispatch = (
253
254
  };
254
255
  };
255
256
 
257
+ const mergeProxyConfig = (
258
+ base: TraceProxyConfig,
259
+ override?: TraceConfigOverrides['proxy']
260
+ ): TraceProxyConfig => {
261
+ if (override === undefined) return base;
262
+
263
+ return {
264
+ ...base,
265
+ ...override,
266
+ };
267
+ };
268
+
256
269
  const DEFAULTS: ITraceConfig = Object.freeze({
257
270
  enabled: false,
258
271
  connection: undefined,
259
272
  observeConnection: undefined,
273
+ serviceTag: undefined,
274
+ proxy: {
275
+ enabled: false,
276
+ url: undefined,
277
+ path: '/zin/trace/write',
278
+ keyId: undefined,
279
+ secret: undefined,
280
+ timeoutMs: 30000,
281
+ },
260
282
  pruneAfterHours: 24,
261
283
  ignoreRoutes: ['/trace', '/health', '/ping'],
262
284
  ignorePaths: [],
@@ -342,6 +364,7 @@ export const TraceConfig = Object.freeze({
342
364
  return Object.freeze({
343
365
  ...DEFAULTS,
344
366
  ...overrides,
367
+ proxy: mergeProxyConfig(DEFAULTS.proxy, overrides.proxy),
345
368
  contentDispatch: mergeContentDispatch(DEFAULTS.contentDispatch, overrides.contentDispatch),
346
369
  watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
347
370
  redaction: {
@@ -3,9 +3,20 @@
3
3
  * Mounts the SPA + all REST API endpoints under the configured basePath.
4
4
  * Auth is NOT applied here — callers add middleware via routeOptions.
5
5
  */
6
- import { appConfig, Router, useDatabase, type IRouter, type RouteOptions } from '@zintrust/core';
6
+ import {
7
+ appConfig,
8
+ ErrorFactory,
9
+ Router,
10
+ useDatabase,
11
+ type IRouter,
12
+ type RouteOptions,
13
+ } from '@zintrust/core';
7
14
  import { TraceConfig } from '../config';
8
15
  import { TraceStorage } from '../storage';
16
+ import {
17
+ assertTraceConnectionResolved,
18
+ resolveDashboardTraceConnectionName,
19
+ } from '../TraceConnection';
9
20
  import type { ITraceStorage } from '../types';
10
21
  import {
11
22
  addMonitoring,
@@ -36,29 +47,6 @@ export type TraceDashboardRegistrationOptions = TraceDashboardOptions & {
36
47
  connectionName?: string;
37
48
  };
38
49
 
39
- type GlobalTraceDashboardState = {
40
- __zintrust_system_trace_connection_name__?: string;
41
- };
42
-
43
- const resolveDashboardConnectionName = (connectionName?: string): string | undefined => {
44
- const explicitConnection = connectionName?.trim();
45
- if (explicitConnection !== undefined && explicitConnection !== '') {
46
- return explicitConnection;
47
- }
48
-
49
- const runtimeConnection = (
50
- globalThis as GlobalTraceDashboardState
51
- ).__zintrust_system_trace_connection_name__?.trim();
52
- if (runtimeConnection !== undefined && runtimeConnection !== '') {
53
- return runtimeConnection;
54
- }
55
-
56
- const configuredConnection = TraceConfig.merge().connection?.trim();
57
- return configuredConnection === undefined || configuredConnection === ''
58
- ? undefined
59
- : configuredConnection;
60
- };
61
-
62
50
  export const registerTraceRoutes = (
63
51
  router: IRouter,
64
52
  storage: ITraceStorage,
@@ -108,9 +96,19 @@ export const registerTraceDashboard = (
108
96
  router: IRouter,
109
97
  options: TraceDashboardRegistrationOptions = {}
110
98
  ): void => {
111
- const storage = TraceStorage.resolveStorage(
112
- useDatabase(undefined, resolveDashboardConnectionName(options.connectionName))
99
+ const connectionName = resolveDashboardTraceConnectionName(
100
+ { ErrorFactory },
101
+ {
102
+ explicitConnectionName: options.connectionName,
103
+ configuredConnectionName: TraceConfig.merge().connection,
104
+ }
113
105
  );
106
+ const db = useDatabase(undefined, connectionName);
107
+ assertTraceConnectionResolved({ ErrorFactory }, db, {
108
+ connectionName,
109
+ envKey: 'TRACE_DB_CONNECTION',
110
+ });
111
+ const storage = TraceStorage.resolveStorage(db);
114
112
 
115
113
  registerTraceRoutes(router, storage, options);
116
114
  };
@@ -78,7 +78,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
78
78
  .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)}
79
79
  .table-wrap{overflow:auto;padding:0 12px 12px}.table-wrap 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}
80
80
  .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;overflow-wrap:anywhere}.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;overflow-wrap:anywhere}.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}.trace-disclosure{padding:0;overflow:hidden}.trace-disclosure[open]{padding-bottom:18px}.trace-disclosure .trace-item-summary{margin-top:0}.trace-disclosure-body{display:grid;gap:12px;padding:0 18px}.trace-summary{list-style:none;cursor:pointer;padding:18px}.trace-summary::-webkit-details-marker{display:none}.trace-summary-main{display:grid;gap:10px;min-width:0;flex:1}.trace-summary-copy{display:grid;gap:6px;min-width:0}.trace-summary-copy .summary,.trace-summary-copy .summary-sub{display:block;overflow-wrap:anywhere}.trace-disclosure-body .summary-sub{overflow-wrap:anywhere}
81
- .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
+ .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,.status-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}.status-pill{white-space:nowrap}.status-pill.status-2xx{background:rgba(34,197,94,.16);color:#bbf7d0;border-color:rgba(34,197,94,.24)}.status-pill.status-4xx{background:rgba(245,158,11,.16);color:#fde68a;border-color:rgba(245,158,11,.24)}.status-pill.status-5xx{background:rgba(239,68,68,.16);color:#fecaca;border-color:rgba(239,68,68,.24)}.status-pill.status-other{background:rgba(148,163,184,.14);color:#e2e8f0;border-color:rgba(148,163,184,.2)}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}html[data-theme='light'] .status-pill.status-2xx{color:#166534}html[data-theme='light'] .status-pill.status-4xx{color:#92400e}html[data-theme='light'] .status-pill.status-5xx{color:#b91c1c}html[data-theme='light'] .status-pill.status-other{color:#334155}
82
82
  .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}
83
83
  .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}
84
84
  .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.wrap{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.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}.inline-collapse{margin:0;border-top:1px solid var(--line);background:var(--surface-strong)}.inline-collapse summary{cursor:pointer;list-style:none;padding:14px 16px;font-size:.82rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:800}.inline-collapse summary::-webkit-details-marker{display:none}.inline-collapse[open] summary{border-bottom:1px solid var(--line)}.inline-collapse .code-block{border-top:none}.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}
@@ -264,6 +264,21 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
264
264
  return 'type-pill pill-' + String(type || '').replace(/_/g, '-') + requestMethodClass(entry);
265
265
  };
266
266
 
267
+ const statusToneClass = (value) => {
268
+ const status = Number(value);
269
+ if (!Number.isFinite(status)) return 'status-other';
270
+ if (status >= 500 && status < 600) return 'status-5xx';
271
+ if (status >= 400 && status < 500) return 'status-4xx';
272
+ if (status >= 200 && status < 300) return 'status-2xx';
273
+ return 'status-other';
274
+ };
275
+
276
+ const statusBadgeHtml = (value) => {
277
+ const status = Number(value);
278
+ if (!Number.isFinite(status)) return '';
279
+ return '<span class="status-pill ' + statusToneClass(status) + '">' + escapeHtml(String(status)) + '</span>';
280
+ };
281
+
267
282
  const timeSince = (value) => {
268
283
  const createdAt = Number(value);
269
284
  if (!Number.isFinite(createdAt)) return 'Unknown';
@@ -524,6 +539,19 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
524
539
  };
525
540
 
526
541
  const entrySummaryHtml = (entry) => {
542
+ const content = entry && entry.content ? entry.content : {};
543
+ if (entry.type === 'request') {
544
+ return '<div class="summary">' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span></div><div class="summary-sub">Incoming request</div>';
545
+ }
546
+ if (entry.type === 'client_request') {
547
+ const clientParts = [
548
+ content.method ? '<span class="mono">' + escapeHtml(content.method) + '</span>' : '',
549
+ content.url ? '<span class="mono">' + escapeHtml(content.url) + '</span>' : '',
550
+ statusBadgeHtml(content.responseStatus) || (content.error ? '<span class="status-pill status-5xx">Failed</span>' : '')
551
+ ].filter(Boolean).join(' ');
552
+ return '<div class="summary">' + clientParts + '</div><div class="summary-sub">Outbound HTTP call</div>';
553
+ }
554
+
527
555
  const summary = escapeHtml(entrySummaryText(entry) || 'No summary available');
528
556
  const secondary = [
529
557
  entry.type === 'request' ? 'Incoming request' : '',
@@ -619,7 +647,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
619
647
  renderMetricBox('Request', [
620
648
  { label: 'Method', value: escapeHtml(content.method || '') },
621
649
  { label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
622
- { label: 'Status', value: escapeHtml(content.responseStatus || (content.error ? 'Failed' : 'Pending')) },
650
+ { label: 'Status', value: statusBadgeHtml(content.responseStatus) || escapeHtml(content.error ? 'Failed' : 'Pending') },
623
651
  { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
624
652
  ]),
625
653
  renderMetricBox('Runtime', [
@@ -803,7 +831,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
803
831
  ].join(''),
804
832
  payload: detailJson(content.payload || {}, 'Payload Json'),
805
833
  headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
806
- 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, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
834
+ response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: statusBadgeHtml(content.responseStatus) || 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, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
807
835
  queries: renderDetailBatchPanel('queries'),
808
836
  middleware: renderDetailBatchPanel('middleware'),
809
837
  models: renderDetailBatchPanel('models'),
@@ -818,7 +846,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
818
846
  '<span class="back-link" data-action="close-detail"><- Back to entries</span>',
819
847
  '<section class="panel detail-card">',
820
848
  '<div>' + (entry.type === 'request'
821
- ? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
849
+ ? '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + statusBadgeHtml(content.responseStatus) + ' <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
822
850
  : '<span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
823
851
  '<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>',
824
852
  '<div class="trace-tabs">',
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export { TraceContext } from './context';
31
31
  // ---------------------------------------------------------------------------
32
32
  export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
33
33
  export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
34
+ export { registerTraceIngestGateway, TraceIngestGateway } from './ingest/TraceIngestGateway';
34
35
 
35
36
  // ---------------------------------------------------------------------------
36
37
  // Watchers (named re-exports for use with custom wiring)