@zintrust/trace 0.4.75

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 (128) hide show
  1. package/README.md +288 -0
  2. package/dist/build-manifest.json +365 -0
  3. package/dist/cli-register.d.ts +9 -0
  4. package/dist/cli-register.js +32 -0
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +38 -0
  7. package/dist/context.d.ts +18 -0
  8. package/dist/context.js +86 -0
  9. package/dist/dashboard/handlers.d.ts +15 -0
  10. package/dist/dashboard/handlers.js +179 -0
  11. package/dist/dashboard/routes.d.ts +19 -0
  12. package/dist/dashboard/routes.js +50 -0
  13. package/dist/dashboard/ui.d.ts +2 -0
  14. package/dist/dashboard/ui.js +870 -0
  15. package/dist/index.d.ts +35 -0
  16. package/dist/index.js +50 -0
  17. package/dist/migrations/20260331000001_create_zin_debugger_entries_table.d.ts +10 -0
  18. package/dist/migrations/20260331000001_create_zin_debugger_entries_table.js +28 -0
  19. package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.d.ts +10 -0
  20. package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.js +21 -0
  21. package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.d.ts +10 -0
  22. package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.js +17 -0
  23. package/dist/migrations/index.d.ts +6 -0
  24. package/dist/migrations/index.js +4 -0
  25. package/dist/plugin.d.ts +1 -0
  26. package/dist/plugin.js +3 -0
  27. package/dist/register.d.ts +1 -0
  28. package/dist/register.js +140 -0
  29. package/dist/storage/DebuggerStorage.d.ts +13 -0
  30. package/dist/storage/DebuggerStorage.js +195 -0
  31. package/dist/storage/TraceStorage.d.ts +13 -0
  32. package/dist/storage/TraceStorage.js +195 -0
  33. package/dist/storage/index.d.ts +2 -0
  34. package/dist/storage/index.js +1 -0
  35. package/dist/types.d.ts +270 -0
  36. package/dist/types.js +25 -0
  37. package/dist/ui.d.ts +8 -0
  38. package/dist/ui.js +7 -0
  39. package/dist/utils/authTag.d.ts +5 -0
  40. package/dist/utils/authTag.js +18 -0
  41. package/dist/utils/familyHash.d.ts +1 -0
  42. package/dist/utils/familyHash.js +8 -0
  43. package/dist/utils/redact.d.ts +6 -0
  44. package/dist/utils/redact.js +49 -0
  45. package/dist/utils/requestFilter.d.ts +4 -0
  46. package/dist/utils/requestFilter.js +26 -0
  47. package/dist/utils/stackFrame.d.ts +6 -0
  48. package/dist/utils/stackFrame.js +38 -0
  49. package/dist/watchers/AuthWatcher.d.ts +6 -0
  50. package/dist/watchers/AuthWatcher.js +49 -0
  51. package/dist/watchers/BatchWatcher.d.ts +6 -0
  52. package/dist/watchers/BatchWatcher.js +46 -0
  53. package/dist/watchers/CacheWatcher.d.ts +6 -0
  54. package/dist/watchers/CacheWatcher.js +51 -0
  55. package/dist/watchers/CommandWatcher.d.ts +6 -0
  56. package/dist/watchers/CommandWatcher.js +49 -0
  57. package/dist/watchers/DumpWatcher.d.ts +7 -0
  58. package/dist/watchers/DumpWatcher.js +41 -0
  59. package/dist/watchers/EventWatcher.d.ts +6 -0
  60. package/dist/watchers/EventWatcher.js +42 -0
  61. package/dist/watchers/ExceptionWatcher.d.ts +4 -0
  62. package/dist/watchers/ExceptionWatcher.js +103 -0
  63. package/dist/watchers/GateWatcher.d.ts +6 -0
  64. package/dist/watchers/GateWatcher.js +45 -0
  65. package/dist/watchers/HttpClientWatcher.d.ts +6 -0
  66. package/dist/watchers/HttpClientWatcher.js +50 -0
  67. package/dist/watchers/HttpWatcher.d.ts +2 -0
  68. package/dist/watchers/HttpWatcher.js +71 -0
  69. package/dist/watchers/JobWatcher.d.ts +10 -0
  70. package/dist/watchers/JobWatcher.js +108 -0
  71. package/dist/watchers/LogWatcher.d.ts +2 -0
  72. package/dist/watchers/LogWatcher.js +50 -0
  73. package/dist/watchers/MailWatcher.d.ts +6 -0
  74. package/dist/watchers/MailWatcher.js +45 -0
  75. package/dist/watchers/MiddlewareWatcher.d.ts +6 -0
  76. package/dist/watchers/MiddlewareWatcher.js +41 -0
  77. package/dist/watchers/ModelWatcher.d.ts +6 -0
  78. package/dist/watchers/ModelWatcher.js +42 -0
  79. package/dist/watchers/NotificationWatcher.d.ts +6 -0
  80. package/dist/watchers/NotificationWatcher.js +42 -0
  81. package/dist/watchers/QueryWatcher.d.ts +2 -0
  82. package/dist/watchers/QueryWatcher.js +72 -0
  83. package/dist/watchers/RedisWatcher.d.ts +7 -0
  84. package/dist/watchers/RedisWatcher.js +38 -0
  85. package/dist/watchers/ScheduleWatcher.d.ts +6 -0
  86. package/dist/watchers/ScheduleWatcher.js +46 -0
  87. package/dist/watchers/ViewWatcher.d.ts +6 -0
  88. package/dist/watchers/ViewWatcher.js +36 -0
  89. package/package.json +59 -0
  90. package/src/cli-register.ts +63 -0
  91. package/src/config.ts +46 -0
  92. package/src/context.ts +101 -0
  93. package/src/dashboard/handlers.ts +197 -0
  94. package/src/dashboard/routes.ts +101 -0
  95. package/src/dashboard/ui.ts +879 -0
  96. package/src/dashboard/zintrust-debuger.svg +30 -0
  97. package/src/index.ts +88 -0
  98. package/src/plugin.ts +9 -0
  99. package/src/register.ts +219 -0
  100. package/src/storage/TraceStorage.ts +306 -0
  101. package/src/storage/index.ts +2 -0
  102. package/src/types.ts +317 -0
  103. package/src/ui.ts +9 -0
  104. package/src/utils/authTag.ts +20 -0
  105. package/src/utils/familyHash.ts +8 -0
  106. package/src/utils/redact.ts +64 -0
  107. package/src/utils/requestFilter.ts +33 -0
  108. package/src/utils/stackFrame.ts +44 -0
  109. package/src/watchers/AuthWatcher.ts +50 -0
  110. package/src/watchers/BatchWatcher.ts +52 -0
  111. package/src/watchers/CacheWatcher.ts +58 -0
  112. package/src/watchers/CommandWatcher.ts +55 -0
  113. package/src/watchers/DumpWatcher.ts +42 -0
  114. package/src/watchers/EventWatcher.ts +43 -0
  115. package/src/watchers/ExceptionWatcher.ts +114 -0
  116. package/src/watchers/GateWatcher.ts +50 -0
  117. package/src/watchers/HttpClientWatcher.ts +56 -0
  118. package/src/watchers/HttpWatcher.ts +94 -0
  119. package/src/watchers/JobWatcher.ts +121 -0
  120. package/src/watchers/LogWatcher.ts +61 -0
  121. package/src/watchers/MailWatcher.ts +47 -0
  122. package/src/watchers/MiddlewareWatcher.ts +42 -0
  123. package/src/watchers/ModelWatcher.ts +48 -0
  124. package/src/watchers/NotificationWatcher.ts +43 -0
  125. package/src/watchers/QueryWatcher.ts +85 -0
  126. package/src/watchers/RedisWatcher.ts +39 -0
  127. package/src/watchers/ScheduleWatcher.ts +54 -0
  128. package/src/watchers/ViewWatcher.ts +37 -0
package/src/types.ts ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Types for @zintrust/trace
3
+ * Sealed type definitions — no side effects.
4
+ */
5
+ import type { IDatabase } from '@zintrust/core';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Entry types used by the trace event stream.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export const EntryType = Object.freeze({
12
+ REQUEST: 'request',
13
+ QUERY: 'query',
14
+ EXCEPTION: 'exception',
15
+ LOG: 'log',
16
+ JOB: 'job',
17
+ CACHE: 'cache',
18
+ SCHEDULE: 'schedule',
19
+ MAIL: 'mail',
20
+ AUTH: 'auth',
21
+ EVENT: 'event',
22
+ MODEL: 'model',
23
+ NOTIFICATION: 'notification',
24
+ REDIS: 'redis',
25
+ GATE: 'gate',
26
+ MIDDLEWARE: 'middleware',
27
+ COMMAND: 'command',
28
+ BATCH: 'batch',
29
+ DUMP: 'dump',
30
+ VIEW: 'view',
31
+ CLIENT_REQUEST: 'client_request',
32
+ } as const);
33
+
34
+ export type EntryTypeValue = (typeof EntryType)[keyof typeof EntryType];
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Per-type content shapes
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export interface RequestContent {
41
+ method: string;
42
+ uri: string;
43
+ headers: Record<string, string>;
44
+ payload: Record<string, unknown>;
45
+ responseStatus: number;
46
+ responseHeaders: Record<string, string>;
47
+ duration: number;
48
+ memory: number | null;
49
+ middleware: string[];
50
+ hostname: string;
51
+ userId?: string;
52
+ }
53
+
54
+ export interface QueryContent {
55
+ connection: string;
56
+ sql: string;
57
+ time: number;
58
+ duration: number;
59
+ slow: boolean;
60
+ hash: string;
61
+ hostname: string;
62
+ }
63
+
64
+ export interface ExceptionContent {
65
+ class: string;
66
+ file: string;
67
+ line: number;
68
+ message: string;
69
+ trace: Array<{ file: string; line: number; function?: string }>;
70
+ linePreview: Record<string, string>;
71
+ occurrences: number;
72
+ hostname: string;
73
+ userId?: string;
74
+ }
75
+
76
+ export interface LogContent {
77
+ level: string;
78
+ message: string;
79
+ context?: Record<string, unknown>;
80
+ hostname: string;
81
+ }
82
+
83
+ export interface JobContent {
84
+ status: 'pending' | 'processed' | 'failed';
85
+ connection: string;
86
+ queue: string;
87
+ name: string;
88
+ tries?: number;
89
+ timeout?: number;
90
+ data?: unknown;
91
+ exception?: { message: string; trace: Array<{ file: string; line: number }> };
92
+ hostname: string;
93
+ }
94
+
95
+ export interface CacheContent {
96
+ operation: 'get' | 'set' | 'delete' | 'clear' | 'has';
97
+ key: string;
98
+ hit?: boolean;
99
+ duration: number;
100
+ hostname: string;
101
+ }
102
+
103
+ export interface ScheduleContent {
104
+ name: string;
105
+ expression: string;
106
+ status: 'ran' | 'failed' | 'skipped';
107
+ duration: number;
108
+ output?: string;
109
+ hostname: string;
110
+ }
111
+
112
+ export interface MailContent {
113
+ to: string;
114
+ subject: string;
115
+ template?: string;
116
+ hostname: string;
117
+ }
118
+
119
+ export interface AuthContent {
120
+ event: 'login' | 'logout' | 'failed';
121
+ userId?: string;
122
+ hostname: string;
123
+ }
124
+
125
+ export interface EventContent {
126
+ name: string;
127
+ payload?: unknown;
128
+ listenerCount: number;
129
+ hostname: string;
130
+ }
131
+
132
+ export interface ModelContent {
133
+ action: 'create' | 'update' | 'delete';
134
+ model: string;
135
+ id?: string | number;
136
+ changes?: Record<string, unknown>;
137
+ hostname: string;
138
+ }
139
+
140
+ export interface NotificationContent {
141
+ channels: string[];
142
+ notifiable?: string;
143
+ notification: string;
144
+ hostname: string;
145
+ }
146
+
147
+ export interface RedisContent {
148
+ command: string;
149
+ duration: number;
150
+ hostname: string;
151
+ }
152
+
153
+ export interface GateContent {
154
+ ability: string;
155
+ result: 'allowed' | 'denied';
156
+ userId?: string;
157
+ subject?: string;
158
+ hostname: string;
159
+ }
160
+
161
+ export interface MiddlewareContent {
162
+ name: string;
163
+ event: 'before' | 'after';
164
+ duration?: number;
165
+ hostname: string;
166
+ }
167
+
168
+ export interface CommandContent {
169
+ name: string;
170
+ arguments: Record<string, unknown>;
171
+ exitCode: number;
172
+ duration: number;
173
+ output?: string;
174
+ hostname: string;
175
+ }
176
+
177
+ export interface BatchContent {
178
+ name: string;
179
+ total: number;
180
+ processed: number;
181
+ failed: number;
182
+ status: 'pending' | 'processing' | 'finished' | 'failed';
183
+ hostname: string;
184
+ }
185
+
186
+ export interface DumpContent {
187
+ value: unknown;
188
+ file?: string;
189
+ line?: number;
190
+ hostname: string;
191
+ }
192
+
193
+ export interface ViewContent {
194
+ template: string;
195
+ duration: number;
196
+ hostname: string;
197
+ }
198
+
199
+ export interface ClientRequestContent {
200
+ method: string;
201
+ url: string;
202
+ requestHeaders: Record<string, string>;
203
+ responseStatus: number;
204
+ duration: number;
205
+ hostname: string;
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Core domain records
210
+ // ---------------------------------------------------------------------------
211
+
212
+ export interface ITraceEntry<T = unknown> {
213
+ uuid: string;
214
+ batchId: string;
215
+ familyHash?: string;
216
+ type: EntryTypeValue;
217
+ content: T;
218
+ tags: string[];
219
+ isLatest: boolean;
220
+ createdAt: number;
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Storage interface
225
+ // ---------------------------------------------------------------------------
226
+
227
+ export interface QueryEntriesOptions {
228
+ type?: EntryTypeValue;
229
+ tag?: string;
230
+ batchId?: string;
231
+ from?: number;
232
+ to?: number;
233
+ page?: number;
234
+ perPage?: number;
235
+ }
236
+
237
+ export interface ITraceStorage {
238
+ writeEntry(entry: ITraceEntry): Promise<void>;
239
+ updateEntry(
240
+ uuid: string,
241
+ patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
242
+ ): Promise<void>;
243
+ markFamilyStale(familyHash: string, exceptUuid: string): Promise<void>;
244
+ queryEntries(opts: QueryEntriesOptions): Promise<{ data: ITraceEntry[]; total: number }>;
245
+ getEntry(uuid: string): Promise<ITraceEntry | null>;
246
+ getBatch(batchId: string): Promise<ITraceEntry[]>;
247
+ prune(olderThanMs: number, keepExceptions?: boolean): Promise<number>;
248
+ clear(): Promise<void>;
249
+ getMonitoring(): Promise<string[]>;
250
+ addMonitoring(tag: string): Promise<void>;
251
+ removeMonitoring(tag: string): Promise<void>;
252
+ stats(): Promise<Record<EntryTypeValue, number>>;
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Watcher interface
257
+ // ---------------------------------------------------------------------------
258
+
259
+ export interface ITraceWatcherConfig {
260
+ storage: ITraceStorage;
261
+ config: ITraceConfig;
262
+ db?: IDatabase;
263
+ /** Optional: provide to allow HttpWatcher to register as global middleware. */
264
+ registerMiddleware?: (
265
+ fn: (req: unknown, res: unknown, next: () => Promise<void>) => Promise<void>
266
+ ) => void;
267
+ }
268
+
269
+ export interface ITraceWatcher {
270
+ register(opts: ITraceWatcherConfig): () => void;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Config interface
275
+ // ---------------------------------------------------------------------------
276
+
277
+ export type RedactionConfig = {
278
+ headers: string[];
279
+ body: string[];
280
+ query: string[];
281
+ };
282
+
283
+ export type WatcherToggles = {
284
+ request?: boolean;
285
+ query?: boolean;
286
+ exception?: boolean;
287
+ log?: boolean;
288
+ job?: boolean;
289
+ cache?: boolean;
290
+ schedule?: boolean;
291
+ mail?: boolean;
292
+ auth?: boolean;
293
+ event?: boolean;
294
+ model?: boolean;
295
+ notification?: boolean;
296
+ redis?: boolean;
297
+ gate?: boolean;
298
+ middleware?: boolean;
299
+ command?: boolean;
300
+ batch?: boolean;
301
+ dump?: boolean;
302
+ view?: boolean;
303
+ clientRequest?: boolean;
304
+ };
305
+
306
+ export interface ITraceConfig {
307
+ enabled: boolean;
308
+ connection?: string;
309
+ pruneAfterHours: number;
310
+ ignoreRoutes: string[];
311
+ slowQueryThreshold: number;
312
+ logMinLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
313
+ watchers: WatcherToggles;
314
+ redaction: RedactionConfig;
315
+ }
316
+
317
+ export type TraceConfigOverrides = Partial<ITraceConfig>;
package/src/ui.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lightweight UI/dashboard entrypoint for @zintrust/trace.
3
+ *
4
+ * Import this subpath when you only need trace dashboard registration
5
+ * without pulling in the package root re-export surface.
6
+ */
7
+
8
+ export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
9
+ export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
@@ -0,0 +1,20 @@
1
+ import { TraceContext } from '../context';
2
+
3
+ const resolveAuthTag = (): string | undefined => {
4
+ const userId = TraceContext.getUserId();
5
+ if (userId === undefined || userId === '') return undefined;
6
+ return `Auth:${userId}`;
7
+ };
8
+
9
+ const appendAuthTag = (tags: string[]): string[] => {
10
+ const authTag = resolveAuthTag();
11
+ if (authTag === undefined || tags.includes(authTag)) return tags;
12
+ return [...tags, authTag];
13
+ };
14
+
15
+ export const AuthTag = Object.freeze({
16
+ append: appendAuthTag,
17
+ resolve: resolveAuthTag,
18
+ });
19
+
20
+ export default AuthTag;
@@ -0,0 +1,8 @@
1
+ export const familyHash = (input: string): string => {
2
+ let hash = 2166136261;
3
+ for (let index = 0; index < input.length; index++) {
4
+ hash ^= input.codePointAt(index) ?? 0;
5
+ hash = (hash * 16777619) >>> 0;
6
+ }
7
+ return hash.toString(16).padStart(8, '0');
8
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Redaction helpers for @zintrust/trace watchers.
3
+ */
4
+
5
+ const REDACTED = '[REDACTED]';
6
+
7
+ const redactQuerySegment = (segment: string, fields: Set<string>): string => {
8
+ const separatorIndex = segment.indexOf('=');
9
+ if (separatorIndex <= 0) return segment;
10
+
11
+ const key = segment.slice(0, separatorIndex);
12
+ const value = segment.slice(separatorIndex + 1);
13
+ if (!fields.has(key.toLowerCase())) return `${key}=${value}`;
14
+
15
+ return `${key}=${REDACTED}`;
16
+ };
17
+
18
+ export const redactHeaders = (
19
+ headers: Record<string, string>,
20
+ fields: string[]
21
+ ): Record<string, string> => {
22
+ const lower = new Set(fields.map((f) => f.toLowerCase()));
23
+ const out: Record<string, string> = {};
24
+ for (const [k, v] of Object.entries(headers)) {
25
+ out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
26
+ }
27
+ return out;
28
+ };
29
+
30
+ export const redactObject = (
31
+ obj: Record<string, unknown>,
32
+ fields: string[]
33
+ ): Record<string, unknown> => {
34
+ const lower = new Set(fields.map((f) => f.toLowerCase()));
35
+ const out: Record<string, unknown> = {};
36
+ for (const [k, v] of Object.entries(obj)) {
37
+ out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
38
+ }
39
+ return out;
40
+ };
41
+
42
+ export const redactString = (value: string, fields: string[]): string => {
43
+ const lower = new Set(fields.map((f) => f.toLowerCase()));
44
+ if (value === '') return value;
45
+
46
+ let output = '';
47
+ let segmentStart = 0;
48
+
49
+ for (let index = 0; index <= value.length; index += 1) {
50
+ const isBoundary = index === value.length || value[index] === '&';
51
+ if (!isBoundary) continue;
52
+
53
+ const segment = value.slice(segmentStart, index);
54
+ output += redactQuerySegment(segment, lower);
55
+
56
+ if (index < value.length) {
57
+ output += '&';
58
+ }
59
+
60
+ segmentStart = index + 1;
61
+ }
62
+
63
+ return output;
64
+ };
@@ -0,0 +1,33 @@
1
+ import { TraceContext } from '../context';
2
+
3
+ const normalizePath = (input: string): string => {
4
+ const trimmed = input.trim();
5
+ const [pathOnly] = trimmed.split('?');
6
+ if (!pathOnly || pathOnly === '') return '/';
7
+ return pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
8
+ };
9
+
10
+ const matchesIgnoredPath = (path: string, ignoreRoutes: string[]): boolean => {
11
+ const normalizedPath = normalizePath(path);
12
+
13
+ return ignoreRoutes.some((route) => {
14
+ const normalizedRoute = normalizePath(route);
15
+ return (
16
+ normalizedPath === normalizedRoute ||
17
+ normalizedPath.startsWith(
18
+ normalizedRoute.endsWith('/') ? normalizedRoute : `${normalizedRoute}/`
19
+ )
20
+ );
21
+ });
22
+ };
23
+
24
+ const shouldIgnoreCurrentRequest = (ignoreRoutes: string[]): boolean => {
25
+ const currentPath = TraceContext.getRequestPath();
26
+ if (typeof currentPath !== 'string' || currentPath === '') return false;
27
+ return matchesIgnoredPath(currentPath, ignoreRoutes);
28
+ };
29
+
30
+ export const RequestFilter = Object.freeze({
31
+ matchesIgnoredPath,
32
+ shouldIgnoreCurrentRequest,
33
+ });
@@ -0,0 +1,44 @@
1
+ type StackFrame = { file: string; line: number };
2
+
3
+ const FRAME_PREFIX = 'at ';
4
+
5
+ const parsePositiveInt = (value: string): number | null => {
6
+ if (value === '') return null;
7
+
8
+ for (const char of value) {
9
+ if (char < '0' || char > '9') return null;
10
+ }
11
+
12
+ const parsed = Number.parseInt(value, 10);
13
+ return Number.isNaN(parsed) ? null : parsed;
14
+ };
15
+
16
+ const parseFrameLocation = (value: string): StackFrame | null => {
17
+ const columnSeparatorIndex = value.lastIndexOf(':');
18
+ if (columnSeparatorIndex <= 0) return null;
19
+
20
+ const lineSeparatorIndex = value.lastIndexOf(':', columnSeparatorIndex - 1);
21
+ if (lineSeparatorIndex <= 0) return null;
22
+
23
+ const file = value.slice(0, lineSeparatorIndex).trim();
24
+ const line = parsePositiveInt(value.slice(lineSeparatorIndex + 1, columnSeparatorIndex));
25
+ const column = parsePositiveInt(value.slice(columnSeparatorIndex + 1));
26
+
27
+ if (file === '' || line === null || column === null) return null;
28
+ return { file, line };
29
+ };
30
+
31
+ export const parseStackFrameLine = (line: string): StackFrame | null => {
32
+ const trimmed = line.trim();
33
+ if (!trimmed.startsWith(FRAME_PREFIX)) return null;
34
+
35
+ const body = trimmed.slice(FRAME_PREFIX.length).trim();
36
+ if (body === '') return null;
37
+
38
+ const wrappedStartIndex = body.lastIndexOf(' (');
39
+ if (wrappedStartIndex !== -1 && body.endsWith(')')) {
40
+ return parseFrameLocation(body.slice(wrappedStartIndex + 2, -1));
41
+ }
42
+
43
+ return parseFrameLocation(body);
44
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * AuthWatcher — records login/logout/failed auth events.
3
+ * Credentials are never stored; only the outcome.
4
+ */
5
+ import { TraceContext } from '../context';
6
+ import type { AuthContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
7
+ import { EntryType } from '../types';
8
+ import { RequestFilter } from '../utils/requestFilter';
9
+
10
+ let _storage: ITraceWatcherConfig['storage'] | null = null;
11
+ let _ignoreRoutes: string[] = [];
12
+
13
+ const emit = (event: AuthContent['event'], userId?: string): void => {
14
+ if (!_storage) return;
15
+ if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
16
+ const content: AuthContent = {
17
+ event,
18
+ userId,
19
+ hostname: TraceContext.getHostname(),
20
+ };
21
+ const tags: string[] = [];
22
+ if (userId) tags.push(`Auth:${userId}`);
23
+ if (event === 'failed') tags.push('failed');
24
+
25
+ _storage
26
+ .writeEntry({
27
+ uuid: crypto.randomUUID(),
28
+ batchId: TraceContext.getBatchId(),
29
+ type: EntryType.AUTH,
30
+ content,
31
+ tags,
32
+ isLatest: true,
33
+ createdAt: TraceContext.now(),
34
+ })
35
+ .catch(() => undefined);
36
+ };
37
+
38
+ export const AuthWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
39
+ emit,
40
+
41
+ register({ storage, config }: ITraceWatcherConfig): () => void {
42
+ if (config.watchers.auth === false) return () => undefined;
43
+ _storage = storage;
44
+ _ignoreRoutes = config.ignoreRoutes;
45
+ return () => {
46
+ _storage = null;
47
+ _ignoreRoutes = [];
48
+ };
49
+ },
50
+ });
@@ -0,0 +1,52 @@
1
+ import { TraceContext } from '../context';
2
+ import type { BatchContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
3
+ import { EntryType } from '../types';
4
+ import { RequestFilter } from '../utils/requestFilter';
5
+
6
+ let _storage: ITraceWatcherConfig['storage'] | null = null;
7
+ let _ignoreRoutes: string[] = [];
8
+
9
+ const emit = (
10
+ name: string,
11
+ total: number,
12
+ processed: number,
13
+ failed: number,
14
+ status: BatchContent['status']
15
+ ): void => {
16
+ if (!_storage) return;
17
+ if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
18
+ const tags = [name];
19
+ if (failed > 0) tags.push('failed');
20
+ const content: BatchContent = {
21
+ name,
22
+ total,
23
+ processed,
24
+ failed,
25
+ status,
26
+ hostname: TraceContext.getHostname(),
27
+ };
28
+ _storage
29
+ .writeEntry({
30
+ uuid: crypto.randomUUID(),
31
+ batchId: TraceContext.getBatchId(),
32
+ type: EntryType.BATCH,
33
+ content,
34
+ tags,
35
+ isLatest: true,
36
+ createdAt: TraceContext.now(),
37
+ })
38
+ .catch(() => undefined);
39
+ };
40
+
41
+ export const BatchWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
42
+ emit,
43
+ register({ storage, config }: ITraceWatcherConfig): () => void {
44
+ if (config.watchers.batch === false) return () => undefined;
45
+ _storage = storage;
46
+ _ignoreRoutes = config.ignoreRoutes;
47
+ return () => {
48
+ _storage = null;
49
+ _ignoreRoutes = [];
50
+ };
51
+ },
52
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CacheWatcher — records cache operations.
3
+ * Call CacheWatcher.emit() from within your cache driver instrumentation.
4
+ */
5
+ import { TraceContext } from '../context';
6
+ import type { CacheContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
7
+ import { EntryType } from '../types';
8
+ import { AuthTag } from '../utils/authTag';
9
+ import { redactString } from '../utils/redact';
10
+ import { RequestFilter } from '../utils/requestFilter';
11
+
12
+ let _storage: ITraceWatcherConfig['storage'] | null = null;
13
+ let _redactionFields: string[] = [];
14
+ let _ignoreRoutes: string[] = [];
15
+
16
+ const emit = (
17
+ operation: CacheContent['operation'],
18
+ key: string,
19
+ duration: number,
20
+ hit?: boolean
21
+ ): void => {
22
+ if (!_storage) return;
23
+ if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
24
+ const safeKey = redactString(key, _redactionFields);
25
+ const content: CacheContent = {
26
+ operation,
27
+ key: safeKey,
28
+ hit,
29
+ duration,
30
+ hostname: TraceContext.getHostname(),
31
+ };
32
+ _storage
33
+ .writeEntry({
34
+ uuid: crypto.randomUUID(),
35
+ batchId: TraceContext.getBatchId(),
36
+ type: EntryType.CACHE,
37
+ content,
38
+ tags: AuthTag.append([]),
39
+ isLatest: true,
40
+ createdAt: TraceContext.now(),
41
+ })
42
+ .catch(() => undefined);
43
+ };
44
+
45
+ export const CacheWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
46
+ emit,
47
+
48
+ register({ storage, config }: ITraceWatcherConfig): () => void {
49
+ if (config.watchers.cache === false) return () => undefined;
50
+ _storage = storage;
51
+ _redactionFields = config.redaction.query;
52
+ _ignoreRoutes = config.ignoreRoutes;
53
+ return () => {
54
+ _storage = null;
55
+ _ignoreRoutes = [];
56
+ };
57
+ },
58
+ });