feat(core): adjust subscription related mixpanel (#7536)

This commit is contained in:
CatsJuice 2024-07-26 02:49:15 +00:00
parent 549e7befed
commit a714961b20
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
13 changed files with 169 additions and 92 deletions

View File

@ -104,7 +104,7 @@ export const AIUsagePanel = () => {
> >
{copilotActionLimit === 'unlimited' ? ( {copilotActionLimit === 'unlimited' ? (
hasPaymentFeature && aiSubscription?.canceledAt ? ( hasPaymentFeature && aiSubscription?.canceledAt ? (
<AIResume /> <AIResume module="billing subscription list" />
) : ( ) : (
<Button onClick={openBilling}> <Button onClick={openBilling}>
{t['com.affine.payment.ai.usage.change-button-label']()} {t['com.affine.payment.ai.usage.change-button-label']()}

View File

@ -32,7 +32,7 @@ import {
import { useMutation } from '../../../../../hooks/use-mutation'; import { useMutation } from '../../../../../hooks/use-mutation';
import { useQuery } from '../../../../../hooks/use-query'; import { useQuery } from '../../../../../hooks/use-query';
import { AuthService, SubscriptionService } from '../../../../../modules/cloud'; import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
import { mixpanel, popupWindow } from '../../../../../utils'; import { mixpanel, mixpanelTrack, popupWindow } from '../../../../../utils';
import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary'; import { SWRErrorBoundary } from '../../../../pure/swr-error-bundary';
import { CancelAction, ResumeAction } from '../plans/actions'; import { CancelAction, ResumeAction } from '../plans/actions';
import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions'; import { AICancel, AIResume, AISubscribe } from '../plans/ai/actions';
@ -237,19 +237,13 @@ const SubscriptionSettings = () => {
</SettingRow> </SettingRow>
) : ( ) : (
<CancelAction <CancelAction
module="billing subscription list"
open={openCancelModal} open={openCancelModal}
onOpenChange={setOpenCancelModal} onOpenChange={setOpenCancelModal}
> >
<SettingRow <SettingRow
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
mixpanel.track('PlanChangeStarted', {
segment: 'settings panel',
module: 'billing subscription list',
control: 'plan cancel action',
type: proSubscription.plan,
category: proSubscription.recurring,
});
setOpenCancelModal(true); setOpenCancelModal(true);
}} }}
className="dangerous-setting" className="dangerous-setting"
@ -403,9 +397,15 @@ const AIPlanCard = ({ onClick }: { onClick: () => void }) => {
{price?.yearlyAmount ? ( {price?.yearlyAmount ? (
subscription ? ( subscription ? (
subscription.canceledAt ? ( subscription.canceledAt ? (
<AIResume className={styles.planAction} /> <AIResume
module="billing subscription list"
className={styles.planAction}
/>
) : ( ) : (
<AICancel className={styles.planAction} /> <AICancel
module="billing subscription list"
className={styles.planAction}
/>
) )
) : ( ) : (
<AISubscribe className={styles.planAction}> <AISubscribe className={styles.planAction}>
@ -476,13 +476,17 @@ const ResumeSubscription = () => {
const subscription = useService(SubscriptionService).subscription; const subscription = useService(SubscriptionService).subscription;
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setOpen(true); setOpen(true);
mixpanel.track('PlanChangeStarted', { const type = subscription.pro$.value?.plan;
segment: 'settings panel', const category = subscription.pro$.value?.recurring;
module: 'pricing plan list', if (type && category) {
control: 'plan resume action', mixpanelTrack('PlanChangeStarted', {
type: subscription.pro$.value?.plan, segment: 'settings panel',
category: subscription.pro$.value?.recurring, module: 'pricing plan list',
}); control: 'paying',
type,
category,
});
}
}, [subscription.pro$.value?.plan, subscription.pro$.value?.recurring]); }, [subscription.pro$.value?.plan, subscription.pro$.value?.recurring]);
return ( return (

View File

@ -1,11 +1,12 @@
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify'; import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { mixpanel } from '@affine/core/utils'; import type { MixpanelEvents } from '@affine/core/mixpanel';
import { mixpanelTrack } from '@affine/core/utils';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { AuthService, SubscriptionService } from '../../../../../modules/cloud'; import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
import { useDowngradeNotify } from '../../../subscription-landing/notify'; import { useDowngradeNotify } from '../../../subscription-landing/notify';
@ -20,16 +21,30 @@ export const CancelAction = ({
children, children,
open, open,
onOpenChange, onOpenChange,
module,
}: { }: {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
module: MixpanelEvents['PlanChangeStarted']['module'];
} & PropsWithChildren) => { } & PropsWithChildren) => {
const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
const [isMutating, setIsMutating] = useState(false); const [isMutating, setIsMutating] = useState(false);
const subscription = useService(SubscriptionService).subscription; const subscription = useService(SubscriptionService).subscription;
const proSubscription = useLiveData(subscription.pro$);
const authService = useService(AuthService); const authService = useService(AuthService);
const downgradeNotify = useDowngradeNotify(); const downgradeNotify = useDowngradeNotify();
useEffect(() => {
if (!open || !proSubscription) return;
mixpanelTrack('PlanChangeStarted', {
segment: 'settings panel',
module,
control: 'cancel',
type: proSubscription.plan,
category: proSubscription.recurring,
});
}, [module, open, proSubscription]);
const downgrade = useAsyncCallback(async () => { const downgrade = useAsyncCallback(async () => {
try { try {
const account = authService.session.account$.value; const account = authService.session.account$.value;
@ -41,13 +56,14 @@ export const CancelAction = ({
// refresh idempotency key // refresh idempotency key
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
onOpenChange(false); onOpenChange(false);
mixpanel.track('ChangePlanSucceeded', { const proSubscription = subscription.pro$.value;
segment: 'settings panel', if (proSubscription) {
module: 'pricing plan list', mixpanelTrack('PlanChangeSucceeded', {
control: 'plan cancel action', control: 'cancel',
type: subscription.pro$.value?.plan, type: proSubscription.plan,
category: subscription.pro$.value?.recurring, category: proSubscription.recurring,
}); });
}
if (account && prevRecurring) { if (account && prevRecurring) {
downgradeNotify( downgradeNotify(
getDowngradeQuestionnaireLink({ getDowngradeQuestionnaireLink({
@ -110,13 +126,14 @@ export const ResumeAction = ({
// refresh idempotency key // refresh idempotency key
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
onOpenChange(false); onOpenChange(false);
mixpanel.track('ChangePlanSucceeded', { const proSubscription = subscription.pro$.value;
segment: 'settings panel', if (proSubscription) {
module: 'pricing plan list', mixpanelTrack('PlanChangeSucceeded', {
control: 'plan resume action', control: 'paying',
type: subscription.pro$.value?.plan, type: proSubscription.plan,
category: subscription.pro$.value?.recurring, category: proSubscription.recurring,
}); });
}
} finally { } finally {
setIsMutating(false); setIsMutating(false);
} }

View File

@ -2,16 +2,19 @@ import { Button, type ButtonProps, useConfirmModal } from '@affine/component';
import { useDowngradeNotify } from '@affine/core/components/affine/subscription-landing/notify'; import { useDowngradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify'; import { getDowngradeQuestionnaireLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import type { MixpanelEvents } from '@affine/core/mixpanel';
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud'; import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
import { mixpanel } from '@affine/core/utils'; import { mixpanel, mixpanelTrack } from '@affine/core/utils';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useState } from 'react'; import { useState } from 'react';
export interface AICancelProps extends ButtonProps {} export interface AICancelProps extends ButtonProps {
export const AICancel = ({ ...btnProps }: AICancelProps) => { module: MixpanelEvents['PlanChangeStarted']['module'];
}
export const AICancel = ({ module, ...btnProps }: AICancelProps) => {
const t = useI18n(); const t = useI18n();
const [isMutating, setMutating] = useState(false); const [isMutating, setMutating] = useState(false);
const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
@ -22,12 +25,16 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
const downgradeNotify = useDowngradeNotify(); const downgradeNotify = useDowngradeNotify();
const cancel = useAsyncCallback(async () => { const cancel = useAsyncCallback(async () => {
mixpanel.track('PlanChangeStarted', { const aiSubscription = subscription.ai$.value;
segment: 'settings panel', if (aiSubscription) {
control: 'plan cancel action', mixpanelTrack('PlanChangeStarted', {
type: subscription.ai$.value?.plan, module,
category: subscription.ai$.value?.recurring, segment: 'settings panel',
}); control: 'cancel',
type: SubscriptionPlan.AI,
category: aiSubscription.recurring,
});
}
openConfirmModal({ openConfirmModal({
title: t['com.affine.payment.ai.action.cancel.confirm.title'](), title: t['com.affine.payment.ai.action.cancel.confirm.title'](),
description: description:
@ -51,9 +58,10 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
SubscriptionPlan.AI SubscriptionPlan.AI
); );
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
mixpanel.track('ChangePlanSucceeded', { mixpanel.track('PlanChangeSucceeded', {
segment: 'settings panel', segment: 'settings panel',
control: 'plan cancel action', control: 'plan cancel action',
type: subscription.ai$.value?.plan,
}); });
const account = authService.session.account$.value; const account = authService.session.account$.value;
const prevRecurring = subscription.ai$.value?.recurring; const prevRecurring = subscription.ai$.value?.recurring;
@ -74,6 +82,7 @@ export const AICancel = ({ ...btnProps }: AICancelProps) => {
}, },
}); });
}, [ }, [
module,
subscription, subscription,
openConfirmModal, openConfirmModal,
t, t,

View File

@ -5,8 +5,9 @@ import {
useConfirmModal, useConfirmModal,
} from '@affine/component'; } from '@affine/component';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import type { MixpanelEvents } from '@affine/core/mixpanel';
import { SubscriptionService } from '@affine/core/modules/cloud'; import { SubscriptionService } from '@affine/core/modules/cloud';
import { mixpanel } from '@affine/core/utils'; import { mixpanelTrack } from '@affine/core/utils';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons/rc'; import { SingleSelectSelectSolidIcon } from '@blocksuite/icons/rc';
@ -15,9 +16,11 @@ import { cssVar } from '@toeverything/theme';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useState } from 'react'; import { useState } from 'react';
export interface AIResumeProps extends ButtonProps {} export interface AIResumeProps extends ButtonProps {
module: MixpanelEvents['PlanChangeStarted']['module'];
}
export const AIResume = ({ ...btnProps }: AIResumeProps) => { export const AIResume = ({ module, ...btnProps }: AIResumeProps) => {
const t = useI18n(); const t = useI18n();
const [idempotencyKey, setIdempotencyKey] = useState(nanoid()); const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
const subscription = useService(SubscriptionService).subscription; const subscription = useService(SubscriptionService).subscription;
@ -27,12 +30,16 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => {
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const resume = useAsyncCallback(async () => { const resume = useAsyncCallback(async () => {
mixpanel.track('PlanChangeStarted', { const aiSubscription = subscription.ai$.value;
segment: 'settings panel', if (aiSubscription) {
control: 'plan resume action', mixpanelTrack('PlanChangeStarted', {
type: subscription.ai$.value?.plan, module,
category: subscription.ai$.value?.recurring, segment: 'settings panel',
}); control: 'paying',
type: aiSubscription.plan,
category: aiSubscription.recurring,
});
}
openConfirmModal({ openConfirmModal({
title: t['com.affine.payment.ai.action.resume.confirm.title'](), title: t['com.affine.payment.ai.action.resume.confirm.title'](),
@ -51,10 +58,13 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => {
idempotencyKey, idempotencyKey,
SubscriptionPlan.AI SubscriptionPlan.AI
); );
mixpanel.track('ChangePlanSucceeded', { if (aiSubscription) {
segment: 'settings panel', mixpanelTrack('PlanChangeSucceeded', {
control: 'plan resume action', category: aiSubscription.recurring,
}); control: 'paying',
type: aiSubscription.plan,
});
}
notify({ notify({
icon: <SingleSelectSelectSolidIcon />, icon: <SingleSelectSelectSolidIcon />,
iconColor: cssVar('processingColor'), iconColor: cssVar('processingColor'),
@ -66,7 +76,7 @@ export const AIResume = ({ ...btnProps }: AIResumeProps) => {
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
}, },
}); });
}, [openConfirmModal, t, subscription, idempotencyKey]); }, [subscription, openConfirmModal, t, module, idempotencyKey]);
return ( return (
<Button loading={isMutating} onClick={resume} type="primary" {...btnProps}> <Button loading={isMutating} onClick={resume} type="primary" {...btnProps}>

View File

@ -53,9 +53,15 @@ export const AIPlan = () => {
isLoggedIn ? ( isLoggedIn ? (
subscription ? ( subscription ? (
subscription.canceledAt ? ( subscription.canceledAt ? (
<AIResume className={styles.purchaseButton} /> <AIResume
module="pricing plan list"
className={styles.purchaseButton}
/>
) : ( ) : (
<AICancel className={styles.purchaseButton} /> <AICancel
module="pricing plan list"
className={styles.purchaseButton}
/>
) )
) : ( ) : (
<> <>

View File

@ -3,7 +3,7 @@ import { Tooltip } from '@affine/component/ui/tooltip';
import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify'; import { generateSubscriptionCallbackLink } from '@affine/core/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { AuthService, SubscriptionService } from '@affine/core/modules/cloud'; import { AuthService, SubscriptionService } from '@affine/core/modules/cloud';
import { popupWindow } from '@affine/core/utils'; import { mixpanelTrack, popupWindow } from '@affine/core/utils';
import type { SubscriptionRecurring } from '@affine/graphql'; import type { SubscriptionRecurring } from '@affine/graphql';
import { SubscriptionPlan, SubscriptionStatus } from '@affine/graphql'; import { SubscriptionPlan, SubscriptionStatus } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
@ -179,7 +179,6 @@ const CurrentPlan = () => {
const Downgrade = ({ disabled }: { disabled?: boolean }) => { const Downgrade = ({ disabled }: { disabled?: boolean }) => {
const t = useI18n(); const t = useI18n();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const subscription = useService(SubscriptionService).subscription;
const tooltipContent = disabled const tooltipContent = disabled
? t['com.affine.payment.downgraded-tooltip']() ? t['com.affine.payment.downgraded-tooltip']()
@ -187,17 +186,10 @@ const Downgrade = ({ disabled }: { disabled?: boolean }) => {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setOpen(true); setOpen(true);
mixpanel.track('PlanChangeStarted', { }, []);
segment: 'settings panel',
module: 'pricing plan list',
control: 'billing cancel action',
type: subscription.pro$.value?.plan,
category: subscription.pro$.value?.recurring,
});
}, [subscription.pro$.value?.plan, subscription.pro$.value?.recurring]);
return ( return (
<CancelAction open={open} onOpenChange={setOpen}> <CancelAction module="pricing plan list" open={open} onOpenChange={setOpen}>
<Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}> <Tooltip content={tooltipContent} rootOptions={{ delayDuration: 0 }}>
<div className={styles.planAction}> <div className={styles.planAction}>
<Button <Button
@ -345,11 +337,11 @@ const ChangeRecurring = ({
const subscription = useService(SubscriptionService).subscription; const subscription = useService(SubscriptionService).subscription;
const onStartChange = useCallback(() => { const onStartChange = useCallback(() => {
mixpanel.track('PlanChangeStarted', { mixpanelTrack('PlanChangeStarted', {
segment: 'settings panel', segment: 'settings panel',
module: 'pricing plan list', module: 'pricing plan list',
control: 'plan resume action', control: 'paying',
type: 'cloud pro subscription', type: SubscriptionPlan.Pro,
category: to, category: to,
}); });
setOpen(true); setOpen(true);
@ -428,14 +420,17 @@ const ResumeButton = () => {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setOpen(true); setOpen(true);
mixpanel.track('PlanChangeStarted', { const pro = subscription.pro$.value;
segment: 'settings panel', if (pro) {
module: 'pricing plan list', mixpanelTrack('PlanChangeStarted', {
control: 'pricing plan action', segment: 'settings panel',
type: 'cloud pro subscription', module: 'pricing plan list',
category: subscription.pro$.value?.recurring, control: 'paying',
}); type: SubscriptionPlan.Pro,
}, [subscription.pro$.value?.recurring]); category: pro.recurring,
});
}
}, [subscription.pro$.value]);
return ( return (
<ResumeAction open={open} onOpenChange={setOpen}> <ResumeAction open={open} onOpenChange={setOpen}>

View File

@ -1,6 +1,6 @@
import { useUpgradeNotify } from '@affine/core/components/affine/subscription-landing/notify'; import { useUpgradeNotify } from '@affine/core/components/affine/subscription-landing/notify';
import { mixpanelTrack } from '@affine/core/utils';
import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql'; import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
import mixpanel from 'mixpanel-browser';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
@ -130,10 +130,10 @@ export const useSubscriptionNotifyReader = () => {
localStorage.removeItem(localStorageKey); localStorage.removeItem(localStorageKey);
// mixpanel // mixpanel
mixpanel.track('PlanUpgradeSucceeded', { mixpanelTrack('PlanChangeSucceeded', {
segment: 'settings panel', category: recurring,
control: 'plan upgrade action', type: plan,
plan: plan, control: 'new subscription',
}); });
} catch (err) { } catch (err) {
console.error('Failed to parse subscription callback link', err); console.error('Failed to parse subscription callback link', err);

View File

@ -0,0 +1,7 @@
import type { PlanChangeStartedEvent } from './plan-change-started';
import type { PlanChangeSucceededEvent } from './plan-change-succeed';
export interface MixpanelEvents {
PlanChangeStarted: PlanChangeStartedEvent;
PlanChangeSucceeded: PlanChangeSucceededEvent;
}

View File

@ -0,0 +1,15 @@
import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
/**
* Before subscription plan changed
*/
export interface PlanChangeStartedEvent {
segment: 'settings panel';
module: 'pricing plan list' | 'billing subscription list';
control:
| 'new subscription' // no subscription before
| 'cancel'
| 'paying'; // resume: subscribed before
type: SubscriptionPlan;
category: SubscriptionRecurring;
}

View File

@ -0,0 +1,9 @@
import type { PlanChangeStartedEvent } from './plan-change-started';
/**
* Subscription plan changed successfully
*/
export type PlanChangeSucceededEvent = Pick<
PlanChangeStartedEvent,
'control' | 'type' | 'category'
>;

View File

@ -83,10 +83,6 @@ export const Component = () => {
}); });
setMessage('Redirecting...'); setMessage('Redirecting...');
location.href = checkout; location.href = checkout;
mixpanel.track('PlanChangeSucceeded', {
type: plan,
category: recurring,
});
if (plan) { if (plan) {
mixpanel.people.set({ mixpanel.people.set({
[SubscriptionPlan.AI === plan ? 'ai plan' : plan]: plan, [SubscriptionPlan.AI === plan ? 'ai plan' : plan]: plan,

View File

@ -2,6 +2,8 @@ import { DebugLogger } from '@affine/debug';
import type { OverridedMixpanel } from 'mixpanel-browser'; import type { OverridedMixpanel } from 'mixpanel-browser';
import mixpanelBrowser from 'mixpanel-browser'; import mixpanelBrowser from 'mixpanel-browser';
import type { MixpanelEvents } from '../mixpanel';
const logger = new DebugLogger('affine:mixpanel'); const logger = new DebugLogger('affine:mixpanel');
export const mixpanel = process.env.MIXPANEL_TOKEN export const mixpanel = process.env.MIXPANEL_TOKEN
@ -31,3 +33,10 @@ function createProxyHandler(property?: string | symbol) {
} as ProxyHandler<OverridedMixpanel>; } as ProxyHandler<OverridedMixpanel>;
return handler; return handler;
} }
export function mixpanelTrack<T extends keyof MixpanelEvents>(
event: T,
properties?: MixpanelEvents[T]
) {
return mixpanel.track(event, properties);
}