From feb6d5cae84be01bcb71f7f714aa9aab3643f3c4 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Wed, 19 Mar 2025 16:01:11 -0600 Subject: [PATCH 1/5] CORE-764: new sentry logger --- src/components/ToggleButtonGroup.spec.tsx | 3 +- .../ToggleButtonGroup.spec.tsx.snap | 139 +++++++++++++++++- src/sentryLogger/sentryLog.spec.ts | 70 +++++++++ src/sentryLogger/sentryLog.ts | 38 +++++ 4 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/sentryLogger/sentryLog.spec.ts create mode 100644 src/sentryLogger/sentryLog.ts 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": "2016-04-20T20:55:53.847Z", + "type": "navigation", + "event_id": "any-id", + "category": "any-category", + "message": "This is a test", + "data": { + "from": "/login", + "to": "/dashboard" + } + } + ); + + expect(logFn).toHaveBeenCalledWith( + { + "timestamp": "2016-04-20T20:55:53.847Z", + "type": "navigation", + "level": "info", + "message": "This is a test", + "event_id": "any-id", + "category": "any-category", + "data": { + "from": "/login", + "to": "/dashboard" + } + } + ); + }); + + it('logs with missing fields', () => { + logger.setContext({ extra: 'context' }); + logger.logEvent( + Level.Warn, + {}, + ); + + expect(logFn).toHaveBeenCalledWith( + { + "timestamp": undefined, + "category": "", + "event_id": "", + "type": "", + "message": "", + "level": "warning", + "data": {} + } + ); + }); +}); diff --git a/src/sentryLogger/sentryLog.ts b/src/sentryLogger/sentryLog.ts new file mode 100644 index 00000000..425dfc18 --- /dev/null +++ b/src/sentryLogger/sentryLog.ts @@ -0,0 +1,38 @@ +import { addBreadcrumb, SeverityLevel } from "@sentry/react"; +import { createCoreLogger, Level } from '@openstax/ts-utils/services/logger'; +import { JsonCompatibleStruct } from "@openstax/ts-utils/routing"; + +/** + * Creates a logger that creates breadcrumbs using Sentry. + * + * More info: https://develop.sentry.dev/sdk/data-model/event-payloads/breadcrumbs/ + */ + +const serializeLevel = (level: Level): string => + level === Level.Warn ? 'warning' : level; + +const serializeBreadcrumb = (level: Level, breadcrumb: JsonCompatibleStruct): { + type?: string; + level?: SeverityLevel; + event_id?: string; + category?: string; + message?: string; + timestamp?: number; + data?: { [key: string]: any }; +} => { + const { type, event_id, category, message, timestamp, data } = breadcrumb; + // Casting values to remove JsonCompatibleStruct type from event + return { + type: (type?? '') as string, + level: serializeLevel(level) as SeverityLevel, + "event_id": (event_id ?? '') as string, + category: (category ?? '') as string, + message: (message ?? '') as string, + timestamp: timestamp as number ?? undefined, + data: (data ?? {}) as { [key: string]: any }, + } +}; + +export const createSentryLogger = () => createCoreLogger((level, breadcrumb) => + addBreadcrumb(serializeBreadcrumb(level, breadcrumb)) +); \ No newline at end of file From 05eb4785c47d7989113ccedd85af75debe9902b4 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Thu, 20 Mar 2025 10:43:11 -0600 Subject: [PATCH 2/5] CORE-764: fix casting issues, append rest of data --- src/sentryLogger/sentryLog.spec.ts | 16 ++++++------ src/sentryLogger/sentryLog.ts | 40 +++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/sentryLogger/sentryLog.spec.ts b/src/sentryLogger/sentryLog.spec.ts index d93001e4..795c0512 100644 --- a/src/sentryLogger/sentryLog.spec.ts +++ b/src/sentryLogger/sentryLog.spec.ts @@ -25,9 +25,11 @@ describe('createConsoleLogger', () => { "event_id": "any-id", "category": "any-category", "message": "This is a test", - "data": { - "from": "/login", - "to": "/dashboard" + "from": "/login", + "to": "/dashboard", + "profile": {}, + "address": { + "city": "New York" } } ); @@ -38,18 +40,19 @@ describe('createConsoleLogger', () => { "type": "navigation", "level": "info", "message": "This is a test", - "event_id": "any-id", "category": "any-category", "data": { "from": "/login", - "to": "/dashboard" + "to": "/dashboard", + "event_id": "any-id", + "extra": "context", + "city": "New York", } } ); }); it('logs with missing fields', () => { - logger.setContext({ extra: 'context' }); logger.logEvent( Level.Warn, {}, @@ -59,7 +62,6 @@ describe('createConsoleLogger', () => { { "timestamp": undefined, "category": "", - "event_id": "", "type": "", "message": "", "level": "warning", diff --git a/src/sentryLogger/sentryLog.ts b/src/sentryLogger/sentryLog.ts index 425dfc18..e02bf962 100644 --- a/src/sentryLogger/sentryLog.ts +++ b/src/sentryLogger/sentryLog.ts @@ -1,6 +1,6 @@ import { addBreadcrumb, SeverityLevel } from "@sentry/react"; import { createCoreLogger, Level } from '@openstax/ts-utils/services/logger'; -import { JsonCompatibleStruct } from "@openstax/ts-utils/routing"; +import { JsonCompatibleArray, JsonCompatibleStruct, JsonCompatibleValue } from "@openstax/ts-utils/routing"; /** * Creates a logger that creates breadcrumbs using Sentry. @@ -8,28 +8,44 @@ import { JsonCompatibleStruct } from "@openstax/ts-utils/routing"; * More info: https://develop.sentry.dev/sdk/data-model/event-payloads/breadcrumbs/ */ -const serializeLevel = (level: Level): string => +const checkTypeOf = ( + value: JsonCompatibleStruct | JsonCompatibleValue | JsonCompatibleArray, + typeValue: string, + defaultValue: any, +) => + value && typeof value === typeValue ? value : defaultValue + ; + +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): { type?: string; level?: SeverityLevel; - event_id?: string; category?: string; message?: string; timestamp?: number; data?: { [key: string]: any }; } => { - const { type, event_id, category, message, timestamp, data } = breadcrumb; - // Casting values to remove JsonCompatibleStruct type from event + const { type, category, message, timestamp, ...rest } = breadcrumb; + return { - type: (type?? '') as string, - level: serializeLevel(level) as SeverityLevel, - "event_id": (event_id ?? '') as string, - category: (category ?? '') as string, - message: (message ?? '') as string, - timestamp: timestamp as number ?? undefined, - data: (data ?? {}) as { [key: string]: any }, + type: checkTypeOf(type, 'string', ''), + level: serializeLevel(level), + category: checkTypeOf(category, 'string', ''), + message: checkTypeOf(message, 'string', ''), + timestamp: checkTypeOf(timestamp, 'string', undefined), + data: flattenObj(rest), } }; From 5762d214ce36d013ab7547095d042c70f693e8ef Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Thu, 20 Mar 2025 17:03:51 -0600 Subject: [PATCH 3/5] CORE-764: add export to index --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 11b9091c..8c93e40d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,4 +31,5 @@ export * from './contexts'; export * from './hooks'; export * from './theme'; export * from './types'; +export * from './sentryLogger/sentryLog'; export type { Key } from 'react-aria-components'; From 5168453aab58b081ae09f4f2b3eaa5dee9ae3ac7 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Fri, 21 Mar 2025 09:28:49 -0600 Subject: [PATCH 4/5] CORE-764: validate timestamp as number --- src/sentryLogger/sentryLog.spec.ts | 4 ++-- src/sentryLogger/sentryLog.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentryLogger/sentryLog.spec.ts b/src/sentryLogger/sentryLog.spec.ts index 795c0512..b2825843 100644 --- a/src/sentryLogger/sentryLog.spec.ts +++ b/src/sentryLogger/sentryLog.spec.ts @@ -20,7 +20,7 @@ describe('createConsoleLogger', () => { logger.logEvent( Level.Info, { - "timestamp": "2016-04-20T20:55:53.847Z", + "timestamp": 1461165894000, "type": "navigation", "event_id": "any-id", "category": "any-category", @@ -36,7 +36,7 @@ describe('createConsoleLogger', () => { expect(logFn).toHaveBeenCalledWith( { - "timestamp": "2016-04-20T20:55:53.847Z", + "timestamp": 1461165894000, "type": "navigation", "level": "info", "message": "This is a test", diff --git a/src/sentryLogger/sentryLog.ts b/src/sentryLogger/sentryLog.ts index e02bf962..c8e89032 100644 --- a/src/sentryLogger/sentryLog.ts +++ b/src/sentryLogger/sentryLog.ts @@ -44,7 +44,7 @@ const serializeBreadcrumb = (level: Level, breadcrumb: JsonCompatibleStruct): { level: serializeLevel(level), category: checkTypeOf(category, 'string', ''), message: checkTypeOf(message, 'string', ''), - timestamp: checkTypeOf(timestamp, 'string', undefined), + timestamp: checkTypeOf(timestamp, 'number', undefined), data: flattenObj(rest), } }; From 6f3266c24f06cc5d35e2e7a133832f842ab14e0c Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Fri, 21 Mar 2025 10:38:07 -0600 Subject: [PATCH 5/5] CORE-764: simplify typeof check, remove type key and set category as log --- src/sentryLogger/sentryLog.spec.ts | 8 ++------ src/sentryLogger/sentryLog.ts | 27 ++++++++++----------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/sentryLogger/sentryLog.spec.ts b/src/sentryLogger/sentryLog.spec.ts index b2825843..d77ed9b4 100644 --- a/src/sentryLogger/sentryLog.spec.ts +++ b/src/sentryLogger/sentryLog.spec.ts @@ -21,9 +21,7 @@ describe('createConsoleLogger', () => { Level.Info, { "timestamp": 1461165894000, - "type": "navigation", "event_id": "any-id", - "category": "any-category", "message": "This is a test", "from": "/login", "to": "/dashboard", @@ -37,10 +35,9 @@ describe('createConsoleLogger', () => { expect(logFn).toHaveBeenCalledWith( { "timestamp": 1461165894000, - "type": "navigation", "level": "info", "message": "This is a test", - "category": "any-category", + "category": "log", "data": { "from": "/login", "to": "/dashboard", @@ -61,8 +58,7 @@ describe('createConsoleLogger', () => { expect(logFn).toHaveBeenCalledWith( { "timestamp": undefined, - "category": "", - "type": "", + "category": "log", "message": "", "level": "warning", "data": {} diff --git a/src/sentryLogger/sentryLog.ts b/src/sentryLogger/sentryLog.ts index c8e89032..5284fb46 100644 --- a/src/sentryLogger/sentryLog.ts +++ b/src/sentryLogger/sentryLog.ts @@ -1,21 +1,12 @@ import { addBreadcrumb, SeverityLevel } from "@sentry/react"; import { createCoreLogger, Level } from '@openstax/ts-utils/services/logger'; -import { JsonCompatibleArray, JsonCompatibleStruct, JsonCompatibleValue } from "@openstax/ts-utils/routing"; +import { JsonCompatibleStruct } from "@openstax/ts-utils/routing"; /** - * Creates a logger that creates breadcrumbs using Sentry. - * - * More info: https://develop.sentry.dev/sdk/data-model/event-payloads/breadcrumbs/ + * Flatten nested objects + * @see https://stackoverflow.com/a/70377608 */ -const checkTypeOf = ( - value: JsonCompatibleStruct | JsonCompatibleValue | JsonCompatibleArray, - typeValue: string, - defaultValue: any, -) => - value && typeof value === typeValue ? value : defaultValue - ; - const flattenObj = (obj: { [x: string]: any; }) => Object.keys(obj).reduce((acc: { [x: string]: any; }, curKey) => { if (typeof obj[curKey] === 'object') { @@ -30,7 +21,6 @@ const serializeLevel = (level: Level): SeverityLevel => level === Level.Warn ? 'warning' : level; const serializeBreadcrumb = (level: Level, breadcrumb: JsonCompatibleStruct): { - type?: string; level?: SeverityLevel; category?: string; message?: string; @@ -40,15 +30,18 @@ const serializeBreadcrumb = (level: Level, breadcrumb: JsonCompatibleStruct): { const { type, category, message, timestamp, ...rest } = breadcrumb; return { - type: checkTypeOf(type, 'string', ''), level: serializeLevel(level), - category: checkTypeOf(category, 'string', ''), - message: checkTypeOf(message, 'string', ''), - timestamp: checkTypeOf(timestamp, 'number', undefined), + 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