Просмотр исходного кода

Merge pull request #33805 from overleaf/mg-ai-paywall-analytics

Add paywall-prompt and paywall-click events to ai paywalls

GitOrigin-RevId: aa7de15a990ad1833e3dda65d5fb50f60bb7c9e3
Malik Glossop 2 месяцев назад
Родитель
Сommit
fa26367aa9

+ 60 - 28
services/web/frontend/js/shared/components/ai-paywall-notification.tsx

@@ -1,7 +1,9 @@
+import { useEffect } from 'react'
 import Notification from '@/shared/components/notification'
-import UpgradeButton from '@/features/ide-react/components/toolbar/upgrade-button'
+import PaywallUpgradeButton from '@/shared/components/paywall-upgrade-button'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { useUserFeaturesContext } from '@/shared/context/user-features-context'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
 import { useTranslation } from 'react-i18next'
 import { formatSecondsToHoursAndMinutes } from '@/shared/utils/time'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
@@ -9,14 +11,20 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
 import getMeta from '@/utils/meta'
 const hasUnlimitedAi = getMeta('ol-hasUnlimitedAi')
 
-type aiFeatureLocations = 'errorAssist' | 'workbench'
+type AiFeatureLocations = 'errorAssist' | 'workbench'
+type PaywallType = 'assistant' | 'workbench'
+
+const paywallTypeByLocation: Record<AiFeatureLocations, PaywallType> = {
+  workbench: 'workbench',
+  errorAssist: 'assistant',
+}
 
 function AiPaywallNotification({
   isActionBelowContent = false,
   featureLocation,
 }: {
   isActionBelowContent?: boolean
-  featureLocation: aiFeatureLocations
+  featureLocation: AiFeatureLocations
 }) {
   const {
     hasSuggestionsLeft,
@@ -123,29 +131,12 @@ function AiPaywallNotification({
       />
     )
   }
-
-  const message = t('upgrade_for_unlimited_access_to_ai', {
-    time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
-  })
   return (
-    <>
-      <Notification
-        type="info"
-        title={t('youve_hit_your_daily_ai_limit')}
-        content={message}
-        isDismissible={false}
-        customIcon={null}
-        isActionBelowContent={isActionBelowContent}
-        action={
-          <UpgradeButton
-            className="px-2.5 py-2"
-            referrer="ai"
-            source={featureLocation}
-          />
-        }
-        className="ai-upgrade-paywall-btn ai-paywall-notification"
-      />
-    </>
+    <UpgradePaywall
+      secondsTillReset={secondsTillReset}
+      isActionBelowContent={isActionBelowContent}
+      featureLocation={featureLocation}
+    />
   )
 }
 
@@ -154,7 +145,7 @@ function GroupsPaywall({
   featureLocation,
 }: {
   secondsTillReset: number
-  featureLocation: aiFeatureLocations
+  featureLocation: AiFeatureLocations
 }) {
   const { t } = useTranslation()
 
@@ -186,7 +177,7 @@ function CommonsPaywall({
   featureLocation,
 }: {
   secondsTillReset: number
-  featureLocation: aiFeatureLocations
+  featureLocation: AiFeatureLocations
 }) {
   const { t } = useTranslation()
 
@@ -218,7 +209,7 @@ function FairUseLimit({
   featureLocation,
 }: {
   secondsTillReset: number
-  featureLocation: aiFeatureLocations
+  featureLocation: AiFeatureLocations
 }) {
   const { t } = useTranslation()
 
@@ -254,4 +245,45 @@ function FairUseLimit({
   )
 }
 
+function UpgradePaywall({
+  secondsTillReset,
+  isActionBelowContent,
+  featureLocation,
+}: {
+  secondsTillReset: number
+  isActionBelowContent: boolean
+  featureLocation: AiFeatureLocations
+}) {
+  const { t } = useTranslation()
+  const { sendEvent } = useEditorAnalytics()
+  const paywallType = paywallTypeByLocation[featureLocation]
+
+  useEffect(() => {
+    sendEvent('paywall-prompt', {
+      'paywall-type': paywallType,
+    })
+  }, [sendEvent, paywallType])
+
+  return (
+    <Notification
+      type="info"
+      title={t('youve_hit_your_daily_ai_limit')}
+      content={t('upgrade_for_unlimited_access_to_ai', {
+        time: formatSecondsToHoursAndMinutes(t, secondsTillReset),
+      })}
+      isDismissible={false}
+      customIcon={null}
+      isActionBelowContent={isActionBelowContent}
+      action={
+        <PaywallUpgradeButton
+          referrer="ai"
+          paywallType={paywallType}
+          className="px-2.5 py-2"
+        />
+      }
+      className="ai-upgrade-paywall-btn ai-paywall-notification"
+    />
+  )
+}
+
 export default AiPaywallNotification

+ 40 - 0
services/web/frontend/js/shared/components/paywall-upgrade-button.tsx

@@ -0,0 +1,40 @@
+import OLButton from '@/shared/components/ol/ol-button'
+import getMeta from '@/utils/meta'
+import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
+import { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export default function PaywallUpgradeButton({
+  referrer,
+  paywallType,
+  className,
+}: {
+  referrer: string
+  paywallType: string
+  className?: string
+}) {
+  const { t } = useTranslation()
+  const { sendEvent } = useEditorAnalytics()
+  const user = getMeta('ol-user')
+
+  const handleClick = useCallback(() => {
+    sendEvent('paywall-click', {
+      upgradeType: user.hasPaidSubscription ? 'add-on' : 'standalone',
+      'paywall-type': paywallType,
+    })
+  }, [sendEvent, user.hasPaidSubscription, paywallType])
+
+  return (
+    <OLButton
+      variant="premium"
+      size="sm"
+      href={`/user/subscription/choose-your-plan?itm_referrer=${referrer}&itm_campaign=${paywallType}`}
+      target="_blank"
+      rel="noreferrer"
+      className={className}
+      onClick={handleClick}
+    >
+      {t('upgrade')}
+    </OLButton>
+  )
+}

+ 60 - 0
services/web/test/frontend/shared/components/paywall-upgrade-button.test.tsx

@@ -0,0 +1,60 @@
+import { expect } from 'chai'
+import sinon from 'sinon'
+import { screen, render } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import PaywallUpgradeButton from '../../../../frontend/js/shared/components/paywall-upgrade-button'
+import * as eventTracking from '@/infrastructure/event-tracking'
+
+describe('<PaywallUpgradeButton />', function () {
+  let sendMBSpy: sinon.SinonSpy
+
+  beforeEach(function () {
+    sendMBSpy = sinon.spy(eventTracking, 'sendMB')
+    window.metaAttributesCache.set('ol-user', { hasPaidSubscription: false })
+  })
+
+  afterEach(function () {
+    sendMBSpy.restore()
+  })
+
+  it('upgrade link has required attributes', function () {
+    render(<PaywallUpgradeButton referrer="ai" paywallType="assistant" />)
+
+    const upgradeLink = screen.getByRole('link', { name: 'Upgrade' })
+    expect(upgradeLink.getAttribute('href')).to.equal(
+      '/user/subscription/choose-your-plan?itm_referrer=ai&itm_campaign=assistant'
+    )
+    expect(upgradeLink.getAttribute('target')).to.equal('_blank')
+    expect(upgradeLink.getAttribute('rel')).to.equal('noreferrer')
+  })
+
+  it('sends paywall-click with standalone upgradeType for users without a paid subscription', async function () {
+    render(<PaywallUpgradeButton referrer="ai" paywallType="assistant" />)
+
+    await userEvent.click(screen.getByRole('link', { name: 'Upgrade' }))
+
+    expect(sendMBSpy).to.have.been.calledOnceWith(
+      'paywall-click',
+      sinon.match({
+        upgradeType: 'standalone',
+        'paywall-type': 'assistant',
+      })
+    )
+  })
+
+  it('sends paywall-click with add-on upgradeType for users with a paid subscription', async function () {
+    window.metaAttributesCache.set('ol-user', { hasPaidSubscription: true })
+
+    render(<PaywallUpgradeButton referrer="ai" paywallType="workbench" />)
+
+    await userEvent.click(screen.getByRole('link', { name: 'Upgrade' }))
+
+    expect(sendMBSpy).to.have.been.calledOnceWith(
+      'paywall-click',
+      sinon.match({
+        upgradeType: 'add-on',
+        'paywall-type': 'workbench',
+      })
+    )
+  })
+})