From feda2d47b003fdc1bfa4720abd743d6259382c80 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Sat, 1 Nov 2025 23:52:04 -0700 Subject: [PATCH] feat(telemetry): track settings changes (#6504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../settings/components/SettingItem.vue | 21 +++- .../cloud/MixpanelTelemetryProvider.ts | 9 +- src/platform/telemetry/types.ts | 16 +++ .../settings/components/SettingItem.test.ts | 107 ++++++++++++++++++ 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 tests-ui/tests/platform/settings/components/SettingItem.test.ts diff --git a/src/platform/settings/components/SettingItem.vue b/src/platform/settings/components/SettingItem.vue index c83a4e77a..91f92b2e9 100644 --- a/src/platform/settings/components/SettingItem.vue +++ b/src/platform/settings/components/SettingItem.vue @@ -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 ( + 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 + }) + } } diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index e8639bde9..2168c90d0 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -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) } diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index b070fd341..abe517bca 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -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 diff --git a/tests-ui/tests/platform/settings/components/SettingItem.test.ts b/tests-ui/tests/platform/settings/components/SettingItem.test.ts new file mode 100644 index 000000000..c32548f83 --- /dev/null +++ b/tests-ui/tests/platform/settings/components/SettingItem.test.ts @@ -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: '
', + 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() + }) +})