From 2a9d3bb168ac551153f3de8f09b8fe174a620139 Mon Sep 17 00:00:00 2001 From: roo hutton Date: Mon, 30 Jun 2025 12:11:36 +0100 Subject: [PATCH] Merge pull request #26566 from overleaf/rh-cio-event-segmentation Include customer.io segmentation in recurly/stripe subscription events GitOrigin-RevId: 54483e17eec5251ac7871d43a3aa1e074dcfe98d --- .../Features/SplitTests/SplitTestHandler.js | 8 +- .../Subscription/RecurlyEventHandler.js | 18 +++ .../Subscription/RecurlyEventHandlerTests.js | 128 +++++++++++++++--- 3 files changed, 130 insertions(+), 24 deletions(-) diff --git a/services/web/app/src/Features/SplitTests/SplitTestHandler.js b/services/web/app/src/Features/SplitTests/SplitTestHandler.js index 5dcf00967b..4bd904fdd4 100644 --- a/services/web/app/src/Features/SplitTests/SplitTestHandler.js +++ b/services/web/app/src/Features/SplitTests/SplitTestHandler.js @@ -145,16 +145,12 @@ async function hasUserBeenAssignedToVariant( ignoreVersion = false ) { try { - const { session, query = {} } = req + const { session = {}, query = {} } = req const splitTest = await _getSplitTest(splitTestName) const currentVersion = SplitTestUtils.getCurrentVersion(splitTest) - if ( - !userId || - !SessionManager.isUserLoggedIn(session) || - !currentVersion?.active - ) { + if (!userId || !currentVersion?.active) { return false } diff --git a/services/web/app/src/Features/Subscription/RecurlyEventHandler.js b/services/web/app/src/Features/Subscription/RecurlyEventHandler.js index e0d2531239..51314b4b31 100644 --- a/services/web/app/src/Features/Subscription/RecurlyEventHandler.js +++ b/services/web/app/src/Features/Subscription/RecurlyEventHandler.js @@ -12,6 +12,16 @@ 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) @@ -67,6 +77,7 @@ async function _sendSubscriptionResumedEvent(userId, eventData) { plan_code: planCode, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -89,6 +100,7 @@ async function _sendSubscriptionPausedEvent(userId, eventData) { plan_code: planCode, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -111,6 +123,7 @@ async function _sendSubscriptionStartedEvent(userId, eventData) { has_ai_add_on: hasAiAddOn, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -158,6 +171,7 @@ async function _sendSubscriptionUpdatedEvent(userId, eventData) { has_ai_add_on: hasAiAddOn, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -190,6 +204,7 @@ async function _sendSubscriptionCancelledEvent(userId, eventData) { has_ai_add_on: hasAiAddOn, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -217,6 +232,7 @@ async function _sendSubscriptionExpiredEvent(userId, eventData) { has_ai_add_on: hasAiAddOn, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -249,6 +265,7 @@ async function _sendSubscriptionRenewedEvent(userId, eventData) { has_ai_add_on: hasAiAddOn, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( @@ -280,6 +297,7 @@ async function _sendSubscriptionReactivatedEvent(userId, eventData) { has_ai_add_on: hasAiAddOn, subscriptionId, payment_provider: 'recurly', + 'customerio-integration': eventData['customerio-integration'], } ) AnalyticsManager.setUserPropertyForUserInBackground( diff --git a/services/web/test/unit/src/Subscription/RecurlyEventHandlerTests.js b/services/web/test/unit/src/Subscription/RecurlyEventHandlerTests.js index 2528f0a451..5620d0f106 100644 --- a/services/web/test/unit/src/Subscription/RecurlyEventHandlerTests.js +++ b/services/web/test/unit/src/Subscription/RecurlyEventHandlerTests.js @@ -41,14 +41,15 @@ describe('RecurlyEventHandler', function () { getAssignmentForUser: sinon.stub().resolves({ variant: 'default', }), + hasUserBeenAssignedToVariant: sinon.stub().resolves(false), }, }), }, }) }) - it('with new_subscription_notification - free trial', function () { - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + it('with new_subscription_notification - free trial', async function () { + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'new_subscription_notification', this.eventData ) @@ -63,6 +64,48 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, + } + ) + sinon.assert.calledWith( + this.AnalyticsManager.setUserPropertyForUserInBackground, + this.userId, + 'subscription-plan-code', + this.planCode + ) + sinon.assert.calledWith( + this.AnalyticsManager.setUserPropertyForUserInBackground, + this.userId, + 'subscription-state', + 'active' + ) + sinon.assert.calledWith( + this.AnalyticsManager.setUserPropertyForUserInBackground, + this.userId, + 'subscription-is-trial', + true + ) + }) + + it('with new_subscription_notification - free trial with customerio integration enabled', async function () { + this.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true) + + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + 'new_subscription_notification', + this.eventData + ) + sinon.assert.calledWith( + this.AnalyticsManager.recordEventForUserInBackground, + this.userId, + 'subscription-started', + { + plan_code: this.planCode, + quantity: 1, + is_trial: true, + has_ai_add_on: false, + subscriptionId: this.eventData.subscription.uuid, + payment_provider: 'recurly', + 'customerio-integration': true, } ) sinon.assert.calledWith( @@ -94,7 +137,7 @@ describe('RecurlyEventHandler', function () { sinon.assert.called(this.SubscriptionEmailHandler.sendTrialOnboardingEmail) }) - it('with new_subscription_notification - no free trial', function () { + it('with new_subscription_notification - no free trial', async function () { this.eventData.subscription.current_period_started_at = new Date( '2021-02-10 12:34:56' ) @@ -103,7 +146,7 @@ describe('RecurlyEventHandler', function () { ) this.eventData.subscription.quantity = 3 - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'new_subscription_notification', this.eventData ) @@ -118,6 +161,7 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, } ) sinon.assert.calledWith( @@ -134,10 +178,10 @@ describe('RecurlyEventHandler', function () { ) }) - it('with updated_subscription_notification', function () { + it('with updated_subscription_notification', async function () { this.planCode = 'new-plan-code' this.eventData.subscription.plan.plan_code = this.planCode - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'updated_subscription_notification', this.eventData ) @@ -152,6 +196,50 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, + } + ) + sinon.assert.calledWith( + this.AnalyticsManager.setUserPropertyForUserInBackground, + this.userId, + 'subscription-plan-code', + this.planCode + ) + sinon.assert.calledWith( + this.AnalyticsManager.setUserPropertyForUserInBackground, + this.userId, + 'subscription-state', + 'active' + ) + sinon.assert.calledWith( + this.AnalyticsManager.setUserPropertyForUserInBackground, + this.userId, + 'subscription-is-trial', + true + ) + }) + + it('with updated_subscription_notification with customerio integration enabled', async function () { + this.SplitTestHandler.promises.hasUserBeenAssignedToVariant.resolves(true) + this.planCode = 'new-plan-code' + this.eventData.subscription.plan.plan_code = this.planCode + + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + 'updated_subscription_notification', + this.eventData + ) + sinon.assert.calledWith( + this.AnalyticsManager.recordEventForUserInBackground, + this.userId, + 'subscription-updated', + { + plan_code: this.planCode, + quantity: 1, + is_trial: true, + has_ai_add_on: false, + subscriptionId: this.eventData.subscription.uuid, + payment_provider: 'recurly', + 'customerio-integration': true, } ) sinon.assert.calledWith( @@ -191,6 +279,7 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, } ) sinon.assert.calledWith( @@ -207,9 +296,9 @@ describe('RecurlyEventHandler', function () { ) }) - it('with expired_subscription_notification', function () { + it('with expired_subscription_notification', async function () { this.eventData.subscription.state = 'expired' - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'expired_subscription_notification', this.eventData ) @@ -224,6 +313,7 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, } ) sinon.assert.calledWith( @@ -246,8 +336,8 @@ describe('RecurlyEventHandler', function () { ) }) - it('with renewed_subscription_notification', function () { - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + it('with renewed_subscription_notification', async function () { + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'renewed_subscription_notification', this.eventData ) @@ -262,12 +352,13 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, } ) }) - it('with reactivated_account_notification', function () { - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + it('with reactivated_account_notification', async function () { + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'reactivated_account_notification', this.eventData ) @@ -281,11 +372,12 @@ describe('RecurlyEventHandler', function () { has_ai_add_on: false, subscriptionId: this.eventData.subscription.uuid, payment_provider: 'recurly', + 'customerio-integration': false, } ) }) - it('with paid_charge_invoice_notification', function () { + it('with paid_charge_invoice_notification', async function () { const invoice = { invoice_number: 1234, currency: 'USD', @@ -298,7 +390,7 @@ describe('RecurlyEventHandler', function () { collection_method: 'automatic', subscription_ids: ['abcd1234', 'defa3214'], } - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'paid_charge_invoice_notification', { account: { @@ -325,8 +417,8 @@ describe('RecurlyEventHandler', function () { ) }) - it('with paid_charge_invoice_notification and total_in_cents 0', function () { - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + it('with paid_charge_invoice_notification and total_in_cents 0', async function () { + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'paid_charge_invoice_notification', { account: { @@ -341,8 +433,8 @@ describe('RecurlyEventHandler', function () { sinon.assert.notCalled(this.AnalyticsManager.recordEventForUserInBackground) }) - it('with closed_invoice_notification', function () { - this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( + it('with closed_invoice_notification', async function () { + await this.RecurlyEventHandler.sendRecurlyAnalyticsEvent( 'closed_invoice_notification', { account: {