hierarchical-area-logger 0.2.0

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,453 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { prettyStack, prettyError, createRootLogEntry } from '../src/utils';
3
+ import { Details, Method, RootPayload } from '../src/types';
4
+
5
+ describe('prettyStack', () => {
6
+ it('should return empty array for undefined input', () => {
7
+ const result = prettyStack(undefined);
8
+ expect(result).toEqual([]);
9
+ });
10
+
11
+ it('should return empty array for null input', () => {
12
+ const result = prettyStack(null as unknown as string);
13
+ expect(result).toEqual([]);
14
+ });
15
+
16
+ it('should return empty array for empty string input', () => {
17
+ const result = prettyStack('');
18
+ expect(result).toEqual([]);
19
+ });
20
+
21
+ it('should filter out lines starting with "Error: "', () => {
22
+ const stack = `Error: Test error
23
+ at Object.test (/path/to/file.js:1:1)
24
+ at Object.test2 (/path/to/file.js:2:2)`;
25
+
26
+ const result = prettyStack(stack);
27
+ expect(result.join('\n')).not.toContain('Error: Test error');
28
+ expect(result).toContain('at Object.test (/path/to/file.js:1:1)');
29
+ expect(result).toContain('at Object.test2 (/path/to/file.js:2:2)');
30
+ });
31
+
32
+ it('should trim whitespace from each line', () => {
33
+ const stack = ` at Object.test (/path/to/file.js:1:1)
34
+ at Object.test2 (/path/to/file.js:2:2)`;
35
+
36
+ const result = prettyStack(stack);
37
+ expect(result).toContain('at Object.test (/path/to/file.js:1:1)');
38
+ expect(result).toContain('at Object.test2 (/path/to/file.js:2:2)');
39
+ });
40
+
41
+ it('should remove file:// prefix from lines', () => {
42
+ const stack = `at Object.test (file:///path/to/file.js:1:1)
43
+ at Object.test2 (file:///another/path.js:2:2)`;
44
+
45
+ const result = prettyStack(stack);
46
+
47
+ expect(result).toEqual(
48
+ expect.arrayContaining([
49
+ 'at Object.test (/path/to/file.js:1:1)',
50
+ 'at Object.test2 (/another/path.js:2:2)',
51
+ ])
52
+ );
53
+ expect(result.join('\n')).not.toContain('file://');
54
+ });
55
+
56
+ it('should replace .wrangler paths in stack traces', () => {
57
+ const stack = `at Object.test (file:///some/path/.wrangler/test/file.js:1:1)
58
+ at Object.test2 (file:///another/path/file.js:2:2)`;
59
+
60
+ const result = prettyStack(stack);
61
+ // Based on actual behavior, it keeps the .wrangler prefix
62
+ expect(result).toEqual(
63
+ expect.arrayContaining([
64
+ 'at Object.test (/.wrangler/test/file.js:1:1)',
65
+ 'at Object.test2 (/another/path/file.js:2:2)',
66
+ ])
67
+ );
68
+ });
69
+
70
+ it('should replace node_modules paths in stack traces', () => {
71
+ const stack = `at Object.test (file:///some/path/node_modules/module/file.js:1:1)
72
+ at Object.test2 (file:///another/path/file.js:2:2)`;
73
+
74
+ const result = prettyStack(stack);
75
+ // Based on actual behavior, it keeps the /node_modules prefix
76
+ expect(result).toEqual(
77
+ expect.arrayContaining([
78
+ 'at Object.test (/node_modules/module/file.js:1:1)',
79
+ 'at Object.test2 (/another/path/file.js:2:2)',
80
+ ])
81
+ );
82
+ });
83
+
84
+ it('should handle complex stack traces with mixed paths', () => {
85
+ const stack = `Error: Complex error
86
+ at Object.test1 (file:///project/.wrangler/test.js:1:1)
87
+ at Object.test2 (file:///project/node_modules/module/index.js:2:2)
88
+ at Object.test3 (file:///normal/path/file.js:3:3)`;
89
+
90
+ const result = prettyStack(stack);
91
+
92
+ expect(result).toEqual(
93
+ expect.arrayContaining([
94
+ 'at Object.test1 (/.wrangler/test.js:1:1)',
95
+ 'at Object.test2 (/node_modules/module/index.js:2:2)',
96
+ 'at Object.test3 (/normal/path/file.js:3:3)',
97
+ ])
98
+ );
99
+ expect(result.join('\n')).not.toContain('file://');
100
+ });
101
+
102
+ it('should handle stack traces without matching regex pattern', () => {
103
+ const stack = `at Object.test (file:///simple/path/file.js:1:1)
104
+ at Object.test2 (file:///another/path/file.js:2:2)`;
105
+
106
+ const result = prettyStack(stack);
107
+ expect(result).toEqual(
108
+ expect.arrayContaining([
109
+ 'at Object.test (/simple/path/file.js:1:1)',
110
+ 'at Object.test2 (/another/path/file.js:2:2)',
111
+ ])
112
+ );
113
+ });
114
+
115
+ it('should preserve order of stack lines', () => {
116
+ const stack = `Error: Test error
117
+ at Object.first (/path/first.js:1:1)
118
+ at Object.second (/path/second.js:2:2)
119
+ at Object.third (/path/third.js:3:3)`;
120
+
121
+ const result = prettyStack(stack);
122
+ expect(result[0]).toContain('at Object.first (/path/first.js:1:1)');
123
+ expect(result[1]).toContain('at Object.second (/path/second.js:2:2)');
124
+ expect(result[2]).toContain('at Object.third (/path/third.js:3:3)');
125
+ });
126
+ });
127
+
128
+ describe('prettyError', () => {
129
+ it('should extract message and stack from standard Error', () => {
130
+ const errorMessage = 'Test error message';
131
+ const stackMessage =
132
+ 'Error: Test error message\n at Object.test (/path/to/file.js:1:1)';
133
+
134
+ const error = new Error(errorMessage);
135
+ error.stack = stackMessage;
136
+
137
+ const result = prettyError(error);
138
+
139
+ expect(result).toHaveProperty('message', errorMessage);
140
+ expect(result).toHaveProperty('stack');
141
+ expect(Array.isArray(result.stack)).toBe(true);
142
+ });
143
+
144
+ it('should handle Error without stack', () => {
145
+ const error = new Error('Test error');
146
+ delete (error as Error & { stack?: string }).stack;
147
+
148
+ const result = prettyError(error);
149
+
150
+ expect(result.message).toBe('Test error');
151
+ expect(result.stack).toEqual([]);
152
+ });
153
+
154
+ it('should handle Error with empty message', () => {
155
+ const error = new Error('');
156
+ error.stack = 'Error: \n at Object.test (/path/to/file.js:1:1)';
157
+
158
+ const result = prettyError(error);
159
+
160
+ expect(result.message).toBe('');
161
+ expect(result.stack).toBeDefined();
162
+ });
163
+
164
+ it('should handle custom Error classes', () => {
165
+ class CustomError extends Error {
166
+ constructor(message: string) {
167
+ super(message);
168
+ this.name = 'CustomError';
169
+ }
170
+ }
171
+
172
+ const error = new CustomError('Custom error message');
173
+ error.stack =
174
+ 'CustomError: Custom error message\n at Object.test (/path/to/file.js:1:1)';
175
+
176
+ const result = prettyError(error);
177
+
178
+ expect(result.message).toBe('Custom error message');
179
+ expect(result.stack).toBeDefined();
180
+ });
181
+
182
+ it('should process the stack through prettyStack', () => {
183
+ const error = new Error('Test error');
184
+ error.stack = `Error: Test error
185
+ at Object.test (file:///path/.wrangler/file.js:1:1)`;
186
+
187
+ const result = prettyError(error);
188
+
189
+ const stackString = result.stack.join('\n');
190
+ expect(stackString).not.toContain('file://');
191
+ expect(stackString).toContain('file.js:1:1');
192
+ expect(stackString).not.toContain('Error: Test error');
193
+ });
194
+ });
195
+
196
+ describe('createRootLogEntry', () => {
197
+ const mockDetails: Details = { service: 'test-service', version: '1.0.0' };
198
+ const mockEventId = 'test-event-123';
199
+
200
+ it('should create basic root log entry with required fields', () => {
201
+ const options = {
202
+ path: '/api/test',
203
+ method: 'get' as Method,
204
+ details: mockDetails,
205
+ eventId: mockEventId,
206
+ };
207
+
208
+ const result = createRootLogEntry(options);
209
+
210
+ expect(result).toHaveProperty('root');
211
+ expect(Array.isArray(result.root)).toBe(true);
212
+ expect(result.root).toHaveLength(1);
213
+
214
+ const entry = result.root[0];
215
+ expect(entry.type).toBe('info');
216
+ expect(entry.message).toBe('Request received');
217
+ expect(entry.payload).toBeDefined();
218
+ expect(entry.timestamp).toBeDefined();
219
+ expect(typeof entry.timestamp).toBe('number');
220
+ });
221
+
222
+ it('should handle path correctly', () => {
223
+ const options = {
224
+ path: '/api/users/123',
225
+ method: 'get' as Method,
226
+ details: mockDetails,
227
+ eventId: mockEventId,
228
+ };
229
+
230
+ const result = createRootLogEntry(options);
231
+ const payload = result.root[0].payload as RootPayload;
232
+
233
+ expect(payload.path).toBe('/api/users/123');
234
+ });
235
+
236
+ it('should handle method correctly', () => {
237
+ const options = {
238
+ path: '/api/test',
239
+ method: 'post' as Method,
240
+ details: mockDetails,
241
+ eventId: mockEventId,
242
+ };
243
+
244
+ const result = createRootLogEntry(options);
245
+ const payload = result.root[0].payload as RootPayload;
246
+
247
+ expect(payload.path).toBe('/api/test');
248
+ expect(payload.method).toBe('post');
249
+ });
250
+
251
+ it('should include parentEventId when provided', () => {
252
+ const parentEventId = 'parent-event-456';
253
+ const options = {
254
+ path: '/api/test',
255
+ method: 'put' as Method,
256
+ details: mockDetails,
257
+ eventId: mockEventId,
258
+ parentEventId,
259
+ };
260
+
261
+ const result = createRootLogEntry(options);
262
+ const payload = result.root[0].payload as RootPayload;
263
+
264
+ expect(payload.parentEventId).toBe(parentEventId);
265
+ });
266
+
267
+ it('should not include parentEventId when not provided', () => {
268
+ const options = {
269
+ path: '/api/test',
270
+ method: 'delete' as Method,
271
+ details: mockDetails,
272
+ eventId: mockEventId,
273
+ };
274
+
275
+ const result = createRootLogEntry(options);
276
+ const payload = result.root[0].payload as RootPayload;
277
+
278
+ expect(payload.parentEventId).toBeUndefined();
279
+ });
280
+
281
+ it('should add error log when withParentEventId is true but no parentEventId provided', () => {
282
+ const options = {
283
+ path: '/api/test',
284
+ method: 'patch' as Method,
285
+ details: mockDetails,
286
+ eventId: mockEventId,
287
+ withParentEventId: true,
288
+ };
289
+
290
+ const result = createRootLogEntry(options);
291
+
292
+ expect(result.root).toHaveLength(2);
293
+
294
+ const infoEntry = result.root[0];
295
+ const errorEntry = result.root[1];
296
+
297
+ expect(infoEntry.type).toBe('info');
298
+ expect(infoEntry.message).toBe('Request received');
299
+
300
+ expect(errorEntry.type).toBe('error');
301
+ expect(errorEntry.message).toBe('Parent event ID expected but not found');
302
+ expect(errorEntry.payload).toBeUndefined();
303
+ });
304
+
305
+ it('should not add error log when withParentEventId is true and parentEventId is provided', () => {
306
+ const options = {
307
+ path: '/api/test',
308
+ method: 'head' as Method,
309
+ details: mockDetails,
310
+ eventId: mockEventId,
311
+ parentEventId: 'parent-123',
312
+ withParentEventId: true,
313
+ };
314
+
315
+ const result = createRootLogEntry(options);
316
+
317
+ expect(result.root).toHaveLength(1);
318
+ expect(result.root[0].type).toBe('info');
319
+ });
320
+
321
+ it('should not add error log when withParentEventId is false', () => {
322
+ const options = {
323
+ path: '/api/test',
324
+ method: 'options' as Method,
325
+ details: mockDetails,
326
+ eventId: mockEventId,
327
+ withParentEventId: false,
328
+ };
329
+
330
+ const result = createRootLogEntry(options);
331
+
332
+ expect(result.root).toHaveLength(1);
333
+ expect(result.root[0].type).toBe('info');
334
+ });
335
+
336
+ it('should handle all HTTP methods', () => {
337
+ const methods: Method[] = [
338
+ 'get',
339
+ 'post',
340
+ 'put',
341
+ 'delete',
342
+ 'patch',
343
+ 'head',
344
+ 'options',
345
+ 'trace',
346
+ 'connect',
347
+ ];
348
+
349
+ methods.forEach((method) => {
350
+ const options = {
351
+ path: '/api/test',
352
+ method,
353
+ details: mockDetails,
354
+ eventId: mockEventId,
355
+ };
356
+
357
+ const result = createRootLogEntry(options);
358
+ const payload = result.root[0].payload as RootPayload;
359
+
360
+ expect(payload.method).toBe(method);
361
+ });
362
+ });
363
+
364
+ it('should preserve complex details object', () => {
365
+ const complexDetails: Details = {
366
+ service: 'api-gateway',
367
+ version: '2.1.0',
368
+ environment: 'production',
369
+ region: 'us-east-1',
370
+ userId: 12345,
371
+ metadata: { feature: 'logging', enabled: true },
372
+ };
373
+
374
+ const options = {
375
+ path: '/api/test',
376
+ method: 'get' as Method,
377
+ details: complexDetails,
378
+ eventId: mockEventId,
379
+ };
380
+
381
+ const result = createRootLogEntry(options);
382
+ const payload = result.root[0].payload as RootPayload;
383
+
384
+ expect(payload.details).toEqual(complexDetails);
385
+ });
386
+
387
+ it('should generate reasonable timestamps', () => {
388
+ const beforeTime = Date.now();
389
+
390
+ const options = {
391
+ path: '/api/test',
392
+ method: 'get' as Method,
393
+ details: mockDetails,
394
+ eventId: mockEventId,
395
+ };
396
+
397
+ const result = createRootLogEntry(options);
398
+ const afterTime = Date.now();
399
+
400
+ const timestamp = result.root[0].timestamp;
401
+
402
+ expect(timestamp).toBeGreaterThanOrEqual(beforeTime);
403
+ expect(timestamp).toBeLessThanOrEqual(afterTime);
404
+ });
405
+
406
+ it('should handle different paths', () => {
407
+ const options = {
408
+ path: '/api/users',
409
+ method: 'get' as Method,
410
+ details: mockDetails,
411
+ eventId: mockEventId,
412
+ };
413
+
414
+ const result = createRootLogEntry(options);
415
+ const payload = result.root[0].payload as RootPayload;
416
+
417
+ expect(payload.path).toBe('/api/users');
418
+ });
419
+
420
+ it('should handle paths with different segments', () => {
421
+ const options = {
422
+ path: '/users?page=2&limit=10',
423
+ method: 'get' as Method,
424
+ details: mockDetails,
425
+ eventId: mockEventId,
426
+ };
427
+
428
+ const result = createRootLogEntry(options);
429
+ const payload = result.root[0].payload as RootPayload;
430
+
431
+ // Based on actual behavior, the full path including query params is preserved
432
+ expect(payload.path).toBe('/users?page=2&limit=10');
433
+ });
434
+
435
+ it('should maintain correct payload structure type', () => {
436
+ const options = {
437
+ path: '/api/test',
438
+ method: 'get' as Method,
439
+ details: mockDetails,
440
+ eventId: mockEventId,
441
+ parentEventId: 'parent-123',
442
+ };
443
+
444
+ const result = createRootLogEntry(options);
445
+ const payload = result.root[0].payload as RootPayload;
446
+
447
+ expect(payload).toHaveProperty('path');
448
+ expect(payload).toHaveProperty('method');
449
+ expect(payload).toHaveProperty('details');
450
+ expect(payload).toHaveProperty('eventId');
451
+ expect(payload).toHaveProperty('parentEventId');
452
+ });
453
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "dom"],
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "moduleResolution": "node",
14
+ "allowSyntheticDefaultImports": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ splitting: false,
10
+ minify: false,
11
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ });