|
1 | 1 | import { performance } from 'node:perf_hooks'; |
2 | 2 | import { beforeEach, describe, expect, it, vi } from 'vitest'; |
| 3 | +import { installExitHandlers } from '../exit-process.js'; |
3 | 4 | import type { ActionTrackEntryPayload } from '../user-timing-extensibility-api.type.js'; |
4 | | -import { Profiler, type ProfilerOptions } from './profiler.js'; |
| 5 | +import { NodeJsProfiler, Profiler, type ProfilerOptions } from './profiler.js'; |
| 6 | + |
| 7 | +// Spy on installExitHandlers to capture handlers |
| 8 | +vi.mock('../exit-process.js'); |
5 | 9 |
|
6 | 10 | describe('Profiler', () => { |
7 | 11 | const getProfiler = (overrides?: Partial<ProfilerOptions>) => |
@@ -424,3 +428,176 @@ describe('Profiler', () => { |
424 | 428 | expect(workFn).toHaveBeenCalled(); |
425 | 429 | }); |
426 | 430 | }); |
| 431 | +describe('NodeJsProfiler', () => { |
| 432 | + const mockInstallExitHandlers = vi.mocked(installExitHandlers); |
| 433 | + |
| 434 | + let capturedOnError: |
| 435 | + | (( |
| 436 | + error: unknown, |
| 437 | + kind: 'uncaughtException' | 'unhandledRejection', |
| 438 | + ) => void) |
| 439 | + | undefined; |
| 440 | + let capturedOnExit: |
| 441 | + | ((code: number, reason: import('../exit-process.js').CloseReason) => void) |
| 442 | + | undefined; |
| 443 | + const createProfiler = (overrides?: Partial<ProfilerOptions>) => |
| 444 | + new NodeJsProfiler({ |
| 445 | + prefix: 'cp', |
| 446 | + track: 'test-track', |
| 447 | + format: { |
| 448 | + encode: v => JSON.stringify(v), |
| 449 | + }, |
| 450 | + ...overrides, |
| 451 | + }); |
| 452 | + |
| 453 | + let profiler: NodeJsProfiler<Record<string, ActionTrackEntryPayload>>; |
| 454 | + |
| 455 | + beforeEach(() => { |
| 456 | + capturedOnError = undefined; |
| 457 | + capturedOnExit = undefined; |
| 458 | + |
| 459 | + mockInstallExitHandlers.mockImplementation(options => { |
| 460 | + capturedOnError = options?.onError; |
| 461 | + capturedOnExit = options?.onExit; |
| 462 | + }); |
| 463 | + |
| 464 | + performance.clearMarks(); |
| 465 | + performance.clearMeasures(); |
| 466 | + // eslint-disable-next-line functional/immutable-data |
| 467 | + delete process.env.CP_PROFILING; |
| 468 | + }); |
| 469 | + |
| 470 | + it('installs exit handlers on construction', () => { |
| 471 | + expect(() => createProfiler()).not.toThrow(); |
| 472 | + |
| 473 | + expect(mockInstallExitHandlers).toHaveBeenCalledWith({ |
| 474 | + onError: expect.any(Function), |
| 475 | + onExit: expect.any(Function), |
| 476 | + }); |
| 477 | + }); |
| 478 | + |
| 479 | + it('setEnabled toggles profiler state', () => { |
| 480 | + profiler = createProfiler({ enabled: true }); |
| 481 | + expect(profiler.isEnabled()).toBe(true); |
| 482 | + |
| 483 | + profiler.setEnabled(false); |
| 484 | + expect(profiler.isEnabled()).toBe(false); |
| 485 | + |
| 486 | + profiler.setEnabled(true); |
| 487 | + expect(profiler.isEnabled()).toBe(true); |
| 488 | + }); |
| 489 | + |
| 490 | + it('marks fatal errors and shuts down profiler on uncaughtException', () => { |
| 491 | + profiler = createProfiler({ enabled: true }); |
| 492 | + |
| 493 | + const testError = new Error('Test fatal error'); |
| 494 | + capturedOnError?.call(profiler, testError, 'uncaughtException'); |
| 495 | + |
| 496 | + expect(performance.getEntriesByType('mark')).toStrictEqual([ |
| 497 | + { |
| 498 | + name: 'Fatal Error', |
| 499 | + detail: { |
| 500 | + devtools: { |
| 501 | + color: 'error', |
| 502 | + dataType: 'marker', |
| 503 | + properties: [ |
| 504 | + ['Error Type', 'Error'], |
| 505 | + ['Error Message', 'Test fatal error'], |
| 506 | + ], |
| 507 | + tooltipText: 'uncaughtException caused fatal error', |
| 508 | + }, |
| 509 | + }, |
| 510 | + duration: 0, |
| 511 | + entryType: 'mark', |
| 512 | + startTime: 0, |
| 513 | + }, |
| 514 | + ]); |
| 515 | + }); |
| 516 | + |
| 517 | + it('marks fatal errors and shuts down profiler on unhandledRejection', () => { |
| 518 | + profiler = createProfiler({ enabled: true }); |
| 519 | + expect(profiler.isEnabled()).toBe(true); |
| 520 | + |
| 521 | + capturedOnError?.call( |
| 522 | + profiler, |
| 523 | + new Error('Test fatal error'), |
| 524 | + 'unhandledRejection', |
| 525 | + ); |
| 526 | + |
| 527 | + expect(performance.getEntriesByType('mark')).toStrictEqual([ |
| 528 | + { |
| 529 | + name: 'Fatal Error', |
| 530 | + detail: { |
| 531 | + devtools: { |
| 532 | + color: 'error', |
| 533 | + dataType: 'marker', |
| 534 | + properties: [ |
| 535 | + ['Error Type', 'Error'], |
| 536 | + ['Error Message', 'Test fatal error'], |
| 537 | + ], |
| 538 | + tooltipText: 'unhandledRejection caused fatal error', |
| 539 | + }, |
| 540 | + }, |
| 541 | + duration: 0, |
| 542 | + entryType: 'mark', |
| 543 | + startTime: 0, |
| 544 | + }, |
| 545 | + ]); |
| 546 | + }); |
| 547 | + it('shutdown method shuts down profiler', () => { |
| 548 | + profiler = createProfiler({ enabled: true }); |
| 549 | + const setEnabledSpy = vi.spyOn(profiler, 'setEnabled'); |
| 550 | + const sinkCloseSpy = vi.spyOn((profiler as any).sink, 'close'); |
| 551 | + expect(profiler.isEnabled()).toBe(true); |
| 552 | + |
| 553 | + (profiler as any).shutdown(); |
| 554 | + |
| 555 | + expect(setEnabledSpy).toHaveBeenCalledTimes(1); |
| 556 | + expect(setEnabledSpy).toHaveBeenCalledWith(false); |
| 557 | + expect(sinkCloseSpy).toHaveBeenCalledTimes(1); |
| 558 | + expect(profiler.isEnabled()).toBe(false); |
| 559 | + }); |
| 560 | + it('exit handler shuts down profiler', () => { |
| 561 | + profiler = createProfiler({ enabled: true }); |
| 562 | + const shutdownSpy = vi.spyOn(profiler, 'shutdown' as any); |
| 563 | + expect(profiler.isEnabled()).toBe(true); |
| 564 | + |
| 565 | + capturedOnExit?.(0, { kind: 'exit' }); |
| 566 | + |
| 567 | + expect(profiler.isEnabled()).toBe(false); |
| 568 | + expect(shutdownSpy).toHaveBeenCalledTimes(1); |
| 569 | + }); |
| 570 | + |
| 571 | + it('close method shuts down profiler', () => { |
| 572 | + profiler = createProfiler({ enabled: true }); |
| 573 | + const shutdownSpy = vi.spyOn(profiler, 'shutdown' as any); |
| 574 | + expect(profiler.isEnabled()).toBe(true); |
| 575 | + |
| 576 | + profiler.close(); |
| 577 | + |
| 578 | + expect(shutdownSpy).toHaveBeenCalledTimes(1); |
| 579 | + expect(profiler.isEnabled()).toBe(false); |
| 580 | + }); |
| 581 | + |
| 582 | + it('error handler does nothing when profiler is disabled', () => { |
| 583 | + profiler = createProfiler({ enabled: false }); // Start disabled |
| 584 | + expect(profiler.isEnabled()).toBe(false); |
| 585 | + |
| 586 | + const testError = new Error('Test error'); |
| 587 | + capturedOnError?.call(profiler, testError, 'uncaughtException'); |
| 588 | + |
| 589 | + // Should not create any marks when disabled |
| 590 | + expect(performance.getEntriesByType('mark')).toHaveLength(0); |
| 591 | + }); |
| 592 | + |
| 593 | + it('exit handler does nothing when profiler is disabled', () => { |
| 594 | + profiler = createProfiler({ enabled: false }); // Start disabled |
| 595 | + expect(profiler.isEnabled()).toBe(false); |
| 596 | + |
| 597 | + // Should not call shutdown when disabled |
| 598 | + const shutdownSpy = vi.spyOn(profiler, 'shutdown' as any); |
| 599 | + capturedOnExit?.(0, { kind: 'exit' }); |
| 600 | + |
| 601 | + expect(shutdownSpy).not.toHaveBeenCalled(); |
| 602 | + }); |
| 603 | +}); |
0 commit comments