test_phone_stats_home.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import synapse
  2. from synapse.app.phone_stats_home import start_phone_stats_home
  3. from synapse.rest.client import login, room
  4. from synapse.server import HomeServer
  5. from synapse.util import Clock
  6. from tests import unittest
  7. from tests.server import ThreadedMemoryReactorClock
  8. from tests.unittest import HomeserverTestCase
  9. FIVE_MINUTES_IN_SECONDS = 300
  10. ONE_DAY_IN_SECONDS = 86400
  11. class PhoneHomeTestCase(HomeserverTestCase):
  12. servlets = [
  13. synapse.rest.admin.register_servlets_for_client_rest_resource,
  14. room.register_servlets,
  15. login.register_servlets,
  16. ]
  17. # Override the retention time for the user_ips table because otherwise it
  18. # gets pruned too aggressively for our R30 test.
  19. @unittest.override_config({"user_ips_max_age": "365d"})
  20. def test_r30_minimum_usage(self) -> None:
  21. """
  22. Tests the minimum amount of interaction necessary for the R30 metric
  23. to consider a user 'retained'.
  24. """
  25. # Register a user, log it in, create a room and send a message
  26. user_id = self.register_user("u1", "secret!")
  27. access_token = self.login("u1", "secret!")
  28. room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token)
  29. self.helper.send(room_id, "message", tok=access_token)
  30. # Check the R30 results do not count that user.
  31. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  32. self.assertEqual(r30_results, {"all": 0})
  33. # Advance 30 days (+ 1 second, because strict inequality causes issues if we are
  34. # bang on 30 days later).
  35. self.reactor.advance(30 * ONE_DAY_IN_SECONDS + 1)
  36. # (Make sure the user isn't somehow counted by this point.)
  37. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  38. self.assertEqual(r30_results, {"all": 0})
  39. # Send a message (this counts as activity)
  40. self.helper.send(room_id, "message2", tok=access_token)
  41. # We have to wait some time for _update_client_ips_batch to get
  42. # called and update the user_ips table.
  43. self.reactor.advance(2 * 60 * 60)
  44. # *Now* the user is counted.
  45. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  46. self.assertEqual(r30_results, {"all": 1, "unknown": 1})
  47. # Advance 29 days. The user has now not posted for 29 days.
  48. self.reactor.advance(29 * ONE_DAY_IN_SECONDS)
  49. # The user is still counted.
  50. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  51. self.assertEqual(r30_results, {"all": 1, "unknown": 1})
  52. # Advance another day. The user has now not posted for 30 days.
  53. self.reactor.advance(ONE_DAY_IN_SECONDS)
  54. # The user is now no longer counted in R30.
  55. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  56. self.assertEqual(r30_results, {"all": 0})
  57. def test_r30_minimum_usage_using_default_config(self) -> None:
  58. """
  59. Tests the minimum amount of interaction necessary for the R30 metric
  60. to consider a user 'retained'.
  61. N.B. This test does not override the `user_ips_max_age` config setting,
  62. which defaults to 28 days.
  63. """
  64. # Register a user, log it in, create a room and send a message
  65. user_id = self.register_user("u1", "secret!")
  66. access_token = self.login("u1", "secret!")
  67. room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token)
  68. self.helper.send(room_id, "message", tok=access_token)
  69. # Check the R30 results do not count that user.
  70. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  71. self.assertEqual(r30_results, {"all": 0})
  72. # Advance 30 days (+ 1 second, because strict inequality causes issues if we are
  73. # bang on 30 days later).
  74. self.reactor.advance(30 * ONE_DAY_IN_SECONDS + 1)
  75. # (Make sure the user isn't somehow counted by this point.)
  76. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  77. self.assertEqual(r30_results, {"all": 0})
  78. # Send a message (this counts as activity)
  79. self.helper.send(room_id, "message2", tok=access_token)
  80. # We have to wait some time for _update_client_ips_batch to get
  81. # called and update the user_ips table.
  82. self.reactor.advance(2 * 60 * 60)
  83. # *Now* the user is counted.
  84. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  85. self.assertEqual(r30_results, {"all": 1, "unknown": 1})
  86. # Advance 27 days. The user has now not posted for 27 days.
  87. self.reactor.advance(27 * ONE_DAY_IN_SECONDS)
  88. # The user is still counted.
  89. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  90. self.assertEqual(r30_results, {"all": 1, "unknown": 1})
  91. # Advance another day. The user has now not posted for 28 days.
  92. self.reactor.advance(ONE_DAY_IN_SECONDS)
  93. # The user is now no longer counted in R30.
  94. # (This is because the user_ips table has been pruned, which by default
  95. # only preserves the last 28 days of entries.)
  96. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  97. self.assertEqual(r30_results, {"all": 0})
  98. def test_r30_user_must_be_retained_for_at_least_a_month(self) -> None:
  99. """
  100. Tests that a newly-registered user must be retained for a whole month
  101. before appearing in the R30 statistic, even if they post every day
  102. during that time!
  103. """
  104. # Register a user and send a message
  105. user_id = self.register_user("u1", "secret!")
  106. access_token = self.login("u1", "secret!")
  107. room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token)
  108. self.helper.send(room_id, "message", tok=access_token)
  109. # Check the user does not contribute to R30 yet.
  110. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  111. self.assertEqual(r30_results, {"all": 0})
  112. for _ in range(30):
  113. # This loop posts a message every day for 30 days
  114. self.reactor.advance(ONE_DAY_IN_SECONDS)
  115. self.helper.send(room_id, "I'm still here", tok=access_token)
  116. # Notice that the user *still* does not contribute to R30!
  117. r30_results = self.get_success(
  118. self.hs.get_datastores().main.count_r30_users()
  119. )
  120. self.assertEqual(r30_results, {"all": 0})
  121. self.reactor.advance(ONE_DAY_IN_SECONDS)
  122. self.helper.send(room_id, "Still here!", tok=access_token)
  123. # *Now* the user appears in R30.
  124. r30_results = self.get_success(self.hs.get_datastores().main.count_r30_users())
  125. self.assertEqual(r30_results, {"all": 1, "unknown": 1})
  126. class PhoneHomeR30V2TestCase(HomeserverTestCase):
  127. servlets = [
  128. synapse.rest.admin.register_servlets_for_client_rest_resource,
  129. room.register_servlets,
  130. login.register_servlets,
  131. ]
  132. def _advance_to(self, desired_time_secs: float) -> None:
  133. now = self.hs.get_clock().time()
  134. assert now < desired_time_secs
  135. self.reactor.advance(desired_time_secs - now)
  136. def make_homeserver(
  137. self, reactor: ThreadedMemoryReactorClock, clock: Clock
  138. ) -> HomeServer:
  139. hs = super(PhoneHomeR30V2TestCase, self).make_homeserver(reactor, clock)
  140. # We don't want our tests to actually report statistics, so check
  141. # that it's not enabled
  142. assert not hs.config.metrics.report_stats
  143. # This starts the needed data collection that we rely on to calculate
  144. # R30v2 metrics.
  145. start_phone_stats_home(hs)
  146. return hs
  147. def test_r30v2_minimum_usage(self) -> None:
  148. """
  149. Tests the minimum amount of interaction necessary for the R30v2 metric
  150. to consider a user 'retained'.
  151. """
  152. # Register a user, log it in, create a room and send a message
  153. user_id = self.register_user("u1", "secret!")
  154. access_token = self.login("u1", "secret!")
  155. room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token)
  156. self.helper.send(room_id, "message", tok=access_token)
  157. first_post_at = self.hs.get_clock().time()
  158. # Give time for user_daily_visits table to be updated.
  159. # (user_daily_visits is updated every 5 minutes using a looping call.)
  160. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  161. store = self.hs.get_datastores().main
  162. # Check the R30 results do not count that user.
  163. r30_results = self.get_success(store.count_r30v2_users())
  164. self.assertEqual(
  165. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  166. )
  167. # Advance 31 days.
  168. # (R30v2 includes users with **more** than 30 days between the two visits,
  169. # and user_daily_visits records the timestamp as the start of the day.)
  170. self.reactor.advance(31 * ONE_DAY_IN_SECONDS)
  171. # Also advance 5 minutes to let another user_daily_visits update occur
  172. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  173. # (Make sure the user isn't somehow counted by this point.)
  174. r30_results = self.get_success(store.count_r30v2_users())
  175. self.assertEqual(
  176. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  177. )
  178. # Send a message (this counts as activity)
  179. self.helper.send(room_id, "message2", tok=access_token)
  180. # We have to wait a few minutes for the user_daily_visits table to
  181. # be updated by a background process.
  182. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  183. # *Now* the user is counted.
  184. r30_results = self.get_success(store.count_r30v2_users())
  185. self.assertEqual(
  186. r30_results, {"all": 1, "android": 0, "electron": 0, "ios": 0, "web": 0}
  187. )
  188. # Advance to JUST under 60 days after the user's first post
  189. self._advance_to(first_post_at + 60 * ONE_DAY_IN_SECONDS - 5)
  190. # Check the user is still counted.
  191. r30_results = self.get_success(store.count_r30v2_users())
  192. self.assertEqual(
  193. r30_results, {"all": 1, "android": 0, "electron": 0, "ios": 0, "web": 0}
  194. )
  195. # Advance into the next day. The user's first activity is now more than 60 days old.
  196. self._advance_to(first_post_at + 60 * ONE_DAY_IN_SECONDS + 5)
  197. # Check the user is now no longer counted in R30.
  198. r30_results = self.get_success(store.count_r30v2_users())
  199. self.assertEqual(
  200. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  201. )
  202. def test_r30v2_user_must_be_retained_for_at_least_a_month(self) -> None:
  203. """
  204. Tests that a newly-registered user must be retained for a whole month
  205. before appearing in the R30v2 statistic, even if they post every day
  206. during that time!
  207. """
  208. # set a custom user-agent to impersonate Element/Android.
  209. headers = (
  210. (
  211. "User-Agent",
  212. "Element/1.1 (Linux; U; Android 9; MatrixAndroidSDK_X 0.0.1)",
  213. ),
  214. )
  215. # Register a user and send a message
  216. user_id = self.register_user("u1", "secret!")
  217. access_token = self.login("u1", "secret!", custom_headers=headers)
  218. room_id = self.helper.create_room_as(
  219. room_creator=user_id, tok=access_token, custom_headers=headers
  220. )
  221. self.helper.send(room_id, "message", tok=access_token, custom_headers=headers)
  222. # Give time for user_daily_visits table to be updated.
  223. # (user_daily_visits is updated every 5 minutes using a looping call.)
  224. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  225. store = self.hs.get_datastores().main
  226. # Check the user does not contribute to R30 yet.
  227. r30_results = self.get_success(store.count_r30v2_users())
  228. self.assertEqual(
  229. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  230. )
  231. for _ in range(30):
  232. # This loop posts a message every day for 30 days
  233. self.reactor.advance(ONE_DAY_IN_SECONDS - FIVE_MINUTES_IN_SECONDS)
  234. self.helper.send(
  235. room_id, "I'm still here", tok=access_token, custom_headers=headers
  236. )
  237. # give time for user_daily_visits to update
  238. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  239. # Notice that the user *still* does not contribute to R30!
  240. r30_results = self.get_success(store.count_r30v2_users())
  241. self.assertEqual(
  242. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  243. )
  244. # advance yet another day with more activity
  245. self.reactor.advance(ONE_DAY_IN_SECONDS)
  246. self.helper.send(
  247. room_id, "Still here!", tok=access_token, custom_headers=headers
  248. )
  249. # give time for user_daily_visits to update
  250. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  251. # *Now* the user appears in R30.
  252. r30_results = self.get_success(store.count_r30v2_users())
  253. self.assertEqual(
  254. r30_results, {"all": 1, "android": 1, "electron": 0, "ios": 0, "web": 0}
  255. )
  256. def test_r30v2_returning_dormant_users_not_counted(self) -> None:
  257. """
  258. Tests that dormant users (users inactive for a long time) do not
  259. contribute to R30v2 when they return for just a single day.
  260. This is a key difference between R30 and R30v2.
  261. """
  262. # set a custom user-agent to impersonate Element/iOS.
  263. headers = (
  264. (
  265. "User-Agent",
  266. "Riot/1.4 (iPhone; iOS 13; Scale/4.00)",
  267. ),
  268. )
  269. # Register a user and send a message
  270. user_id = self.register_user("u1", "secret!")
  271. access_token = self.login("u1", "secret!", custom_headers=headers)
  272. room_id = self.helper.create_room_as(
  273. room_creator=user_id, tok=access_token, custom_headers=headers
  274. )
  275. self.helper.send(room_id, "message", tok=access_token, custom_headers=headers)
  276. # the user goes inactive for 2 months
  277. self.reactor.advance(60 * ONE_DAY_IN_SECONDS)
  278. # the user returns for one day, perhaps just to check out a new feature
  279. self.helper.send(room_id, "message", tok=access_token, custom_headers=headers)
  280. # Give time for user_daily_visits table to be updated.
  281. # (user_daily_visits is updated every 5 minutes using a looping call.)
  282. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  283. store = self.hs.get_datastores().main
  284. # Check that the user does not contribute to R30v2, even though it's been
  285. # more than 30 days since registration.
  286. r30_results = self.get_success(store.count_r30v2_users())
  287. self.assertEqual(
  288. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  289. )
  290. # Check that this is a situation where old R30 differs:
  291. # old R30 DOES count this as 'retained'.
  292. r30_results = self.get_success(store.count_r30_users())
  293. self.assertEqual(r30_results, {"all": 1, "ios": 1})
  294. # Now we want to check that the user will still be able to appear in
  295. # R30v2 as long as the user performs some other activity between
  296. # 30 and 60 days later.
  297. self.reactor.advance(32 * ONE_DAY_IN_SECONDS)
  298. self.helper.send(room_id, "message", tok=access_token, custom_headers=headers)
  299. # (give time for tables to update)
  300. self.reactor.advance(FIVE_MINUTES_IN_SECONDS)
  301. # Check the user now satisfies the requirements to appear in R30v2.
  302. r30_results = self.get_success(store.count_r30v2_users())
  303. self.assertEqual(
  304. r30_results, {"all": 1, "ios": 1, "android": 0, "electron": 0, "web": 0}
  305. )
  306. # Advance to 59.5 days after the user's first R30v2-eligible activity.
  307. self.reactor.advance(27.5 * ONE_DAY_IN_SECONDS)
  308. # Check the user still appears in R30v2.
  309. r30_results = self.get_success(store.count_r30v2_users())
  310. self.assertEqual(
  311. r30_results, {"all": 1, "ios": 1, "android": 0, "electron": 0, "web": 0}
  312. )
  313. # Advance to 60.5 days after the user's first R30v2-eligible activity.
  314. self.reactor.advance(ONE_DAY_IN_SECONDS)
  315. # Check the user no longer appears in R30v2.
  316. r30_results = self.get_success(store.count_r30v2_users())
  317. self.assertEqual(
  318. r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0}
  319. )