AnalyticsManagerTests.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. const SandboxedModule = require('sandboxed-module')
  2. const path = require('path')
  3. const sinon = require('sinon')
  4. const MockRequest = require('../helpers/MockRequest')
  5. const MockResponse = require('../helpers/MockResponse')
  6. const { assert } = require('chai')
  7. const { ObjectID } = require('mongodb')
  8. const MODULE_PATH = path.join(
  9. __dirname,
  10. '../../../../app/src/Features/Analytics/AnalyticsManager'
  11. )
  12. describe('AnalyticsManager', function () {
  13. beforeEach(function () {
  14. this.fakeUserId = 'dbfc9438d14996f73dd172fb'
  15. this.analyticsId = 'ecdb935a-52f3-4f91-aebc-7a70d2ffbb55'
  16. this.Settings = {
  17. analytics: { enabled: true },
  18. }
  19. this.analyticsEventsQueue = {
  20. add: sinon.stub().resolves(),
  21. process: sinon.stub().resolves(),
  22. }
  23. this.analyticsEditingSessionQueue = {
  24. add: sinon.stub().resolves(),
  25. process: sinon.stub().resolves(),
  26. }
  27. this.onboardingEmailsQueue = {
  28. add: sinon.stub().resolves(),
  29. process: sinon.stub().resolves(),
  30. }
  31. this.analyticsUserPropertiesQueue = {
  32. add: sinon.stub().resolves(),
  33. process: sinon.stub().resolves(),
  34. }
  35. const self = this
  36. this.Queues = {
  37. getQueue: queueName => {
  38. switch (queueName) {
  39. case 'analytics-events':
  40. return self.analyticsEventsQueue
  41. case 'analytics-editing-sessions':
  42. return self.analyticsEditingSessionQueue
  43. case 'emails-onboarding':
  44. return self.onboardingEmailsQueue
  45. case 'analytics-user-properties':
  46. return self.analyticsUserPropertiesQueue
  47. default:
  48. throw new Error('Unexpected queue name')
  49. }
  50. },
  51. createScheduledJob: sinon.stub().resolves(),
  52. }
  53. this.backgroundRequest = sinon.stub().yields()
  54. this.request = sinon.stub().yields()
  55. this.AnalyticsManager = SandboxedModule.require(MODULE_PATH, {
  56. requires: {
  57. '@overleaf/settings': this.Settings,
  58. '../../infrastructure/Queues': this.Queues,
  59. './UserAnalyticsIdCache': (this.UserAnalyticsIdCache = {
  60. get: sinon.stub().resolves(this.analyticsId),
  61. }),
  62. },
  63. })
  64. })
  65. describe('ignores when', function () {
  66. it('user is smoke test user', function () {
  67. this.Settings.smokeTest = { userId: this.fakeUserId }
  68. this.AnalyticsManager.identifyUser(this.fakeUserId, '')
  69. sinon.assert.notCalled(this.Queues.createScheduledJob)
  70. })
  71. it('analytics service is disabled', function () {
  72. this.Settings.analytics.enabled = false
  73. this.AnalyticsManager.identifyUser(this.fakeUserId, '')
  74. sinon.assert.notCalled(this.Queues.createScheduledJob)
  75. })
  76. it('userId is missing', function () {
  77. this.AnalyticsManager.identifyUser(undefined, this.analyticsId)
  78. sinon.assert.notCalled(this.Queues.createScheduledJob)
  79. })
  80. it('analyticsId is missing', function () {
  81. this.AnalyticsManager.identifyUser(
  82. new ObjectID(this.fakeUserId),
  83. undefined
  84. )
  85. sinon.assert.notCalled(this.Queues.createScheduledJob)
  86. })
  87. it('analyticsId is not a valid UUID', function () {
  88. this.AnalyticsManager.identifyUser(
  89. new ObjectID(this.fakeUserId),
  90. this.fakeUserId
  91. )
  92. sinon.assert.notCalled(this.Queues.createScheduledJob)
  93. })
  94. it('userId and analyticsId are the same Mongo ID', function () {
  95. this.AnalyticsManager.identifyUser(
  96. new ObjectID(this.fakeUserId),
  97. new ObjectID(this.fakeUserId)
  98. )
  99. sinon.assert.notCalled(this.Queues.createScheduledJob)
  100. })
  101. })
  102. describe('queues the appropriate message for', function () {
  103. it('identifyUser', function () {
  104. const analyticsId = 'bd101c4c-722f-4204-9e2d-8303e5d9c120'
  105. this.AnalyticsManager.identifyUser(this.fakeUserId, analyticsId, true)
  106. sinon.assert.calledWithMatch(
  107. this.Queues.createScheduledJob,
  108. 'analytics-events',
  109. {
  110. name: 'identify',
  111. data: {
  112. userId: this.fakeUserId,
  113. analyticsId,
  114. isNewUser: true,
  115. createdAt: sinon.match.date,
  116. },
  117. },
  118. 60000
  119. )
  120. })
  121. it('recordEventForUser', async function () {
  122. const event = 'fake-event'
  123. await this.AnalyticsManager.recordEventForUser(
  124. this.fakeUserId,
  125. event,
  126. null
  127. )
  128. sinon.assert.calledWithMatch(this.analyticsEventsQueue.add, 'event', {
  129. analyticsId: this.analyticsId,
  130. event,
  131. segmentation: null,
  132. isLoggedIn: true,
  133. })
  134. })
  135. it('updateEditingSession', function () {
  136. const projectId = '789ghi'
  137. const countryCode = 'fr'
  138. const segmentation = { editorType: 'abc' }
  139. this.AnalyticsManager.updateEditingSession(
  140. this.fakeUserId,
  141. projectId,
  142. countryCode,
  143. segmentation
  144. )
  145. sinon.assert.calledWithMatch(
  146. this.analyticsEditingSessionQueue.add,
  147. 'editing-session',
  148. {
  149. userId: this.fakeUserId,
  150. projectId,
  151. countryCode,
  152. segmentation,
  153. }
  154. )
  155. })
  156. })
  157. describe('AnalyticsIdMiddleware', function () {
  158. beforeEach(function () {
  159. this.userId = '123abc'
  160. this.analyticsId = 'bccd308c-5d72-426e-a106-662e88557795'
  161. const self = this
  162. this.AnalyticsManager = SandboxedModule.require(MODULE_PATH, {
  163. requires: {
  164. '@overleaf/settings': {},
  165. '../../infrastructure/Queues': {
  166. getQueue: queueName => {
  167. switch (queueName) {
  168. case 'analytics-events':
  169. return self.analyticsEventsQueue
  170. case 'analytics-editing-sessions':
  171. return self.analyticsEditingSessionQueue
  172. case 'emails-onboarding':
  173. return self.onboardingEmailsQueue
  174. case 'analytics-user-properties':
  175. return self.analyticsUserPropertiesQueue
  176. default:
  177. throw new Error('Unexpected queue name')
  178. }
  179. },
  180. },
  181. './UserAnalyticsIdCache': {},
  182. uuid: {
  183. v4: () => this.analyticsId,
  184. },
  185. },
  186. })
  187. this.req = new MockRequest()
  188. this.req.session = {}
  189. this.res = new MockResponse()
  190. this.next = () => {}
  191. })
  192. it('sets session.analyticsId with no user in session', async function () {
  193. await this.AnalyticsManager.analyticsIdMiddleware(
  194. this.req,
  195. this.res,
  196. this.next
  197. )
  198. assert.equal(this.analyticsId, this.req.session.analyticsId)
  199. })
  200. it('does not update analyticsId when existing, with no user in session', async function () {
  201. this.req.session.analyticsId = 'foo'
  202. await this.AnalyticsManager.analyticsIdMiddleware(
  203. this.req,
  204. this.res,
  205. this.next
  206. )
  207. assert.equal('foo', this.req.session.analyticsId)
  208. })
  209. it('sets session.analyticsId with a logged in user in session having an analyticsId', async function () {
  210. this.req.session.user = {
  211. _id: this.userId,
  212. analyticsId: this.analyticsId,
  213. }
  214. await this.AnalyticsManager.analyticsIdMiddleware(
  215. this.req,
  216. this.res,
  217. this.next
  218. )
  219. assert.equal(this.analyticsId, this.req.session.analyticsId)
  220. })
  221. it('sets session.analyticsId with a legacy user session without an analyticsId', async function () {
  222. this.req.session.user = {
  223. _id: this.userId,
  224. analyticsId: undefined,
  225. }
  226. await this.AnalyticsManager.analyticsIdMiddleware(
  227. this.req,
  228. this.res,
  229. this.next
  230. )
  231. assert.equal(this.userId, this.req.session.analyticsId)
  232. })
  233. it('updates session.analyticsId with a legacy user session without an analyticsId if different', async function () {
  234. this.req.session.user = {
  235. _id: this.userId,
  236. analyticsId: undefined,
  237. }
  238. this.req.analyticsId = 'foo'
  239. await this.AnalyticsManager.analyticsIdMiddleware(
  240. this.req,
  241. this.res,
  242. this.next
  243. )
  244. assert.equal(this.userId, this.req.session.analyticsId)
  245. })
  246. it('does not update session.analyticsId with a legacy user session without an analyticsId if same', async function () {
  247. this.req.session.user = {
  248. _id: this.userId,
  249. analyticsId: undefined,
  250. }
  251. this.req.analyticsId = this.userId
  252. await this.AnalyticsManager.analyticsIdMiddleware(
  253. this.req,
  254. this.res,
  255. this.next
  256. )
  257. assert.equal(this.userId, this.req.session.analyticsId)
  258. await this.AnalyticsManager.analyticsIdMiddleware(
  259. this.req,
  260. this.res,
  261. this.next
  262. )
  263. assert.equal(this.userId, this.req.session.analyticsId)
  264. })
  265. })
  266. })