refresh_features.mjs 5.7 KB

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