feat: integrate nightly survey system into app (#8480)

## Summary

Wires the nightly survey system into the app by adding a controller
component and a convenience composable for feature-site usage tracking.

## Changes

- **What**: NightlySurveyController iterates enabled surveys from the
registry and renders a NightlySurveyPopover for each.
useSurveyFeatureTracking wraps useFeatureUsageTracker with a
config-enabled guard for use at feature call sites.
- **Tree-shaking**: Controller is loaded via defineAsyncComponent behind
a compile-time isNightly/isCloud/isDesktop guard in SideToolbar.vue, so
the entire survey module subtree is eliminated from cloud/desktop/stable
builds.

## Review Focus

- DCE pattern: controller imported conditionally via
defineAsyncComponent + distribution guard (same pattern as
ComfyRunButton/index.ts)
- useSurveyFeatureTracking short-circuits early when config is
absent/disabled (avoids initializing tracker storage)
- No user-facing behavior change: FEATURE_SURVEYS registry is still
empty

## Part of Nightly Survey System

This is part 5 of a stacked PR chain:
1. feat/feature-usage-tracker - useFeatureUsageTracker (merged in #8189)
2. feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3. feat/survey-config - surveyRegistry.ts (#8355, merged)
4. feat/survey-popover - NightlySurveyPopover.vue (#9083, merged)
5. **feat/survey-integration** - NightlySurveyController.vue (this PR)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2026-03-12 09:06:26 -07:00
committed by GitHub
parent 34b1799b21
commit c602dce375
4 changed files with 146 additions and 2 deletions

View File

@@ -48,13 +48,24 @@
</div>
</div>
<HelpCenterPopups :is-small="isSmall" />
<Suspense v-if="NightlySurveyController">
<component :is="NightlySurveyController" />
</Suspense>
</nav>
</template>
<script setup lang="ts">
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'es-toolkit/compat'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
computed,
defineAsyncComponent,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
@@ -62,7 +73,7 @@ import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { isCloud } from '@/platform/distribution/types'
import { isCloud, isDesktop, isNightly } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -78,6 +89,13 @@ import SidebarIcon from './SidebarIcon.vue'
import SidebarLogoutIcon from './SidebarLogoutIcon.vue'
import SidebarTemplatesButton from './SidebarTemplatesButton.vue'
const NightlySurveyController =
isNightly && !isCloud && !isDesktop
? defineAsyncComponent(
() => import('@/platform/surveys/NightlySurveyController.vue')
)
: undefined
const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue'
import NightlySurveyPopover from './NightlySurveyPopover.vue'
import { getEnabledSurveys } from './surveyRegistry'
const enabledSurveys = computed(() => getEnabledSurveys())
</script>
<template>
<template v-for="config in enabledSurveys" :key="config.featureId">
<NightlySurveyPopover :config="config" />
</template>
</template>

View File

@@ -0,0 +1,77 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const getSurveyConfig = vi.hoisted(() =>
vi.fn<(featureId: string) => { enabled: boolean } | undefined>()
)
vi.mock('./surveyRegistry', () => ({
getSurveyConfig
}))
describe('useSurveyFeatureTracking', () => {
beforeEach(() => {
localStorage.clear()
vi.resetModules()
getSurveyConfig.mockReset()
})
afterEach(() => {
localStorage.clear()
})
it('tracks usage when config is enabled', async () => {
getSurveyConfig.mockReturnValue({ enabled: true })
const { useSurveyFeatureTracking } =
await import('./useSurveyFeatureTracking')
const { trackFeatureUsed, useCount } =
useSurveyFeatureTracking('test-feature')
expect(useCount.value).toBe(0)
trackFeatureUsed()
expect(useCount.value).toBe(1)
})
it('does not track when config is disabled', async () => {
getSurveyConfig.mockReturnValue({ enabled: false })
const { useSurveyFeatureTracking } =
await import('./useSurveyFeatureTracking')
const { trackFeatureUsed, useCount } =
useSurveyFeatureTracking('disabled-feature')
trackFeatureUsed()
expect(useCount.value).toBe(0)
})
it('tracks usage when config exists without enabled field', async () => {
getSurveyConfig.mockReturnValue({} as { enabled: boolean })
const { useSurveyFeatureTracking } =
await import('./useSurveyFeatureTracking')
const { trackFeatureUsed, useCount } = useSurveyFeatureTracking(
'implicit-enabled-feature'
)
trackFeatureUsed()
expect(useCount.value).toBe(1)
})
it('does not track when config does not exist', async () => {
getSurveyConfig.mockReturnValue(undefined)
const { useSurveyFeatureTracking } =
await import('./useSurveyFeatureTracking')
const { trackFeatureUsed, useCount } = useSurveyFeatureTracking(
'nonexistent-feature'
)
trackFeatureUsed()
expect(useCount.value).toBe(0)
})
})

View File

@@ -0,0 +1,35 @@
import { computed } from 'vue'
import { getSurveyConfig } from './surveyRegistry'
import { useFeatureUsageTracker } from './useFeatureUsageTracker'
/**
* Convenience composable for tracking feature usage for surveys.
* Use this at the feature site to track when a feature is used.
*
* @example
* ```typescript
* const { trackFeatureUsed } = useSurveyFeatureTracking('simple-mode')
*
* function onFeatureAction() {
* trackFeatureUsed()
* }
* ```
*/
export function useSurveyFeatureTracking(featureId: string) {
const config = getSurveyConfig(featureId)
if (config?.enabled === false || !config) {
return {
trackFeatureUsed: () => {},
useCount: computed(() => 0)
}
}
const { trackUsage, useCount } = useFeatureUsageTracker(featureId)
return {
trackFeatureUsed: trackUsage,
useCount
}
}