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)
This commit is contained in:
Benjamin Lu
2025-11-01 23:52:04 -07:00
committed by GitHub
parent 2d22c9f7f0
commit feda2d47b0
4 changed files with 149 additions and 4 deletions

View File

@@ -31,6 +31,8 @@ import FormItem from '@/components/common/FormItem.vue'
import { st } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SettingOption, SettingParams } from '@/platform/settings/types'
import { useTelemetry } from '@/platform/telemetry'
import type { Settings } from '@/schemas/apiSchema'
import { normalizeI18nKey } from '@/utils/formatUtil'
const props = defineProps<{
@@ -75,7 +77,22 @@ const formItem = computed(() => {
const settingStore = useSettingStore()
const settingValue = computed(() => settingStore.get(props.setting.id))
const updateSettingValue = async (value: any) => {
await settingStore.set(props.setting.id, value)
const updateSettingValue = async <K extends keyof Settings>(
newValue: Settings[K]
) => {
const telemetry = useTelemetry()
const settingId = props.setting.id
const previousValue = settingValue.value
await settingStore.set(settingId, newValue)
const normalizedValue = settingStore.get(settingId)
if (previousValue !== normalizedValue) {
telemetry?.trackSettingChanged({
setting_id: settingId,
previous_value: previousValue,
new_value: normalizedValue
})
}
}
</script>

View File

@@ -12,7 +12,6 @@ import { app } from '@/scripts/app'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { NodeSourceType } from '@/types/nodeSource'
import { reduceAllNodes } from '@/utils/graphTraversalUtil'
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
import type {
AuthMetadata,
@@ -27,20 +26,22 @@ import type {
NodeSearchResultMetadata,
PageVisibilityMetadata,
RunButtonProperties,
SettingChangedMetadata,
SurveyResponses,
TabCountMetadata,
TelemetryEventName,
TelemetryEventProperties,
TelemetryProvider,
TemplateFilterMetadata,
TemplateLibraryMetadata,
TemplateLibraryClosedMetadata,
TemplateLibraryMetadata,
TemplateMetadata,
UiButtonClickMetadata,
WorkflowCreatedMetadata,
WorkflowImportMetadata
} from '../../types'
import { TelemetryEvents } from '../../types'
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
interface QueuedEvent {
eventName: TelemetryEventName
@@ -333,6 +334,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata)
}
trackSettingChanged(metadata: SettingChangedMetadata): void {
this.trackEvent(TelemetryEvents.SETTING_CHANGED, metadata)
}
trackUiButtonClicked(metadata: UiButtonClickMetadata): void {
this.trackEvent(TelemetryEvents.UI_BUTTON_CLICKED, metadata)
}

View File

@@ -159,6 +159,15 @@ export interface TabCountMetadata {
tab_count: number
}
/**
* Settings change metadata
*/
export interface SettingChangedMetadata {
setting_id: string
previous_value?: unknown
new_value?: unknown
}
/**
* Node search metadata
*/
@@ -306,6 +315,9 @@ export interface TelemetryProvider {
trackExecutionError(metadata: ExecutionErrorMetadata): void
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void
// Settings events
trackSettingChanged(metadata: SettingChangedMetadata): void
// Generic UI button click events
trackUiButtonClicked(metadata: UiButtonClickMetadata): void
}
@@ -365,6 +377,9 @@ export const TelemetryEvents = {
// Template Filter Analytics
TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed',
// Settings
SETTING_CHANGED: 'app:setting_changed',
// Help Center Analytics
HELP_CENTER_OPENED: 'app:help_center_opened',
HELP_RESOURCE_CLICKED: 'app:help_resource_clicked',
@@ -404,6 +419,7 @@ export type TelemetryEventProperties =
| NodeSearchMetadata
| NodeSearchResultMetadata
| TemplateFilterMetadata
| SettingChangedMetadata
| UiButtonClickMetadata
| HelpCenterOpenedMetadata
| HelpResourceClickedMetadata

View File

@@ -0,0 +1,107 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import SettingItem from '@/platform/settings/components/SettingItem.vue'
import type { SettingParams } from '@/platform/settings/types'
import { i18n } from '@/i18n'
/**
* Verifies that SettingItem emits telemetry when its value changes
* and suppresses telemetry when the value remains the same.
*/
const trackSettingChanged = vi.fn()
vi.mock('@/platform/telemetry', () => ({
useTelemetry: vi.fn(() => ({
trackSettingChanged
}))
}))
const mockGet = vi.fn()
const mockSet = vi.fn()
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: mockGet,
set: mockSet
})
}))
/**
* Minimal stub for FormItem that allows emitting `update:form-value`.
*/
const FormItemUpdateStub = {
template: '<div />',
emits: ['update:form-value'],
props: ['id', 'item', 'formValue']
}
describe('SettingItem (telemetry UI tracking)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const mountComponent = (setting: SettingParams) => {
return shallowMount(SettingItem, {
global: {
plugins: [i18n],
stubs: {
FormItem: FormItemUpdateStub,
Tag: true
}
},
props: {
setting
}
})
}
it('tracks telemetry when value changes via UI (uses normalized value)', async () => {
const settingParams: SettingParams = {
id: 'main.sub.setting.name',
name: 'Telemetry Visible',
type: 'text',
defaultValue: 'default'
}
mockGet.mockReturnValueOnce('default').mockReturnValueOnce('normalized')
mockSet.mockResolvedValue(undefined)
const wrapper = mountComponent(settingParams)
const newValue = 'newvalue'
const formItem = wrapper.findComponent(FormItemUpdateStub)
formItem.vm.$emit('update:form-value', newValue)
await flushPromises()
expect(trackSettingChanged).toHaveBeenCalledTimes(1)
expect(trackSettingChanged).toHaveBeenCalledWith(
expect.objectContaining({
setting_id: 'main.sub.setting.name',
previous_value: 'default',
new_value: 'normalized'
})
)
})
it('does not track telemetry when normalized value does not change', async () => {
const settingParams: SettingParams = {
id: 'main.sub.setting.name',
name: 'Telemetry Visible',
type: 'text',
defaultValue: 'same'
}
mockGet.mockReturnValueOnce('same').mockReturnValueOnce('same')
mockSet.mockResolvedValue(undefined)
const wrapper = mountComponent(settingParams)
const unchangedValue = 'same'
const formItem = wrapper.findComponent(FormItemUpdateStub)
formItem.vm.$emit('update:form-value', unchangedValue)
await flushPromises()
expect(trackSettingChanged).not.toHaveBeenCalled()
})
})