extract_onboardingdatacollection_never_used_latex.mjs 5.6 KB

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