mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-11 16:10:05 +00:00
add telemetry event for subscription cancellation (#6684)
emits event after going to dashboard and returning to page and having subscription status change from subscribed to not subscribed. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6684-add-telemetry-event-for-subscription-cancellation-2aa6d73d365081009770de6d1db2b701) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -14,12 +14,13 @@ import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'
|
||||
|
||||
type CloudSubscriptionCheckoutResponse = {
|
||||
checkout_url: string
|
||||
}
|
||||
|
||||
type CloudSubscriptionStatusResponse = {
|
||||
export type CloudSubscriptionStatusResponse = {
|
||||
is_active: boolean
|
||||
subscription_id: string
|
||||
renewal_date: string | null
|
||||
@@ -28,6 +29,7 @@ type CloudSubscriptionStatusResponse = {
|
||||
|
||||
function useSubscriptionInternal() {
|
||||
const subscriptionStatus = ref<CloudSubscriptionStatusResponse | null>(null)
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
const isSubscribedOrIsNotCloud = computed(() => {
|
||||
if (!isCloud || !window.__CONFIG__?.subscription_required) return true
|
||||
@@ -103,8 +105,21 @@ function useSubscriptionInternal() {
|
||||
void dialogService.showSubscriptionRequiredDialog()
|
||||
}
|
||||
|
||||
const shouldWatchCancellation = (): boolean =>
|
||||
Boolean(isCloud && window.__CONFIG__?.subscription_required)
|
||||
|
||||
const { startCancellationWatcher, stopCancellationWatcher } =
|
||||
useSubscriptionCancellationWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription: isSubscribedOrIsNotCloud,
|
||||
subscriptionStatus,
|
||||
telemetry,
|
||||
shouldWatchCancellation
|
||||
})
|
||||
|
||||
const manageSubscription = async () => {
|
||||
await accessBillingPortal()
|
||||
startCancellationWatcher()
|
||||
}
|
||||
|
||||
const requireActiveSubscription = async (): Promise<void> => {
|
||||
@@ -168,6 +183,7 @@ function useSubscriptionInternal() {
|
||||
await fetchSubscriptionStatus()
|
||||
} else {
|
||||
subscriptionStatus.value = null
|
||||
stopCancellationWatcher()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { onScopeDispose, ref } from 'vue'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { defaultWindow, useEventListener, useTimeoutFn } from '@vueuse/core'
|
||||
|
||||
import type { TelemetryProvider } from '@/platform/telemetry/types'
|
||||
|
||||
import type { CloudSubscriptionStatusResponse } from './useSubscription'
|
||||
|
||||
const MAX_CANCELLATION_ATTEMPTS = 4
|
||||
const CANCELLATION_BASE_DELAY_MS = 5000
|
||||
const CANCELLATION_BACKOFF_MULTIPLIER = 3 // 5s, 15s, 45s, 135s intervals
|
||||
|
||||
type CancellationWatcherOptions = {
|
||||
fetchStatus: () => Promise<CloudSubscriptionStatusResponse | null | void>
|
||||
isActiveSubscription: ComputedRef<boolean>
|
||||
subscriptionStatus: Ref<CloudSubscriptionStatusResponse | null>
|
||||
telemetry: Pick<TelemetryProvider, 'trackMonthlySubscriptionCancelled'> | null
|
||||
shouldWatchCancellation: () => boolean
|
||||
}
|
||||
|
||||
export function useSubscriptionCancellationWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription,
|
||||
subscriptionStatus,
|
||||
telemetry,
|
||||
shouldWatchCancellation
|
||||
}: CancellationWatcherOptions) {
|
||||
const watcherActive = ref(false)
|
||||
const cancellationAttempts = ref(0)
|
||||
const cancellationTracked = ref(false)
|
||||
const cancellationCheckInFlight = ref(false)
|
||||
const nextDelay = ref(CANCELLATION_BASE_DELAY_MS)
|
||||
let detachFocusListener: (() => void) | null = null
|
||||
|
||||
const { start: startTimer, stop: stopTimer } = useTimeoutFn(
|
||||
() => {
|
||||
void checkForCancellation()
|
||||
},
|
||||
nextDelay,
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
const stopCancellationWatcher = () => {
|
||||
watcherActive.value = false
|
||||
stopTimer()
|
||||
cancellationAttempts.value = 0
|
||||
cancellationCheckInFlight.value = false
|
||||
if (detachFocusListener) {
|
||||
detachFocusListener()
|
||||
detachFocusListener = null
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleNextCancellationCheck = () => {
|
||||
if (!watcherActive.value) return
|
||||
|
||||
if (cancellationAttempts.value >= MAX_CANCELLATION_ATTEMPTS) {
|
||||
stopCancellationWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
nextDelay.value =
|
||||
CANCELLATION_BASE_DELAY_MS *
|
||||
CANCELLATION_BACKOFF_MULTIPLIER ** cancellationAttempts.value
|
||||
cancellationAttempts.value += 1
|
||||
startTimer()
|
||||
}
|
||||
|
||||
const checkForCancellation = async (triggeredFromFocus = false) => {
|
||||
if (!watcherActive.value || cancellationCheckInFlight.value) return
|
||||
|
||||
cancellationCheckInFlight.value = true
|
||||
try {
|
||||
await fetchStatus()
|
||||
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!cancellationTracked.value) {
|
||||
cancellationTracked.value = true
|
||||
try {
|
||||
telemetry?.trackMonthlySubscriptionCancelled()
|
||||
} catch (telemetryError) {
|
||||
console.error(
|
||||
'[Subscription] Failed to track cancellation telemetry:',
|
||||
telemetryError
|
||||
)
|
||||
}
|
||||
}
|
||||
stopCancellationWatcher()
|
||||
return
|
||||
}
|
||||
|
||||
if (!triggeredFromFocus) {
|
||||
scheduleNextCancellationCheck()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Subscription] Error checking cancellation status:', error)
|
||||
scheduleNextCancellationCheck()
|
||||
} finally {
|
||||
cancellationCheckInFlight.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startCancellationWatcher = () => {
|
||||
if (!shouldWatchCancellation() || !subscriptionStatus.value?.is_active) {
|
||||
return
|
||||
}
|
||||
|
||||
stopCancellationWatcher()
|
||||
watcherActive.value = true
|
||||
cancellationTracked.value = false
|
||||
cancellationAttempts.value = 0
|
||||
if (!detachFocusListener && defaultWindow) {
|
||||
detachFocusListener = useEventListener(defaultWindow, 'focus', () => {
|
||||
if (!watcherActive.value) return
|
||||
void checkForCancellation(true)
|
||||
})
|
||||
}
|
||||
scheduleNextCancellationCheck()
|
||||
}
|
||||
|
||||
onScopeDispose(() => {
|
||||
stopCancellationWatcher()
|
||||
})
|
||||
|
||||
return {
|
||||
startCancellationWatcher,
|
||||
stopCancellationWatcher
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user