@zintrust/trace 0.4.75 → 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 (73) hide show
  1. package/README.md +101 -15
  2. package/dist/build-manifest.json +210 -162
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +123 -4
  5. package/dist/dashboard/routes.js +4 -4
  6. package/dist/dashboard/ui.js +80 -23
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +30 -25
  9. package/dist/migrations/{20260331000001_create_zin_debugger_entries_table.d.ts → 20260331000001_create_zin_trace_entries_table.d.ts} +2 -2
  10. package/dist/migrations/{20260331000001_create_zin_debugger_entries_table.js → 20260331000001_create_zin_trace_entries_table.js} +5 -5
  11. package/dist/migrations/{20260331000002_create_zin_debugger_entries_tags_table.d.ts → 20260331000002_create_zin_trace_entries_tags_table.d.ts} +2 -2
  12. package/dist/migrations/{20260331000002_create_zin_debugger_entries_tags_table.js → 20260331000002_create_zin_trace_entries_tags_table.js} +5 -5
  13. package/dist/migrations/{20260331000003_create_zin_debugger_monitoring_table.d.ts → 20260331000003_create_zin_trace_monitoring_table.d.ts} +2 -2
  14. package/dist/migrations/{20260331000003_create_zin_debugger_monitoring_table.js → 20260331000003_create_zin_trace_monitoring_table.js} +4 -4
  15. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
  16. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
  17. package/dist/migrations/index.d.ts +3 -3
  18. package/dist/migrations/index.js +5 -4
  19. package/dist/register.js +130 -32
  20. package/dist/storage/DebuggerStorage.js +1 -1
  21. package/dist/storage/TraceContentRedaction.d.ts +4 -0
  22. package/dist/storage/TraceContentRedaction.js +33 -0
  23. package/dist/storage/TraceEntryFiltering.d.ts +4 -0
  24. package/dist/storage/TraceEntryFiltering.js +13 -0
  25. package/dist/storage/TraceStorage.js +36 -6
  26. package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
  27. package/dist/storage/TraceWriteDiagnostics.js +98 -0
  28. package/dist/storage/index.js +1 -1
  29. package/dist/types.d.ts +37 -20
  30. package/dist/ui.js +1 -1
  31. package/dist/utils/authTag.js +1 -1
  32. package/dist/utils/entryFilter.d.ts +4 -0
  33. package/dist/utils/entryFilter.js +95 -0
  34. package/dist/utils/redact.d.ts +1 -0
  35. package/dist/utils/redact.js +43 -9
  36. package/dist/utils/requestFilter.js +1 -1
  37. package/dist/watchers/AuthWatcher.js +3 -3
  38. package/dist/watchers/BatchWatcher.js +3 -3
  39. package/dist/watchers/CacheWatcher.js +5 -5
  40. package/dist/watchers/CommandWatcher.js +5 -5
  41. package/dist/watchers/DumpWatcher.js +3 -3
  42. package/dist/watchers/EventWatcher.js +4 -4
  43. package/dist/watchers/ExceptionWatcher.js +6 -6
  44. package/dist/watchers/GateWatcher.js +3 -3
  45. package/dist/watchers/HttpClientWatcher.js +6 -6
  46. package/dist/watchers/HttpWatcher.js +108 -24
  47. package/dist/watchers/JobWatcher.js +4 -4
  48. package/dist/watchers/LogWatcher.js +5 -4
  49. package/dist/watchers/MailWatcher.js +3 -3
  50. package/dist/watchers/MiddlewareWatcher.js +3 -3
  51. package/dist/watchers/ModelWatcher.js +3 -3
  52. package/dist/watchers/NotificationWatcher.js +4 -4
  53. package/dist/watchers/QueryWatcher.js +5 -5
  54. package/dist/watchers/RedisWatcher.js +4 -4
  55. package/dist/watchers/ScheduleWatcher.js +3 -3
  56. package/dist/watchers/ViewWatcher.js +3 -3
  57. package/package.json +4 -4
  58. package/src/config.ts +152 -5
  59. package/src/dashboard/routes.ts +6 -2
  60. package/src/dashboard/ui.ts +80 -23
  61. package/src/index.ts +7 -0
  62. package/src/register.ts +137 -10
  63. package/src/storage/TraceContentRedaction.ts +44 -0
  64. package/src/storage/TraceEntryFiltering.ts +14 -0
  65. package/src/storage/TraceStorage.ts +52 -5
  66. package/src/storage/TraceWriteDiagnostics.ts +174 -0
  67. package/src/types.ts +40 -20
  68. package/src/utils/entryFilter.ts +108 -0
  69. package/src/utils/redact.ts +57 -9
  70. package/src/watchers/CommandWatcher.ts +1 -1
  71. package/src/watchers/HttpClientWatcher.ts +1 -1
  72. package/src/watchers/HttpWatcher.ts +132 -21
  73. package/src/watchers/LogWatcher.ts +27 -27
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Migration: CreateZinDebuggerMonitoringTable
3
- * Creates the tag watchlist table for @zintrust/system-debugger
2
+ * Migration: CreateZinTraceMonitoringTable
3
+ * Creates the tag watchlist table for @zintrust/trace
4
4
  */
5
5
  import { MigrationSchema } from '@zintrust/core';
6
6
  export const migration = {
7
7
  async up(db) {
8
8
  const schema = MigrationSchema.create(db);
9
- await schema.create('zin_debugger_monitoring', (table) => {
9
+ await schema.create('zin_trace_monitoring', (table) => {
10
10
  table.string('tag').primary();
11
11
  });
12
12
  },
13
13
  async down(db) {
14
14
  const schema = MigrationSchema.create(db);
15
- await schema.dropIfExists('zin_debugger_monitoring');
15
+ await schema.dropIfExists('zin_trace_monitoring');
16
16
  },
17
17
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Migration: WidenTraceCreatedAtForSql
3
+ * Ensures SQL engines that treat INTEGER as 32-bit can store millisecond timestamps.
4
+ */
5
+ import { type IDatabase } from '@zintrust/core';
6
+ export interface Migration {
7
+ up(db: IDatabase): Promise<void>;
8
+ down(db: IDatabase): Promise<void>;
9
+ }
10
+ export declare const migration: Migration;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Migration: WidenTraceCreatedAtForSql
3
+ * Ensures SQL engines that treat INTEGER as 32-bit can store millisecond timestamps.
4
+ */
5
+ import { MigrationSchema } from '@zintrust/core';
6
+ const alterCreatedAt = async (db) => {
7
+ const driver = db.getType?.() ?? 'sqlite';
8
+ if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote')
9
+ return;
10
+ const schema = MigrationSchema.create(db);
11
+ if (!(await schema.hasTable('zin_trace_entries')))
12
+ return;
13
+ if (!(await schema.hasColumn('zin_trace_entries', 'created_at')))
14
+ return;
15
+ if (driver === 'mysql') {
16
+ await db.query('ALTER TABLE zin_trace_entries MODIFY COLUMN created_at BIGINT UNSIGNED NOT NULL', []);
17
+ return;
18
+ }
19
+ if (driver === 'postgresql') {
20
+ await db.query('ALTER TABLE zin_trace_entries ALTER COLUMN created_at TYPE BIGINT USING created_at::bigint', []);
21
+ return;
22
+ }
23
+ if (driver === 'sqlserver') {
24
+ await db.query('ALTER TABLE zin_trace_entries ALTER COLUMN created_at BIGINT NOT NULL', []);
25
+ }
26
+ };
27
+ export const migration = {
28
+ async up(db) {
29
+ await alterCreatedAt(db);
30
+ },
31
+ async down(_db) {
32
+ return;
33
+ },
34
+ };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Migrations index for @zintrust/system-debugger
2
+ * Migrations index for @zintrust/trace
3
3
  * Export all migrations as an ordered array.
4
4
  */
5
- export type { Migration } from './20260331000001_create_zin_debugger_entries_table';
6
- export declare const migrations: import("./20260331000001_create_zin_debugger_entries_table").Migration[];
5
+ export type { Migration } from './20260331000001_create_zin_trace_entries_table';
6
+ export declare const migrations: import("./20260331000001_create_zin_trace_entries_table").Migration[];
@@ -1,4 +1,5 @@
1
- import { migration as createEntries } from './20260331000001_create_zin_debugger_entries_table.js';
2
- import { migration as createEntriesTags } from './20260331000002_create_zin_debugger_entries_tags_table.js';
3
- import { migration as createMonitoring } from './20260331000003_create_zin_debugger_monitoring_table.js';
4
- export const migrations = [createEntries, createEntriesTags, createMonitoring];
1
+ import { migration as createEntries } from './20260331000001_create_zin_trace_entries_table.js';
2
+ import { migration as createEntriesTags } from './20260331000002_create_zin_trace_entries_tags_table.js';
3
+ import { migration as createMonitoring } from './20260331000003_create_zin_trace_monitoring_table.js';
4
+ import { migration as widenCreatedAt } from './20260407193000_widen_trace_created_at_for_sql.js';
5
+ export const migrations = [createEntries, createEntriesTags, createMonitoring, widenCreatedAt];
package/dist/register.js CHANGED
@@ -19,9 +19,12 @@
19
19
  * middleware: ['admin'],
20
20
  * });
21
21
  */
22
- import { TraceConfig } from './config';
23
- import { TraceContext } from './context';
24
- import { TraceStorage } from './storage';
22
+ import { TraceConfig } from './config.js';
23
+ import { TraceContext } from './context.js';
24
+ import { TraceStorage } from './storage/index.js';
25
+ import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
26
+ import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
27
+ import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
25
28
  const globalTraceRegisterState = globalThis;
26
29
  globalTraceRegisterState.__zintrust_system_trace_plugin_requested__ = true;
27
30
  const traceAlreadyInitialized = globalTraceRegisterState.__zintrust_system_trace_register_initialized__ === true;
@@ -61,49 +64,144 @@ const resolveTraceConnectionName = (env, configuredConnection) => {
61
64
  }
62
65
  return resolveDefaultConnection();
63
66
  };
67
+ const isObjectValue = (value) => {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ };
70
+ const parseEnvList = (rawValue) => {
71
+ const value = rawValue.trim();
72
+ if (value === '')
73
+ return undefined;
74
+ if (value.startsWith('[')) {
75
+ try {
76
+ const parsed = JSON.parse(value);
77
+ if (Array.isArray(parsed)) {
78
+ return parsed
79
+ .filter((entry) => typeof entry === 'string')
80
+ .map((entry) => entry.trim())
81
+ .filter((entry) => entry !== '');
82
+ }
83
+ }
84
+ catch {
85
+ // fall through to CSV parsing
86
+ }
87
+ }
88
+ return value
89
+ .split(',')
90
+ .map((entry) => entry.trim())
91
+ .filter((entry) => entry !== '');
92
+ };
93
+ const resolveTraceStartupOverrides = (core) => {
94
+ const traceConfigFile = core.StartupConfigFile?.Trace;
95
+ if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '')
96
+ return undefined;
97
+ const overrides = core.StartupConfigFileRegistry?.get(traceConfigFile);
98
+ return isObjectValue(overrides) ? overrides : undefined;
99
+ };
100
+ const buildTraceRedactionOverrides = (input) => {
101
+ const redaction = {
102
+ ...(isObjectValue(input.startupOverrides?.redaction) ? input.startupOverrides?.redaction : {}),
103
+ };
104
+ if (input.redactionKeys === undefined) {
105
+ // no-op
106
+ }
107
+ else {
108
+ redaction.keys = input.redactionKeys;
109
+ }
110
+ if (input.redactionHeaders === undefined) {
111
+ // no-op
112
+ }
113
+ else {
114
+ redaction.headers = input.redactionHeaders;
115
+ }
116
+ if (input.redactionBody === undefined) {
117
+ // no-op
118
+ }
119
+ else {
120
+ redaction.body = input.redactionBody;
121
+ }
122
+ if (input.redactionQuery === undefined) {
123
+ // no-op
124
+ }
125
+ else {
126
+ redaction.query = input.redactionQuery;
127
+ }
128
+ return Object.keys(redaction).length > 0
129
+ ? redaction
130
+ : undefined;
131
+ };
64
132
  const core = (await importCore());
65
133
  const Env = core.Env;
134
+ const startupOverrides = resolveTraceStartupOverrides(core);
66
135
  if (!traceAlreadyInitialized && Env) {
67
- const enabled = Env.getBool('TRACE_ENABLED', false);
136
+ const enabled = startupOverrides?.enabled === true || Env.getBool('TRACE_ENABLED', false);
68
137
  if (enabled) {
69
- const connection = Env.get('TRACE_DB_CONNECTION', '') || undefined;
70
- const pruneAfterHours = Env.getInt('TRACE_PRUNE_HOURS', 24);
71
- const slowQueryThreshold = Env.getInt('TRACE_SLOW_QUERY_MS', 100);
72
- const logMinLevel = Env.get('TRACE_LOG_LEVEL', 'info');
138
+ const connectionRaw = Env.get('TRACE_DB_CONNECTION', '').trim();
139
+ const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
140
+ const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
141
+ const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
142
+ const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
143
+ const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
144
+ const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
145
+ const redactionQuery = parseEnvList(Env.get('TRACE_REDACT_QUERY', ''));
146
+ const connection = connectionRaw === '' ? startupOverrides?.connection : connectionRaw;
147
+ const pruneAfterHours = pruneAfterHoursRaw === ''
148
+ ? startupOverrides?.pruneAfterHours
149
+ : Number.parseInt(pruneAfterHoursRaw, 10);
150
+ const slowQueryThreshold = slowQueryThresholdRaw === ''
151
+ ? startupOverrides?.slowQueryThreshold
152
+ : Number.parseInt(slowQueryThresholdRaw, 10);
153
+ const logMinLevel = (logMinLevelRaw === '' ? startupOverrides?.logMinLevel : logMinLevelRaw);
154
+ const redaction = buildTraceRedactionOverrides({
155
+ startupOverrides,
156
+ redactionBody,
157
+ redactionHeaders,
158
+ redactionKeys,
159
+ redactionQuery,
160
+ });
73
161
  const config = TraceConfig.merge({
162
+ ...startupOverrides,
74
163
  enabled,
75
164
  connection,
76
- pruneAfterHours,
77
- slowQueryThreshold,
165
+ ...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
166
+ ? { pruneAfterHours }
167
+ : {}),
168
+ ...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
169
+ ? { slowQueryThreshold }
170
+ : {}),
78
171
  logMinLevel,
172
+ ...(redaction === undefined ? {} : { redaction }),
79
173
  });
80
- const db = core.useDatabase?.(undefined, resolveTraceConnectionName(Env, connection));
174
+ const resolvedConnectionName = resolveTraceConnectionName(Env, config.connection);
175
+ const db = core.useDatabase?.(undefined, resolvedConnectionName);
81
176
  if (db) {
82
- const storage = TraceStorage.resolveStorage(db);
177
+ const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(db), config), config.redaction), {
178
+ connectionName: resolvedConnectionName,
179
+ logger: core.Logger,
180
+ });
83
181
  if (core.RequestContext) {
84
182
  TraceContext.setRequestContextImpl(core.RequestContext);
85
183
  }
86
184
  const [{ HttpWatcher }, { QueryWatcher }, { LogWatcher }, { ExceptionWatcher }, { JobWatcher }, { CacheWatcher }, { ScheduleWatcher }, { MailWatcher }, { AuthWatcher }, { EventWatcher }, { ModelWatcher }, { NotificationWatcher }, { RedisWatcher }, { GateWatcher }, { MiddlewareWatcher }, { CommandWatcher }, { BatchWatcher }, { DumpWatcher }, { ViewWatcher }, { HttpClientWatcher },] = await Promise.all([
87
- import('./watchers/HttpWatcher'),
88
- import('./watchers/QueryWatcher'),
89
- import('./watchers/LogWatcher'),
90
- import('./watchers/ExceptionWatcher'),
91
- import('./watchers/JobWatcher'),
92
- import('./watchers/CacheWatcher'),
93
- import('./watchers/ScheduleWatcher'),
94
- import('./watchers/MailWatcher'),
95
- import('./watchers/AuthWatcher'),
96
- import('./watchers/EventWatcher'),
97
- import('./watchers/ModelWatcher'),
98
- import('./watchers/NotificationWatcher'),
99
- import('./watchers/RedisWatcher'),
100
- import('./watchers/GateWatcher'),
101
- import('./watchers/MiddlewareWatcher'),
102
- import('./watchers/CommandWatcher'),
103
- import('./watchers/BatchWatcher'),
104
- import('./watchers/DumpWatcher'),
105
- import('./watchers/ViewWatcher'),
106
- import('./watchers/HttpClientWatcher'),
185
+ import('./watchers/HttpWatcher.js'),
186
+ import('./watchers/QueryWatcher.js'),
187
+ import('./watchers/LogWatcher.js'),
188
+ import('./watchers/ExceptionWatcher.js'),
189
+ import('./watchers/JobWatcher.js'),
190
+ import('./watchers/CacheWatcher.js'),
191
+ import('./watchers/ScheduleWatcher.js'),
192
+ import('./watchers/MailWatcher.js'),
193
+ import('./watchers/AuthWatcher.js'),
194
+ import('./watchers/EventWatcher.js'),
195
+ import('./watchers/ModelWatcher.js'),
196
+ import('./watchers/NotificationWatcher.js'),
197
+ import('./watchers/RedisWatcher.js'),
198
+ import('./watchers/GateWatcher.js'),
199
+ import('./watchers/MiddlewareWatcher.js'),
200
+ import('./watchers/CommandWatcher.js'),
201
+ import('./watchers/BatchWatcher.js'),
202
+ import('./watchers/DumpWatcher.js'),
203
+ import('./watchers/ViewWatcher.js'),
204
+ import('./watchers/HttpClientWatcher.js'),
107
205
  ]);
108
206
  const watcherArgs = { storage, config, db };
109
207
  HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
@@ -1,4 +1,4 @@
1
- import { familyHash } from '../utils/familyHash';
1
+ import { familyHash } from '../utils/familyHash.js';
2
2
  const TABLE_ENTRIES = 'zin_trace_entries';
3
3
  const TABLE_TAGS = 'zin_trace_entries_tags';
4
4
  const TABLE_MONITORING = 'zin_trace_monitoring';
@@ -0,0 +1,4 @@
1
+ import type { ITraceStorage, RedactionConfig } from '../types';
2
+ export declare const TraceContentRedaction: Readonly<{
3
+ wrapStorage(storage: ITraceStorage, redaction: RedactionConfig): ITraceStorage;
4
+ }>;
@@ -0,0 +1,33 @@
1
+ import { redactUnknown } from '../utils/redact.js';
2
+ const collectRedactionFields = (redaction) => {
3
+ return [
4
+ ...new Set([...redaction.keys, ...redaction.headers, ...redaction.body, ...redaction.query]),
5
+ ];
6
+ };
7
+ const redactTraceEntry = (entry, redaction) => {
8
+ return {
9
+ ...entry,
10
+ content: redactUnknown(entry.content, collectRedactionFields(redaction)),
11
+ };
12
+ };
13
+ const redactTracePatch = (patch, redaction) => {
14
+ if (patch.content === undefined)
15
+ return patch;
16
+ return {
17
+ ...patch,
18
+ content: redactUnknown(patch.content, collectRedactionFields(redaction)),
19
+ };
20
+ };
21
+ export const TraceContentRedaction = Object.freeze({
22
+ wrapStorage(storage, redaction) {
23
+ return Object.freeze({
24
+ ...storage,
25
+ writeEntry: async (entry) => {
26
+ await storage.writeEntry(redactTraceEntry(entry, redaction));
27
+ },
28
+ updateEntry: async (uuid, patch) => {
29
+ await storage.updateEntry(uuid, redactTracePatch(patch, redaction));
30
+ },
31
+ });
32
+ },
33
+ });
@@ -0,0 +1,4 @@
1
+ import type { ITraceConfig, ITraceStorage } from '../types';
2
+ export declare const TraceEntryFiltering: Readonly<{
3
+ wrapStorage(storage: ITraceStorage, config: ITraceConfig): ITraceStorage;
4
+ }>;
@@ -0,0 +1,13 @@
1
+ import { TraceEntryFilter } from '../utils/entryFilter.js';
2
+ export const TraceEntryFiltering = Object.freeze({
3
+ wrapStorage(storage, config) {
4
+ return Object.freeze({
5
+ ...storage,
6
+ async writeEntry(entry) {
7
+ if (!TraceEntryFilter.shouldCapture(entry, config))
8
+ return;
9
+ await storage.writeEntry(entry);
10
+ },
11
+ });
12
+ },
13
+ });
@@ -1,8 +1,40 @@
1
- import { familyHash } from '../utils/familyHash';
1
+ import { familyHash } from '../utils/familyHash.js';
2
2
  const TABLE_ENTRIES = 'zin_trace_entries';
3
3
  const TABLE_TAGS = 'zin_trace_entries_tags';
4
4
  const TABLE_MONITORING = 'zin_trace_monitoring';
5
5
  const generateUuid = () => crypto.randomUUID();
6
+ const buildIgnoreInsert = (db, table, columns, conflictColumns) => {
7
+ const columnList = columns.join(', ');
8
+ const placeholders = columns.map(() => '?').join(', ');
9
+ const driver = db.getType?.() ?? 'sqlite';
10
+ if (driver === 'sqlite' || driver === 'd1' || driver === 'd1-remote') {
11
+ return `INSERT OR IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
12
+ }
13
+ if (driver === 'mysql') {
14
+ return `INSERT IGNORE INTO ${table} (${columnList}) VALUES (${placeholders})`;
15
+ }
16
+ if (driver === 'postgresql') {
17
+ return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
18
+ }
19
+ if (driver === 'sqlserver') {
20
+ const sourceColumns = columns.map((_, index) => `v${index + 1}`);
21
+ const selectClause = sourceColumns.map((name) => `? AS ${name}`).join(', ');
22
+ const conflictClause = conflictColumns
23
+ .map((column) => `target.${column} = source.${column}`)
24
+ .join(' AND ');
25
+ const insertValues = columns.map((column) => `source.${column}`).join(', ');
26
+ const sourceProjection = columns
27
+ .map((column, index) => `${sourceColumns[index]} AS ${column}`)
28
+ .join(', ');
29
+ return [
30
+ `MERGE INTO ${table} WITH (HOLDLOCK) AS target`,
31
+ `USING (SELECT ${sourceProjection} FROM (SELECT ${selectClause}) seed) AS source`,
32
+ `ON ${conflictClause}`,
33
+ `WHEN NOT MATCHED THEN INSERT (${columnList}) VALUES (${insertValues});`,
34
+ ].join(' ');
35
+ }
36
+ return `INSERT INTO ${table} (${columnList}) VALUES (${placeholders})`;
37
+ };
6
38
  const rowToEntry = (row, tags) => ({
7
39
  uuid: row.uuid,
8
40
  batchId: row.batch_id,
@@ -16,11 +48,9 @@ const rowToEntry = (row, tags) => ({
16
48
  const insertTags = async (db, uuid, tags) => {
17
49
  if (tags.length === 0)
18
50
  return;
51
+ const sql = buildIgnoreInsert(db, TABLE_TAGS, ['entry_uuid', 'tag'], ['entry_uuid', 'tag']);
19
52
  await Promise.all(tags.map(async (tag) => {
20
- await db.execute(`INSERT OR IGNORE INTO ${TABLE_TAGS} (entry_uuid, tag) VALUES (?, ?)`, [
21
- uuid,
22
- tag,
23
- ]);
53
+ await db.execute(sql, [uuid, tag]);
24
54
  }));
25
55
  };
26
56
  const buildEntryFilters = (opts) => {
@@ -158,7 +188,7 @@ const createStorage = (db) => {
158
188
  return rows.map((row) => row.tag);
159
189
  };
160
190
  const addMonitoring = async (tag) => {
161
- await db.execute(`INSERT OR IGNORE INTO ${TABLE_MONITORING} (tag) VALUES (?)`, [tag]);
191
+ await db.execute(buildIgnoreInsert(db, TABLE_MONITORING, ['tag'], ['tag']), [tag]);
162
192
  };
163
193
  const removeMonitoring = async (tag) => {
164
194
  await db.execute(`DELETE FROM ${TABLE_MONITORING} WHERE tag = ?`, [tag]);
@@ -0,0 +1,19 @@
1
+ import type { ITraceStorage } from '../types';
2
+ type TraceLogger = {
3
+ warn: (message: string, context?: Record<string, unknown>) => void;
4
+ };
5
+ type TraceWriteDiagnosticsSnapshot = {
6
+ degraded: boolean;
7
+ lastErrorMessage: string | null;
8
+ lastFailureAt: number | null;
9
+ totalFailures: number;
10
+ };
11
+ export declare const TraceWriteDiagnostics: Readonly<{
12
+ getSnapshot(): TraceWriteDiagnosticsSnapshot;
13
+ reset(): void;
14
+ wrapStorage(storage: ITraceStorage, options: {
15
+ connectionName: string;
16
+ logger?: TraceLogger;
17
+ }): ITraceStorage;
18
+ }>;
19
+ export {};
@@ -0,0 +1,98 @@
1
+ const LOG_WINDOW_MS = 30_000;
2
+ const diagnosticsState = {
3
+ degraded: false,
4
+ lastErrorMessage: null,
5
+ lastFailureAt: null,
6
+ lastLoggedAtByFingerprint: new Map(),
7
+ totalFailures: 0,
8
+ };
9
+ const getErrorMessage = (error) => {
10
+ if (error instanceof Error && error.message.trim() !== '')
11
+ return error.message;
12
+ if (typeof error === 'string' && error.trim() !== '')
13
+ return error;
14
+ try {
15
+ const serialized = JSON.stringify(error);
16
+ if (typeof serialized === 'string' && serialized !== '')
17
+ return serialized;
18
+ }
19
+ catch {
20
+ // ignore serialization failures
21
+ }
22
+ return 'Unknown trace storage error';
23
+ };
24
+ const buildFingerprint = (context) => {
25
+ return [
26
+ context.connectionName,
27
+ context.operation,
28
+ context.watcherType ?? 'unknown',
29
+ getErrorMessage(context.error),
30
+ ].join('|');
31
+ };
32
+ const reportFailure = (logger, context) => {
33
+ const now = Date.now();
34
+ const errorMessage = getErrorMessage(context.error);
35
+ const fingerprint = buildFingerprint(context);
36
+ const lastLoggedAt = diagnosticsState.lastLoggedAtByFingerprint.get(fingerprint);
37
+ diagnosticsState.degraded = true;
38
+ diagnosticsState.lastErrorMessage = errorMessage;
39
+ diagnosticsState.lastFailureAt = now;
40
+ diagnosticsState.totalFailures += 1;
41
+ if (!logger)
42
+ return;
43
+ if (typeof lastLoggedAt === 'number' && now - lastLoggedAt < LOG_WINDOW_MS)
44
+ return;
45
+ diagnosticsState.lastLoggedAtByFingerprint.set(fingerprint, now);
46
+ logger.warn('[trace] Trace storage write degraded', {
47
+ connectionName: context.connectionName,
48
+ error: errorMessage,
49
+ lastFailureAt: now,
50
+ operation: context.operation,
51
+ totalFailures: diagnosticsState.totalFailures,
52
+ watcherType: context.watcherType ?? null,
53
+ });
54
+ };
55
+ const wrapStorageMethod = (method, describeFailure, connectionName, logger) => {
56
+ return async (...args) => {
57
+ try {
58
+ return await method(...args);
59
+ }
60
+ catch (error) {
61
+ reportFailure(logger, {
62
+ ...describeFailure(...args),
63
+ connectionName,
64
+ error,
65
+ });
66
+ throw error;
67
+ }
68
+ };
69
+ };
70
+ export const TraceWriteDiagnostics = Object.freeze({
71
+ getSnapshot() {
72
+ return {
73
+ degraded: diagnosticsState.degraded,
74
+ lastErrorMessage: diagnosticsState.lastErrorMessage,
75
+ lastFailureAt: diagnosticsState.lastFailureAt,
76
+ totalFailures: diagnosticsState.totalFailures,
77
+ };
78
+ },
79
+ reset() {
80
+ diagnosticsState.degraded = false;
81
+ diagnosticsState.lastErrorMessage = null;
82
+ diagnosticsState.lastFailureAt = null;
83
+ diagnosticsState.totalFailures = 0;
84
+ diagnosticsState.lastLoggedAtByFingerprint.clear();
85
+ },
86
+ wrapStorage(storage, options) {
87
+ return Object.freeze({
88
+ ...storage,
89
+ writeEntry: wrapStorageMethod(storage.writeEntry.bind(storage), (entry) => ({ operation: 'writeEntry', watcherType: entry.type }), options.connectionName, options.logger),
90
+ updateEntry: wrapStorageMethod(storage.updateEntry.bind(storage), (_uuid, _patch) => ({ operation: 'updateEntry' }), options.connectionName, options.logger),
91
+ markFamilyStale: wrapStorageMethod(storage.markFamilyStale.bind(storage), (_familyHash, _exceptUuid) => ({ operation: 'markFamilyStale' }), options.connectionName, options.logger),
92
+ prune: wrapStorageMethod(storage.prune.bind(storage), (_olderThanMs, _keepExceptions) => ({ operation: 'prune' }), options.connectionName, options.logger),
93
+ clear: wrapStorageMethod(storage.clear.bind(storage), () => ({ operation: 'clear' }), options.connectionName, options.logger),
94
+ addMonitoring: wrapStorageMethod(storage.addMonitoring.bind(storage), (_tag) => ({ operation: 'addMonitoring' }), options.connectionName, options.logger),
95
+ removeMonitoring: wrapStorageMethod(storage.removeMonitoring.bind(storage), (_tag) => ({ operation: 'removeMonitoring' }), options.connectionName, options.logger),
96
+ });
97
+ },
98
+ });
@@ -1 +1 @@
1
- export { TraceStorage } from './TraceStorage';
1
+ export { TraceStorage } from './TraceStorage.js';
package/dist/types.d.ts CHANGED
@@ -33,6 +33,7 @@ export interface RequestContent {
33
33
  payload: Record<string, unknown>;
34
34
  responseStatus: number;
35
35
  responseHeaders: Record<string, string>;
36
+ responseBody?: unknown;
36
37
  duration: number;
37
38
  memory: number | null;
38
39
  middleware: string[];
@@ -231,31 +232,47 @@ export interface ITraceWatcher {
231
232
  register(opts: ITraceWatcherConfig): () => void;
232
233
  }
233
234
  export type RedactionConfig = {
235
+ keys: string[];
234
236
  headers: string[];
235
237
  body: string[];
236
238
  query: string[];
237
239
  };
240
+ export type TraceFilterRule = {
241
+ enabled?: boolean;
242
+ include?: string[];
243
+ exclude?: string[];
244
+ };
245
+ export type TraceRequestWatcherConfig = TraceFilterRule & {
246
+ all?: TraceFilterRule;
247
+ get?: TraceFilterRule;
248
+ post?: TraceFilterRule;
249
+ put?: TraceFilterRule;
250
+ patch?: TraceFilterRule;
251
+ delete?: TraceFilterRule;
252
+ };
253
+ export type TraceWatcherToggle = boolean | TraceFilterRule;
254
+ export type TraceRequestWatcherToggle = boolean | TraceRequestWatcherConfig;
238
255
  export type WatcherToggles = {
239
- request?: boolean;
240
- query?: boolean;
241
- exception?: boolean;
242
- log?: boolean;
243
- job?: boolean;
244
- cache?: boolean;
245
- schedule?: boolean;
246
- mail?: boolean;
247
- auth?: boolean;
248
- event?: boolean;
249
- model?: boolean;
250
- notification?: boolean;
251
- redis?: boolean;
252
- gate?: boolean;
253
- middleware?: boolean;
254
- command?: boolean;
255
- batch?: boolean;
256
- dump?: boolean;
257
- view?: boolean;
258
- clientRequest?: boolean;
256
+ request?: TraceRequestWatcherToggle;
257
+ query?: TraceWatcherToggle;
258
+ exception?: TraceWatcherToggle;
259
+ log?: TraceWatcherToggle;
260
+ job?: TraceWatcherToggle;
261
+ cache?: TraceWatcherToggle;
262
+ schedule?: TraceWatcherToggle;
263
+ mail?: TraceWatcherToggle;
264
+ auth?: TraceWatcherToggle;
265
+ event?: TraceWatcherToggle;
266
+ model?: TraceWatcherToggle;
267
+ notification?: TraceWatcherToggle;
268
+ redis?: TraceWatcherToggle;
269
+ gate?: TraceWatcherToggle;
270
+ middleware?: TraceWatcherToggle;
271
+ command?: TraceWatcherToggle;
272
+ batch?: TraceWatcherToggle;
273
+ dump?: TraceWatcherToggle;
274
+ view?: TraceWatcherToggle;
275
+ clientRequest?: TraceWatcherToggle;
259
276
  };
260
277
  export interface ITraceConfig {
261
278
  enabled: boolean;
package/dist/ui.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * Import this subpath when you only need trace dashboard registration
5
5
  * without pulling in the package root re-export surface.
6
6
  */
7
- export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
7
+ export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes.js';
@@ -1,4 +1,4 @@
1
- import { TraceContext } from '../context';
1
+ import { TraceContext } from '../context.js';
2
2
  const resolveAuthTag = () => {
3
3
  const userId = TraceContext.getUserId();
4
4
  if (userId === undefined || userId === '')
@@ -0,0 +1,4 @@
1
+ import type { ITraceConfig, ITraceEntry } from '../types';
2
+ export declare const TraceEntryFilter: Readonly<{
3
+ shouldCapture(entry: ITraceEntry, config: ITraceConfig): boolean;
4
+ }>;