mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-03 11:54:07 +00:00
Compare commits
9 Commits
feat/node-
...
feat/app-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ee77bdbc7 | ||
|
|
ae266f3040 | ||
|
|
9706bacabc | ||
|
|
219f8feaca | ||
|
|
2eb14a1f8d | ||
|
|
16877fec25 | ||
|
|
8afbcdc72e | ||
|
|
82f51e373a | ||
|
|
ea7bfc4919 |
110
src/composables/useModeTimeTracking.test.ts
Normal file
110
src/composables/useModeTimeTracking.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Ref } from 'vue'
|
||||
import { effectScope, nextTick, ref } from 'vue'
|
||||
|
||||
import type { AppMode } from './useAppMode'
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
telemetry: null as { trackModeTimeSpent: ReturnType<typeof vi.fn> } | null,
|
||||
mode: null as unknown as Ref<AppMode>
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => hoisted.telemetry
|
||||
}))
|
||||
|
||||
vi.mock('./useAppMode', () => ({
|
||||
useAppMode: () => ({ mode: hoisted.mode })
|
||||
}))
|
||||
|
||||
import { useModeTimeTracking } from './useModeTimeTracking'
|
||||
|
||||
function setVisibility(state: 'visible' | 'hidden') {
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: state,
|
||||
configurable: true
|
||||
})
|
||||
document.dispatchEvent(new Event('visibilitychange'))
|
||||
}
|
||||
|
||||
describe('useModeTimeTracking', () => {
|
||||
let scope: ReturnType<typeof effectScope>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(0)
|
||||
hoisted.telemetry = { trackModeTimeSpent: vi.fn() }
|
||||
hoisted.mode = ref<AppMode>('graph')
|
||||
setVisibility('visible')
|
||||
scope = effectScope()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
scope.stop()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
const track = () => hoisted.telemetry!.trackModeTimeSpent
|
||||
|
||||
it('emits the elapsed seconds for the previous mode on mode change', async () => {
|
||||
scope.run(() => useModeTimeTracking())
|
||||
|
||||
vi.setSystemTime(5000)
|
||||
hoisted.mode.value = 'app'
|
||||
await nextTick()
|
||||
|
||||
expect(track()).toHaveBeenCalledWith({
|
||||
mode: 'graph',
|
||||
duration_seconds: 5
|
||||
})
|
||||
})
|
||||
|
||||
it('excludes time while the tab is hidden', async () => {
|
||||
hoisted.mode = ref<AppMode>('app')
|
||||
scope.run(() => useModeTimeTracking())
|
||||
|
||||
vi.setSystemTime(2000)
|
||||
setVisibility('hidden')
|
||||
expect(track()).toHaveBeenCalledWith({ mode: 'app', duration_seconds: 2 })
|
||||
|
||||
track().mockClear()
|
||||
vi.setSystemTime(10_000)
|
||||
setVisibility('visible')
|
||||
vi.setSystemTime(11_000)
|
||||
|
||||
hoisted.mode.value = 'graph'
|
||||
await nextTick()
|
||||
|
||||
expect(track()).toHaveBeenCalledTimes(1)
|
||||
expect(track()).toHaveBeenCalledWith({ mode: 'app', duration_seconds: 1 })
|
||||
})
|
||||
|
||||
it('does not emit sub-second durations', async () => {
|
||||
scope.run(() => useModeTimeTracking())
|
||||
|
||||
vi.setSystemTime(300)
|
||||
hoisted.mode.value = 'app'
|
||||
await nextTick()
|
||||
|
||||
expect(track()).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('flushes the final mode when the scope is disposed', () => {
|
||||
hoisted.mode = ref<AppMode>('app')
|
||||
scope.run(() => useModeTimeTracking())
|
||||
|
||||
vi.setSystemTime(3000)
|
||||
scope.stop()
|
||||
|
||||
expect(track()).toHaveBeenCalledWith({ mode: 'app', duration_seconds: 3 })
|
||||
})
|
||||
|
||||
it('no-ops without a telemetry provider', async () => {
|
||||
hoisted.telemetry = null
|
||||
expect(() => scope.run(() => useModeTimeTracking())).not.toThrow()
|
||||
|
||||
vi.setSystemTime(5000)
|
||||
hoisted.mode.value = 'app'
|
||||
await expect(nextTick()).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
65
src/composables/useModeTimeTracking.ts
Normal file
65
src/composables/useModeTimeTracking.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { tryOnScopeDispose, useEventListener } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
import type { AppMode } from './useAppMode'
|
||||
import { useAppMode } from './useAppMode'
|
||||
|
||||
/**
|
||||
* Tracks active (tab-visible) time spent in each {@link AppMode} and emits an
|
||||
* `app:mode_time_spent` event whenever the user leaves a mode, backgrounds the
|
||||
* tab, or tears down. Hidden-tab time is excluded, and flushing on tab-hide
|
||||
* means a closing tab is still captured (`visibilitychange` fires before
|
||||
* unload). Summing `duration_seconds` grouped by `mode` yields time per mode.
|
||||
*
|
||||
* Side-effecting composable: call once from a long-lived scope (GraphView).
|
||||
*/
|
||||
export function useModeTimeTracking() {
|
||||
const dispatcher = useTelemetry()
|
||||
if (!dispatcher) return
|
||||
const trackModeTimeSpent = dispatcher.trackModeTimeSpent.bind(dispatcher)
|
||||
|
||||
const { mode } = useAppMode()
|
||||
|
||||
const isVisible = () => document.visibilityState === 'visible'
|
||||
|
||||
let trackedMode: AppMode = mode.value
|
||||
let accumulatedMs = 0
|
||||
let segmentStart: number | null = isVisible() ? Date.now() : null
|
||||
|
||||
function closeSegment() {
|
||||
if (segmentStart !== null) {
|
||||
accumulatedMs += Date.now() - segmentStart
|
||||
segmentStart = null
|
||||
}
|
||||
}
|
||||
|
||||
function flush() {
|
||||
closeSegment()
|
||||
const durationSeconds = Math.round(accumulatedMs / 1000)
|
||||
accumulatedMs = 0
|
||||
if (durationSeconds >= 1) {
|
||||
trackModeTimeSpent({
|
||||
mode: trackedMode,
|
||||
duration_seconds: durationSeconds
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(mode, (newMode) => {
|
||||
flush()
|
||||
trackedMode = newMode
|
||||
segmentStart = isVisible() ? Date.now() : null
|
||||
})
|
||||
|
||||
useEventListener(document, 'visibilitychange', () => {
|
||||
if (isVisible()) {
|
||||
segmentStart = Date.now()
|
||||
} else {
|
||||
flush()
|
||||
}
|
||||
})
|
||||
|
||||
tryOnScopeDispose(flush)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
BeginCheckoutMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ModeTimeSpentMetadata,
|
||||
ShareFlowMetadata,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
@@ -176,6 +177,10 @@ export class TelemetryRegistry implements TelemetryDispatcher {
|
||||
this.dispatch((provider) => provider.trackEnterLinear?.(metadata))
|
||||
}
|
||||
|
||||
trackModeTimeSpent(metadata: ModeTimeSpentMetadata): void {
|
||||
this.dispatch((provider) => provider.trackModeTimeSpent?.(metadata))
|
||||
}
|
||||
|
||||
trackShareFlow(metadata: ShareFlowMetadata): void {
|
||||
this.dispatch((provider) => provider.trackShareFlow?.(metadata))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
BeginCheckoutMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ModeTimeSpentMetadata,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
ExecutionTriggerSource,
|
||||
@@ -280,7 +281,15 @@ export class GtmTelemetryProvider implements TelemetryProvider {
|
||||
|
||||
trackEnterLinear(metadata: EnterLinearMetadata): void {
|
||||
this.pushEvent('app_mode_opened', {
|
||||
source: metadata.source
|
||||
source: metadata.source,
|
||||
open_source: metadata.open_source
|
||||
})
|
||||
}
|
||||
|
||||
trackModeTimeSpent(metadata: ModeTimeSpentMetadata): void {
|
||||
this.pushEvent('mode_time_spent', {
|
||||
mode: metadata.mode,
|
||||
duration_seconds: metadata.duration_seconds
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ import type {
|
||||
EnterLinearMetadata,
|
||||
ExecutionErrorMetadata,
|
||||
ExecutionSuccessMetadata,
|
||||
ModeTimeSpentMetadata,
|
||||
ShareFlowMetadata,
|
||||
SurveyResponses,
|
||||
TemplateLibraryClosedMetadata,
|
||||
@@ -287,6 +288,10 @@ describe('MixpanelTelemetryProvider — direct event tracking methods', () => {
|
||||
default_view: 'graph'
|
||||
}
|
||||
const enterLinearMetadata: EnterLinearMetadata = {}
|
||||
const modeTimeSpentMetadata: ModeTimeSpentMetadata = {
|
||||
mode: 'app',
|
||||
duration_seconds: 42
|
||||
}
|
||||
const shareFlowMetadata: ShareFlowMetadata = { step: 'dialog_opened' }
|
||||
const executionErrorMetadata: ExecutionErrorMetadata = { jobId: 'job-1' }
|
||||
const executionSuccessMetadata: ExecutionSuccessMetadata = { jobId: 'job-1' }
|
||||
@@ -350,6 +355,11 @@ describe('MixpanelTelemetryProvider — direct event tracking methods', () => {
|
||||
(p) => p.trackEnterLinear(enterLinearMetadata),
|
||||
TelemetryEvents.ENTER_LINEAR_MODE
|
||||
],
|
||||
[
|
||||
'trackModeTimeSpent',
|
||||
(p) => p.trackModeTimeSpent(modeTimeSpentMetadata),
|
||||
TelemetryEvents.MODE_TIME_SPENT
|
||||
],
|
||||
[
|
||||
'trackShareFlow',
|
||||
(p) => p.trackShareFlow(shareFlowMetadata),
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
CreditTopupMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ModeTimeSpentMetadata,
|
||||
ShareFlowMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionTriggerSource,
|
||||
@@ -380,6 +381,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata)
|
||||
}
|
||||
|
||||
trackModeTimeSpent(metadata: ModeTimeSpentMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.MODE_TIME_SPENT, metadata)
|
||||
}
|
||||
|
||||
trackShareFlow(metadata: ShareFlowMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.SHARE_FLOW, metadata)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
AuthMetadata,
|
||||
DefaultViewSetMetadata,
|
||||
EnterLinearMetadata,
|
||||
ModeTimeSpentMetadata,
|
||||
ShareFlowMetadata,
|
||||
ExecutionContext,
|
||||
ExecutionErrorMetadata,
|
||||
@@ -391,6 +392,10 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
|
||||
this.trackEvent(TelemetryEvents.ENTER_LINEAR_MODE, metadata)
|
||||
}
|
||||
|
||||
trackModeTimeSpent(metadata: ModeTimeSpentMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.MODE_TIME_SPENT, metadata)
|
||||
}
|
||||
|
||||
trackShareFlow(metadata: ShareFlowMetadata): void {
|
||||
this.trackEvent(TelemetryEvents.SHARE_FLOW, metadata)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* 3. Check dist/assets/*.js files contain no tracking code
|
||||
*/
|
||||
|
||||
import type { AppMode } from '@/composables/useAppMode'
|
||||
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
|
||||
@@ -165,6 +166,12 @@ export interface WorkflowImportMetadata {
|
||||
|
||||
export interface EnterLinearMetadata {
|
||||
source?: string
|
||||
/**
|
||||
* The workflow's entry path for this session. `'shared_url'` means App Mode
|
||||
* was reached via a link someone shared (`?share=`), as opposed to the user
|
||||
* opening their own/original workflow.
|
||||
*/
|
||||
open_source?: AppModeOpenSource
|
||||
workflow_id?: string
|
||||
}
|
||||
|
||||
@@ -174,6 +181,16 @@ export interface WorkflowSavedMetadata {
|
||||
workflow_id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode time-spent metadata. Emitted when leaving a mode (or hiding the tab),
|
||||
* reporting the active (tab-visible) seconds spent in that mode. Summing
|
||||
* `duration_seconds` grouped by `mode` yields time spent per mode.
|
||||
*/
|
||||
export interface ModeTimeSpentMetadata {
|
||||
mode: AppMode
|
||||
duration_seconds: number
|
||||
}
|
||||
|
||||
export interface DefaultViewSetMetadata {
|
||||
default_view: 'app' | 'graph'
|
||||
}
|
||||
@@ -201,6 +218,12 @@ export type WorkflowOpenSource = NonNullable<
|
||||
WorkflowImportMetadata['open_source']
|
||||
>
|
||||
|
||||
/**
|
||||
* Narrower type for app mode entry tracking.
|
||||
* Maps WorkflowOpenSource values to allowed app mode sources.
|
||||
*/
|
||||
export type AppModeOpenSource = 'shared_url' | 'original' | 'unknown'
|
||||
|
||||
/**
|
||||
* Template library metadata
|
||||
*/
|
||||
@@ -439,6 +462,7 @@ export interface TelemetryProvider {
|
||||
trackWorkflowSaved?(metadata: WorkflowSavedMetadata): void
|
||||
trackDefaultViewSet?(metadata: DefaultViewSetMetadata): void
|
||||
trackEnterLinear?(metadata: EnterLinearMetadata): void
|
||||
trackModeTimeSpent?(metadata: ModeTimeSpentMetadata): void
|
||||
trackShareFlow?(metadata: ShareFlowMetadata): void
|
||||
|
||||
// Page visibility events
|
||||
@@ -526,6 +550,7 @@ export const TelemetryEvents = {
|
||||
WORKFLOW_IMPORTED: 'app:workflow_imported',
|
||||
WORKFLOW_OPENED: 'app:workflow_opened',
|
||||
ENTER_LINEAR_MODE: 'app:app_mode_opened',
|
||||
MODE_TIME_SPENT: 'app:mode_time_spent',
|
||||
SHARE_FLOW: 'app:share_flow',
|
||||
|
||||
// Page Visibility
|
||||
@@ -602,6 +627,7 @@ export type TelemetryEventProperties =
|
||||
| HelpCenterClosedMetadata
|
||||
| WorkflowCreatedMetadata
|
||||
| EnterLinearMetadata
|
||||
| ModeTimeSpentMetadata
|
||||
| ShareFlowMetadata
|
||||
| WorkflowSavedMetadata
|
||||
| DefaultViewSetMetadata
|
||||
|
||||
@@ -61,10 +61,12 @@ function makeWorkflowData(
|
||||
}
|
||||
}
|
||||
|
||||
const { mockConfirm, mockTrackWorkflowSaved } = vi.hoisted(() => ({
|
||||
mockConfirm: vi.fn(),
|
||||
mockTrackWorkflowSaved: vi.fn()
|
||||
}))
|
||||
const { mockConfirm, mockTrackWorkflowSaved, mockTrackEnterLinear } =
|
||||
vi.hoisted(() => ({
|
||||
mockConfirm: vi.fn(),
|
||||
mockTrackWorkflowSaved: vi.fn(),
|
||||
mockTrackEnterLinear: vi.fn()
|
||||
}))
|
||||
|
||||
const draftStoreMocks = vi.hoisted(() => ({
|
||||
saveDraft: vi.fn(() => true),
|
||||
@@ -108,7 +110,7 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
trackDefaultViewSet: vi.fn(),
|
||||
trackWorkflowSaved: mockTrackWorkflowSaved,
|
||||
trackEnterLinear: vi.fn()
|
||||
trackEnterLinear: mockTrackEnterLinear
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -844,6 +846,69 @@ describe('useWorkflowService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('app:app_mode_opened open_source', () => {
|
||||
beforeEach(() => {
|
||||
mockOpenWorkflow()
|
||||
mockTrackEnterLinear.mockClear()
|
||||
})
|
||||
|
||||
it('tags the entry as shared_url when loaded from a shared link', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: true }),
|
||||
'shared_url'
|
||||
)
|
||||
|
||||
expect(mockTrackEnterLinear).toHaveBeenCalledWith({
|
||||
source: 'workflow',
|
||||
open_source: 'shared_url'
|
||||
})
|
||||
})
|
||||
|
||||
it('defaults open_source to unknown when no source is provided', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: true })
|
||||
)
|
||||
|
||||
expect(mockTrackEnterLinear).toHaveBeenCalledWith({
|
||||
source: 'workflow',
|
||||
open_source: 'unknown'
|
||||
})
|
||||
})
|
||||
|
||||
it('tags the entry as original when loaded from a non-shared source', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: true }),
|
||||
'template'
|
||||
)
|
||||
|
||||
expect(mockTrackEnterLinear).toHaveBeenCalledWith({
|
||||
source: 'workflow',
|
||||
open_source: 'original'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not fire when the workflow is not in app mode', async () => {
|
||||
const workflow = createModeTestWorkflow({ loaded: false })
|
||||
|
||||
await service.afterLoadNewGraph(
|
||||
workflow,
|
||||
makeWorkflowData({ linearMode: false }),
|
||||
'shared_url'
|
||||
)
|
||||
|
||||
expect(mockTrackEnterLinear).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('round-trip mode preservation', () => {
|
||||
it('each workflow retains its own mode across tab switches', () => {
|
||||
const workflow1 = createModeTestWorkflow({
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type {
|
||||
WorkflowOpenSource,
|
||||
AppModeOpenSource
|
||||
} from '@/platform/telemetry/types'
|
||||
import { workflowTelemetryId } from '@/platform/telemetry/utils/workflowTelemetryId'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
// eslint-disable-next-line import-x/no-restricted-paths
|
||||
@@ -44,6 +48,13 @@ function linearModeToAppMode(linearMode: unknown): AppMode | null {
|
||||
return linearMode ? 'app' : 'graph'
|
||||
}
|
||||
|
||||
function normalizeOpenSource(
|
||||
source: WorkflowOpenSource | undefined
|
||||
): AppModeOpenSource {
|
||||
if (source === 'shared_url') return 'shared_url'
|
||||
return source ? 'original' : 'unknown'
|
||||
}
|
||||
|
||||
export const useWorkflowService = () => {
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -456,7 +467,8 @@ export const useWorkflowService = () => {
|
||||
*/
|
||||
const afterLoadNewGraph = async (
|
||||
value: string | ComfyWorkflow | null,
|
||||
workflowData: ComfyWorkflowJSON
|
||||
workflowData: ComfyWorkflowJSON,
|
||||
openSource?: WorkflowOpenSource
|
||||
) => {
|
||||
const workflowStore = useWorkspaceStore().workflow
|
||||
const { isAppMode } = useAppMode()
|
||||
@@ -471,6 +483,7 @@ export const useWorkflowService = () => {
|
||||
if (!wasAppMode && workflow.initialMode === 'app') {
|
||||
useTelemetry()?.trackEnterLinear({
|
||||
source: 'workflow',
|
||||
open_source: normalizeOpenSource(openSource),
|
||||
workflow_id: workflowTelemetryId(workflow)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1433,7 +1433,11 @@ export class ComfyApp {
|
||||
}
|
||||
useTelemetry()?.trackWorkflowOpened(telemetryPayload)
|
||||
useTelemetry()?.trackWorkflowImported(telemetryPayload)
|
||||
await useWorkflowService().afterLoadNewGraph(workflow, serializedGraph)
|
||||
await useWorkflowService().afterLoadNewGraph(
|
||||
workflow,
|
||||
serializedGraph,
|
||||
openSource
|
||||
)
|
||||
|
||||
// If the canvas was not visible and we're a fresh load, resize the canvas and fit the view
|
||||
// This fixes switching from app mode to a new graph mode workflow (e.g. load template)
|
||||
|
||||
@@ -54,6 +54,7 @@ import InviteAcceptedToast from '@/platform/workspace/components/toasts/InviteAc
|
||||
import RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue'
|
||||
import { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'
|
||||
import { useCoreCommands } from '@/composables/useCoreCommands'
|
||||
import { useModeTimeTracking } from '@/composables/useModeTimeTracking'
|
||||
import { useQueuePolling } from '@/platform/remote/comfyui/useQueuePolling'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useReconnectQueueRefresh } from '@/composables/useReconnectQueueRefresh'
|
||||
@@ -123,6 +124,12 @@ const telemetry = useTelemetry()
|
||||
const authStore = useAuthStore()
|
||||
let hasTrackedLogin = false
|
||||
|
||||
// Track active time spent in each mode (cloud only). Must run in the
|
||||
// synchronous setup scope so the composable's scope-bound cleanup registers.
|
||||
if (isCloud && telemetry) {
|
||||
useModeTimeTracking()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => colorPaletteStore.completedActivePalette,
|
||||
(newTheme) => {
|
||||
|
||||
Reference in New Issue
Block a user