From 6b69ea7dc1c6964903b1ee27a4dc8d089a239604 Mon Sep 17 00:00:00 2001 From: roo hutton Date: Fri, 7 Aug 2026 09:18:35 +0100 Subject: [PATCH] Merge pull request #36194 from overleaf/rh-cio-split-tests [web] send active split test assignments to customer.io GitOrigin-RevId: 3036d6442e8419326f4b641482d65bd5114fe3da --- .../Project/ProjectListController.mjs | 16 +++++++ .../SplitTests/SplitTestUserGetter.mjs | 2 +- .../Project/ProjectListController.test.mjs | 47 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index 53bf7c835c..cb9f6949da 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -578,6 +578,21 @@ async function projectListPage(req, res, next) { } } + let splitTestUserProperties + if (isSaas) { + try { + ;[splitTestUserProperties] = await Modules.promises.hooks.fire( + 'getSplitTestUserProperties', + userId + ) + } catch (err) { + logger.error( + { err, userId }, + 'Failed to build split test user properties for customer.io' + ) + } + } + Modules.promises.hooks .fire('setUserProperties', userId, { overleaf_id: userId, @@ -602,6 +617,7 @@ async function projectListPage(req, res, next) { ...(groupRole && { group_role: groupRole }), is_managed_user: Boolean(user.enrollment?.managedBy), ...(user.email && { email: user.email }), + ...splitTestUserProperties, }) .catch(err => { logger.error({ err }, 'Failed to set user properties for customer.io') diff --git a/services/web/app/src/Features/SplitTests/SplitTestUserGetter.mjs b/services/web/app/src/Features/SplitTests/SplitTestUserGetter.mjs index 39c3d43269..7790d556d1 100644 --- a/services/web/app/src/Features/SplitTests/SplitTestUserGetter.mjs +++ b/services/web/app/src/Features/SplitTests/SplitTestUserGetter.mjs @@ -45,7 +45,7 @@ function getProjection(splitTestName) { /** * @param id - * @param {string} splitTestName + * @param {string} [splitTestName] test to get; omit to fetch all assignments. * @param {string} path * @return {Promise} */ diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index fcbfb7d8d1..780c3de46a 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -568,6 +568,53 @@ describe('ProjectListController', function () { ) }) + it('should send packed split test assignments to customer.io', async function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.Modules.promises.hooks.fire + .withArgs('getSplitTestUserProperties', ctx.user._id) + .resolves([{ split_test_assignments: { 'test-a': 'variant-1' } }]) + ctx.res.render = () => {} + + await ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( + 'setUserProperties', + ctx.user._id, + sinon.match({ + split_test_assignments: { 'test-a': 'variant-1' }, + }) + ) + }) + + it('should not request split test user properties in a non-saas environment', async function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(false) + ctx.res.render = () => {} + + await ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + + expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( + 'getSplitTestUserProperties', + sinon.match.any + ) + }) + + it('should still send other user properties when building split test assignments fails', async function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.Modules.promises.hooks.fire + .withArgs('getSplitTestUserProperties', ctx.user._id) + .rejects(new Error('boom')) + ctx.res.render = () => {} + + await ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + + const call = ctx.Modules.promises.hooks.fire + .getCalls() + .find(call => call.args[0] === 'setUserProperties') + expect(call).to.exist + expect(call.args[2]).to.not.have.property('split_test_assignments') + expect(call.args[2]).to.have.property('overleaf_id', ctx.user._id) + }) + it('should send enterprise_commons=true when user has commons from an enterprise_commons institution', async function (ctx) { ctx.Features.hasFeature.withArgs('saas').returns(true) ctx.UserGetter.promises.getUserFullEmails.resolves([