SplitTestHandler.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. const UserGetter = require('../User/UserGetter')
  2. const UserUpdater = require('../User/UserUpdater')
  3. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  4. const Settings = require('@overleaf/settings')
  5. const _ = require('lodash')
  6. const crypto = require('crypto')
  7. const OError = require('@overleaf/o-error')
  8. const { callbackify } = require('util')
  9. const duplicateSplitTest = _.findKey(
  10. _.groupBy(Settings.splitTests, 'id'),
  11. group => {
  12. return group.length > 1
  13. }
  14. )
  15. if (duplicateSplitTest) {
  16. throw new OError(
  17. `Split test IDs must be unique: ${duplicateSplitTest} is defined at least twice`
  18. )
  19. }
  20. const ACTIVE_SPLIT_TESTS = []
  21. for (const splitTest of Settings.splitTests) {
  22. for (const variant of splitTest.variants) {
  23. if (variant.id === 'default') {
  24. throw new OError(
  25. `Split test variant ID cannot be 'default' (reserved value), defined in split test ${JSON.stringify(
  26. splitTest
  27. )}`
  28. )
  29. }
  30. }
  31. const totalVariantsRolloutPercent = _.sumBy(
  32. splitTest.variants,
  33. 'rolloutPercent'
  34. )
  35. if (splitTest.active) {
  36. if (totalVariantsRolloutPercent > 100) {
  37. for (const variant of splitTest.variants) {
  38. variant.rolloutPercent =
  39. (variant.rolloutPercent * 100) / totalVariantsRolloutPercent
  40. }
  41. }
  42. if (totalVariantsRolloutPercent > 0) {
  43. ACTIVE_SPLIT_TESTS.push(splitTest)
  44. }
  45. }
  46. }
  47. async function getTestSegmentation(userId, splitTestId) {
  48. const splitTest = _.find(ACTIVE_SPLIT_TESTS, ['id', splitTestId])
  49. if (splitTest) {
  50. const alreadyAssignedVariant = await getAlreadyAssignedVariant(
  51. userId,
  52. splitTestId
  53. )
  54. if (alreadyAssignedVariant) {
  55. return {
  56. enabled: true,
  57. variant: alreadyAssignedVariant,
  58. }
  59. } else {
  60. const variant = await assignUserToVariant(userId, splitTest)
  61. return {
  62. enabled: true,
  63. variant,
  64. }
  65. }
  66. }
  67. return {
  68. enabled: false,
  69. }
  70. }
  71. async function getAlreadyAssignedVariant(userId, splitTestId) {
  72. const user = await UserGetter.promises.getUser(userId, { splitTests: 1 })
  73. if (user && user.splitTests) {
  74. return user.splitTests[splitTestId]
  75. }
  76. return undefined
  77. }
  78. async function assignUserToVariant(userId, splitTest) {
  79. let userIdAsPercentile = await _getPercentile(userId, splitTest.id)
  80. let selectedVariant = 'default'
  81. for (const variant of splitTest.variants) {
  82. if (userIdAsPercentile < variant.rolloutPercent) {
  83. selectedVariant = variant.id
  84. break
  85. } else {
  86. userIdAsPercentile -= variant.rolloutPercent
  87. }
  88. }
  89. await UserUpdater.promises.updateUser(userId, {
  90. $set: {
  91. [`splitTests.${splitTest.id}`]: selectedVariant,
  92. },
  93. })
  94. AnalyticsManager.setUserProperty(
  95. userId,
  96. `split-test-${splitTest.id}`,
  97. selectedVariant
  98. )
  99. return selectedVariant
  100. }
  101. function _getPercentile(userId, splitTestId) {
  102. const hash = crypto
  103. .createHash('md5')
  104. .update(userId + splitTestId)
  105. .digest('hex')
  106. const hashPrefix = hash.substr(0, 8)
  107. return Math.floor((parseInt(hashPrefix, 16) / 0xffffffff) * 100)
  108. }
  109. module.exports = {
  110. /**
  111. * @deprecated: use SplitTestV2Handler.getAssignment instead
  112. */
  113. getTestSegmentation: callbackify(getTestSegmentation),
  114. promises: {
  115. /**
  116. * @deprecated: use SplitTestV2Handler.promises.getAssignment instead
  117. */
  118. getTestSegmentation,
  119. },
  120. }