diff --git a/services/web/app/src/Features/Subscription/FeaturesHelper.js b/services/web/app/src/Features/Subscription/FeaturesHelper.js index 16ed84317a..fe90391c86 100644 --- a/services/web/app/src/Features/Subscription/FeaturesHelper.js +++ b/services/web/app/src/Features/Subscription/FeaturesHelper.js @@ -1,4 +1,5 @@ const _ = require('lodash') +const Settings = require('@overleaf/settings') /** * Merge feature sets coming from different sources @@ -96,4 +97,18 @@ function compareFeatures(currentFeatures, expectedFeatures) { return mismatchReasons } -module.exports = { mergeFeatures, isFeatureSetBetter, compareFeatures } +function getMatchedFeatureSet(features) { + for (const [name, featureSet] of Object.entries(Settings.features)) { + if (_.isEqual(features, featureSet)) { + return name + } + } + return 'mixed' +} + +module.exports = { + mergeFeatures, + isFeatureSetBetter, + compareFeatures, + getMatchedFeatureSet, +} diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.js b/services/web/app/src/Features/Subscription/FeaturesUpdater.js index fff97d76f9..adfedcd955 100644 --- a/services/web/app/src/Features/Subscription/FeaturesUpdater.js +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.js @@ -41,7 +41,7 @@ async function refreshFeatures(userId, reason) { const features = await computeFeatures(userId) logger.log({ userId, features }, 'updating user features') - const matchedFeatureSet = _getMatchedFeatureSet(features) + const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features) AnalyticsManager.setUserPropertyForUser( userId, 'feature-set', @@ -189,15 +189,6 @@ async function doSyncFromV1(v1UserId) { return refreshFeatures(user._id, 'sync-v1') } -function _getMatchedFeatureSet(features) { - for (const [name, featureSet] of Object.entries(Settings.features)) { - if (_.isEqual(features, featureSet)) { - return name - } - } - return 'mixed' -} - module.exports = { featuresEpochIsCurrent, computeFeatures: callbackify(computeFeatures), diff --git a/services/web/scripts/backfill_user_properties.js b/services/web/scripts/backfill_user_properties.js new file mode 100644 index 0000000000..a1678c0b72 --- /dev/null +++ b/services/web/scripts/backfill_user_properties.js @@ -0,0 +1,60 @@ +const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY, 10) || 10 + +const { batchedUpdateWithResultHandling } = require('./helpers/batchedUpdate') +const { promiseMapWithLimit } = require('../app/src/util/promises') +const SubscriptionLocator = require('../app/src/features/Subscription/SubscriptionLocator') +const PlansLocator = require('../app/src/features/Subscription/PlansLocator') +const FeaturesHelper = require('../app/src/features/Subscription/FeaturesHelper') +const AnalyticsManager = require('../app/src/features/Analytics/AnalyticsManager') + +async function getGroupSubscriptionPlanCode(userId) { + const subscriptions = + await SubscriptionLocator.promises.getMemberSubscriptions(userId) + let bestPlanCode = null + let bestFeatures = {} + for (const subscription of subscriptions) { + const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) + if ( + plan && + FeaturesHelper.isFeatureSetBetter(plan.features, bestFeatures) + ) { + bestPlanCode = plan.planCode + bestFeatures = plan.features + } + } + return bestPlanCode +} + +async function processUser(user) { + const analyticsId = user.analyticsId || user._id + + const groupSubscriptionPlanCode = await getGroupSubscriptionPlanCode(user._id) + if (groupSubscriptionPlanCode) { + await AnalyticsManager.setUserPropertyForAnalyticsId( + analyticsId, + 'group-subscription-plan-code', + groupSubscriptionPlanCode + ) + } + + const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(user.features) + if (matchedFeatureSet !== 'personal') { + await AnalyticsManager.setUserPropertyForAnalyticsId( + analyticsId, + 'feature-set', + matchedFeatureSet + ) + } +} + +async function processBatch(_, users) { + await promiseMapWithLimit(WRITE_CONCURRENCY, users, async user => { + await processUser(user) + }) +} + +batchedUpdateWithResultHandling('users', {}, processBatch, { + _id: true, + analyticsId: true, + features: true, +}) diff --git a/services/web/scripts/helpers/batchedUpdate.js b/services/web/scripts/helpers/batchedUpdate.js index aabc262b64..5a98938d10 100644 --- a/services/web/scripts/helpers/batchedUpdate.js +++ b/services/web/scripts/helpers/batchedUpdate.js @@ -95,8 +95,14 @@ async function batchedUpdate( return updated } -function batchedUpdateWithResultHandling(collection, query, update) { - batchedUpdate(collection, query, update) +function batchedUpdateWithResultHandling( + collection, + query, + update, + projection, + options +) { + batchedUpdate(collection, query, update, projection, options) .then(updated => { console.error({ updated }) process.exit(0) diff --git a/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js b/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js index 12675055ae..fe98564335 100644 --- a/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js +++ b/services/web/test/unit/src/Subscription/FeaturesUpdaterTests.js @@ -105,15 +105,11 @@ describe('FeaturesUpdater', function () { this.Modules = { promises: { hooks: { fire: sinon.stub().resolves() } }, } - this.FeaturesHelper = { - mergeFeatures: sinon.stub().callsFake((a, b) => ({ ...a, ...b })), - } this.FeaturesUpdater = SandboxedModule.require(MODULE_PATH, { requires: { './UserFeaturesUpdater': this.UserFeaturesUpdater, './SubscriptionLocator': this.SubscriptionLocator, - './FeaturesHelper': this.FeaturesHelper, '@overleaf/settings': this.Settings, '../Referal/ReferalFeatures': this.ReferalFeatures, './V1SubscriptionManager': this.V1SubscriptionManager,