From 24b16c130431be243dad63bbe53559303715c10f Mon Sep 17 00:00:00 2001 From: roo hutton Date: Fri, 10 Apr 2026 10:54:11 +0100 Subject: [PATCH] Merge pull request #32611 from overleaf/rh-cio-email-sdk Add identifiers to cio calls to avoid profile splits GitOrigin-RevId: fec8cad4e87f8df910d729bd00acbf26d0931102 --- .../Features/Subscription/FeaturesUpdater.mjs | 5 +++-- services/web/app/views/_customer_io.pug | 7 ++++--- .../src/Subscription/FeaturesUpdater.test.mjs | 21 +++++++++++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs b/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs index b84c0b482a..e3b94499c5 100644 --- a/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.mjs @@ -42,6 +42,7 @@ async function refreshFeatures(userId, reason) { const user = await UserGetter.promises.getUser(userId, { _id: 1, features: 1, + email: 1, }) const oldFeatures = _.clone(user.features) const features = await computeFeatures(userId) @@ -57,14 +58,14 @@ async function refreshFeatures(userId, reason) { const { features: newFeatures, featuresChanged } = await UserFeaturesUpdater.promises.updateFeatures(userId, features) - // TODO: this call is quite expensive, so ideally we'd update cio with something - // that doesn't require the best subscription to be computed, ie. the plan code (or type) const bestSubscriptionType = await _getBestSubscriptionType(userId) Modules.promises.hooks .fire('setUserProperties', userId, { features, 'best-subscription-type': bestSubscriptionType, + overleafId: userId, + ...(user.email && { email: user.email }), }) .catch(err => { logger.error({ err, userId }, 'Failed to sync features to customer.io') diff --git a/services/web/app/views/_customer_io.pug b/services/web/app/views/_customer_io.pug index 9ff8bb4bcc..0d101ac314 100644 --- a/services/web/app/views/_customer_io.pug +++ b/services/web/app/views/_customer_io.pug @@ -1,8 +1,9 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId) - script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=((user && user._id) || '')). + script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=((user && user._id) || ''), data-user-email=((user && user.email) || '')). var cioSettings = document.querySelector('#cio-loader').dataset; var analyticsId = cioSettings.sessionAnalyticsId; var userId = cioSettings.userId; + var userEmail = cioSettings.userEmail; var siteId = cioSettings.cioSiteId; var writeKey = cioSettings.cioWriteKey; @@ -22,6 +23,6 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId analytics.setAnonymousId(analyticsId); } if (userId) { - analytics.identify(userId); - }; + analytics.identify(userId, userEmail ? { email: userEmail } : {}); + } }}(); diff --git a/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs b/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs index 926308eba1..e8cef6f13b 100644 --- a/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs +++ b/services/web/test/unit/src/Subscription/FeaturesUpdater.test.mjs @@ -426,6 +426,27 @@ describe('FeaturesUpdater', function () { { features: ctx.Settings.features.all, 'best-subscription-type': 'individual', + overleafId: ctx.user._id, + } + ) + }) + }) + + describe('when user has an email', function () { + beforeEach(async function (ctx) { + ctx.user.email = 'user@example.com' + await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test') + }) + + it('should include email in customer.io properties', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( + 'setUserProperties', + ctx.user._id, + { + features: ctx.Settings.features.all, + 'best-subscription-type': 'individual', + overleafId: ctx.user._id, + email: 'user@example.com', } ) })