Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions redisinsight/ui/src/slices/browser/rejson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from 'uiSrc/utils'
import successMessages from 'uiSrc/components/notifications/success-messages'
import { parseJsonData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/utils'

import {
GetRejsonRlResponseDto,
RemoveRejsonRlResponse,
Expand All @@ -38,6 +37,7 @@ import {
import { AppDispatch, RootState } from '../store'

export const JSON_LENGTH_TO_FORCE_RETRIEVE = 200
const TELEMETRY_KEY_LEVEL_ENTIRE_KEY = 'entireKey'

export const initialState: InitialStateRejson = {
loading: false,
Expand Down Expand Up @@ -216,6 +216,7 @@ export function setReJSONDataAction(

try {
const state = stateInit()

const { status } = await apiService.patch<GetRejsonRlResponseDto>(
getUrl(
state.connections.instances.connectedInstance?.id,
Expand All @@ -230,19 +231,24 @@ export function setReJSONDataAction(

if (isStatusSuccessful(status)) {
try {
const { editorType } = state.browser.rejson
const keyLevel =
editorType === EditorType.Text
? TELEMETRY_KEY_LEVEL_ENTIRE_KEY
: getJsonPathLevel(path)
sendEventTelemetry({
event: getBasedOnViewTypeEvent(
state.browser.keys?.viewType,
TelemetryEvent[
`BROWSER_JSON_PROPERTY_${isEditMode ? 'EDITED' : 'ADDED'}`
],
TelemetryEvent[
`TREE_VIEW_JSON_PROPERTY_${isEditMode ? 'EDITED' : 'ADDED'}`
],
isEditMode
? TelemetryEvent.BROWSER_JSON_PROPERTY_EDITED
: TelemetryEvent.BROWSER_JSON_PROPERTY_ADDED,
isEditMode
? TelemetryEvent.TREE_VIEW_JSON_PROPERTY_EDITED
: TelemetryEvent.TREE_VIEW_JSON_PROPERTY_ADDED,
),
eventData: {
databaseId: state.connections.instances?.connectedInstance?.id,
keyLevel: getJsonPathLevel(path),
keyLevel,
},
})
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import thunk from 'redux-thunk'
import configureStore from 'redux-mock-store'
import { EditorType } from 'uiSrc/slices/interfaces'

const mockStore = configureStore([thunk])

const originalConsoleError = console.error

// Suppress Redux warnings about missing reducers
beforeAll(() => {
console.error = (...args: any[]) => {
const message = args[0]
if (
typeof message === 'string' &&
message.includes('No reducer provided for key')
) {
return
}

originalConsoleError(...args)
}
})
Comment on lines +9 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why for this test specifically?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a brand new test suite, and I'm adding it here. I'm suppressing the expected Redux warnings to keep the logs clean and free of noise.


afterAll(() => {
console.error = originalConsoleError
})

describe('setReJSONDataAction', () => {
let store: any
let sendEventTelemetryMock: jest.Mock
let setReJSONDataAction: any
let apiService: any

beforeEach(async () => {
jest.resetModules()

sendEventTelemetryMock = jest.fn()

jest.doMock('uiSrc/telemetry', () => {
const actual = jest.requireActual('uiSrc/telemetry')
return {
...actual,
sendEventTelemetry: sendEventTelemetryMock,
getBasedOnViewTypeEvent: jest.fn(() => 'mocked_event'),
}
})

jest.doMock('uiSrc/slices/browser/keys', () => {
const actual = jest.requireActual('uiSrc/slices/browser/keys')
return {
...actual,
refreshKeyInfoAction: () => ({ type: 'DUMMY_REFRESH' }),
}
})

const rejson = await import('uiSrc/slices/browser/rejson')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the dynamic imports?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mocking process for this file is a bit tricky. It seems something gets imported before Jest has a chance to apply the usual jest.mock() logic, so the standard mocking approach doesn’t work here. That’s why this file uses jest.resetModules() - to force a clean module state.

setReJSONDataAction = rejson.setReJSONDataAction
apiService = (await import('uiSrc/services')).apiService

store = mockStore({
browser: {
rejson: { editorType: 'Default' },
keys: { viewType: 'Browser' },
},
app: {
info: { encoding: 'utf8' },
},
connections: {
instances: {
connectedInstance: {
id: 'instance-id',
},
},
},
})

apiService.patch = jest.fn().mockResolvedValue({ status: 200 })
apiService.post = jest.fn().mockResolvedValue({ status: 200, data: {} })

jest.clearAllMocks()
})

it('should call sendEventTelemetry with correct args', async () => {
await store.dispatch(setReJSONDataAction('key', '$', '{}', true, 100))

expect(sendEventTelemetryMock).toHaveBeenCalledWith({
event: 'mocked_event',
eventData: {
databaseId: 'instance-id',
keyLevel: 0,
},
})
})

it('should set entireKey: true when editor is Text', async () => {
store = mockStore({
...store.getState(),
browser: {
...store.getState().browser,
rejson: { editorType: EditorType.Text },
},
})

await store.dispatch(setReJSONDataAction('key', '$', '{}', true, 100))

expect(sendEventTelemetryMock).toHaveBeenCalledWith(
expect.objectContaining({
eventData: expect.objectContaining({
keyLevel: 'entireKey',
}),
}),
)
})

it('should compute keyLevel from nested path', async () => {
const nestedPath = '$.foo.bar[1].nested.key' // 5 levels of nesting

await store.dispatch(
setReJSONDataAction('key', nestedPath, '{}', true, 100),
)

expect(sendEventTelemetryMock).toHaveBeenCalledWith(
expect.objectContaining({
eventData: expect.objectContaining({
keyLevel: 5,
}),
}),
)
})
})
18 changes: 8 additions & 10 deletions redisinsight/ui/src/telemetry/telemetryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/
import isGlob from 'is-glob'
import { cloneDeep, get } from 'lodash'
import jsonpath from 'jsonpath'
import { Maybe, isRedisearchAvailable } from 'uiSrc/utils'
import { ApiEndpoints, KeyTypes } from 'uiSrc/constants'
import { KeyViewType } from 'uiSrc/slices/interfaces/keys'
Expand Down Expand Up @@ -135,18 +134,17 @@ const getBasedOnViewTypeEvent = (
}
}

const getJsonPathLevel = (path: string): string => {
const getJsonPathLevel = (path: string): number => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some tests verifying this funciton works as expected won't hurt

try {
if (path === '$') {
return 'root'
}
const levelsLength = jsonpath.parse(
`$${path.startsWith('$') ? '.' : '..'}${path}`,
).length
if (!path || path === '$') return 0

const stripped = path.startsWith('$.') ? path.slice(2) : path.slice(1)

const parts = stripped.split(/[.[\]]/).filter(Boolean)

return levelsLength === 2 ? 'root' : `${levelsLength - 2}`
return parts.length
} catch (e) {
return 'root'
return 0
}
}

Expand Down
49 changes: 48 additions & 1 deletion redisinsight/ui/src/telemetry/tests/telemetryUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { RootState, store } from 'uiSrc/slices/store'
import { TelemetryEvent } from '../events'
import { getRedisModulesSummary, getFreeDbFlag } from '../telemetryUtils'
import {
getRedisModulesSummary,
getFreeDbFlag,
getJsonPathLevel,
} from '../telemetryUtils'

const DEFAULT_SUMMARY = Object.freeze({
RediSearch: { loaded: false },
Expand Down Expand Up @@ -154,3 +158,46 @@ describe('determineFreeDbFlag', () => {
expect(result).toEqual({})
})
})

describe('getJsonPathLevel', () => {
it('returns 0 for empty or root path', () => {
expect(getJsonPathLevel('')).toBe(0)
expect(getJsonPathLevel('$')).toBe(0)
})

it('returns 1 for top-level properties', () => {
expect(getJsonPathLevel('$.foo')).toBe(1)
expect(getJsonPathLevel('$[0]')).toBe(1)
})

it('returns correct level for nested dot paths', () => {
expect(getJsonPathLevel('$.foo.bar')).toBe(2)
expect(getJsonPathLevel('$.foo.bar.baz')).toBe(3)
})

it('returns correct level for mixed dot and bracket paths', () => {
expect(getJsonPathLevel('$.foo[0].bar')).toBe(3)
expect(getJsonPathLevel('$[0].foo.bar')).toBe(3)
expect(getJsonPathLevel('$[0][1][2]')).toBe(3)
})

it('returns correct level for complex mixed paths', () => {
expect(getJsonPathLevel('$.foo[1].bar[2].baz')).toBe(5)
expect(getJsonPathLevel('$[0].foo[1].bar')).toBe(4)
})

it('handles malformed paths gracefully', () => {
expect(getJsonPathLevel('.foo.bar')).toBe(2) // missing $
expect(getJsonPathLevel('foo.bar')).toBe(2) // missing $
expect(getJsonPathLevel('$foo.bar')).toBe(2) // $ not followed by dot
})

it('returns 0 if an exception is thrown (e.g., non-string)', () => {
// @ts-expect-error testing runtime failure
expect(getJsonPathLevel(null)).toBe(0)
// @ts-expect-error testing runtime failure
expect(getJsonPathLevel(undefined)).toBe(0)
// @ts-expect-error testing runtime failure
expect(getJsonPathLevel({})).toBe(0)
})
})
Loading