Skip to content

Commit 459b72b

Browse files
authored
RI-6953: Send telemetry events for ReJSON used editor (#4537)
* fix path depth bug * refactor getJsonPathLevel to return number instead of string
1 parent 23ee64d commit 459b72b

File tree

4 files changed

+200
-19
lines changed

4 files changed

+200
-19
lines changed

redisinsight/ui/src/slices/browser/rejson.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
} from 'uiSrc/utils'
2020
import successMessages from 'uiSrc/components/notifications/success-messages'
2121
import { parseJsonData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/utils'
22-
2322
import {
2423
GetRejsonRlResponseDto,
2524
RemoveRejsonRlResponse,
@@ -38,6 +37,7 @@ import {
3837
import { AppDispatch, RootState } from '../store'
3938

4039
export const JSON_LENGTH_TO_FORCE_RETRIEVE = 200
40+
const TELEMETRY_KEY_LEVEL_ENTIRE_KEY = 'entireKey'
4141

4242
export const initialState: InitialStateRejson = {
4343
loading: false,
@@ -216,6 +216,7 @@ export function setReJSONDataAction(
216216

217217
try {
218218
const state = stateInit()
219+
219220
const { status } = await apiService.patch<GetRejsonRlResponseDto>(
220221
getUrl(
221222
state.connections.instances.connectedInstance?.id,
@@ -230,19 +231,24 @@ export function setReJSONDataAction(
230231

231232
if (isStatusSuccessful(status)) {
232233
try {
234+
const { editorType } = state.browser.rejson
235+
const keyLevel =
236+
editorType === EditorType.Text
237+
? TELEMETRY_KEY_LEVEL_ENTIRE_KEY
238+
: getJsonPathLevel(path)
233239
sendEventTelemetry({
234240
event: getBasedOnViewTypeEvent(
235241
state.browser.keys?.viewType,
236-
TelemetryEvent[
237-
`BROWSER_JSON_PROPERTY_${isEditMode ? 'EDITED' : 'ADDED'}`
238-
],
239-
TelemetryEvent[
240-
`TREE_VIEW_JSON_PROPERTY_${isEditMode ? 'EDITED' : 'ADDED'}`
241-
],
242+
isEditMode
243+
? TelemetryEvent.BROWSER_JSON_PROPERTY_EDITED
244+
: TelemetryEvent.BROWSER_JSON_PROPERTY_ADDED,
245+
isEditMode
246+
? TelemetryEvent.TREE_VIEW_JSON_PROPERTY_EDITED
247+
: TelemetryEvent.TREE_VIEW_JSON_PROPERTY_ADDED,
242248
),
243249
eventData: {
244250
databaseId: state.connections.instances?.connectedInstance?.id,
245-
keyLevel: getJsonPathLevel(path),
251+
keyLevel,
246252
},
247253
})
248254
} catch (error) {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import thunk from 'redux-thunk'
2+
import configureStore from 'redux-mock-store'
3+
import { EditorType } from 'uiSrc/slices/interfaces'
4+
5+
const mockStore = configureStore([thunk])
6+
7+
const originalConsoleError = console.error
8+
9+
// Suppress Redux warnings about missing reducers
10+
beforeAll(() => {
11+
console.error = (...args: any[]) => {
12+
const message = args[0]
13+
if (
14+
typeof message === 'string' &&
15+
message.includes('No reducer provided for key')
16+
) {
17+
return
18+
}
19+
20+
originalConsoleError(...args)
21+
}
22+
})
23+
24+
afterAll(() => {
25+
console.error = originalConsoleError
26+
})
27+
28+
describe('setReJSONDataAction', () => {
29+
let store: any
30+
let sendEventTelemetryMock: jest.Mock
31+
let setReJSONDataAction: any
32+
let apiService: any
33+
34+
beforeEach(async () => {
35+
jest.resetModules()
36+
37+
sendEventTelemetryMock = jest.fn()
38+
39+
jest.doMock('uiSrc/telemetry', () => {
40+
const actual = jest.requireActual('uiSrc/telemetry')
41+
return {
42+
...actual,
43+
sendEventTelemetry: sendEventTelemetryMock,
44+
getBasedOnViewTypeEvent: jest.fn(() => 'mocked_event'),
45+
}
46+
})
47+
48+
jest.doMock('uiSrc/slices/browser/keys', () => {
49+
const actual = jest.requireActual('uiSrc/slices/browser/keys')
50+
return {
51+
...actual,
52+
refreshKeyInfoAction: () => ({ type: 'DUMMY_REFRESH' }),
53+
}
54+
})
55+
56+
const rejson = await import('uiSrc/slices/browser/rejson')
57+
setReJSONDataAction = rejson.setReJSONDataAction
58+
apiService = (await import('uiSrc/services')).apiService
59+
60+
store = mockStore({
61+
browser: {
62+
rejson: { editorType: 'Default' },
63+
keys: { viewType: 'Browser' },
64+
},
65+
app: {
66+
info: { encoding: 'utf8' },
67+
},
68+
connections: {
69+
instances: {
70+
connectedInstance: {
71+
id: 'instance-id',
72+
},
73+
},
74+
},
75+
})
76+
77+
apiService.patch = jest.fn().mockResolvedValue({ status: 200 })
78+
apiService.post = jest.fn().mockResolvedValue({ status: 200, data: {} })
79+
80+
jest.clearAllMocks()
81+
})
82+
83+
it('should call sendEventTelemetry with correct args', async () => {
84+
await store.dispatch(setReJSONDataAction('key', '$', '{}', true, 100))
85+
86+
expect(sendEventTelemetryMock).toHaveBeenCalledWith({
87+
event: 'mocked_event',
88+
eventData: {
89+
databaseId: 'instance-id',
90+
keyLevel: 0,
91+
},
92+
})
93+
})
94+
95+
it('should set entireKey: true when editor is Text', async () => {
96+
store = mockStore({
97+
...store.getState(),
98+
browser: {
99+
...store.getState().browser,
100+
rejson: { editorType: EditorType.Text },
101+
},
102+
})
103+
104+
await store.dispatch(setReJSONDataAction('key', '$', '{}', true, 100))
105+
106+
expect(sendEventTelemetryMock).toHaveBeenCalledWith(
107+
expect.objectContaining({
108+
eventData: expect.objectContaining({
109+
keyLevel: 'entireKey',
110+
}),
111+
}),
112+
)
113+
})
114+
115+
it('should compute keyLevel from nested path', async () => {
116+
const nestedPath = '$.foo.bar[1].nested.key' // 5 levels of nesting
117+
118+
await store.dispatch(
119+
setReJSONDataAction('key', nestedPath, '{}', true, 100),
120+
)
121+
122+
expect(sendEventTelemetryMock).toHaveBeenCalledWith(
123+
expect.objectContaining({
124+
eventData: expect.objectContaining({
125+
keyLevel: 5,
126+
}),
127+
}),
128+
)
129+
})
130+
})

redisinsight/ui/src/telemetry/telemetryUtils.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55
import isGlob from 'is-glob'
66
import { cloneDeep, get } from 'lodash'
7-
import jsonpath from 'jsonpath'
87
import { Maybe, isRedisearchAvailable } from 'uiSrc/utils'
98
import { ApiEndpoints, KeyTypes } from 'uiSrc/constants'
109
import { KeyViewType } from 'uiSrc/slices/interfaces/keys'
@@ -135,18 +134,17 @@ const getBasedOnViewTypeEvent = (
135134
}
136135
}
137136

138-
const getJsonPathLevel = (path: string): string => {
137+
const getJsonPathLevel = (path: string): number => {
139138
try {
140-
if (path === '$') {
141-
return 'root'
142-
}
143-
const levelsLength = jsonpath.parse(
144-
`$${path.startsWith('$') ? '.' : '..'}${path}`,
145-
).length
139+
if (!path || path === '$') return 0
140+
141+
const stripped = path.startsWith('$.') ? path.slice(2) : path.slice(1)
142+
143+
const parts = stripped.split(/[.[\]]/).filter(Boolean)
146144

147-
return levelsLength === 2 ? 'root' : `${levelsLength - 2}`
145+
return parts.length
148146
} catch (e) {
149-
return 'root'
147+
return 0
150148
}
151149
}
152150

redisinsight/ui/src/telemetry/tests/telemetryUtils.spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { RootState, store } from 'uiSrc/slices/store'
22
import { TelemetryEvent } from '../events'
3-
import { getRedisModulesSummary, getFreeDbFlag } from '../telemetryUtils'
3+
import {
4+
getRedisModulesSummary,
5+
getFreeDbFlag,
6+
getJsonPathLevel,
7+
} from '../telemetryUtils'
48

59
const DEFAULT_SUMMARY = Object.freeze({
610
RediSearch: { loaded: false },
@@ -154,3 +158,46 @@ describe('determineFreeDbFlag', () => {
154158
expect(result).toEqual({})
155159
})
156160
})
161+
162+
describe('getJsonPathLevel', () => {
163+
it('returns 0 for empty or root path', () => {
164+
expect(getJsonPathLevel('')).toBe(0)
165+
expect(getJsonPathLevel('$')).toBe(0)
166+
})
167+
168+
it('returns 1 for top-level properties', () => {
169+
expect(getJsonPathLevel('$.foo')).toBe(1)
170+
expect(getJsonPathLevel('$[0]')).toBe(1)
171+
})
172+
173+
it('returns correct level for nested dot paths', () => {
174+
expect(getJsonPathLevel('$.foo.bar')).toBe(2)
175+
expect(getJsonPathLevel('$.foo.bar.baz')).toBe(3)
176+
})
177+
178+
it('returns correct level for mixed dot and bracket paths', () => {
179+
expect(getJsonPathLevel('$.foo[0].bar')).toBe(3)
180+
expect(getJsonPathLevel('$[0].foo.bar')).toBe(3)
181+
expect(getJsonPathLevel('$[0][1][2]')).toBe(3)
182+
})
183+
184+
it('returns correct level for complex mixed paths', () => {
185+
expect(getJsonPathLevel('$.foo[1].bar[2].baz')).toBe(5)
186+
expect(getJsonPathLevel('$[0].foo[1].bar')).toBe(4)
187+
})
188+
189+
it('handles malformed paths gracefully', () => {
190+
expect(getJsonPathLevel('.foo.bar')).toBe(2) // missing $
191+
expect(getJsonPathLevel('foo.bar')).toBe(2) // missing $
192+
expect(getJsonPathLevel('$foo.bar')).toBe(2) // $ not followed by dot
193+
})
194+
195+
it('returns 0 if an exception is thrown (e.g., non-string)', () => {
196+
// @ts-expect-error testing runtime failure
197+
expect(getJsonPathLevel(null)).toBe(0)
198+
// @ts-expect-error testing runtime failure
199+
expect(getJsonPathLevel(undefined)).toBe(0)
200+
// @ts-expect-error testing runtime failure
201+
expect(getJsonPathLevel({})).toBe(0)
202+
})
203+
})

0 commit comments

Comments
 (0)