Compare commits

...

3 Commits

Author SHA1 Message Date
Glary-Bot
2cda150eaa test: cover PostHog trackPaywallViewed dispatch 2026-05-20 04:38:07 +00:00
Glary-Bot
e75d2f8940 feat: instrument personal subscription dialog + dedupe GTM event
Addresses code review:
- Add 'subscription_required_dialog' surface for the personal-tier
  SubscriptionRequiredDialogContent.vue (the dialog reached via
  showSubscriptionDialog() for non-workspace users), which was
  previously uninstrumented despite being a primary paywall path.
- Map trackPaywallViewed() in GtmTelemetryProvider to a distinct
  'paywall_viewed' GA4 event instead of 'view_promotion', which
  trackSubscription('modal_opened') already emits — avoids
  double-counting impressions on the GTM side.
2026-05-20 04:36:44 +00:00
Glary-Bot
863bc73b10 feat: track paywall_viewed across billing entry points
Adds an app:paywall_viewed PostHog/Mixpanel/GA4 event fired in onMounted
of the four primary paywall surfaces:

- FreeTierDialogContent (out-of-credits + subscription-required)
- SubscriptionRequiredDialogContentWorkspace (team workspace)
- UploadModelUpgradeModal (upload-gated upsell)
- InviteMemberUpsellDialogContent (invite-gated upsell)

The event carries surface, current_tier, and (where applicable) reason
so funnel analysis can measure conversion per entry point.

Fixes MAR-204
2026-05-20 04:21:10 +00:00
11 changed files with 117 additions and 7 deletions

View File

@@ -12,13 +12,16 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import UploadModelUpgradeModalBody from '@/platform/assets/components/UploadModelUpgradeModalBody.vue'
import UploadModelUpgradeModalFooter from '@/platform/assets/components/UploadModelUpgradeModalFooter.vue'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogStore } from '@/stores/dialogStore'
const dialogStore = useDialogStore()
const { showSubscriptionDialog } = useBillingContext()
const { showSubscriptionDialog, subscription } = useBillingContext()
function handleClose() {
dialogStore.closeDialog({ key: 'upload-model-upgrade' })
@@ -27,4 +30,11 @@ function handleClose() {
function handleSubscribe() {
showSubscriptionDialog()
}
onMounted(() => {
useTelemetry()?.trackPaywallViewed({
surface: 'upload_model_upgrade_modal',
current_tier: subscription.value?.tier?.toLowerCase()
})
})
</script>

View File

@@ -99,15 +99,16 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { getTierCredits } from '@/platform/cloud/subscription/constants/tierPricing'
import { useTelemetry } from '@/platform/telemetry'
defineProps<{
const { reason } = defineProps<{
reason?: SubscriptionDialogReason
}>()
@@ -116,7 +117,15 @@ defineEmits<{
upgrade: []
}>()
const { formattedRenewalDate } = useSubscription()
const { formattedRenewalDate, subscriptionTier } = useSubscription()
const freeTierCredits = computed(() => getTierCredits('free'))
onMounted(() => {
useTelemetry()?.trackPaywallViewed({
surface: 'free_tier_dialog',
current_tier: subscriptionTier.value?.toLowerCase(),
reason
})
})
</script>

View File

@@ -145,7 +145,7 @@
</template>
<script setup lang="ts">
import { computed, watch } from 'vue'
import { computed, onMounted, watch } from 'vue'
import CloudBadge from '@/components/topbar/CloudBadge.vue'
import Button from '@/components/ui/button/Button.vue'
@@ -169,7 +169,7 @@ const emit = defineEmits<{
close: [subscribed: boolean]
}>()
const { isActiveSubscription } = useBillingContext()
const { isActiveSubscription, subscription } = useBillingContext()
const isSubscriptionEnabled = (): boolean =>
Boolean(isCloud && window.__CONFIG__?.subscription_required)
@@ -199,6 +199,14 @@ watch(
}
)
onMounted(() => {
telemetry?.trackPaywallViewed({
surface: 'subscription_required_dialog',
current_tier: subscription.value?.tier?.toLowerCase(),
reason
})
})
const handleSubscribed = () => {
emit('close', true)
}

View File

@@ -15,6 +15,7 @@ import type {
NodeSearchMetadata,
NodeSearchResultMetadata,
PageViewMetadata,
PaywallViewedMetadata,
PageVisibilityMetadata,
SettingChangedMetadata,
SubscriptionMetadata,
@@ -77,6 +78,10 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackSubscription?.(event, metadata))
}
trackPaywallViewed(metadata: PaywallViewedMetadata): void {
this.dispatch((provider) => provider.trackPaywallViewed?.(metadata))
}
trackBeginCheckout(metadata: BeginCheckoutMetadata): void {
this.dispatch((provider) => provider.trackBeginCheckout?.(metadata))
}

View File

@@ -15,6 +15,7 @@ import type {
PageVisibilityMetadata,
SettingChangedMetadata,
ShareFlowMetadata,
PaywallViewedMetadata,
SubscriptionMetadata,
SubscriptionSuccessMetadata,
SurveyResponses,
@@ -164,6 +165,10 @@ export class GtmTelemetryProvider implements TelemetryProvider {
this.pushEvent(ga4EventName, metadata ? { ...metadata } : undefined)
}
trackPaywallViewed(metadata: PaywallViewedMetadata): void {
this.pushEvent('paywall_viewed', { ...metadata })
}
trackSignupOpened(): void {
this.pushEvent('signup_opened')
}

View File

@@ -30,6 +30,7 @@ import type {
PageVisibilityMetadata,
RunButtonProperties,
SettingChangedMetadata,
PaywallViewedMetadata,
SubscriptionMetadata,
SubscriptionSuccessMetadata,
SurveyResponses,
@@ -232,6 +233,10 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(eventName, metadata)
}
trackPaywallViewed(metadata: PaywallViewedMetadata): void {
this.trackEvent(TelemetryEvents.PAYWALL_VIEWED, metadata)
}
trackAddApiCreditButtonClicked(): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
}

View File

@@ -179,6 +179,26 @@ describe('PostHogTelemetryProvider', () => {
{}
)
})
it('captures paywall_viewed with surface and tier metadata', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
provider.trackPaywallViewed({
surface: 'free_tier_dialog',
current_tier: 'free',
reason: 'out_of_credits'
})
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.PAYWALL_VIEWED,
{
surface: 'free_tier_dialog',
current_tier: 'free',
reason: 'out_of_credits'
}
)
})
})
describe('disabled events', () => {

View File

@@ -25,6 +25,7 @@ import type {
PageVisibilityMetadata,
RunButtonProperties,
SettingChangedMetadata,
PaywallViewedMetadata,
SubscriptionMetadata,
SubscriptionSuccessMetadata,
SurveyResponses,
@@ -252,6 +253,10 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
this.trackEvent(eventName, metadata)
}
trackPaywallViewed(metadata: PaywallViewedMetadata): void {
this.trackEvent(TelemetryEvents.PAYWALL_VIEWED, metadata)
}
trackAddApiCreditButtonClicked(): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
}

View File

@@ -340,6 +340,22 @@ export interface SubscriptionMetadata {
reason?: SubscriptionDialogReason
}
/**
* Paywall surface identifier — distinguishes which UI entry point triggered
* the impression. Used to measure conversion across the various billing
* entry points (subscription required dialog, out-of-credits, upsells, etc.).
*/
export type PaywallSurface =
| 'free_tier_dialog'
| 'subscription_required_dialog'
| 'subscription_required_dialog_workspace'
| 'upload_model_upgrade_modal'
| 'invite_member_upsell'
export interface PaywallViewedMetadata extends SubscriptionMetadata {
surface: PaywallSurface
}
export interface BeginCheckoutMetadata
extends Record<string, unknown>, CheckoutAttributionMetadata {
user_id: string
@@ -390,6 +406,7 @@ export interface TelemetryProvider {
event: 'modal_opened' | 'subscribe_clicked',
metadata?: SubscriptionMetadata
): void
trackPaywallViewed?(metadata: PaywallViewedMetadata): void
trackBeginCheckout?(metadata: BeginCheckoutMetadata): void
trackMonthlySubscriptionSucceeded?(
metadata?: SubscriptionSuccessMetadata
@@ -489,6 +506,7 @@ export const TelemetryEvents = {
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
MONTHLY_SUBSCRIPTION_CANCELLED: 'app:monthly_subscription_cancelled',
PAYWALL_VIEWED: 'app:paywall_viewed',
ADD_API_CREDIT_BUTTON_CLICKED: 'app:add_api_credit_button_clicked',
API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED:
'app:api_credit_topup_button_purchase_clicked',

View File

@@ -94,8 +94,12 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import { useTelemetry } from '@/platform/telemetry'
import { useSubscriptionCheckout } from '@/platform/workspace/composables/useSubscriptionCheckout'
import PricingTableWorkspace from './PricingTableWorkspace.vue'
@@ -107,6 +111,16 @@ const { onClose, reason } = defineProps<{
reason?: SubscriptionDialogReason
}>()
const { subscription } = useBillingContext()
onMounted(() => {
useTelemetry()?.trackPaywallViewed({
surface: 'subscription_required_dialog_workspace',
current_tier: subscription.value?.tier?.toLowerCase(),
reason
})
})
const emit = defineEmits<{
close: [subscribed: boolean]
}>()

View File

@@ -50,12 +50,16 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogStore } from '@/stores/dialogStore'
const dialogStore = useDialogStore()
const { isActiveSubscription, showSubscriptionDialog } = useBillingContext()
const { isActiveSubscription, showSubscriptionDialog, subscription } =
useBillingContext()
function onDismiss() {
dialogStore.closeDialog({ key: 'invite-member-upsell' })
@@ -65,4 +69,11 @@ function onUpgrade() {
dialogStore.closeDialog({ key: 'invite-member-upsell' })
showSubscriptionDialog()
}
onMounted(() => {
useTelemetry()?.trackPaywallViewed({
surface: 'invite_member_upsell',
current_tier: subscription.value?.tier?.toLowerCase()
})
})
</script>