diff --git a/src/components/ToggleButtonGroup.spec.tsx b/src/components/ToggleButtonGroup.spec.tsx index 35878f4e..c1b42476 100644 --- a/src/components/ToggleButtonGroup.spec.tsx +++ b/src/components/ToggleButtonGroup.spec.tsx @@ -30,7 +30,8 @@ describe('ToggleButtonGroup', () => { selectionMode ${'multiple'} ${'single'} - `(`matches snapshot with selectionMode #selectionMode`, ({selectionMode}) => { + ${undefined} + `(`matches snapshot with selectionMode $selectionMode`, ({selectionMode}) => { const tree = renderer.create( `; -exports[`ToggleButtonGroup matches snapshot with selectionMode #selectionMode 2`] = ` +exports[`ToggleButtonGroup matches snapshot with selectionMode single 1`] = ` +
+ + + + + +
+`; + +exports[`ToggleButtonGroup matches snapshot with selectionMode undefined 1`] = `
{ + let logFn: jest.SpyInstance; + let logger: Logger; + + beforeEach(() => { + logger = createSentryLogger(); + logFn = jest.spyOn(Sentry, 'addBreadcrumb').mockImplementation(() => null); + }); + + afterEach(() => { + logFn.mockReset(); + }); + + it('logs all fields', () => { + logger.setContext({ extra: 'context' }); + logger.logEvent( + Level.Info, + { + "timestamp": 1461165894000, + "event_id": "any-id", + "message": "This is a test", + "from": "/login", + "to": "/dashboard", + "profile": {}, + "address": { + "city": "New York" + } + } + ); + + expect(logFn).toHaveBeenCalledWith( + { + "timestamp": 1461165894000, + "level": "info", + "message": "This is a test", + "category": "log", + "data": { + "from": "/login", + "to": "/dashboard", + "event_id": "any-id", + "extra": "context", + "city": "New York", + } + } + ); + }); + + it('logs with missing fields', () => { + logger.logEvent( + Level.Warn, + {}, + ); + + expect(logFn).toHaveBeenCalledWith( + { + "timestamp": undefined, + "category": "log", + "message": "", + "level": "warning", + "data": {} + } + ); + }); +}); diff --git a/src/sentryLogger/sentryLog.ts b/src/sentryLogger/sentryLog.ts new file mode 100644 index 00000000..5284fb46 --- /dev/null +++ b/src/sentryLogger/sentryLog.ts @@ -0,0 +1,47 @@ +import { addBreadcrumb, SeverityLevel } from "@sentry/react"; +import { createCoreLogger, Level } from '@openstax/ts-utils/services/logger'; +import { JsonCompatibleStruct } from "@openstax/ts-utils/routing"; + +/** + * Flatten nested objects + * @see https://stackoverflow.com/a/70377608 + */ + +const flattenObj = (obj: { [x: string]: any; }) => + Object.keys(obj).reduce((acc: { [x: string]: any; }, curKey) => { + if (typeof obj[curKey] === 'object') { + acc = { ...acc, ...flattenObj(obj[curKey]) } + } else { + acc[curKey] = obj[curKey] + } + return acc + }, {}); + +const serializeLevel = (level: Level): SeverityLevel => + level === Level.Warn ? 'warning' : level; + +const serializeBreadcrumb = (level: Level, breadcrumb: JsonCompatibleStruct): { + level?: SeverityLevel; + category?: string; + message?: string; + timestamp?: number; + data?: { [key: string]: any }; +} => { + const { type, category, message, timestamp, ...rest } = breadcrumb; + + return { + level: serializeLevel(level), + category: 'log', + message: typeof message === 'string' ? message : '', + timestamp: typeof timestamp === 'number' ? timestamp : undefined, + data: flattenObj(rest), + } +}; + +/** + * Creates a logger that creates breadcrumbs using Sentry. + * @see https://develop.sentry.dev/sdk/data-model/event-payloads/breadcrumbs/ + */ +export const createSentryLogger = () => createCoreLogger((level, breadcrumb) => + addBreadcrumb(serializeBreadcrumb(level, breadcrumb)) +); \ No newline at end of file