extract_onboardingdatacollection_never_used_latex.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. const csv = require('csv')
  2. const fs = require('fs')
  3. const minimist = require('minimist')
  4. const {
  5. OnboardingDataCollection,
  6. } = require('../app/src/models/OnboardingDataCollection')
  7. const { User } = require('../app/src/models/User')
  8. const SubscriptionLocator = require('../app/src/Features/Subscription/SubscriptionLocator')
  9. const Settings = require('@overleaf/settings')
  10. const { fetchJson } = require('@overleaf/fetch-utils')
  11. /**
  12. * This script extracts ODC data with some extra fields, and filters on registration date and LaTeX experience
  13. *
  14. * It will:
  15. * — filter for used_latex=never
  16. * — augment rows with user registered date and email addresses
  17. * — filter on users registered after a certain date
  18. * — export updated CSV
  19. *
  20. * Usage:
  21. * - Locally:
  22. * - docker compose exec web bash
  23. * - node scripts/extract_onboardingdatacollection_never_used_latex.js
  24. * - On the server:
  25. * - rake connect:app[staging,web]
  26. * - node scripts/extract_onboardingdatacollection_never_used_latex.js
  27. * - exit
  28. * - kubectl cp web-standalone-prod-XXXXX:/tmp/odc_neverUsedLatex.csv ~/odc_neverUsedLatex.csv
  29. */
  30. function usage() {
  31. console.log(
  32. `
  33. Onboarding Data Collection extraction, outputs to /tmp/odc_neverUsedLatex.csv
  34. Usage:
  35. node scripts/extract_onboardingdatacollection_never_used_latex.js [--registeredBefore=<date>] [--studentsOnly] [--includeSignUpDate] [--includeCountry] [--includePlanCode]
  36. Options:
  37. --help Show this screen
  38. --registeredBefore=<date> Limit to users registered before ISO 8601 date (eg. 2024-08-01)
  39. --studentsOnly Only include users whose primary occupation is 'university' or 'school'
  40. --includeSignUpDate Include signUpDate column
  41. --includeCountry Include countryCode column (inferred from institution and possibly missing)
  42. --includePlanCode Include planCode column
  43. `
  44. )
  45. }
  46. function parseArgs() {
  47. const argv = minimist(process.argv.slice(2), {
  48. string: ['registeredBefore'],
  49. bool: [
  50. 'help',
  51. 'studentsOnly',
  52. 'includeSignUpDate',
  53. 'includeCountry',
  54. 'includePlanCode',
  55. ],
  56. default: {
  57. help: false,
  58. studentsOnly: false,
  59. includeSignUpDate: false,
  60. includeCountry: false,
  61. includePlanCode: false,
  62. registeredBefore: '2024-02-18',
  63. },
  64. })
  65. if (argv.help) {
  66. usage()
  67. process.exit(0)
  68. }
  69. return argv
  70. }
  71. async function getEmails(userIds, { registeredBefore }) {
  72. const userEmails = await User.find(
  73. { _id: { $in: userIds }, signUpDate: { $lte: new Date(registeredBefore) } },
  74. { email: 1, signUpDate: 1 }
  75. ).exec()
  76. return userEmails.map(({ email, signUpDate }) => ({
  77. email,
  78. signUpDate: new Date(signUpDate).toISOString(),
  79. }))
  80. }
  81. async function getUsers({ studentsOnly }) {
  82. const odcCriteria = { usedLatex: 'never' }
  83. if (studentsOnly) {
  84. odcCriteria.primaryOccupation = 'university'
  85. }
  86. const cursor = OnboardingDataCollection.find(odcCriteria).cursor()
  87. const userIds = []
  88. const institutionNames = []
  89. for (let doc = await cursor.next(); doc != null; doc = await cursor.next()) {
  90. userIds.push(doc._id.toString())
  91. institutionNames.push(doc.institutionName)
  92. }
  93. return { userIds, institutionNames }
  94. }
  95. async function getUserPlanCodes(users) {
  96. const planCodes = []
  97. for await (const user of users) {
  98. const subscription =
  99. await SubscriptionLocator.promises.getUsersSubscription(user)
  100. planCodes.push(subscription?.planCode || 'free')
  101. }
  102. return planCodes
  103. }
  104. // inferred from institution so will not always be available or accurate
  105. async function getUserCountries(institutions) {
  106. const countryCodes = []
  107. // cache any institutions we lookup to avoid making duplicate calls
  108. const institutionLookups = {}
  109. for await (const inst of institutions) {
  110. if (!inst) {
  111. countryCodes.push(undefined)
  112. continue
  113. }
  114. if (institutionLookups[inst]) {
  115. countryCodes.push(institutionLookups[inst])
  116. continue
  117. }
  118. try {
  119. const url = `${Settings.apis.web.url}/institutions/search?search=${encodeURIComponent(inst)}&max_results=1`
  120. const response = await fetchJson(url)
  121. countryCodes.push(response[0]?.country_code)
  122. institutionLookups[inst] = response[0]?.country_code
  123. } catch (e) {
  124. // if institution search fails just move on
  125. console.log(`Error when looking up institution ${inst}: ${e.message}`)
  126. countryCodes.push(undefined)
  127. }
  128. }
  129. return countryCodes
  130. }
  131. async function runScript() {
  132. const columns = ['email']
  133. const args = parseArgs()
  134. if (args.includeSignUpDate) {
  135. columns.push('signUpDate')
  136. }
  137. const users = await getUsers(args)
  138. let userEmails = await getEmails(users.userIds, args)
  139. if (args.includePlanCode) {
  140. columns.push('planCode')
  141. const planCodes = await getUserPlanCodes(users.userIds)
  142. userEmails = userEmails.map((user, index) => {
  143. user.planCode = planCodes[index]
  144. return user
  145. })
  146. }
  147. if (args.includeCountry) {
  148. columns.push('country')
  149. const countryCodes = await getUserCountries(users.institutionNames)
  150. userEmails = userEmails.map((user, index) => {
  151. user.country = countryCodes[index]
  152. return user
  153. })
  154. }
  155. console.log('Starting to write to csv file...')
  156. csv.stringify(
  157. userEmails,
  158. {
  159. header: true,
  160. columns,
  161. },
  162. function (err, output) {
  163. fs.writeFileSync('/tmp/odc_neverUsedLatex.csv', output)
  164. if (err) {
  165. console.log('error writing csv output: ', err)
  166. process.exit(1)
  167. }
  168. process.exit()
  169. }
  170. )
  171. }
  172. runScript().catch(err => {
  173. console.error(err)
  174. process.exit(1)
  175. })