diff --git a/analytics/lib/counts.py b/analytics/lib/counts.py index d9550e74c0..9e3d2e26cf 100644 --- a/analytics/lib/counts.py +++ b/analytics/lib/counts.py @@ -375,6 +375,24 @@ count_message_by_stream_query = """ """ zerver_count_message_by_stream = ZerverCountQuery(Message, StreamCount, count_message_by_stream_query) +check_useractivityinterval_by_user_query = """ + INSERT INTO analytics_usercount + (user_id, realm_id, value, property, subgroup, end_time) + SELECT + zerver_userprofile.id, zerver_userprofile.realm_id, 1, '%(property)s', %(subgroup)s, %%(time_end)s + FROM zerver_userprofile + JOIN zerver_useractivityinterval + ON + zerver_userprofile.id = zerver_useractivityinterval.user_profile_id + WHERE + zerver_useractivityinterval.end >= %%(time_start)s AND + zerver_useractivityinterval.start < %%(time_end)s + %(join_args)s + GROUP BY zerver_userprofile.id %(group_by_clause)s +""" +zerver_check_useractivityinterval_by_user = ZerverCountQuery( + UserActivityInterval, UserCount, check_useractivityinterval_by_user_query) + def do_pull_minutes_active(stat, start_time, end_time): # type: (CountStat, datetime, datetime) -> None timer_start = time.time() @@ -410,6 +428,10 @@ count_stats_ = [ (Message, 'sending_client_id'), CountStat.DAY), CountStat('messages_in_stream:is_bot:day', zerver_count_message_by_stream, {}, (UserProfile, 'is_bot'), CountStat.DAY), + # The minutes=15 part is due to the 15 minutes added in + # zerver.lib.actions.do_update_user_activity_interval. + CountStat('15day_actives::day', zerver_check_useractivityinterval_by_user, {}, + None, CountStat.DAY, interval=timedelta(days=15)-timedelta(minutes=15)), LoggingCountStat('active_users_log:is_bot:day', RealmCount, CountStat.DAY), CustomPullCountStat('minutes_active::day', UserCount, CountStat.DAY, do_pull_minutes_active) ] diff --git a/analytics/tests/test_counts.py b/analytics/tests/test_counts.py index 859d76006f..103b2965c5 100644 --- a/analytics/tests/test_counts.py +++ b/analytics/tests/test_counts.py @@ -489,6 +489,50 @@ class TestCountStats(AnalyticsTestCase): user_profile=user, start=self.TIME_ZERO-start_offset, end=self.TIME_ZERO-end_offset) + def test_15day_actives(self): + # type: () -> None + stat = COUNT_STATS['15day_actives::day'] + self.current_property = stat.property + + _15day = 15*self.DAY - 15*self.MINUTE + + # Outside time range, should not appear. Also tests upper boundary. + user1 = self.create_user() + self.create_interval(user1, _15day + self.DAY, _15day + timedelta(seconds=1)) + self.create_interval(user1, timedelta(0), -self.HOUR) + + # On lower boundary, should appear + user2 = self.create_user() + self.create_interval(user2, _15day + self.DAY, _15day) + + # Multiple intervals, including one outside boundary + user3 = self.create_user() + self.create_interval(user3, 20*self.DAY, 19*self.DAY) + self.create_interval(user3, 20*self.HOUR, 19*self.HOUR) + self.create_interval(user3, 20*self.MINUTE, 19*self.MINUTE) + + # Intervals crossing boundary + user4 = self.create_user() + self.create_interval(user4, 20*self.DAY, 10*self.DAY) + user5 = self.create_user() + self.create_interval(user5, self.MINUTE, -self.MINUTE) + + # Interval subsuming time range + user6 = self.create_user() + self.create_interval(user6, 20*self.DAY, -2*self.DAY) + + # Second realm + user7 = self.create_user(realm=self.second_realm) + self.create_interval(user7, 20*self.MINUTE, 19*self.MINUTE) + + do_fill_count_stat_at_hour(stat, self.TIME_ZERO) + self.assertTableState(UserCount, ['value', 'user'], + [[1, user2], [1, user3], [1, user4], [1, user5], [1, user6], [1, user7]]) + self.assertTableState(RealmCount, ['value', 'realm'], + [[5, self.default_realm], [1, self.second_realm]]) + self.assertTableState(InstallationCount, ['value'], [[6]]) + self.assertTableState(StreamCount, [], []) + def test_minutes_active(self): # type: () -> None stat = COUNT_STATS['minutes_active::day'] diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 89fa65a7fc..c85fbcdf9f 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -2301,6 +2301,8 @@ def streams_to_dicts_sorted(streams): def do_update_user_activity_interval(user_profile, log_time): # type: (UserProfile, datetime.datetime) -> None + # Update any stats in analytics.lib.counts.count_stats_ that rely on + # this if the 15 minutes is changed to something else. effective_end = log_time + datetime.timedelta(minutes=15) # This code isn't perfect, because with various races we might end # up creating two overlapping intervals, but that shouldn't happen