Merge pull request #29838 from overleaf/rh-cio-remove-limits

Enable customer.io for all users

GitOrigin-RevId: 74d8424924ec30ad3a6cb48c15e285cc696186fd
This commit is contained in:
roo hutton
2026-01-29 09:05:59 +00:00
committed by Copybot
parent da6015591a
commit 77c66132b0
6 changed files with 51 additions and 177 deletions
@@ -807,6 +807,12 @@ const _ProjectController = {
userIsMemberOfGroupSubscription
)
AnalyticsManager.setUserPropertyForUserInBackground(
userId,
'customer-io-integration',
true
)
const template =
detachRole === 'detached'
? 'project/ide-react-detached'
@@ -845,15 +851,6 @@ const _ProjectController = {
fullFeatureSet?.aiErrorAssistant) &&
!assistantDisabled
const customerIoEnabled =
await SplitTestHandler.promises.hasUserBeenAssignedToVariant(
req,
userId,
'customer-io-trial-conversion',
'enabled',
true
)
const addonPrices =
isOverleafAssistBundleEnabled &&
(await ProjectController._getAddonPrices(req, res))
@@ -971,7 +968,7 @@ const _ProjectController = {
isSaas: Features.hasFeature('saas'),
shouldLoadHotjar,
isOverleafAssistBundleEnabled,
customerIoEnabled,
customerIoEnabled: true,
addonPrices,
compileSettings: {
compileTimeout: ownerFeatures?.compileTimeout,
@@ -179,6 +179,13 @@ async function projectListPage(req, res, next) {
user.refProviders = _.mapValues(user.refProviders, Boolean)
let onboardingDataCollection
let customerIoEnabled = false
let subjectArea
let usedLatex
let primaryOccupation
let role
if (isSaas) {
await SplitTestSessionHandler.promises.sessionMaintenance(req, user)
@@ -227,6 +234,26 @@ async function projectListPage(req, res, next) {
) {
return res.redirect('/user/emails/primary-email-check')
}
onboardingDataCollection = await OnboardingDataCollection.findById(
userId,
'subjectArea usedLatex primaryOccupation role'
)
if (onboardingDataCollection) {
subjectArea = onboardingDataCollection.subjectArea
usedLatex = onboardingDataCollection.usedLatex
primaryOccupation = onboardingDataCollection.primaryOccupation
role = onboardingDataCollection.role
}
customerIoEnabled = true
AnalyticsManager.setUserPropertyForUserInBackground(
userId,
'customer-io-integration',
true
)
}
const tags = await TagsHandler.promises.getAllTags(userId)
@@ -274,6 +301,10 @@ async function projectListPage(req, res, next) {
return result
})
const commonsInstitution = userAffiliations.find(
affiliation => affiliation.institution?.commonsAccount
)?.institution?.name
const portalTemplates = _buildPortalTemplatesList(userAffiliations)
const { allInReconfirmNotificationPeriods } = userEmailsData
@@ -474,64 +505,9 @@ async function projectListPage(req, res, next) {
logger.error({ err: error }, 'Failed to get individual subscription')
}
const affiliations = userAffiliations || []
const commonsInstitution = affiliations.find(
affiliation => affiliation.institution?.commonsAccount
)?.institution?.name
let onboardingDataCollection
let subjectArea
let usedLatex
let primaryOccupation
let role
// customer.io: Premium nudge experiment
// Only do customer-io-trial-conversion assignment for users not in India/China and not in group/commons
let customerIoEnabled = false
const aiBlocked = !(await _canUseAIAssist(user))
const hasAiAssist = await _userHasAIAssist(user)
if (!userIsMemberOfGroupSubscription && !commonsInstitution && isSaas) {
try {
const excludedCountries = ['IN', 'CN']
if (!excludedCountries.includes(countryCode)) {
const cioAssignment =
await SplitTestHandler.promises.getAssignmentForUser(
userId,
'customer-io-trial-conversion'
)
if (cioAssignment.variant === 'enabled') {
customerIoEnabled = true
onboardingDataCollection = await OnboardingDataCollection.findById(
userId,
'subjectArea usedLatex primaryOccupation role'
)
if (onboardingDataCollection) {
subjectArea = onboardingDataCollection.subjectArea
usedLatex = onboardingDataCollection.usedLatex
primaryOccupation = onboardingDataCollection.primaryOccupation
role = onboardingDataCollection.role
}
AnalyticsManager.setUserPropertyForUserInBackground(
userId,
'customer-io-integration',
true
)
}
}
} catch (err) {
logger.error(
{ err },
'Error checking geo location for customer-io-trial-conversion'
)
// Fallback to not enabled if geoip fails
customerIoEnabled = false
}
}
await SplitTestHandler.promises.getAssignment(
req,
res,
@@ -1,4 +1,3 @@
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import SubscriptionEmailHandler from './SubscriptionEmailHandler.mjs'
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
@@ -15,6 +14,8 @@ async function sendRecurlyAnalyticsEvent(event, eventData) {
return
}
eventData['customerio-integration'] = true
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
@@ -26,16 +27,6 @@ async function sendRecurlyAnalyticsEvent(event, eventData) {
return
}
const customerIoEnabled =
await SplitTestHandler.promises.hasUserBeenAssignedToVariant(
{},
userId,
'customer-io-trial-conversion',
'enabled',
true
)
eventData['customerio-integration'] = customerIoEnabled || false
switch (event) {
case 'new_subscription_notification':
await _sendSubscriptionStartedEvent(userId, eventData)
@@ -158,17 +149,6 @@ async function _sendSubscriptionStartedEvent(userId, eventData) {
if (isTrial) {
await SubscriptionEmailHandler.sendTrialOnboardingEmail(userId, planCode)
const cioAssignment = await SplitTestHandler.promises.getAssignmentForUser(
userId,
'customer-io-trial-conversion'
)
if (cioAssignment.variant === 'enabled') {
AnalyticsManager.setUserPropertyForUserInBackground(
userId,
'customer-io-integration',
true
)
}
}
}
@@ -417,6 +417,7 @@ describe('ProjectController', function () {
() => ({
default: {
recordEventForUserInBackground: () => {},
setUserPropertyForUserInBackground: () => {},
},
})
)
@@ -10,7 +10,11 @@ const MODULE_PATH = `${import.meta.dirname}/../../../../app/src/Features/Project
// Mock AnalyticsManager as it isn't used in these tests but causes the User model to be imported and redeclares queues
vi.mock('../../../../app/src/Features/Analytics/AnalyticsManager.mjs', () => {
return {}
return {
default: {
setUserPropertyForUserInBackground: () => {},
},
}
})
describe('ProjectListController', function () {
@@ -120,47 +120,6 @@ describe('RecurlyEventHandler', function () {
})
it('with new_subscription_notification - free trial', async function (ctx) {
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
ctx.eventData
)
sinon.assert.calledWith(
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-started',
{
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
ctx.planCode
)
sinon.assert.calledWith(
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with new_subscription_notification - free trial with customerio integration enabled', async function (ctx) {
ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'new_subscription_notification',
ctx.eventData
@@ -232,7 +191,7 @@ describe('RecurlyEventHandler', function () {
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
'customerio-integration': true,
}
)
sinon.assert.calledWith(
@@ -252,49 +211,6 @@ describe('RecurlyEventHandler', function () {
it('with updated_subscription_notification', async function (ctx) {
ctx.planCode = 'new-plan-code'
ctx.eventData.subscription.plan.plan_code = ctx.planCode
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'updated_subscription_notification',
ctx.eventData
)
sinon.assert.calledWith(
ctx.AnalyticsManager.recordEventForUserInBackground,
ctx.userId,
'subscription-updated',
{
plan_code: ctx.planCode,
quantity: 1,
is_trial: true,
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
}
)
sinon.assert.calledWith(
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-plan-code',
ctx.planCode
)
sinon.assert.calledWith(
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-state',
'active'
)
sinon.assert.calledWith(
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
ctx.userId,
'subscription-is-trial',
true
)
})
it('with updated_subscription_notification with customerio integration enabled', async function (ctx) {
ctx.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true)
ctx.planCode = 'new-plan-code'
ctx.eventData.subscription.plan.plan_code = ctx.planCode
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
'updated_subscription_notification',
ctx.eventData
@@ -350,7 +266,7 @@ describe('RecurlyEventHandler', function () {
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
'customerio-integration': true,
}
)
sinon.assert.calledWith(
@@ -384,7 +300,7 @@ describe('RecurlyEventHandler', function () {
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
'customerio-integration': true,
}
)
sinon.assert.calledWith(
@@ -423,7 +339,7 @@ describe('RecurlyEventHandler', function () {
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
'customerio-integration': true,
}
)
})
@@ -443,7 +359,7 @@ describe('RecurlyEventHandler', function () {
has_ai_add_on: false,
subscriptionId: ctx.eventData.subscription.uuid,
payment_provider: 'recurly',
'customerio-integration': false,
'customerio-integration': true,
}
)
})