AnalyticsManager.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. const SessionManager = require('../Authentication/SessionManager')
  2. const UserAnalyticsIdCache = require('./UserAnalyticsIdCache')
  3. const Settings = require('@overleaf/settings')
  4. const Metrics = require('../../infrastructure/Metrics')
  5. const Queues = require('../../infrastructure/Queues')
  6. const uuid = require('uuid')
  7. const _ = require('lodash')
  8. const { expressify } = require('../../util/promises')
  9. const { logger } = require('@overleaf/logger')
  10. const { getAnalyticsIdFromMongoUser } = require('./AnalyticsHelper')
  11. const analyticsEventsQueue = Queues.getQueue('analytics-events')
  12. const analyticsEditingSessionsQueue = Queues.getQueue(
  13. 'analytics-editing-sessions'
  14. )
  15. const analyticsUserPropertiesQueue = Queues.getQueue(
  16. 'analytics-user-properties'
  17. )
  18. const ONE_MINUTE_MS = 60 * 1000
  19. const UUID_REGEXP = /^[\w]{8}(-[\w]{4}){3}-[\w]{12}$/
  20. function identifyUser(userId, analyticsId, isNewUser) {
  21. if (!userId || !analyticsId || !analyticsId.toString().match(UUID_REGEXP)) {
  22. return
  23. }
  24. if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
  25. return
  26. }
  27. Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'identify' })
  28. Queues.createScheduledJob(
  29. 'analytics-events',
  30. {
  31. name: 'identify',
  32. data: { userId, analyticsId, isNewUser, createdAt: new Date() },
  33. },
  34. ONE_MINUTE_MS
  35. )
  36. .then(() => {
  37. Metrics.analyticsQueue.inc({ status: 'added', event_type: 'identify' })
  38. })
  39. .catch(() => {
  40. Metrics.analyticsQueue.inc({ status: 'error', event_type: 'identify' })
  41. })
  42. }
  43. async function recordEventForUser(userId, event, segmentation) {
  44. if (!userId) {
  45. return
  46. }
  47. if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
  48. return
  49. }
  50. const analyticsId = await UserAnalyticsIdCache.get(userId)
  51. if (analyticsId) {
  52. _recordEvent({ analyticsId, userId, event, segmentation, isLoggedIn: true })
  53. }
  54. }
  55. function recordEventForSession(session, event, segmentation) {
  56. const { analyticsId, userId } = getIdsFromSession(session)
  57. if (!analyticsId) {
  58. return
  59. }
  60. if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
  61. return
  62. }
  63. logger.debug({
  64. analyticsId,
  65. userId,
  66. event,
  67. segmentation,
  68. isLoggedIn: !!userId,
  69. createdAt: new Date(),
  70. })
  71. _recordEvent({
  72. analyticsId,
  73. userId,
  74. event,
  75. segmentation,
  76. isLoggedIn: !!userId,
  77. createdAt: new Date(),
  78. })
  79. }
  80. async function setUserPropertyForUser(userId, propertyName, propertyValue) {
  81. if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
  82. return
  83. }
  84. _checkPropertyValue(propertyValue)
  85. const analyticsId = await UserAnalyticsIdCache.get(userId)
  86. if (analyticsId) {
  87. _setUserProperty({ analyticsId, propertyName, propertyValue })
  88. }
  89. }
  90. async function setUserPropertyForAnalyticsId(
  91. analyticsId,
  92. propertyName,
  93. propertyValue
  94. ) {
  95. if (_isAnalyticsDisabled()) {
  96. return
  97. }
  98. _checkPropertyValue(propertyValue)
  99. _setUserProperty({ analyticsId, propertyName, propertyValue })
  100. }
  101. async function setUserPropertyForSession(session, propertyName, propertyValue) {
  102. const { analyticsId, userId } = getIdsFromSession(session)
  103. if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
  104. return
  105. }
  106. _checkPropertyValue(propertyValue)
  107. if (analyticsId) {
  108. _setUserProperty({ analyticsId, propertyName, propertyValue })
  109. }
  110. }
  111. function updateEditingSession(userId, projectId, countryCode, segmentation) {
  112. if (!userId) {
  113. return
  114. }
  115. if (_isAnalyticsDisabled() || _isSmokeTestUser(userId)) {
  116. return
  117. }
  118. Metrics.analyticsQueue.inc({
  119. status: 'adding',
  120. event_type: 'editing-session',
  121. })
  122. analyticsEditingSessionsQueue
  123. .add('editing-session', {
  124. userId,
  125. projectId,
  126. countryCode,
  127. segmentation,
  128. createdAt: new Date(),
  129. })
  130. .then(() => {
  131. Metrics.analyticsQueue.inc({
  132. status: 'added',
  133. event_type: 'editing-session',
  134. })
  135. })
  136. .catch(() => {
  137. Metrics.analyticsQueue.inc({
  138. status: 'error',
  139. event_type: 'editing-session',
  140. })
  141. })
  142. }
  143. function _recordEvent(
  144. { analyticsId, userId, event, segmentation, isLoggedIn },
  145. { delay } = {}
  146. ) {
  147. Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'event' })
  148. analyticsEventsQueue
  149. .add(
  150. 'event',
  151. {
  152. analyticsId,
  153. userId,
  154. event,
  155. segmentation,
  156. isLoggedIn,
  157. createdAt: new Date(),
  158. },
  159. { delay }
  160. )
  161. .then(() => {
  162. Metrics.analyticsQueue.inc({ status: 'added', event_type: 'event' })
  163. })
  164. .catch(() => {
  165. Metrics.analyticsQueue.inc({ status: 'error', event_type: 'event' })
  166. })
  167. }
  168. function _setUserProperty({ analyticsId, propertyName, propertyValue }) {
  169. Metrics.analyticsQueue.inc({
  170. status: 'adding',
  171. event_type: 'user-property',
  172. })
  173. analyticsUserPropertiesQueue
  174. .add('user-property', {
  175. analyticsId,
  176. propertyName,
  177. propertyValue,
  178. createdAt: new Date(),
  179. })
  180. .then(() => {
  181. Metrics.analyticsQueue.inc({
  182. status: 'added',
  183. event_type: 'user-property',
  184. })
  185. })
  186. .catch(() => {
  187. Metrics.analyticsQueue.inc({
  188. status: 'error',
  189. event_type: 'user-property',
  190. })
  191. })
  192. }
  193. function _isSmokeTestUser(userId) {
  194. const smokeTestUserId = Settings.smokeTest && Settings.smokeTest.userId
  195. return (
  196. smokeTestUserId != null &&
  197. userId != null &&
  198. userId.toString() === smokeTestUserId
  199. )
  200. }
  201. function _isAnalyticsDisabled() {
  202. return !(Settings.analytics && Settings.analytics.enabled)
  203. }
  204. function _checkPropertyValue(propertyValue) {
  205. if (propertyValue === undefined) {
  206. throw new Error(
  207. 'propertyValue cannot be undefined, use null to unset a property'
  208. )
  209. }
  210. }
  211. function getIdsFromSession(session) {
  212. const analyticsId = _.get(session, ['analyticsId'])
  213. const userId = SessionManager.getLoggedInUserId(session)
  214. return { analyticsId, userId }
  215. }
  216. async function analyticsIdMiddleware(req, res, next) {
  217. const session = req.session
  218. const sessionUser = SessionManager.getSessionUser(session)
  219. if (sessionUser) {
  220. session.analyticsId = getAnalyticsIdFromMongoUser(sessionUser)
  221. } else if (!session.analyticsId) {
  222. // generate an `analyticsId` if needed
  223. session.analyticsId = uuid.v4()
  224. }
  225. next()
  226. }
  227. module.exports = {
  228. identifyUser,
  229. recordEventForSession,
  230. recordEventForUser,
  231. setUserPropertyForUser,
  232. setUserPropertyForSession,
  233. setUserPropertyForAnalyticsId,
  234. updateEditingSession,
  235. getIdsFromSession,
  236. analyticsIdMiddleware: expressify(analyticsIdMiddleware),
  237. }