diff --git a/code/lib/core-server/src/withTelemetry.test.ts b/code/lib/core-server/src/withTelemetry.test.ts index 519de40c1458..2d511ea7663a 100644 --- a/code/lib/core-server/src/withTelemetry.test.ts +++ b/code/lib/core-server/src/withTelemetry.test.ts @@ -1,10 +1,11 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ /// ; import prompts from 'prompts'; import { loadAllPresets, cache } from '@storybook/core-common'; -import { telemetry } from '@storybook/telemetry'; +import { telemetry, oneWayHash } from '@storybook/telemetry'; -import { withTelemetry } from './withTelemetry'; +import { getErrorLevel, sendTelemetryError, withTelemetry } from './withTelemetry'; jest.mock('prompts'); jest.mock('@storybook/core-common'); @@ -12,222 +13,463 @@ jest.mock('@storybook/telemetry'); const cliOptions = {}; -it('works in happy path', async () => { - const run = jest.fn(); +describe('withTelemetry', () => { + it('works in happy path', async () => { + const run = jest.fn(); - await withTelemetry('dev', { cliOptions }, run); + await withTelemetry('dev', { cliOptions }, run); - expect(telemetry).toHaveBeenCalledTimes(1); - expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); -}); - -it('does not send boot when cli option is passed', async () => { - const run = jest.fn(); + expect(telemetry).toHaveBeenCalledTimes(1); + expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); + }); - await withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run); + it('does not send boot when cli option is passed', async () => { + const run = jest.fn(); - expect(telemetry).toHaveBeenCalledTimes(0); -}); + await withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run); -describe('when command fails', () => { - const error = new Error('An Error!'); - const run = jest.fn(async () => { - throw error; + expect(telemetry).toHaveBeenCalledTimes(0); }); - it('sends boot message', async () => { - await expect(async () => withTelemetry('dev', { cliOptions }, run)).rejects.toThrow(error); + describe('when command fails', () => { + const error = new Error('An Error!'); + const run = jest.fn(async () => { + throw error; + }); - expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); - }); + it('sends boot message', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions, printError: jest.fn() }, run) + ).rejects.toThrow(error); - it('does not send boot when cli option is passed', async () => { - await expect(async () => - withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run) - ).rejects.toThrow(error); + expect(telemetry).toHaveBeenCalledWith('boot', { eventType: 'dev' }, { stripMetadata: true }); + }); - expect(telemetry).toHaveBeenCalledTimes(0); + it('does not send boot when cli option is passed', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions: { disableTelemetry: true }, printError: jest.fn() }, run) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(0); + }); + + it('sends error message when no options are passed', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions, printError: jest.fn() }, run) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send error message when cli opt out is passed', async () => { + await expect(async () => + withTelemetry('dev', { cliOptions: { disableTelemetry: true }, printError: jest.fn() }, run) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(0); + expect(telemetry).not.toHaveBeenCalledWith( + 'error', + expect.objectContaining({}), + expect.objectContaining({}) + ); + }); + + it('does not send full error message when crash reports are disabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ enableCrashReports: false } as any), + }); + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); + + it('does send error message when crash reports are enabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ enableCrashReports: true } as any), + }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send any error message when telemetry is disabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ disableTelemetry: true } as any), + }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(1); + expect(telemetry).not.toHaveBeenCalledWith( + 'error', + expect.objectContaining({}), + expect.objectContaining({}) + ); + }); + + it('does send error messages when telemetry is disabled, but crash reports are enabled', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({ disableTelemetry: true, enableCrashReports: true } as any), + }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send full error messages when disabled crash reports are cached', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(false); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); + + it('does send error messages when enabled crash reports are cached', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(true); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + it('does not send full error messages when disabled crash reports are prompted', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(undefined); + jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: false }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); + + it('does send error messages when enabled crash reports are prompted', async () => { + jest.mocked(loadAllPresets).mockResolvedValueOnce({ + apply: async () => ({} as any), + }); + jest.mocked(cache.get).mockResolvedValueOnce(undefined); + jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: true }); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev', error }), + expect.objectContaining({}) + ); + }); + + // if main.js has errors, we have no way to tell if they've disabled error reporting, + // so we assume they have. + it('does not send full error messages when presets fail to evaluate', async () => { + jest.mocked(loadAllPresets).mockRejectedValueOnce(error); + + await expect(async () => + withTelemetry( + 'dev', + { cliOptions: {} as any, presetOptions: {} as any, printError: jest.fn() }, + run + ) + ).rejects.toThrow(error); + + expect(telemetry).toHaveBeenCalledTimes(2); + expect(telemetry).toHaveBeenCalledWith( + 'error', + expect.objectContaining({ eventType: 'dev' }), + expect.objectContaining({}) + ); + }); }); +}); + +describe('sendTelemetryError', () => { + it('handles error instances and sends telemetry', async () => { + const options: any = { + cliOptions: {}, + skipPrompt: false, + }; + const mockError = new Error('Test error'); + const eventType: any = 'testEventType'; + + jest.mocked(oneWayHash).mockReturnValueOnce('some-hash'); - it('sends error message when no options are passed', async () => { - await expect(async () => withTelemetry('dev', { cliOptions }, run)).rejects.toThrow(error); + await sendTelemetryError(mockError, eventType, options); - expect(telemetry).toHaveBeenCalledTimes(2); expect(telemetry).toHaveBeenCalledWith( 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) + expect.objectContaining({ + error: mockError, + eventType, + isErrorInstance: true, + errorHash: 'some-hash', + }), + expect.any(Object) ); }); - it('does not send error message when cli opt out is passed', async () => { - await expect(async () => - withTelemetry('dev', { cliOptions: { disableTelemetry: true } }, run) - ).rejects.toThrow(error); + it('handles non-error instances and sends telemetry with no-message hash', async () => { + const options: any = { + cliOptions: {}, + skipPrompt: false, + }; + const mockError = { error: new Error('Test error') }; + const eventType: any = 'testEventType'; - expect(telemetry).toHaveBeenCalledTimes(0); - expect(telemetry).not.toHaveBeenCalledWith( + await sendTelemetryError(mockError, eventType, options); + + expect(telemetry).toHaveBeenCalledWith( 'error', - expect.objectContaining({}), - expect.objectContaining({}) + expect.objectContaining({ + error: mockError, + eventType, + isErrorInstance: false, + errorHash: 'no-message', + }), + expect.any(Object) ); }); - it('does not send full error message when crash reports are disabled', async () => { - jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ enableCrashReports: false } as any), - }); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + it('handles error with empty message and sends telemetry with empty-message hash', async () => { + const options: any = { + cliOptions: {}, + skipPrompt: false, + }; + const mockError = new Error(); + const eventType: any = 'testEventType'; + + await sendTelemetryError(mockError, eventType, options); - expect(telemetry).toHaveBeenCalledTimes(2); expect(telemetry).toHaveBeenCalledWith( 'error', - { eventType: 'dev' }, - expect.objectContaining({}) + expect.objectContaining({ + error: mockError, + eventType, + isErrorInstance: true, + errorHash: 'empty-message', + }), + expect.any(Object) ); }); +}); - it('does send error message when crash reports are enabled', async () => { - jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ enableCrashReports: true } as any), - }); +describe('getErrorLevel', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + it('returns "none" when cliOptions.disableTelemetry is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: true, + }, + presetOptions: undefined, + skipPrompt: false, + }; - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); + const errorLevel = await getErrorLevel(options); + + expect(errorLevel).toBe('none'); }); - it('does not send any error message when telemetry is disabled', async () => { - jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ disableTelemetry: true } as any), - }); + it('returns "full" when presetOptions is not provided', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: undefined, + skipPrompt: false, + }; - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(1); - expect(telemetry).not.toHaveBeenCalledWith( - 'error', - expect.objectContaining({}), - expect.objectContaining({}) - ); + expect(errorLevel).toBe('full'); }); - it('does send error messages when telemetry is disabled, but crash reports are enabled', async () => { + it('returns "full" when core.enableCrashReports is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({ disableTelemetry: true, enableCrashReports: true } as any), + apply: async () => ({ enableCrashReports: true } as any), }); + jest.mocked(cache.get).mockResolvedValueOnce(false); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('full'); }); - it('does not send full error messages when disabled crash reports are cached', async () => { + it('returns "error" when core.enableCrashReports is false', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({} as any), + apply: async () => ({ enableCrashReports: false } as any), }); jest.mocked(cache.get).mockResolvedValueOnce(false); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev' }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('error'); }); - it('does send error messages when enabled crash reports are cached', async () => { + it('returns "none" when core.disableTelemetry is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ - apply: async () => ({} as any), + apply: async () => ({ disableTelemetry: true } as any), }); - jest.mocked(cache.get).mockResolvedValueOnce(true); + jest.mocked(cache.get).mockResolvedValueOnce(false); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('none'); }); - it('does not send full error messages when disabled crash reports are prompted', async () => { + it('returns "full" if cache contains crashReports true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: false, + }; + + jest.mocked(cache.get).mockResolvedValueOnce(true); jest.mocked(loadAllPresets).mockResolvedValueOnce({ apply: async () => ({} as any), }); - jest.mocked(cache.get).mockResolvedValueOnce(undefined); - jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: false }); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev' }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('full'); }); - it('does send error messages when enabled crash reports are prompted', async () => { + it('returns "error" when skipPrompt is true', async () => { + const options: any = { + cliOptions: { + disableTelemetry: false, + }, + presetOptions: {}, + skipPrompt: true, + }; + jest.mocked(loadAllPresets).mockResolvedValueOnce({ apply: async () => ({} as any), }); jest.mocked(cache.get).mockResolvedValueOnce(undefined); - jest.mocked(prompts).mockResolvedValueOnce({ enableCrashReports: true }); - - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); - - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev', error }, - expect.objectContaining({}) - ); - }); - - // if main.js has errors, we have no way to tell if they've disabled error reporting, - // so we assume they have. - it('does not send full error messages when presets fail to evaluate', async () => { - jest.mocked(loadAllPresets).mockRejectedValueOnce(error); - await expect(async () => - withTelemetry('dev', { cliOptions: {} as any, presetOptions: {} as any }, run) - ).rejects.toThrow(error); + const errorLevel = await getErrorLevel(options); - expect(telemetry).toHaveBeenCalledTimes(2); - expect(telemetry).toHaveBeenCalledWith( - 'error', - { eventType: 'dev' }, - expect.objectContaining({}) - ); + expect(errorLevel).toBe('error'); }); }); diff --git a/code/lib/core-server/src/withTelemetry.ts b/code/lib/core-server/src/withTelemetry.ts index 0baebb8d97cb..086ee991e5ec 100644 --- a/code/lib/core-server/src/withTelemetry.ts +++ b/code/lib/core-server/src/withTelemetry.ts @@ -4,7 +4,6 @@ import { loadAllPresets, cache } from '@storybook/core-common'; import { telemetry, getPrecedingUpgrade, oneWayHash } from '@storybook/telemetry'; import type { EventType } from '@storybook/telemetry'; import { logger } from '@storybook/node-logger'; -import invariant from 'tiny-invariant'; type TelemetryOptions = { cliOptions: CLIOptions; @@ -32,7 +31,7 @@ const promptCrashReports = async () => { type ErrorLevel = 'none' | 'error' | 'full'; -async function getErrorLevel({ +export async function getErrorLevel({ cliOptions, presetOptions, skipPrompt, @@ -67,7 +66,7 @@ async function getErrorLevel({ } export async function sendTelemetryError( - error: unknown, + _error: unknown, eventType: EventType, options: TelemetryOptions ) { @@ -81,10 +80,7 @@ export async function sendTelemetryError( if (errorLevel !== 'none') { const precedingUpgrade = await getPrecedingUpgrade(); - invariant( - error instanceof Error, - 'The error passed to sendTelemetryError was not an Error, please only send Errors' - ); + const error = _error as Error | Record; let storybookErrorProperties = {}; // if it's an UNCATEGORIZED error, it won't have a coded name, so we just pass the category and source @@ -104,14 +100,23 @@ export async function sendTelemetryError( }; } + let errorHash; + if ('message' in error) { + errorHash = error.message ? oneWayHash(error.message) : 'empty-message'; + } else { + errorHash = 'no-message'; + } + await telemetry( 'error', { + ...storybookErrorProperties, eventType, precedingUpgrade, error: errorLevel === 'full' ? error : undefined, - errorHash: oneWayHash(error.message), - ...storybookErrorProperties, + errorHash, + // if we ever end up sending a non-error instance, we'd like to know + isErrorInstance: error instanceof Error, }, { immediate: true, diff --git a/code/lib/telemetry/src/sanitize.test.ts b/code/lib/telemetry/src/sanitize.test.ts index f5b3a742d3e6..934695e1e70b 100644 --- a/code/lib/telemetry/src/sanitize.test.ts +++ b/code/lib/telemetry/src/sanitize.test.ts @@ -1,7 +1,23 @@ +/* eslint-disable local-rules/no-uncategorized-errors */ import { sanitizeError, cleanPaths } from './sanitize'; describe(`Errors Helpers`, () => { describe(`sanitizeError`, () => { + it(`Sanitizes ansi codes in error`, () => { + const errorMessage = `\u001B[4mStorybook\u001B[0m`; + let e: any; + try { + throw new Error(errorMessage); + } catch (error) { + e = error; + } + + const sanitizedError = sanitizeError(e); + + expect(sanitizedError.message).toEqual('Storybook'); + expect(sanitizedError.stack).toContain('Error: Storybook'); + }); + it(`Sanitizes current path from error stacktraces`, () => { const errorMessage = `this is a test`; let e: any; @@ -69,14 +85,12 @@ describe(`Errors Helpers`, () => { `should clean path on unix: %s`, (filePath) => { const cwdMockPath = `/Users/username/storybook-app`; - const fullPath = `${cwdMockPath}/${filePath}`; - const mockCwd = jest.spyOn(process, `cwd`).mockImplementation(() => cwdMockPath); - const errorMessage = `This path should be sanitized ${fullPath}`; + const errorMessage = `Path 1 /Users/Username/storybook-app/${filePath} Path 2 /Users/username/storybook-app/${filePath}`; expect(cleanPaths(errorMessage, `/`)).toBe( - `This path should be sanitized $SNIP/${filePath}` + `Path 1 $SNIP/${filePath} Path 2 $SNIP/${filePath}` ); mockCwd.mockRestore(); } @@ -86,14 +100,12 @@ describe(`Errors Helpers`, () => { `should clean path on windows: %s`, (filePath) => { const cwdMockPath = `C:\\Users\\username\\storybook-app`; - const fullPath = `${cwdMockPath}\\${filePath}`; - - const mockCwd = jest.spyOn(process, `cwd`).mockImplementation(() => cwdMockPath); - const errorMessage = `This path should be sanitized ${fullPath}`; + const mockCwd = jest.spyOn(process, `cwd`).mockImplementationOnce(() => cwdMockPath); + const errorMessage = `Path 1 C:\\Users\\username\\storybook-app\\${filePath} Path 2 c:\\Users\\username\\storybook-app\\${filePath}`; expect(cleanPaths(errorMessage, `\\`)).toBe( - `This path should be sanitized $SNIP\\${filePath}` + `Path 1 $SNIP\\${filePath} Path 2 $SNIP\\${filePath}` ); mockCwd.mockRestore(); } diff --git a/code/lib/telemetry/src/sanitize.ts b/code/lib/telemetry/src/sanitize.ts index 4c68ed50db94..77e0c1fbda0e 100644 --- a/code/lib/telemetry/src/sanitize.ts +++ b/code/lib/telemetry/src/sanitize.ts @@ -12,6 +12,11 @@ function regexpEscape(str: string): string { return str.replace(/[-[/{}()*+?.\\^$|]/g, `\\$&`); } +export function removeAnsiEscapeCodes(input = ''): string { + // eslint-disable-next-line no-control-regex + return input.replace(/\u001B\[[0-9;]*m/g, ''); +} + export function cleanPaths(str: string, separator: string = sep): string { if (!str) return str; @@ -19,11 +24,11 @@ export function cleanPaths(str: string, separator: string = sep): string { while (stack.length > 1) { const currentPath = stack.join(separator); - const currentRegex = new RegExp(regexpEscape(currentPath), `g`); + const currentRegex = new RegExp(regexpEscape(currentPath), `gi`); str = str.replace(currentRegex, `$SNIP`); const currentPath2 = stack.join(separator + separator); - const currentRegex2 = new RegExp(regexpEscape(currentPath2), `g`); + const currentRegex2 = new RegExp(regexpEscape(currentPath2), `gi`); str = str.replace(currentRegex2, `$SNIP`); stack.pop(); @@ -34,8 +39,13 @@ export function cleanPaths(str: string, separator: string = sep): string { // Takes an Error and returns a sanitized JSON String export function sanitizeError(error: Error, pathSeparator: string = sep) { try { - // Hack because Node - error = JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))); + error = { + ...JSON.parse(JSON.stringify(error)), + message: removeAnsiEscapeCodes(error.message), + stack: removeAnsiEscapeCodes(error.stack), + cause: error.cause, + name: error.name, + }; // Removes all user paths const errorString = cleanPaths(JSON.stringify(error), pathSeparator);