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);