diff --git a/src/platform/settings/settingStore.ts b/src/platform/settings/settingStore.ts index 5a1573efb..64e2acb8d 100644 --- a/src/platform/settings/settingStore.ts +++ b/src/platform/settings/settingStore.ts @@ -4,9 +4,11 @@ import { compare, valid } from 'semver' import { ref } from 'vue' import type { SettingParams } from '@/platform/settings/types' +import { useTelemetry } from '@/platform/telemetry' import type { Settings } from '@/schemas/apiSchema' import { api } from '@/scripts/api' import { app } from '@/scripts/app' +import { useDialogStore } from '@/stores/dialogStore' import type { TreeNode } from '@/types/treeExplorerTypes' export const getSettingInfo = (setting: SettingParams) => { @@ -73,6 +75,39 @@ export const useSettingStore = defineStore('setting', () => { onChange(settingsById.value[key], newValue, oldValue) settingValues.value[key] = newValue await api.storeSetting(key, newValue) + + try { + const dialogStore = useDialogStore() + if (dialogStore.isDialogOpen('global-settings')) { + const telemetry = useTelemetry() + const settingParameter = settingsById.value[key] + const { category, subCategory } = getSettingInfo( + settingParameter ?? + ({ + id: String(key) + } as unknown as SettingParams) + ) + + const inputType = (() => { + const settingType = settingParameter?.type + if (!settingType) return undefined + return typeof settingType === 'function' + ? 'custom' + : String(settingType) + })() + + telemetry?.trackSettingChanged({ + setting_id: String(key), + input_type: inputType, + category, + sub_category: subCategory, + previous_value: oldValue, + new_value: newValue + }) + } + } catch (err) { + console.error('Failed to track setting change', err) + } } /** diff --git a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts index c06f9fdcb..344be360e 100644 --- a/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts +++ b/src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts @@ -9,6 +9,7 @@ import type { NodeSearchResultMetadata, PageVisibilityMetadata, RunButtonProperties, + SettingChangedMetadata, SurveyResponses, TabCountMetadata, TelemetryEventName, @@ -413,6 +414,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider { this.trackEvent(TelemetryEvents.EXECUTION_SUCCESS, metadata) } + trackSettingChanged(metadata: SettingChangedMetadata): void { + this.trackEvent(TelemetryEvents.SETTING_CHANGED, metadata) + } + getExecutionContext(): ExecutionContext { // Try to initialize composables if not ready and not in onboarding mode if (!this._composablesReady && !this.isOnboardingMode) { diff --git a/src/platform/telemetry/types.ts b/src/platform/telemetry/types.ts index ed5c28ecc..86df5787c 100644 --- a/src/platform/telemetry/types.ts +++ b/src/platform/telemetry/types.ts @@ -126,6 +126,18 @@ export interface TabCountMetadata { tab_count: number } +/** + * Settings change metadata + */ +export interface SettingChangedMetadata { + setting_id: string + input_type?: string + category?: string + sub_category?: string + previous_value?: unknown + new_value?: unknown +} + /** * Node search metadata */ @@ -200,6 +212,9 @@ export interface TelemetryProvider { trackExecutionError(metadata: ExecutionErrorMetadata): void trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void + // Settings events + trackSettingChanged(metadata: SettingChangedMetadata): void + // App lifecycle management markAppReady?(): void identifyUser?(userId: string): void @@ -247,6 +262,9 @@ export const TelemetryEvents = { // Template Filter Analytics TEMPLATE_FILTER_CHANGED: 'app:template_filter_changed', + // Settings + SETTING_CHANGED: 'app:setting_changed', + // Execution Lifecycle EXECUTION_START: 'execution_start', EXECUTION_ERROR: 'execution_error', @@ -275,3 +293,4 @@ export type TelemetryEventProperties = | NodeSearchMetadata | NodeSearchResultMetadata | TemplateFilterMetadata + | SettingChangedMetadata diff --git a/tests-ui/tests/store/settingStore.test.ts b/tests-ui/tests/store/settingStore.test.ts index 60d7acd1c..f9e649f40 100644 --- a/tests-ui/tests/store/settingStore.test.ts +++ b/tests-ui/tests/store/settingStore.test.ts @@ -8,6 +8,7 @@ import { import type { SettingParams } from '@/platform/settings/types' import { api } from '@/scripts/api' import { app } from '@/scripts/app' +import { useDialogStore } from '@/stores/dialogStore' // Mock the api vi.mock('@/scripts/api', () => ({ @@ -17,6 +18,14 @@ vi.mock('@/scripts/api', () => ({ } })) +// Mock telemetry provider +const trackSettingChanged = vi.fn() +vi.mock('@/platform/telemetry', () => ({ + useTelemetry: vi.fn(() => ({ + trackSettingChanged + })) +})) + // Mock the app vi.mock('@/scripts/app', () => ({ app: { @@ -399,6 +408,53 @@ describe('useSettingStore', () => { ) }) + it('should send telemetry when global settings dialog is visible', async () => { + const setting: SettingParams = { + id: 'main.sub.setting.name', + name: 'Telemetry Visible', + type: 'text', + defaultValue: 'default' + } + + store.addSetting(setting) + + const dialogStore = useDialogStore() + dialogStore.showDialog({ + key: 'global-settings', + title: 'Settings', + component: {} + }) + + await store.set('main.sub.setting.name', 'newvalue') + + expect(trackSettingChanged).toHaveBeenCalledTimes(1) + expect(trackSettingChanged).toHaveBeenCalledWith( + expect.objectContaining({ + setting_id: 'main.sub.setting.name', + input_type: 'text', + category: 'main', + sub_category: 'sub', + previous_value: 'default', + new_value: 'newvalue' + }) + ) + }) + + it('should not send telemetry when global settings dialog is not visible', async () => { + const setting: SettingParams = { + id: 'main.sub.setting.name', + name: 'Telemetry Invisible', + type: 'text', + defaultValue: 'default' + } + + store.addSetting(setting) + + await store.set('main.sub.setting.name', 'newvalue') + + expect(trackSettingChanged).not.toHaveBeenCalled() + }) + describe('object mutation prevention', () => { beforeEach(() => { const setting: SettingParams = {