-
Notifications
You must be signed in to change notification settings - Fork 65
feat(cc-ui-logging): add PROPS_UPDATED event logging with propsToWatch filter #656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,15 @@ | ||
| import React, {useEffect, useRef} from 'react'; | ||
| import {havePropsChanged, logMetrics} from './metricsLogger'; | ||
| import {getChangedWatchedProps, havePropsChanged, logMetrics} from './metricsLogger'; | ||
|
|
||
| export default function withMetrics<P extends object>(Component: any, widgetName: string) { | ||
| export default function withMetrics<P extends object>( | ||
| Component: any, | ||
| widgetName: string, | ||
| propsToWatch: (keyof P & string)[] = [] | ||
| ) { | ||
| return React.memo( | ||
| (props: P) => { | ||
| const prevPropsRef = useRef<P | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| logMetrics({ | ||
| widgetName, | ||
|
|
@@ -20,7 +26,24 @@ export default function withMetrics<P extends object>(Component: any, widgetName | |
| }; | ||
| }, []); | ||
|
|
||
| // TODO: https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6890 PROPS_UPDATED event | ||
| useEffect(() => { | ||
| if (prevPropsRef.current && propsToWatch.length > 0) { | ||
| const changes = getChangedWatchedProps( | ||
| prevPropsRef.current as Record<string, any>, | ||
| props as Record<string, any>, | ||
|
Comment on lines
+29
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This Useful? React with 👍 / 👎. |
||
| propsToWatch | ||
| ); | ||
| if (changes) { | ||
| logMetrics({ | ||
| widgetName, | ||
| event: 'PROPS_UPDATED', | ||
| props: changes, | ||
| timestamp: Date.now(), | ||
| }); | ||
| } | ||
| } | ||
| prevPropsRef.current = props; | ||
| }); | ||
|
|
||
| return <Component {...props} />; | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import store from '@webex/cc-store'; | ||
| import {logMetrics, havePropsChanged, WidgetMetrics} from '../src/metricsLogger'; | ||
| import {logMetrics, havePropsChanged, getChangedWatchedProps, WidgetMetrics} from '../src/metricsLogger'; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add more comprehensive tests |
||
|
|
||
| describe('metricsLogger', () => { | ||
| store.store.logger = { | ||
|
|
@@ -82,5 +82,161 @@ describe('metricsLogger', () => { | |
| expect(havePropsChanged(undefined, undefined)).toBe(false); | ||
| expect(havePropsChanged(null, undefined)).toBe(true); | ||
| }); | ||
|
|
||
| it('should return false for the same object reference', () => { | ||
| const obj = {a: 1, b: 2}; | ||
| expect(havePropsChanged(obj, obj)).toBe(false); | ||
| }); | ||
|
|
||
| it('should return true when primitive value changes in flat object', () => { | ||
| const prev = {name: 'John', age: 30}; | ||
| const next = {name: 'John', age: 31}; | ||
| expect(havePropsChanged(prev, next)).toBe(true); | ||
| }); | ||
|
|
||
| it('should return false for objects with same primitive values', () => { | ||
| const prev = {name: 'John', age: 30}; | ||
| const next = {name: 'John', age: 30}; | ||
| expect(havePropsChanged(prev, next)).toBe(false); | ||
| }); | ||
|
|
||
| it('should return true when a value changes from object to null', () => { | ||
| const prev = {a: {nested: true}}; | ||
| const next = {a: null}; | ||
| expect(havePropsChanged(prev, next)).toBe(true); | ||
| }); | ||
|
|
||
| it('should return true when a value changes from null to object', () => { | ||
| const prev = {a: null}; | ||
| const next = {a: {nested: true}}; | ||
| expect(havePropsChanged(prev, next)).toBe(true); | ||
| }); | ||
|
|
||
| it('should return true when next has more keys than prev', () => { | ||
| const prev = {a: 1}; | ||
| const next = {a: 1, b: 2}; | ||
| expect(havePropsChanged(prev, next)).toBe(true); | ||
| }); | ||
|
|
||
| it('should handle empty objects', () => { | ||
| expect(havePropsChanged({}, {})).toBe(false); | ||
| }); | ||
|
|
||
| it('should return false when both arrays are different references but same nested objects', () => { | ||
| const prev = {items: [1, 2, 3]}; | ||
| const next = {items: [1, 2, 4]}; | ||
| expect(havePropsChanged(prev, next)).toBe(false); | ||
| }); | ||
|
|
||
| it('should return true when function references differ', () => { | ||
| const prev = {onClick: () => {}}; | ||
| const next = {onClick: () => {}}; | ||
| expect(havePropsChanged(prev, next)).toBe(true); | ||
| }); | ||
|
|
||
| it('should return false when function reference is the same', () => { | ||
| const fn = () => {}; | ||
| const prev = {onClick: fn}; | ||
| const next = {onClick: fn}; | ||
| expect(havePropsChanged(prev, next)).toBe(false); | ||
| }); | ||
|
|
||
| it('should return true when a primitive changes to undefined', () => { | ||
| const prev = {a: 'hello'}; | ||
| const next = {a: undefined}; | ||
| expect(havePropsChanged(prev, next)).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getChangedWatchedProps', () => { | ||
| it('should return null when propsToWatch is empty', () => { | ||
| expect(getChangedWatchedProps({a: 1}, {a: 2}, [])).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null when prev or next is null/undefined', () => { | ||
| expect(getChangedWatchedProps(null, {a: 1}, ['a'])).toBeNull(); | ||
| expect(getChangedWatchedProps({a: 1}, null, ['a'])).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null when watched props have not changed', () => { | ||
| const prev = {name: 'John', timer: 10, age: 30}; | ||
| const next = {name: 'John', timer: 20, age: 30}; | ||
| expect(getChangedWatchedProps(prev, next, ['name', 'age'])).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return changes for watched props that changed', () => { | ||
| const prev = {name: 'John', timer: 10, age: 30}; | ||
| const next = {name: 'Jane', timer: 20, age: 31}; | ||
| const result = getChangedWatchedProps(prev, next, ['name', 'age']); | ||
| expect(result).toEqual({ | ||
| name: {oldValue: 'John', newValue: 'Jane'}, | ||
| age: {oldValue: 30, newValue: 31}, | ||
| }); | ||
| }); | ||
|
|
||
| it('should only report changes for watched props, ignoring unwatched', () => { | ||
| const prev = {name: 'John', timer: 10}; | ||
| const next = {name: 'John', timer: 20}; | ||
| expect(getChangedWatchedProps(prev, next, ['name'])).toBeNull(); | ||
| }); | ||
|
|
||
| it('should handle watched props that do not exist on objects', () => { | ||
| const prev = {name: 'John'}; | ||
| const next = {name: 'John'}; | ||
| expect(getChangedWatchedProps(prev, next, ['name', 'missing'])).toBeNull(); | ||
| }); | ||
|
|
||
| it('should detect when a watched prop changes from undefined to a value', () => { | ||
| const prev = {name: undefined}; | ||
| const next = {name: 'John'}; | ||
| const result = getChangedWatchedProps(prev, next, ['name']); | ||
| expect(result).toEqual({ | ||
| name: {oldValue: undefined, newValue: 'John'}, | ||
| }); | ||
| }); | ||
|
|
||
| it('should detect when a watched prop changes from a value to undefined', () => { | ||
| const prev = {name: 'John'}; | ||
| const next = {name: undefined}; | ||
| const result = getChangedWatchedProps(prev, next, ['name']); | ||
| expect(result).toEqual({ | ||
| name: {oldValue: 'John', newValue: undefined}, | ||
| }); | ||
| }); | ||
|
|
||
| it('should return only the changed watched prop when multiple are watched', () => { | ||
| const prev = {name: 'John', status: 'active', role: 'admin'}; | ||
| const next = {name: 'John', status: 'inactive', role: 'admin'}; | ||
| const result = getChangedWatchedProps(prev, next, ['name', 'status', 'role']); | ||
| expect(result).toEqual({ | ||
| status: {oldValue: 'active', newValue: 'inactive'}, | ||
| }); | ||
| }); | ||
|
|
||
| it('should detect changes for boolean watched props', () => { | ||
| const prev = {isActive: true, name: 'John'}; | ||
| const next = {isActive: false, name: 'John'}; | ||
| const result = getChangedWatchedProps(prev, next, ['isActive']); | ||
| expect(result).toEqual({ | ||
| isActive: {oldValue: true, newValue: false}, | ||
| }); | ||
| }); | ||
|
|
||
| it('should detect changes for numeric watched props', () => { | ||
| const prev = {count: 0, name: 'John'}; | ||
| const next = {count: 5, name: 'John'}; | ||
| const result = getChangedWatchedProps(prev, next, ['count']); | ||
| expect(result).toEqual({ | ||
| count: {oldValue: 0, newValue: 5}, | ||
| }); | ||
| }); | ||
|
|
||
| it('should return null when both prev and next are null', () => { | ||
| expect(getChangedWatchedProps(null, null, ['a'])).toBeNull(); | ||
| }); | ||
|
|
||
| it('should return null when both prev and next are undefined', () => { | ||
| expect(getChangedWatchedProps(undefined, undefined, ['a'])).toBeNull(); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getChangedWatchedPropsstores rawoldValue/newValueobjects, andwithMetricsforwards that payload tologMetrics, which serializes withJSON.stringify. If a watched prop contains a circular or otherwise non-serializable value (common for SDK/model objects),JSON.stringifythrows inside the effect, causing runtime errors instead of emitting metrics. This new PROPS_UPDATED path should sanitize or safely serialize watched values before callinglogMetrics.Useful? React with 👍 / 👎.