2015-11-01 17:14:31 +01:00
|
|
|
from __future__ import absolute_import
|
2016-01-24 03:56:05 +01:00
|
|
|
from __future__ import division
|
2016-04-07 23:11:21 +02:00
|
|
|
from typing import Any, Dict, List, Tuple
|
2016-01-26 02:44:57 +01:00
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
from django.db import connection
|
|
|
|
from django.template import RequestContext, loader
|
2013-11-14 16:52:48 +01:00
|
|
|
from django.core import urlresolvers
|
2014-01-10 21:22:39 +01:00
|
|
|
from django.http import HttpResponseNotFound
|
2016-04-21 08:48:33 +02:00
|
|
|
from jinja2 import Markup as mark_safe
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
from zerver.decorator import has_request_variables, REQ, zulip_internal
|
2013-11-18 20:09:28 +01:00
|
|
|
from zerver.models import get_realm, UserActivity, UserActivityInterval, Realm
|
2013-11-06 13:25:55 +01:00
|
|
|
from zerver.lib.timestamp import timestamp_to_datetime
|
|
|
|
|
2013-12-18 21:13:47 +01:00
|
|
|
from collections import defaultdict
|
2013-11-18 18:28:49 +01:00
|
|
|
from datetime import datetime, timedelta
|
2013-11-06 13:25:55 +01:00
|
|
|
import itertools
|
|
|
|
import time
|
2013-11-18 18:58:39 +01:00
|
|
|
import re
|
2013-11-08 17:19:30 +01:00
|
|
|
import pytz
|
2015-11-01 17:14:31 +01:00
|
|
|
from six.moves import filter
|
2015-11-01 17:14:53 +01:00
|
|
|
from six.moves import map
|
2015-11-01 17:15:05 +01:00
|
|
|
from six.moves import range
|
2015-11-01 17:15:11 +01:00
|
|
|
from six.moves import zip
|
2013-11-08 17:19:30 +01:00
|
|
|
eastern_tz = pytz.timezone('US/Eastern')
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2016-04-21 08:48:33 +02:00
|
|
|
from zproject.jinja2 import render_to_response
|
|
|
|
|
2013-11-18 16:26:56 +01:00
|
|
|
def make_table(title, cols, rows, has_row_class=False):
|
|
|
|
|
|
|
|
if not has_row_class:
|
|
|
|
def fix_row(row):
|
|
|
|
return dict(cells=row, row_class=None)
|
2015-11-01 17:14:53 +01:00
|
|
|
rows = list(map(fix_row, rows))
|
2013-11-18 16:26:56 +01:00
|
|
|
|
2013-11-07 16:38:41 +01:00
|
|
|
data = dict(title=title, cols=cols, rows=rows)
|
|
|
|
|
|
|
|
content = loader.render_to_string(
|
|
|
|
'analytics/ad_hoc_query.html',
|
|
|
|
dict(data=data)
|
|
|
|
)
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
def dictfetchall(cursor):
|
|
|
|
"Returns all rows from a cursor as a dict"
|
|
|
|
desc = cursor.description
|
|
|
|
return [
|
2015-11-01 17:15:11 +01:00
|
|
|
dict(list(zip([col[0] for col in desc], row)))
|
2013-11-06 13:25:55 +01:00
|
|
|
for row in cursor.fetchall()
|
|
|
|
]
|
|
|
|
|
2013-12-18 21:13:47 +01:00
|
|
|
|
|
|
|
def get_realm_day_counts():
|
|
|
|
query = '''
|
|
|
|
select
|
|
|
|
r.domain,
|
2014-01-06 17:46:47 +01:00
|
|
|
(now()::date - pub_date::date) age,
|
2013-12-18 21:13:47 +01:00
|
|
|
count(*) cnt
|
|
|
|
from zerver_message m
|
|
|
|
join zerver_userprofile up on up.id = m.sender_id
|
|
|
|
join zerver_realm r on r.id = up.realm_id
|
2014-02-03 18:20:47 +01:00
|
|
|
join zerver_client c on c.id = m.sending_client_id
|
2013-12-18 21:13:47 +01:00
|
|
|
where
|
|
|
|
(not up.is_bot)
|
|
|
|
and
|
2014-01-06 17:46:47 +01:00
|
|
|
pub_date > now()::date - interval '8 day'
|
2013-12-18 21:13:47 +01:00
|
|
|
and
|
2014-02-03 18:20:47 +01:00
|
|
|
c.name not in ('zephyr_mirror', 'ZulipMonitoring')
|
2013-12-18 21:13:47 +01:00
|
|
|
group by
|
|
|
|
r.domain,
|
|
|
|
age
|
|
|
|
order by
|
|
|
|
r.domain,
|
|
|
|
age
|
|
|
|
'''
|
|
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute(query)
|
|
|
|
rows = dictfetchall(cursor)
|
|
|
|
cursor.close()
|
|
|
|
|
2016-01-26 02:44:57 +01:00
|
|
|
counts = defaultdict(dict) # type: Dict[str, Dict[int, int]]
|
2013-12-18 21:13:47 +01:00
|
|
|
for row in rows:
|
|
|
|
counts[row['domain']][row['age']] = row['cnt']
|
|
|
|
|
2013-12-18 23:02:17 +01:00
|
|
|
|
2013-12-18 21:13:47 +01:00
|
|
|
result = {}
|
|
|
|
for domain in counts:
|
2016-05-12 23:07:30 +02:00
|
|
|
raw_cnts = [counts[domain].get(age, 0) for age in range(8)]
|
|
|
|
min_cnt = min(raw_cnts)
|
|
|
|
max_cnt = max(raw_cnts)
|
2013-12-18 23:02:17 +01:00
|
|
|
|
|
|
|
def format_count(cnt):
|
|
|
|
if cnt == min_cnt:
|
|
|
|
good_bad = 'bad'
|
|
|
|
elif cnt == max_cnt:
|
|
|
|
good_bad = 'good'
|
|
|
|
else:
|
|
|
|
good_bad = 'neutral'
|
|
|
|
|
|
|
|
return '<td class="number %s">%s</td>' % (good_bad, cnt)
|
|
|
|
|
2016-05-12 23:07:30 +02:00
|
|
|
cnts = ''.join(map(format_count, raw_cnts))
|
2013-12-18 21:13:47 +01:00
|
|
|
result[domain] = dict(cnts=cnts)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
def realm_summary_table(realm_minutes):
|
|
|
|
query = '''
|
|
|
|
SELECT
|
|
|
|
realm.domain,
|
|
|
|
coalesce(user_counts.active_user_count, 0) active_user_count,
|
2013-11-14 19:50:32 +01:00
|
|
|
coalesce(at_risk_counts.at_risk_count, 0) at_risk_count,
|
2013-11-06 13:25:55 +01:00
|
|
|
(
|
|
|
|
SELECT
|
|
|
|
count(*)
|
|
|
|
FROM zerver_userprofile up
|
|
|
|
WHERE up.realm_id = realm.id
|
|
|
|
AND is_active
|
|
|
|
AND not is_bot
|
|
|
|
) user_profile_count,
|
|
|
|
(
|
|
|
|
SELECT
|
|
|
|
count(*)
|
|
|
|
FROM zerver_userprofile up
|
|
|
|
WHERE up.realm_id = realm.id
|
|
|
|
AND is_active
|
|
|
|
AND is_bot
|
|
|
|
) bot_count
|
|
|
|
FROM zerver_realm realm
|
|
|
|
LEFT OUTER JOIN
|
|
|
|
(
|
|
|
|
SELECT
|
|
|
|
up.realm_id realm_id,
|
|
|
|
count(distinct(ua.user_profile_id)) active_user_count
|
|
|
|
FROM zerver_useractivity ua
|
|
|
|
JOIN zerver_userprofile up
|
|
|
|
ON up.id = ua.user_profile_id
|
|
|
|
WHERE
|
|
|
|
query in (
|
|
|
|
'/json/send_message',
|
|
|
|
'send_message_backend',
|
2014-01-21 17:10:09 +01:00
|
|
|
'/api/v1/send_message',
|
2016-04-02 20:24:19 +02:00
|
|
|
'/json/update_pointer',
|
|
|
|
'/json/users/me/pointer'
|
2013-11-06 13:25:55 +01:00
|
|
|
)
|
|
|
|
AND
|
|
|
|
last_visit > now() - interval '1 day'
|
2013-11-06 21:35:51 +01:00
|
|
|
AND
|
|
|
|
not is_bot
|
2013-11-06 13:25:55 +01:00
|
|
|
GROUP BY realm_id
|
|
|
|
) user_counts
|
|
|
|
ON user_counts.realm_id = realm.id
|
2013-11-14 19:50:32 +01:00
|
|
|
LEFT OUTER JOIN
|
|
|
|
(
|
|
|
|
SELECT
|
|
|
|
realm_id,
|
|
|
|
count(*) at_risk_count
|
|
|
|
FROM (
|
|
|
|
SELECT
|
|
|
|
realm.id as realm_id,
|
|
|
|
up.email
|
|
|
|
FROM zerver_useractivity ua
|
|
|
|
JOIN zerver_userprofile up
|
|
|
|
ON up.id = ua.user_profile_id
|
|
|
|
JOIN zerver_realm realm
|
|
|
|
ON realm.id = up.realm_id
|
|
|
|
WHERE up.is_active
|
|
|
|
AND (not up.is_bot)
|
|
|
|
AND
|
|
|
|
ua.query in (
|
|
|
|
'/json/send_message',
|
|
|
|
'send_message_backend',
|
2016-04-02 20:24:19 +02:00
|
|
|
'/api/v1/send_message',
|
|
|
|
'/json/update_pointer',
|
|
|
|
'/json/users/me/pointer'
|
2013-11-14 19:50:32 +01:00
|
|
|
)
|
|
|
|
GROUP by realm.id, up.email
|
|
|
|
HAVING max(last_visit) between
|
|
|
|
now() - interval '7 day' and
|
|
|
|
now() - interval '1 day'
|
|
|
|
) as at_risk_users
|
|
|
|
GROUP BY realm_id
|
|
|
|
) at_risk_counts
|
|
|
|
ON at_risk_counts.realm_id = realm.id
|
2015-09-20 08:42:28 +02:00
|
|
|
WHERE EXISTS (
|
2013-11-06 13:25:55 +01:00
|
|
|
SELECT *
|
|
|
|
FROM zerver_useractivity ua
|
|
|
|
JOIN zerver_userprofile up
|
|
|
|
ON up.id = ua.user_profile_id
|
|
|
|
WHERE
|
|
|
|
query in (
|
|
|
|
'/json/send_message',
|
2014-01-21 17:10:09 +01:00
|
|
|
'/api/v1/send_message',
|
2013-11-06 13:25:55 +01:00
|
|
|
'send_message_backend',
|
2016-04-02 20:24:19 +02:00
|
|
|
'/json/update_pointer',
|
|
|
|
'/json/users/me/pointer'
|
2013-11-06 13:25:55 +01:00
|
|
|
)
|
|
|
|
AND
|
|
|
|
up.realm_id = realm.id
|
|
|
|
AND
|
|
|
|
last_visit > now() - interval '2 week'
|
|
|
|
)
|
|
|
|
ORDER BY active_user_count DESC, domain ASC
|
|
|
|
'''
|
|
|
|
|
|
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute(query)
|
|
|
|
rows = dictfetchall(cursor)
|
|
|
|
cursor.close()
|
|
|
|
|
2013-12-18 21:13:47 +01:00
|
|
|
# get messages sent per day
|
|
|
|
counts = get_realm_day_counts()
|
|
|
|
for row in rows:
|
|
|
|
try:
|
|
|
|
row['history'] = counts[row['domain']]['cnts']
|
|
|
|
except:
|
|
|
|
row['history'] = ''
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
# augment data with realm_minutes
|
|
|
|
total_hours = 0
|
|
|
|
for row in rows:
|
|
|
|
domain = row['domain']
|
|
|
|
minutes = realm_minutes.get(domain, 0)
|
|
|
|
hours = minutes / 60.0
|
|
|
|
total_hours += hours
|
|
|
|
row['hours'] = str(int(hours))
|
|
|
|
try:
|
|
|
|
row['hours_per_user'] = '%.1f' % (hours / row['active_user_count'],)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2013-11-14 18:41:23 +01:00
|
|
|
# formatting
|
|
|
|
for row in rows:
|
|
|
|
row['domain'] = realm_activity_link(row['domain'])
|
|
|
|
|
2013-12-23 15:52:52 +01:00
|
|
|
# Count active sites
|
|
|
|
def meets_goal(row):
|
|
|
|
return row['active_user_count'] >= 5
|
|
|
|
|
2015-11-01 17:14:31 +01:00
|
|
|
num_active_sites = len(list(filter(meets_goal, rows)))
|
2013-12-23 15:52:52 +01:00
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
# create totals
|
|
|
|
total_active_user_count = 0
|
|
|
|
total_user_profile_count = 0
|
|
|
|
total_bot_count = 0
|
2016-05-06 11:09:46 +02:00
|
|
|
total_at_risk_count = 0
|
2013-11-06 13:25:55 +01:00
|
|
|
for row in rows:
|
|
|
|
total_active_user_count += int(row['active_user_count'])
|
|
|
|
total_user_profile_count += int(row['user_profile_count'])
|
|
|
|
total_bot_count += int(row['bot_count'])
|
2016-05-06 11:09:46 +02:00
|
|
|
total_at_risk_count += int(row['at_risk_count'])
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2013-12-18 21:13:47 +01:00
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
rows.append(dict(
|
|
|
|
domain='Total',
|
|
|
|
active_user_count=total_active_user_count,
|
|
|
|
user_profile_count=total_user_profile_count,
|
|
|
|
bot_count=total_bot_count,
|
2016-05-06 11:09:46 +02:00
|
|
|
hours=int(total_hours),
|
|
|
|
at_risk_count=total_at_risk_count,
|
2013-11-06 13:25:55 +01:00
|
|
|
))
|
|
|
|
|
|
|
|
content = loader.render_to_string(
|
|
|
|
'analytics/realm_summary_table.html',
|
|
|
|
dict(rows=rows, num_active_sites=num_active_sites)
|
|
|
|
)
|
2013-11-07 16:53:09 +01:00
|
|
|
return content
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
def user_activity_intervals():
|
|
|
|
day_end = timestamp_to_datetime(time.time())
|
2013-11-18 18:28:49 +01:00
|
|
|
day_start = day_end - timedelta(hours=24)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
output = "Per-user online duration for the last 24 hours:\n"
|
2013-11-18 18:28:49 +01:00
|
|
|
total_duration = timedelta(0)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
all_intervals = UserActivityInterval.objects.filter(
|
|
|
|
end__gte=day_start,
|
|
|
|
start__lte=day_end
|
|
|
|
).select_related(
|
|
|
|
'user_profile',
|
|
|
|
'user_profile__realm'
|
|
|
|
).only(
|
|
|
|
'start',
|
|
|
|
'end',
|
|
|
|
'user_profile__email',
|
|
|
|
'user_profile__realm__domain'
|
|
|
|
).order_by(
|
|
|
|
'user_profile__realm__domain',
|
|
|
|
'user_profile__email'
|
|
|
|
)
|
|
|
|
|
|
|
|
by_domain = lambda row: row.user_profile.realm.domain
|
|
|
|
by_email = lambda row: row.user_profile.email
|
|
|
|
|
|
|
|
realm_minutes = {}
|
|
|
|
|
|
|
|
for domain, realm_intervals in itertools.groupby(all_intervals, by_domain):
|
2013-11-18 18:28:49 +01:00
|
|
|
realm_duration = timedelta(0)
|
2013-11-06 13:25:55 +01:00
|
|
|
output += '<hr>%s\n' % (domain,)
|
|
|
|
for email, intervals in itertools.groupby(realm_intervals, by_email):
|
2013-11-18 18:28:49 +01:00
|
|
|
duration = timedelta(0)
|
2013-11-06 13:25:55 +01:00
|
|
|
for interval in intervals:
|
|
|
|
start = max(day_start, interval.start)
|
|
|
|
end = min(day_end, interval.end)
|
|
|
|
duration += end - start
|
|
|
|
|
|
|
|
total_duration += duration
|
|
|
|
realm_duration += duration
|
2016-05-04 23:16:27 +02:00
|
|
|
output += " %-*s%s\n" % (37, email, duration)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
realm_minutes[domain] = realm_duration.total_seconds() / 60
|
|
|
|
|
|
|
|
output += "\nTotal Duration: %s\n" % (total_duration,)
|
|
|
|
output += "\nTotal Duration in minutes: %s\n" % (total_duration.total_seconds() / 60.,)
|
|
|
|
output += "Total Duration amortized to a month: %s" % (total_duration.total_seconds() * 30. / 60.,)
|
|
|
|
content = mark_safe('<pre>' + output + '</pre>')
|
2013-11-07 16:53:09 +01:00
|
|
|
return content, realm_minutes
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
def sent_messages_report(realm):
|
|
|
|
title = 'Recently sent messages for ' + realm
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Date',
|
2013-11-14 21:28:31 +01:00
|
|
|
'Humans',
|
|
|
|
'Bots'
|
2013-11-06 13:25:55 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
query = '''
|
|
|
|
select
|
|
|
|
series.day::date,
|
2013-11-14 21:28:31 +01:00
|
|
|
humans.cnt,
|
|
|
|
bots.cnt
|
2013-11-06 13:25:55 +01:00
|
|
|
from (
|
|
|
|
select generate_series(
|
|
|
|
(now()::date - interval '2 week'),
|
|
|
|
now()::date,
|
|
|
|
interval '1 day'
|
|
|
|
) as day
|
|
|
|
) as series
|
|
|
|
left join (
|
|
|
|
select
|
|
|
|
pub_date::date pub_date,
|
|
|
|
count(*) cnt
|
|
|
|
from zerver_message m
|
|
|
|
join zerver_userprofile up on up.id = m.sender_id
|
|
|
|
join zerver_realm r on r.id = up.realm_id
|
|
|
|
where
|
|
|
|
r.domain = %s
|
2013-11-14 21:28:31 +01:00
|
|
|
and
|
|
|
|
(not up.is_bot)
|
|
|
|
and
|
|
|
|
pub_date > now() - interval '2 week'
|
|
|
|
group by
|
|
|
|
pub_date::date
|
|
|
|
order by
|
|
|
|
pub_date::date
|
|
|
|
) humans on
|
|
|
|
series.day = humans.pub_date
|
|
|
|
left join (
|
|
|
|
select
|
|
|
|
pub_date::date pub_date,
|
|
|
|
count(*) cnt
|
|
|
|
from zerver_message m
|
|
|
|
join zerver_userprofile up on up.id = m.sender_id
|
|
|
|
join zerver_realm r on r.id = up.realm_id
|
|
|
|
where
|
|
|
|
r.domain = %s
|
|
|
|
and
|
|
|
|
up.is_bot
|
2013-11-06 13:25:55 +01:00
|
|
|
and
|
|
|
|
pub_date > now() - interval '2 week'
|
|
|
|
group by
|
|
|
|
pub_date::date
|
|
|
|
order by
|
|
|
|
pub_date::date
|
2013-11-14 21:28:31 +01:00
|
|
|
) bots on
|
|
|
|
series.day = bots.pub_date
|
2013-11-06 13:25:55 +01:00
|
|
|
'''
|
|
|
|
cursor = connection.cursor()
|
2013-11-14 21:28:31 +01:00
|
|
|
cursor.execute(query, [realm, realm])
|
2013-11-06 13:25:55 +01:00
|
|
|
rows = cursor.fetchall()
|
|
|
|
cursor.close()
|
|
|
|
|
2013-11-07 16:38:41 +01:00
|
|
|
return make_table(title, cols, rows)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
def ad_hoc_queries():
|
|
|
|
def get_page(query, cols, title):
|
|
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute(query)
|
|
|
|
rows = cursor.fetchall()
|
2015-11-01 17:14:53 +01:00
|
|
|
rows = list(map(list, rows))
|
2013-11-06 13:25:55 +01:00
|
|
|
cursor.close()
|
|
|
|
|
2013-11-14 17:26:12 +01:00
|
|
|
def fix_rows(i, fixup_func):
|
|
|
|
for row in rows:
|
|
|
|
row[i] = fixup_func(row[i])
|
|
|
|
|
|
|
|
for i, col in enumerate(cols):
|
|
|
|
if col == 'Domain':
|
|
|
|
fix_rows(i, realm_activity_link)
|
2013-11-14 18:25:20 +01:00
|
|
|
elif col in ['Last time', 'Last visit']:
|
|
|
|
fix_rows(i, format_date_for_activity_reports)
|
2013-11-14 17:26:12 +01:00
|
|
|
|
2013-11-07 16:38:41 +01:00
|
|
|
content = make_table(title, cols, rows)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
return dict(
|
|
|
|
content=content,
|
|
|
|
title=title
|
|
|
|
)
|
|
|
|
|
|
|
|
pages = []
|
|
|
|
|
|
|
|
###
|
|
|
|
|
2014-01-08 15:17:15 +01:00
|
|
|
for mobile_type in ['Android', 'ZulipiOS']:
|
|
|
|
title = '%s usage' % (mobile_type,)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2014-01-08 15:17:15 +01:00
|
|
|
query = '''
|
|
|
|
select
|
|
|
|
realm.domain,
|
|
|
|
up.id user_id,
|
|
|
|
client.name,
|
|
|
|
sum(count) as hits,
|
|
|
|
max(last_visit) as last_time
|
|
|
|
from zerver_useractivity ua
|
|
|
|
join zerver_client client on client.id = ua.client_id
|
|
|
|
join zerver_userprofile up on up.id = ua.user_profile_id
|
|
|
|
join zerver_realm realm on realm.id = up.realm_id
|
|
|
|
where
|
|
|
|
client.name like '%s'
|
|
|
|
group by domain, up.id, client.name
|
|
|
|
having max(last_visit) > now() - interval '2 week'
|
|
|
|
order by domain, up.id, client.name
|
|
|
|
''' % (mobile_type,)
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Domain',
|
|
|
|
'User id',
|
|
|
|
'Name',
|
|
|
|
'Hits',
|
|
|
|
'Last time'
|
|
|
|
]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2014-01-08 15:17:15 +01:00
|
|
|
pages.append(get_page(query, cols, title))
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
###
|
|
|
|
|
|
|
|
title = 'Desktop users'
|
|
|
|
|
|
|
|
query = '''
|
|
|
|
select
|
|
|
|
realm.domain,
|
|
|
|
client.name,
|
|
|
|
sum(count) as hits,
|
|
|
|
max(last_visit) as last_time
|
|
|
|
from zerver_useractivity ua
|
|
|
|
join zerver_client client on client.id = ua.client_id
|
|
|
|
join zerver_userprofile up on up.id = ua.user_profile_id
|
|
|
|
join zerver_realm realm on realm.id = up.realm_id
|
|
|
|
where
|
|
|
|
client.name like 'desktop%%'
|
|
|
|
group by domain, client.name
|
|
|
|
having max(last_visit) > now() - interval '2 week'
|
|
|
|
order by domain, client.name
|
|
|
|
'''
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Domain',
|
|
|
|
'Client',
|
|
|
|
'Hits',
|
|
|
|
'Last time'
|
|
|
|
]
|
|
|
|
|
|
|
|
pages.append(get_page(query, cols, title))
|
|
|
|
|
|
|
|
###
|
|
|
|
|
|
|
|
title = 'Integrations by domain'
|
|
|
|
|
|
|
|
query = '''
|
|
|
|
select
|
|
|
|
realm.domain,
|
|
|
|
case
|
|
|
|
when query like '%%external%%' then split_part(query, '/', 5)
|
|
|
|
else client.name
|
|
|
|
end client_name,
|
|
|
|
sum(count) as hits,
|
|
|
|
max(last_visit) as last_time
|
|
|
|
from zerver_useractivity ua
|
|
|
|
join zerver_client client on client.id = ua.client_id
|
|
|
|
join zerver_userprofile up on up.id = ua.user_profile_id
|
|
|
|
join zerver_realm realm on realm.id = up.realm_id
|
|
|
|
where
|
2014-01-21 17:10:09 +01:00
|
|
|
(query in ('send_message_backend', '/api/v1/send_message')
|
2014-01-08 15:17:15 +01:00
|
|
|
and client.name not in ('Android', 'ZulipiOS')
|
2013-11-06 13:25:55 +01:00
|
|
|
and client.name not like 'test: Zulip%%'
|
|
|
|
)
|
|
|
|
or
|
|
|
|
query like '%%external%%'
|
|
|
|
group by domain, client_name
|
|
|
|
having max(last_visit) > now() - interval '2 week'
|
|
|
|
order by domain, client_name
|
|
|
|
'''
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Domain',
|
|
|
|
'Client',
|
|
|
|
'Hits',
|
|
|
|
'Last time'
|
|
|
|
]
|
|
|
|
|
|
|
|
pages.append(get_page(query, cols, title))
|
|
|
|
|
|
|
|
###
|
|
|
|
|
|
|
|
title = 'Integrations by client'
|
|
|
|
|
|
|
|
query = '''
|
|
|
|
select
|
|
|
|
case
|
|
|
|
when query like '%%external%%' then split_part(query, '/', 5)
|
|
|
|
else client.name
|
|
|
|
end client_name,
|
|
|
|
realm.domain,
|
|
|
|
sum(count) as hits,
|
|
|
|
max(last_visit) as last_time
|
|
|
|
from zerver_useractivity ua
|
|
|
|
join zerver_client client on client.id = ua.client_id
|
|
|
|
join zerver_userprofile up on up.id = ua.user_profile_id
|
|
|
|
join zerver_realm realm on realm.id = up.realm_id
|
|
|
|
where
|
2014-01-21 17:10:09 +01:00
|
|
|
(query in ('send_message_backend', '/api/v1/send_message')
|
2014-01-08 15:17:15 +01:00
|
|
|
and client.name not in ('Android', 'ZulipiOS')
|
2013-11-06 13:25:55 +01:00
|
|
|
and client.name not like 'test: Zulip%%'
|
|
|
|
)
|
|
|
|
or
|
|
|
|
query like '%%external%%'
|
|
|
|
group by client_name, domain
|
|
|
|
having max(last_visit) > now() - interval '2 week'
|
|
|
|
order by client_name, domain
|
|
|
|
'''
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Client',
|
|
|
|
'Domain',
|
|
|
|
'Hits',
|
|
|
|
'Last time'
|
|
|
|
]
|
|
|
|
|
|
|
|
pages.append(get_page(query, cols, title))
|
|
|
|
|
|
|
|
return pages
|
|
|
|
|
|
|
|
@zulip_internal
|
|
|
|
@has_request_variables
|
2013-12-18 19:42:04 +01:00
|
|
|
def get_activity(request):
|
2013-11-06 13:25:55 +01:00
|
|
|
duration_content, realm_minutes = user_activity_intervals()
|
|
|
|
counts_content = realm_summary_table(realm_minutes)
|
|
|
|
data = [
|
|
|
|
('Counts', counts_content),
|
|
|
|
('Durations', duration_content),
|
|
|
|
]
|
|
|
|
for page in ad_hoc_queries():
|
2013-11-07 16:53:09 +01:00
|
|
|
data.append((page['title'], page['content']))
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
title = 'Activity'
|
|
|
|
|
|
|
|
return render_to_response(
|
|
|
|
'analytics/activity.html',
|
2013-12-18 19:42:04 +01:00
|
|
|
dict(data=data, title=title, is_home=True),
|
2016-04-21 08:48:33 +02:00
|
|
|
request=request
|
2013-11-06 13:25:55 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def get_user_activity_records_for_realm(realm, is_bot):
|
|
|
|
fields = [
|
|
|
|
'user_profile__full_name',
|
|
|
|
'user_profile__email',
|
|
|
|
'query',
|
|
|
|
'client__name',
|
|
|
|
'count',
|
|
|
|
'last_visit',
|
|
|
|
]
|
|
|
|
|
|
|
|
records = UserActivity.objects.filter(
|
|
|
|
user_profile__realm__domain=realm,
|
2013-11-24 22:31:44 +01:00
|
|
|
user_profile__is_active=True,
|
2013-11-06 13:25:55 +01:00
|
|
|
user_profile__is_bot=is_bot
|
|
|
|
)
|
|
|
|
records = records.order_by("user_profile__email", "-last_visit")
|
|
|
|
records = records.select_related('user_profile', 'client').only(*fields)
|
|
|
|
return records
|
|
|
|
|
|
|
|
def get_user_activity_records_for_email(email):
|
|
|
|
fields = [
|
|
|
|
'user_profile__full_name',
|
|
|
|
'query',
|
|
|
|
'client__name',
|
|
|
|
'count',
|
|
|
|
'last_visit'
|
|
|
|
]
|
|
|
|
|
|
|
|
records = UserActivity.objects.filter(
|
|
|
|
user_profile__email=email
|
|
|
|
)
|
|
|
|
records = records.order_by("-last_visit")
|
|
|
|
records = records.select_related('user_profile', 'client').only(*fields)
|
|
|
|
return records
|
|
|
|
|
|
|
|
def raw_user_activity_table(records):
|
|
|
|
cols = [
|
|
|
|
'query',
|
|
|
|
'client',
|
|
|
|
'count',
|
|
|
|
'last_visit'
|
|
|
|
]
|
|
|
|
|
|
|
|
def row(record):
|
|
|
|
return [
|
|
|
|
record.query,
|
|
|
|
record.client.name,
|
|
|
|
record.count,
|
|
|
|
format_date_for_activity_reports(record.last_visit)
|
|
|
|
]
|
|
|
|
|
2015-11-01 17:14:53 +01:00
|
|
|
rows = list(map(row, records))
|
2013-11-06 13:25:55 +01:00
|
|
|
title = 'Raw Data'
|
2013-11-07 16:38:41 +01:00
|
|
|
return make_table(title, cols, rows)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
def get_user_activity_summary(records):
|
2016-01-26 02:44:57 +01:00
|
|
|
# type: (Any) -> Any
|
|
|
|
summary = {} # type: Dict[str, Dict[str, Any]]
|
2013-11-06 13:25:55 +01:00
|
|
|
def update(action, record):
|
|
|
|
if action not in summary:
|
|
|
|
summary[action] = dict(
|
|
|
|
count=record.count,
|
|
|
|
last_visit=record.last_visit
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
summary[action]['count'] += record.count
|
|
|
|
summary[action]['last_visit'] = max(
|
|
|
|
summary[action]['last_visit'],
|
|
|
|
record.last_visit
|
|
|
|
)
|
|
|
|
|
|
|
|
if records:
|
|
|
|
summary['name'] = records[0].user_profile.full_name
|
|
|
|
|
|
|
|
for record in records:
|
|
|
|
client = record.client.name
|
|
|
|
query = record.query
|
|
|
|
|
|
|
|
update('use', record)
|
|
|
|
|
2013-11-18 18:58:39 +01:00
|
|
|
if client == 'API':
|
|
|
|
m = re.match('/api/.*/external/(.*)', query)
|
|
|
|
if m:
|
|
|
|
client = m.group(1)
|
|
|
|
update(client, record)
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
if client.startswith('desktop'):
|
|
|
|
update('desktop', record)
|
|
|
|
if client == 'website':
|
|
|
|
update('website', record)
|
2013-11-25 21:27:57 +01:00
|
|
|
if ('send_message' in query) or re.search('/api/.*/external/.*', query):
|
2013-11-06 13:25:55 +01:00
|
|
|
update('send', record)
|
2016-04-02 20:24:19 +02:00
|
|
|
if query in ['/json/update_pointer', '/json/users/me/pointer', '/api/v1/update_pointer']:
|
2013-11-06 13:25:55 +01:00
|
|
|
update('pointer', record)
|
|
|
|
update(client, record)
|
|
|
|
|
|
|
|
|
|
|
|
return summary
|
|
|
|
|
|
|
|
def format_date_for_activity_reports(date):
|
|
|
|
if date:
|
2013-11-08 17:19:30 +01:00
|
|
|
return date.astimezone(eastern_tz).strftime('%Y-%m-%d %H:%M')
|
2013-11-06 13:25:55 +01:00
|
|
|
else:
|
|
|
|
return ''
|
|
|
|
|
2013-11-14 16:52:48 +01:00
|
|
|
def user_activity_link(email):
|
|
|
|
url_name = 'analytics.views.get_user_activity'
|
|
|
|
url = urlresolvers.reverse(url_name, kwargs=dict(email=email))
|
|
|
|
email_link = '<a href="%s">%s</a>' % (url, email)
|
|
|
|
return mark_safe(email_link)
|
|
|
|
|
2013-11-14 17:26:12 +01:00
|
|
|
def realm_activity_link(realm):
|
|
|
|
url_name = 'analytics.views.get_realm_activity'
|
|
|
|
url = urlresolvers.reverse(url_name, kwargs=dict(realm=realm))
|
|
|
|
realm_link = '<a href="%s">%s</a>' % (url, realm)
|
|
|
|
return mark_safe(realm_link)
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
def realm_client_table(user_summaries):
|
|
|
|
exclude_keys = [
|
|
|
|
'internal',
|
|
|
|
'name',
|
|
|
|
'use',
|
|
|
|
'send',
|
|
|
|
'pointer',
|
|
|
|
'website',
|
|
|
|
'desktop',
|
|
|
|
]
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
for email, user_summary in user_summaries.items():
|
2013-11-14 16:52:48 +01:00
|
|
|
email_link = user_activity_link(email)
|
2013-11-06 13:25:55 +01:00
|
|
|
name = user_summary['name']
|
|
|
|
for k, v in user_summary.items():
|
|
|
|
if k in exclude_keys:
|
|
|
|
continue
|
|
|
|
client = k
|
|
|
|
count = v['count']
|
|
|
|
last_visit = v['last_visit']
|
|
|
|
row = [
|
|
|
|
format_date_for_activity_reports(last_visit),
|
|
|
|
client,
|
|
|
|
name,
|
|
|
|
email_link,
|
|
|
|
count,
|
|
|
|
]
|
|
|
|
rows.append(row)
|
|
|
|
|
|
|
|
rows = sorted(rows, key=lambda r: r[0], reverse=True)
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Last visit',
|
|
|
|
'Client',
|
|
|
|
'Name',
|
|
|
|
'Email',
|
|
|
|
'Count',
|
|
|
|
]
|
|
|
|
|
|
|
|
title = 'Clients'
|
|
|
|
|
2013-11-07 16:38:41 +01:00
|
|
|
return make_table(title, cols, rows)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
def user_activity_summary_table(user_summary):
|
|
|
|
rows = []
|
|
|
|
for k, v in user_summary.items():
|
|
|
|
if k == 'name':
|
|
|
|
continue
|
|
|
|
client = k
|
|
|
|
count = v['count']
|
|
|
|
last_visit = v['last_visit']
|
|
|
|
row = [
|
|
|
|
format_date_for_activity_reports(last_visit),
|
|
|
|
client,
|
|
|
|
count,
|
|
|
|
]
|
|
|
|
rows.append(row)
|
|
|
|
|
|
|
|
rows = sorted(rows, key=lambda r: r[0], reverse=True)
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'last_visit',
|
|
|
|
'client',
|
|
|
|
'count',
|
|
|
|
]
|
|
|
|
|
|
|
|
title = 'User Activity'
|
2013-11-07 16:38:41 +01:00
|
|
|
return make_table(title, cols, rows)
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2013-11-18 20:09:28 +01:00
|
|
|
def realm_user_summary_table(all_records, admin_emails):
|
2013-11-06 13:25:55 +01:00
|
|
|
user_records = {}
|
|
|
|
|
|
|
|
def by_email(record):
|
|
|
|
return record.user_profile.email
|
|
|
|
|
|
|
|
for email, records in itertools.groupby(all_records, by_email):
|
|
|
|
user_records[email] = get_user_activity_summary(list(records))
|
|
|
|
|
|
|
|
def get_last_visit(user_summary, k):
|
|
|
|
if k in user_summary:
|
2013-11-18 17:07:59 +01:00
|
|
|
return user_summary[k]['last_visit']
|
2013-11-06 13:25:55 +01:00
|
|
|
else:
|
2013-11-18 17:07:59 +01:00
|
|
|
return None
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
def get_count(user_summary, k):
|
|
|
|
if k in user_summary:
|
|
|
|
return user_summary[k]['count']
|
|
|
|
else:
|
|
|
|
return ''
|
|
|
|
|
2013-11-18 17:20:58 +01:00
|
|
|
def is_recent(val):
|
2013-11-18 18:28:49 +01:00
|
|
|
age = datetime.now(val.tzinfo) - val
|
2013-11-18 17:20:58 +01:00
|
|
|
return age.total_seconds() < 5 * 60
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
rows = []
|
|
|
|
for email, user_summary in user_records.items():
|
2013-11-14 16:52:48 +01:00
|
|
|
email_link = user_activity_link(email)
|
2013-11-06 13:25:55 +01:00
|
|
|
sent_count = get_count(user_summary, 'send')
|
2013-11-18 17:20:58 +01:00
|
|
|
cells = [user_summary['name'], email_link, sent_count]
|
2013-11-18 20:09:28 +01:00
|
|
|
row_class = ''
|
2014-01-08 15:17:15 +01:00
|
|
|
for field in ['use', 'send', 'pointer', 'desktop', 'ZulipiOS', 'Android']:
|
2013-11-06 13:25:55 +01:00
|
|
|
val = get_last_visit(user_summary, field)
|
2013-11-18 17:20:58 +01:00
|
|
|
if field == 'use':
|
|
|
|
if val and is_recent(val):
|
2013-11-18 20:09:28 +01:00
|
|
|
row_class += ' recently_active'
|
|
|
|
if email in admin_emails:
|
|
|
|
row_class += ' admin'
|
2013-11-18 17:07:59 +01:00
|
|
|
val = format_date_for_activity_reports(val)
|
2013-11-18 17:20:58 +01:00
|
|
|
cells.append(val)
|
|
|
|
row = dict(cells=cells, row_class=row_class)
|
2013-11-06 13:25:55 +01:00
|
|
|
rows.append(row)
|
|
|
|
|
|
|
|
def by_used_time(row):
|
2013-11-18 17:20:58 +01:00
|
|
|
return row['cells'][3]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
rows = sorted(rows, key=by_used_time, reverse=True)
|
|
|
|
|
|
|
|
cols = [
|
|
|
|
'Name',
|
|
|
|
'Email',
|
|
|
|
'Total sent',
|
|
|
|
'Heard from',
|
|
|
|
'Message sent',
|
|
|
|
'Pointer motion',
|
|
|
|
'Desktop',
|
2014-01-08 15:17:15 +01:00
|
|
|
'ZulipiOS',
|
2013-11-06 13:25:55 +01:00
|
|
|
'Android'
|
|
|
|
]
|
|
|
|
|
|
|
|
title = 'Summary'
|
|
|
|
|
2013-11-18 17:20:58 +01:00
|
|
|
content = make_table(title, cols, rows, has_row_class=True)
|
2013-11-06 13:25:55 +01:00
|
|
|
return user_records, content
|
|
|
|
|
|
|
|
@zulip_internal
|
|
|
|
def get_realm_activity(request, realm):
|
2016-01-26 02:44:57 +01:00
|
|
|
# type: (Any, Any) -> Any
|
|
|
|
data = [] # type: List[Tuple[str, str]]
|
|
|
|
all_user_records = {} # type: Dict[str, Any]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2014-01-10 21:22:39 +01:00
|
|
|
try:
|
2015-10-13 22:54:35 +02:00
|
|
|
admins = get_realm(realm).get_admin_users()
|
2014-01-10 21:22:39 +01:00
|
|
|
except Realm.DoesNotExist:
|
|
|
|
return HttpResponseNotFound("Realm %s does not exist" % (realm,))
|
|
|
|
|
2013-11-18 20:09:28 +01:00
|
|
|
admin_emails = {admin.email for admin in admins}
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
for is_bot, page_title in [(False, 'Humans'), (True, 'Bots')]:
|
2016-01-26 04:12:35 +01:00
|
|
|
all_records = list(get_user_activity_records_for_realm(realm, is_bot))
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2013-11-18 20:09:28 +01:00
|
|
|
user_records, content = realm_user_summary_table(all_records, admin_emails)
|
2013-11-06 13:25:55 +01:00
|
|
|
all_user_records.update(user_records)
|
|
|
|
|
2013-11-07 16:53:09 +01:00
|
|
|
data += [(page_title, content)]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
page_title = 'Clients'
|
|
|
|
content = realm_client_table(all_user_records)
|
2013-11-07 16:53:09 +01:00
|
|
|
data += [(page_title, content)]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
page_title = 'History'
|
|
|
|
content = sent_messages_report(realm)
|
2013-11-07 16:53:09 +01:00
|
|
|
data += [(page_title, content)]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
2013-12-18 19:40:07 +01:00
|
|
|
fix_name = lambda realm: realm.replace('.', '_')
|
|
|
|
|
|
|
|
realm_link = 'https://stats1.zulip.net:444/render/?from=-7days'
|
|
|
|
realm_link += '&target=stats.gauges.staging.users.active.%s.0_16hr' % (fix_name(realm),)
|
|
|
|
|
2013-11-06 13:25:55 +01:00
|
|
|
title = realm
|
|
|
|
return render_to_response(
|
|
|
|
'analytics/activity.html',
|
2013-12-18 19:40:07 +01:00
|
|
|
dict(data=data, realm_link=realm_link, title=title),
|
2016-04-21 08:48:33 +02:00
|
|
|
request=request
|
2013-11-06 13:25:55 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
@zulip_internal
|
|
|
|
def get_user_activity(request, email):
|
|
|
|
records = get_user_activity_records_for_email(email)
|
|
|
|
|
2016-01-26 02:44:57 +01:00
|
|
|
data = [] # type: List[Tuple[str, str]]
|
2013-11-06 13:25:55 +01:00
|
|
|
user_summary = get_user_activity_summary(records)
|
|
|
|
content = user_activity_summary_table(user_summary)
|
|
|
|
|
2013-11-07 16:53:09 +01:00
|
|
|
data += [('Summary', content)]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
content = raw_user_activity_table(records)
|
2013-11-07 16:53:09 +01:00
|
|
|
data += [('Info', content)]
|
2013-11-06 13:25:55 +01:00
|
|
|
|
|
|
|
title = email
|
|
|
|
return render_to_response(
|
|
|
|
'analytics/activity.html',
|
2013-12-18 19:42:04 +01:00
|
|
|
dict(data=data, title=title),
|
2016-04-21 08:48:33 +02:00
|
|
|
request=request
|
2013-11-06 13:25:55 +01:00
|
|
|
)
|