Merge pull request #36496 from overleaf/oa-annual-events

[web] Subscription event segmentation

GitOrigin-RevId: fdc25ac7bc7320ed82617b7c27c12a17eb31d723
This commit is contained in:
Olzhas Askar
2026-09-17 11:59:20 +00:00
committed by Copybot
parent ba4e6ef8ed
commit 6053781e57
14 changed files with 305 additions and 47 deletions
@@ -2,6 +2,7 @@
import Settings from '@overleaf/settings'
import { AI_ADD_ON_CODE, isStandaloneAiAddOnPlanCode } from './AiHelper.mjs'
import FeaturesHelper from './FeaturesHelper.mjs'
import PlansLocator from './PlansLocator.mjs'
/**
* @typedef {InstanceType<typeof import('../../models/Subscription.mjs').Subscription>} MongoSubscription
@@ -120,9 +121,7 @@ function normalizePlanTypeFromPlanCode(planCode) {
if (!planCode) {
return ''
}
const plan = /** @type {Plan[]} */ (Settings.plans).find(
candidate => candidate.planCode === planCode
)
const plan = PlansLocator.findLocalPlanInSettings(planCode)
return normalizePlanType({
plan: {
planCode,
@@ -161,18 +160,6 @@ function getFriendlyPlanName(planType) {
return planType
}
/**
* @param {Nullable<BestSubscription>} [bestSubscription]
* @returns {'annual' | 'monthly' | null}
*/
function getPlanCadence(bestSubscription) {
if (!bestSubscription?.plan) {
return null
}
return bestSubscription.plan.annual ? 'annual' : 'monthly'
}
/**
* @param {Nullable<string>} [planCode]
* @returns {'annual' | 'monthly' | null}
@@ -182,11 +169,9 @@ function getPlanCadenceFromPlanCode(planCode) {
return null
}
const plan = /** @type {Plan[]} */ (Settings.plans).find(
candidate => candidate.planCode === planCode
)
const plan = PlansLocator.findLocalPlanInSettings(planCode)
if (plan) {
return plan.annual ? 'annual' : 'monthly'
return PlansLocator.getPlanCadence({ plan })
}
if (planCode.includes('annual')) {
@@ -348,7 +333,7 @@ function getAiPlanCadence(
if (aiPlan === 'ai-assist-add-on') {
return (
getPlanCadence(bestSubscription) ||
PlansLocator.getPlanCadence(bestSubscription) ||
getPlanCadenceFromPlanCode(individualSubscription?.planCode)
)
}
@@ -443,9 +428,7 @@ function getGroupSize(
}
return allGroupSubscriptions.reduce((largestGroupSize, subscription) => {
const plan = /** @type {Plan[]} */ (Settings.plans).find(
candidate => candidate.planCode === subscription.planCode
)
const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
const groupSize = subscription.membersLimit ?? plan?.membersLimit ?? 0
return Math.max(largestGroupSize, groupSize)
@@ -595,7 +578,7 @@ function getPlanProperties({
}) {
const planType = normalizePlanType(bestSubscription)
const displayPlanType = getFriendlyPlanName(planType)
const planTermLabel = getPlanCadence(bestSubscription)
const planTermLabel = PlansLocator.getPlanCadence(bestSubscription)
const aiPlan = getAiPlanType(
bestSubscription,
individualSubscription,
@@ -165,6 +165,18 @@ function getPlanTypeAndPeriodFromRecurlyPlanCode(recurlyPlanCode) {
return recurlyPlanCodeToPlanTypeAndPeriod[recurlyPlanCode]
}
/**
* Prefer this over `getPlanTypeAndPeriodFromRecurlyPlanCode` when you have the
* plan: that map misses legacy plan codes and license-count group plan codes.
*
* @param {{ plan?: { annual?: boolean } } | null} [subscription] anything carrying a local plan
* @returns {'annual' | 'monthly' | null}
*/
function getPlanCadence(subscription) {
if (!subscription?.plan) return null
return subscription.plan.annual ? 'annual' : 'monthly'
}
/**
* @param {string|null} [planCode]
* @returns {Plan|null}
@@ -219,6 +231,7 @@ export default {
findLocalPlanInSettings,
buildStripeLookupKey,
getPlanTypeAndPeriodFromRecurlyPlanCode,
getPlanCadence,
isGroupPlanCode,
convertLegacyGroupPlanCodeToConsolidatedGroupPlanCodeIfNeeded,
getVersionFromStripeLookupKey,
@@ -29,7 +29,11 @@ async function sendRecurlyAnalyticsEvent(event, eventData) {
await _sendSubscriptionStartedEvent(userId, eventData)
break
case 'updated_subscription_notification':
await _sendSubscriptionUpdatedEvent(userId, eventData)
await _sendSubscriptionUpdatedEvent(
userId,
eventData,
subscription?.planCode
)
break
case 'canceled_subscription_notification':
await _sendSubscriptionCancelledEvent(userId, eventData)
@@ -146,7 +150,11 @@ async function _sendSubscriptionStartedEvent(userId, eventData) {
}
}
async function _sendSubscriptionUpdatedEvent(userId, eventData) {
async function _sendSubscriptionUpdatedEvent(
userId,
eventData,
planCodeBeforeSync
) {
const { planCode, quantity, state, isTrial, hasAiAddOn, subscriptionId } =
_getSubscriptionData(eventData)
AnalyticsManager.recordEventForUserInBackground(
@@ -154,6 +162,10 @@ async function _sendSubscriptionUpdatedEvent(userId, eventData) {
'subscription-updated',
{
plan_code: planCode,
// Mongo sync might already have happened,
// we only want to record when it haven't yet.
previous_plan_code:
planCodeBeforeSync !== planCode ? planCodeBeforeSync : undefined,
quantity,
is_trial: isTrial,
has_ai_add_on: hasAiAddOn,
@@ -244,6 +244,10 @@ async function userSubscriptionPage(req, res) {
req.session,
'subscription-page-view',
{
plan_code: personalSubscription?.planCode,
billing_cycle: PlansLocator.getPlanCadence(personalSubscription),
is_trial: isInTrial,
currency: personalSubscription?.payment?.currency,
domain,
}
)