Skip to content

Commit feda2d4

Browse files
feat(telemetry): track settings changes (#6504)
Summary - Add telemetry event for settings changes when the global settings dialog is open - Clarify variable names in settings store (`settingParameter`, `settingType`) for readability - Introduce `SettingChangedMetadata` and `TelemetryEvents.SETTING_CHANGED` - Implement `trackSettingChanged` in Mixpanel provider - Add focused unit test to verify telemetry triggers when settings dialog is open vs closed Quality - Ran `pnpm lint:fix` and `pnpm typecheck` - Unit tests pass locally Notes - Event fires only when the settings dialog is open (uses `useDialogStore().isDialogOpen('global-settings')`) - OSS builds are unaffected (`useTelemetry()` returns null) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6504-feat-telemetry-track-settings-changes-clarify-names-add-unit-test-29e6d73d3650815ea919d832b310cc46) by [Unito](https://www.unito.io)
1 parent 2d22c9f commit feda2d4

File tree

4 files changed

+149
-4
lines changed

4 files changed

+149
-4
lines changed

src/platform/settings/components/SettingItem.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import FormItem from '@/components/common/FormItem.vue'
3131
import { st } from '@/i18n'
3232
import { useSettingStore } from '@/platform/settings/settingStore'
3333
import type { SettingOption, SettingParams } from '@/platform/settings/types'
34+
import { useTelemetry } from '@/platform/telemetry'
35+
import type { Settings } from '@/schemas/apiSchema'
3436
import { normalizeI18nKey } from '@/utils/formatUtil'
3537
3638
const props = defineProps<{
@@ -75,7 +77,22 @@ const formItem = computed(() => {
7577
7678
const settingStore = useSettingStore()
7779
const settingValue = computed(() => settingStore.get(props.setting.id))
78-
const updateSettingValue = async (value: any) => {
79-
await settingStore.set(props.setting.id, value)
80+
const updateSettingValue = async <K extends keyof Settings>(
81+
newValue: Settings[K]
82+
) => {
83+
const telemetry = useTelemetry()
84+
const settingId = props.setting.id
85+
const previousValue = settingValue.value
86+
87+
await settingStore.set(settingId, newValue)
88+
89+
const normalizedValue = settingStore.get(settingId)
90+
if (previousValue !== normalizedValue) {
91+
telemetry?.trackSettingChanged({
92+
setting_id: settingId,
93+
previous_value: previousValue,
94+
new_value: normalizedValue
95+
})
96+
}
8097
}
8198
</script>

src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { app } from '@/scripts/app'
1212
import { useNodeDefStore } from '@/stores/nodeDefStore'
1313
import { NodeSourceType } from '@/types/nodeSource'
1414
import { reduceAllNodes } from '@/utils/graphTraversalUtil'
15-
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
1615

1716
import type {
1817
AuthMetadata,
@@ -27,20 +26,22 @@ import type {
2726
NodeSearchResultMetadata,
2827
PageVisibilityMetadata,
2928
RunButtonProperties,
29+
SettingChangedMetadata,
3030
SurveyResponses,
3131
TabCountMetadata,
3232
TelemetryEventName,
3333
TelemetryEventProperties,
3434
TelemetryProvider,
3535
TemplateFilterMetadata,
36-
TemplateLibraryMetadata,
3736
TemplateLibraryClosedMetadata,
37+
TemplateLibraryMetadata,
3838
TemplateMetadata,
3939
UiButtonClickMetadata,
4040
WorkflowCreatedMetadata,
4141
WorkflowImportMetadata
4242
} from '../../types'
4343
import { TelemetryEvents } from '../../types'
44+
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
4445

4546
interface QueuedEvent {
4647
eventName: TelemetryEventName
@@ -333,6 +334,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
333334
this.trackEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata)
334335
}
335336

337+
trackSettingChanged(metadata: SettingChangedMetadata): void {
338+
this.trackEvent(TelemetryEvents.SETTING_CHANGED, metadata)
339+
}
340+
336341
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
337342
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
338343
}

src/platform/telemetry/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ export interface TabCountMetadata {
159159
tab_count: number
160160
}
161161

162+
/**
163+
* Settings change metadata
164+
*/
165+
export interface SettingChangedMetadata {
166+
setting_id: string
167+
previous_value?: unknown
168+
new_value?: unknown
169+
}
170+
162171
/**
163172
* Node search metadata
164173
*/
@@ -306,6 +315,9 @@ export interface TelemetryProvider {
306315
trackExecutionError(metadata: ExecutionErrorMetadata): void
307316
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
308317

318+
// Settings events
319+
trackSettingChanged(metadata: SettingChangedMetadata): void
320+
309321
// Generic UI button click events
310322
trackUiButtonClicked(metadata: UiButtonClickMetadata): void
311323
}
@@ -365,6 +377,9 @@ export const TelemetryEvents = {
365377
// Template Filter Analytics
366378
TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed',
367379

380+
// Settings
381+
SETTING_CHANGED: 'app:setting_changed',
382+
368383
// Help Center Analytics
369384
HELP_CENTER_OPENED: 'app:help_center_opened',
370385
HELP_RESOURCE_CLICKED: 'app:help_resource_clicked',
@@ -404,6 +419,7 @@ export type TelemetryEventProperties =
404419
| NodeSearchMetadata
405420
| NodeSearchResultMetadata
406421
| TemplateFilterMetadata
422+
| SettingChangedMetadata
407423
| UiButtonClickMetadata
408424
| HelpCenterOpenedMetadata
409425
| HelpResourceClickedMetadata
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { flushPromises, shallowMount } from '@vue/test-utils'
2+
import { describe, expect, it, vi, beforeEach } from 'vitest'
3+
4+
import SettingItem from '@/platform/settings/components/SettingItem.vue'
5+
import type { SettingParams } from '@/platform/settings/types'
6+
import { i18n } from '@/i18n'
7+
8+
/**
9+
* Verifies that SettingItem emits telemetry when its value changes
10+
* and suppresses telemetry when the value remains the same.
11+
*/
12+
const trackSettingChanged = vi.fn()
13+
vi.mock('@/platform/telemetry', () => ({
14+
useTelemetry: vi.fn(() => ({
15+
trackSettingChanged
16+
}))
17+
}))
18+
19+
const mockGet = vi.fn()
20+
const mockSet = vi.fn()
21+
vi.mock('@/platform/settings/settingStore', () => ({
22+
useSettingStore: () => ({
23+
get: mockGet,
24+
set: mockSet
25+
})
26+
}))
27+
28+
/**
29+
* Minimal stub for FormItem that allows emitting `update:form-value`.
30+
*/
31+
const FormItemUpdateStub = {
32+
template: '<div />',
33+
emits: ['update:form-value'],
34+
props: ['id', 'item', 'formValue']
35+
}
36+
37+
describe('SettingItem (telemetry UI tracking)', () => {
38+
beforeEach(() => {
39+
vi.clearAllMocks()
40+
})
41+
42+
const mountComponent = (setting: SettingParams) => {
43+
return shallowMount(SettingItem, {
44+
global: {
45+
plugins: [i18n],
46+
stubs: {
47+
FormItem: FormItemUpdateStub,
48+
Tag: true
49+
}
50+
},
51+
props: {
52+
setting
53+
}
54+
})
55+
}
56+
57+
it('tracks telemetry when value changes via UI (uses normalized value)', async () => {
58+
const settingParams: SettingParams = {
59+
id: 'main.sub.setting.name',
60+
name: 'Telemetry Visible',
61+
type: 'text',
62+
defaultValue: 'default'
63+
}
64+
65+
mockGet.mockReturnValueOnce('default').mockReturnValueOnce('normalized')
66+
mockSet.mockResolvedValue(undefined)
67+
68+
const wrapper = mountComponent(settingParams)
69+
70+
const newValue = 'newvalue'
71+
const formItem = wrapper.findComponent(FormItemUpdateStub)
72+
formItem.vm.$emit('update:form-value', newValue)
73+
74+
await flushPromises()
75+
76+
expect(trackSettingChanged).toHaveBeenCalledTimes(1)
77+
expect(trackSettingChanged).toHaveBeenCalledWith(
78+
expect.objectContaining({
79+
setting_id: 'main.sub.setting.name',
80+
previous_value: 'default',
81+
new_value: 'normalized'
82+
})
83+
)
84+
})
85+
86+
it('does not track telemetry when normalized value does not change', async () => {
87+
const settingParams: SettingParams = {
88+
id: 'main.sub.setting.name',
89+
name: 'Telemetry Visible',
90+
type: 'text',
91+
defaultValue: 'same'
92+
}
93+
94+
mockGet.mockReturnValueOnce('same').mockReturnValueOnce('same')
95+
mockSet.mockResolvedValue(undefined)
96+
97+
const wrapper = mountComponent(settingParams)
98+
99+
const unchangedValue = 'same'
100+
const formItem = wrapper.findComponent(FormItemUpdateStub)
101+
formItem.vm.$emit('update:form-value', unchangedValue)
102+
103+
await flushPromises()
104+
105+
expect(trackSettingChanged).not.toHaveBeenCalled()
106+
})
107+
})

0 commit comments

Comments
 (0)