refresh_features.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. const { db, waitForDb } = require('../app/src/infrastructure/mongodb')
  2. const minimist = require('minimist')
  3. const _ = require('lodash')
  4. const async = require('async')
  5. const FeaturesUpdater = require('../app/src/Features/Subscription/FeaturesUpdater')
  6. const FeaturesHelper = require('../app/src/Features/Subscription/FeaturesHelper')
  7. const UserFeaturesUpdater = require('../app/src/Features/Subscription/UserFeaturesUpdater')
  8. const AnalyticsManager = require('../app/src/Features/Analytics/AnalyticsManager')
  9. const DropboxHandler = require('../modules/dropbox/app/src/DropboxHandler')
  10. const { OError } = require('../app/src/Features/Errors/Errors')
  11. const logger = require('@overleaf/logger')
  12. const ScriptLogger = {
  13. checkedUsersCount: 0,
  14. mismatchUsersCount: 0,
  15. allDaysSinceLastLoggedIn: [],
  16. allMismatchReasons: {},
  17. recordMismatch: (user, mismatchReasons) => {
  18. const mismatchReasonsString = JSON.stringify(mismatchReasons)
  19. if (ScriptLogger.allMismatchReasons[mismatchReasonsString]) {
  20. ScriptLogger.allMismatchReasons[mismatchReasonsString].push(user._id)
  21. } else {
  22. ScriptLogger.allMismatchReasons[mismatchReasonsString] = [user._id]
  23. }
  24. ScriptLogger.mismatchUsersCount += 1
  25. if (user.lastLoggedIn) {
  26. const daysSinceLastLoggedIn =
  27. (new Date() - user.lastLoggedIn) / 1000 / 3600 / 24
  28. ScriptLogger.allDaysSinceLastLoggedIn.push(daysSinceLastLoggedIn)
  29. }
  30. },
  31. printProgress: () => {
  32. console.warn(
  33. `Users checked: ${ScriptLogger.checkedUsersCount}. Mismatches: ${ScriptLogger.mismatchUsersCount}`
  34. )
  35. },
  36. printSummary: () => {
  37. console.log('All Mismatch Reasons:', ScriptLogger.allMismatchReasons)
  38. console.log('Mismatch Users Count', ScriptLogger.mismatchUsersCount)
  39. console.log(
  40. 'Average Last Logged In (Days):',
  41. _.sum(ScriptLogger.allDaysSinceLastLoggedIn) /
  42. ScriptLogger.allDaysSinceLastLoggedIn.length
  43. )
  44. console.log(
  45. 'Recent Logged In (Last 7 Days):',
  46. _.filter(ScriptLogger.allDaysSinceLastLoggedIn, a => a < 7).length
  47. )
  48. console.log(
  49. 'Recent Logged In (Last 30 Days):',
  50. _.filter(ScriptLogger.allDaysSinceLastLoggedIn, a => a < 30).length
  51. )
  52. },
  53. }
  54. const checkAndUpdateUser = (user, callback) =>
  55. FeaturesUpdater.computeFeatures(user._id, (error, freshFeatures) => {
  56. if (error) {
  57. return callback(error)
  58. }
  59. const mismatchReasons = FeaturesHelper.compareFeatures(
  60. user.features,
  61. freshFeatures
  62. )
  63. if (Object.keys(mismatchReasons).length === 0) {
  64. // features are matching; nothing else to do
  65. return callback()
  66. }
  67. ScriptLogger.recordMismatch(user, mismatchReasons)
  68. if (!COMMIT) {
  69. // not saving features; nothing else to do
  70. return callback()
  71. }
  72. const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(freshFeatures)
  73. AnalyticsManager.setUserPropertyForUser(
  74. user._id,
  75. 'feature-set',
  76. matchedFeatureSet
  77. )
  78. UserFeaturesUpdater.overrideFeatures(
  79. user._id,
  80. freshFeatures,
  81. (error, featuresChanged) => {
  82. if (error) {
  83. return callback(error)
  84. }
  85. if (
  86. mismatchReasons.dropbox !== undefined &&
  87. freshFeatures.dropbox === false
  88. ) {
  89. DropboxHandler.unlinkAccount(
  90. user._id,
  91. { sendEmail: false },
  92. error => {
  93. if (error) {
  94. return callback(
  95. OError.tag(error, 'error unlinking dropbox', {
  96. userId: user._id,
  97. })
  98. )
  99. }
  100. logger.log({ userId: user._id }, 'Unlinked dropbox')
  101. callback(null, featuresChanged)
  102. }
  103. )
  104. } else {
  105. callback(null, featuresChanged)
  106. }
  107. }
  108. )
  109. })
  110. const checkAndUpdateUsers = (users, callback) =>
  111. async.eachLimit(users, ASYNC_LIMIT, checkAndUpdateUser, callback)
  112. const loopForUsers = (skip, callback) => {
  113. db.users
  114. .find({})
  115. .project({ features: 1, lastLoggedIn: 1 })
  116. .sort({ _id: 1 })
  117. .skip(skip)
  118. .limit(FETCH_LIMIT)
  119. .toArray((error, users) => {
  120. if (error) {
  121. return callback(error)
  122. }
  123. if (users.length === 0) {
  124. console.warn('DONE')
  125. return callback()
  126. }
  127. checkAndUpdateUsers(users, error => {
  128. if (error) {
  129. return callback(error)
  130. }
  131. ScriptLogger.checkedUsersCount += users.length
  132. retryCounter = 0
  133. ScriptLogger.printProgress()
  134. ScriptLogger.printSummary()
  135. loopForUsers(MONGO_SKIP + ScriptLogger.checkedUsersCount, callback)
  136. })
  137. })
  138. }
  139. let retryCounter = 0
  140. const run = () =>
  141. loopForUsers(MONGO_SKIP + ScriptLogger.checkedUsersCount, error => {
  142. if (error) {
  143. if (retryCounter < 3) {
  144. console.error(error)
  145. retryCounter += 1
  146. console.warn(`RETRYING IN 60 SECONDS. (${retryCounter}/3)`)
  147. return setTimeout(run, 6000)
  148. }
  149. throw error
  150. }
  151. process.exit()
  152. })
  153. let FETCH_LIMIT, ASYNC_LIMIT, COMMIT, MONGO_SKIP
  154. const setup = () => {
  155. const argv = minimist(process.argv.slice(2))
  156. FETCH_LIMIT = argv.fetch ? argv.fetch : 100
  157. ASYNC_LIMIT = argv.async ? argv.async : 10
  158. MONGO_SKIP = argv.skip ? argv.skip : 0
  159. COMMIT = argv.commit !== undefined
  160. const FORCE = argv.force !== undefined
  161. if (!FORCE) {
  162. console.log(
  163. 'NOTE: features can be automatically refreshed on login (using `featuresEpoch`)\n' +
  164. 'Consider incrementing settings.featuresEpoch instead of running this script.\n' +
  165. 'If you really need to run this script, use refresh_features.js --force.'
  166. )
  167. process.exit(1)
  168. }
  169. if (!COMMIT) {
  170. console.warn('Doing dry run without --commit')
  171. }
  172. if (MONGO_SKIP) {
  173. console.warn(`Skipping first ${MONGO_SKIP} records`)
  174. }
  175. }
  176. waitForDb().then(() => {
  177. setup()
  178. run()
  179. })