mirror of https://github.com/zulip/zulip.git
remote-activity: Display rows for remote realms.
Adds columns for remote realm ID, name and organization type. If a remote server has remote realms attached that are not marked as deactivated by the remote server, then there will be a row in the chart for each remote realm (which duplicates some remote server data). Updates the plan data, revenue and user counts to be for the realm if present and otherwise for the server. Updates the user counts to be total users and guest users, instead of non guest and guest users. The total row for mobile data (users and pushes forwarded) sums each remote server's data once, so while the column duplicates data, the total row should be an accurate total for the installation. Adds 5 queries to the remote activity page test. One is for the additional query for the remote realm plans. The other four are getting the remote realm object and then the user count data for the two remote realms in the test.
This commit is contained in:
parent
8310d1f0be
commit
536aef854c
|
@ -131,7 +131,7 @@ class ActivityTest(ZulipTestCase):
|
|||
hostname="demo.example.com",
|
||||
contact_email="email@example.com",
|
||||
)
|
||||
with self.assert_database_query_count(10):
|
||||
with self.assert_database_query_count(15):
|
||||
result = self.client_get("/activity/remote")
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
|
|
|
@ -11,10 +11,15 @@ from analytics.views.activity_common import (
|
|||
remote_installation_stats_link,
|
||||
remote_installation_support_link,
|
||||
)
|
||||
from corporate.lib.analytics import get_plan_data_by_remote_server
|
||||
from corporate.lib.analytics import get_plan_data_by_remote_realm, get_plan_data_by_remote_server
|
||||
from corporate.lib.stripe import cents_to_dollar_string
|
||||
from zerver.decorator import require_server_admin
|
||||
from zilencer.models import get_remote_server_guest_and_non_guest_count
|
||||
from zerver.models.realms import get_org_type_display_name
|
||||
from zilencer.models import (
|
||||
RemoteRealm,
|
||||
get_remote_realm_guest_and_non_guest_count,
|
||||
get_remote_server_guest_and_non_guest_count,
|
||||
)
|
||||
|
||||
|
||||
@require_server_admin
|
||||
|
@ -39,6 +44,18 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
|
|||
count(distinct(user_id, user_uuid)) as push_user_count
|
||||
from zilencer_remotepushdevicetoken
|
||||
group by server_id
|
||||
),
|
||||
remote_realms as (
|
||||
select
|
||||
server_id,
|
||||
id as realm_id,
|
||||
name as realm_name,
|
||||
org_type as realm_type
|
||||
from zilencer_remoterealm
|
||||
where
|
||||
is_system_bot_realm = False
|
||||
and realm_deactivated = False
|
||||
group by server_id, id, name, org_type
|
||||
)
|
||||
select
|
||||
rserver.id,
|
||||
|
@ -47,10 +64,14 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
|
|||
rserver.last_version,
|
||||
rserver.last_audit_log_update,
|
||||
push_user_count,
|
||||
push_forwarded_count
|
||||
push_forwarded_count,
|
||||
realm_id,
|
||||
realm_name,
|
||||
realm_type
|
||||
from zilencer_remotezulipserver rserver
|
||||
left join mobile_push_forwarded_count on mobile_push_forwarded_count.server_id = rserver.id
|
||||
left join remote_push_devices on remote_push_devices.server_id = rserver.id
|
||||
left join remote_realms on remote_realms.server_id = rserver.id
|
||||
where not deactivated
|
||||
order by push_user_count DESC NULLS LAST
|
||||
"""
|
||||
|
@ -62,12 +83,15 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
|
|||
"Contact email",
|
||||
"Zulip version",
|
||||
"Last audit log update",
|
||||
"Mobile users",
|
||||
"Mobile pushes forwarded",
|
||||
"Server mobile users",
|
||||
"Server mobile pushes",
|
||||
"Realm ID",
|
||||
"Realm name",
|
||||
"Organization Type",
|
||||
"Plan name",
|
||||
"Plan status",
|
||||
"ARR",
|
||||
"Non guest users",
|
||||
"Total users",
|
||||
"Guest users",
|
||||
"Links",
|
||||
]
|
||||
|
@ -78,28 +102,60 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
|
|||
LAST_AUDIT_LOG_DATE = 4
|
||||
MOBILE_USER_COUNT = 5
|
||||
MOBILE_PUSH_COUNT = 6
|
||||
REALM_ID = 7
|
||||
ORG_TYPE = 9
|
||||
ARR = 12
|
||||
TOTAL_USER_COUNT = 13
|
||||
GUEST_COUNT = 14
|
||||
|
||||
rows = get_query_data(query)
|
||||
total_row = []
|
||||
plan_data_by_remote_server = get_plan_data_by_remote_server()
|
||||
plan_data_by_remote_server_and_realm = get_plan_data_by_remote_realm()
|
||||
|
||||
total_row = []
|
||||
server_mobile_data_counted = set()
|
||||
total_revenue = 0
|
||||
total_mobile_users = 0
|
||||
total_pushes = 0
|
||||
|
||||
for row in rows:
|
||||
# Add estimated revenue for server
|
||||
server_plan_data = plan_data_by_remote_server.get(row[SERVER_ID])
|
||||
if server_plan_data is None:
|
||||
row.append("---")
|
||||
row.append("---")
|
||||
row.append("---")
|
||||
# Count mobile users and pushes forwarded, once per server
|
||||
if row[SERVER_ID] not in server_mobile_data_counted:
|
||||
if row[MOBILE_USER_COUNT] is not None:
|
||||
total_mobile_users += row[MOBILE_USER_COUNT] # nocoverage
|
||||
if row[MOBILE_PUSH_COUNT] is not None:
|
||||
total_pushes += row[MOBILE_PUSH_COUNT] # nocoverage
|
||||
server_mobile_data_counted.add(row[SERVER_ID])
|
||||
if row[REALM_ID] is None:
|
||||
plan_data = plan_data_by_remote_server.get(row[SERVER_ID])
|
||||
user_counts = get_remote_server_guest_and_non_guest_count(row[SERVER_ID])
|
||||
else:
|
||||
revenue = cents_to_dollar_string(server_plan_data.annual_revenue)
|
||||
row.append(server_plan_data.current_plan_name)
|
||||
row.append(server_plan_data.current_status)
|
||||
server_remote_realms_data = plan_data_by_remote_server_and_realm.get(row[SERVER_ID])
|
||||
if server_remote_realms_data is not None:
|
||||
plan_data = server_remote_realms_data.get(row[REALM_ID]) # nocoverage
|
||||
else:
|
||||
plan_data = None
|
||||
remote_realm = RemoteRealm.objects.get(id=row[REALM_ID], server_id=row[SERVER_ID])
|
||||
user_counts = get_remote_realm_guest_and_non_guest_count(remote_realm)
|
||||
# Format organization type for realm
|
||||
org_type = row[ORG_TYPE]
|
||||
row[ORG_TYPE] = get_org_type_display_name(org_type)
|
||||
# Add estimated annual revenue and plan data
|
||||
if plan_data is None:
|
||||
row.append("---")
|
||||
row.append("---")
|
||||
row.append("---")
|
||||
else: # nocoverage
|
||||
total_revenue += plan_data.annual_revenue
|
||||
revenue = cents_to_dollar_string(plan_data.annual_revenue)
|
||||
row.append(plan_data.current_plan_name)
|
||||
row.append(plan_data.current_status)
|
||||
row.append(f"${revenue}")
|
||||
# Add user counts
|
||||
remote_server_counts = get_remote_server_guest_and_non_guest_count(row[SERVER_ID])
|
||||
row.append(remote_server_counts.non_guest_user_count)
|
||||
row.append(remote_server_counts.guest_user_count)
|
||||
# Add links
|
||||
total_users = user_counts.non_guest_user_count + user_counts.guest_user_count
|
||||
row.append(total_users)
|
||||
row.append(user_counts.guest_user_count)
|
||||
# Add server links
|
||||
stats = remote_installation_stats_link(row[SERVER_ID])
|
||||
support = remote_installation_support_link(row[SERVER_HOSTNAME])
|
||||
links = stats + " " + support
|
||||
|
@ -112,7 +168,14 @@ def get_remote_server_activity(request: HttpRequest) -> HttpResponse:
|
|||
fix_rows(rows, i, format_none_as_zero)
|
||||
if i == SERVER_ID:
|
||||
total_row.append("Total")
|
||||
elif i in [MOBILE_USER_COUNT, MOBILE_PUSH_COUNT]:
|
||||
elif i == MOBILE_USER_COUNT:
|
||||
total_row.append(str(total_mobile_users))
|
||||
elif i == MOBILE_PUSH_COUNT:
|
||||
total_row.append(str(total_pushes))
|
||||
elif i == ARR:
|
||||
total_revenue_string = f"${cents_to_dollar_string(total_revenue)}"
|
||||
total_row.append(total_revenue_string)
|
||||
elif i in [TOTAL_USER_COUNT, GUEST_COUNT]:
|
||||
total_row.append(str(sum(row[i] for row in rows if row[i] is not None)))
|
||||
else:
|
||||
total_row.append("")
|
||||
|
|
|
@ -56,21 +56,19 @@ def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverag
|
|||
def get_plan_data_by_remote_server() -> Dict[int, RemoteActivityPlanData]: # nocoverage
|
||||
remote_server_plan_data: Dict[int, RemoteActivityPlanData] = {}
|
||||
for plan in CustomerPlan.objects.filter(
|
||||
status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD, customer__realm__isnull=True
|
||||
).select_related("customer__remote_server", "customer__remote_realm"):
|
||||
status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD,
|
||||
customer__realm__isnull=True,
|
||||
customer__remote_realm__isnull=True,
|
||||
customer__remote_server__deactivated=False,
|
||||
).select_related("customer__remote_server"):
|
||||
renewal_cents = 0
|
||||
server_id = None
|
||||
|
||||
if plan.customer.remote_server is not None:
|
||||
server_id = plan.customer.remote_server.id
|
||||
renewal_cents = RemoteServerBillingSession(
|
||||
remote_server=plan.customer.remote_server
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
elif plan.customer.remote_realm is not None:
|
||||
server_id = plan.customer.remote_realm.server.id
|
||||
renewal_cents = RemoteRealmBillingSession(
|
||||
remote_realm=plan.customer.remote_realm
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
assert plan.customer.remote_server is not None
|
||||
server_id = plan.customer.remote_server.id
|
||||
renewal_cents = RemoteServerBillingSession(
|
||||
remote_server=plan.customer.remote_server
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
|
||||
assert server_id is not None
|
||||
|
||||
|
@ -80,9 +78,12 @@ def get_plan_data_by_remote_server() -> Dict[int, RemoteActivityPlanData]: # no
|
|||
current_data = remote_server_plan_data.get(server_id)
|
||||
if current_data is not None:
|
||||
current_revenue = remote_server_plan_data[server_id].annual_revenue
|
||||
current_plans = remote_server_plan_data[server_id].current_plan_name
|
||||
# There should only ever be one CustomerPlan for a remote server with
|
||||
# a status that is less than the CustomerPlan.LIVE_STATUS_THRESHOLD.
|
||||
remote_server_plan_data[server_id] = RemoteActivityPlanData(
|
||||
current_status="Multiple plans",
|
||||
current_plan_name="See support view",
|
||||
current_status="ERROR: MULTIPLE PLANS",
|
||||
current_plan_name=f"{current_plans}, {plan.name}",
|
||||
annual_revenue=current_revenue + renewal_cents,
|
||||
)
|
||||
else:
|
||||
|
@ -92,3 +93,57 @@ def get_plan_data_by_remote_server() -> Dict[int, RemoteActivityPlanData]: # no
|
|||
annual_revenue=renewal_cents,
|
||||
)
|
||||
return remote_server_plan_data
|
||||
|
||||
|
||||
def get_plan_data_by_remote_realm() -> Dict[int, Dict[int, RemoteActivityPlanData]]: # nocoverage
|
||||
remote_server_plan_data_by_realm: Dict[int, Dict[int, RemoteActivityPlanData]] = {}
|
||||
for plan in CustomerPlan.objects.filter(
|
||||
status__lt=CustomerPlan.LIVE_STATUS_THRESHOLD,
|
||||
customer__realm__isnull=True,
|
||||
customer__remote_server__isnull=True,
|
||||
customer__remote_realm__is_system_bot_realm=False,
|
||||
customer__remote_realm__realm_deactivated=False,
|
||||
).select_related("customer__remote_realm"):
|
||||
renewal_cents = 0
|
||||
server_id = None
|
||||
|
||||
assert plan.customer.remote_realm is not None
|
||||
server_id = plan.customer.remote_realm.server.id
|
||||
renewal_cents = RemoteRealmBillingSession(
|
||||
remote_realm=plan.customer.remote_realm
|
||||
).get_customer_plan_renewal_amount(plan, timezone_now())
|
||||
|
||||
assert server_id is not None
|
||||
|
||||
if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
|
||||
renewal_cents *= 12
|
||||
|
||||
plan_data = RemoteActivityPlanData(
|
||||
current_status=plan.get_plan_status_as_text(),
|
||||
current_plan_name=plan.name,
|
||||
annual_revenue=renewal_cents,
|
||||
)
|
||||
|
||||
current_server_data = remote_server_plan_data_by_realm.get(server_id)
|
||||
realm_id = plan.customer.remote_realm.id
|
||||
|
||||
if current_server_data is None:
|
||||
realm_dict = {realm_id: plan_data}
|
||||
remote_server_plan_data_by_realm[server_id] = realm_dict
|
||||
else:
|
||||
assert current_server_data is not None
|
||||
current_realm_data = current_server_data.get(realm_id)
|
||||
if current_realm_data is not None:
|
||||
# There should only ever be one CustomerPlan for a remote realm with
|
||||
# a status that is less than the CustomerPlan.LIVE_STATUS_THRESHOLD.
|
||||
current_revenue = current_realm_data.annual_revenue
|
||||
current_plans = current_realm_data.current_plan_name
|
||||
current_server_data[realm_id] = RemoteActivityPlanData(
|
||||
current_status="ERROR: MULTIPLE PLANS",
|
||||
current_plan_name=f"{current_plans}, {plan.name}",
|
||||
annual_revenue=current_revenue + renewal_cents,
|
||||
)
|
||||
else:
|
||||
current_server_data[realm_id] = plan_data
|
||||
|
||||
return remote_server_plan_data_by_realm
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
<h4>Chart key:</h4>
|
||||
<ul>
|
||||
<li><strong>Mobile pushes forwarded</strong> - last 7 days, including today's current count</li>
|
||||
<li><strong>ARR</strong> (annual recurring revenue) - the number of users they are paying for * annual price/user</li>
|
||||
<li><strong>Server mobile pushes</strong>
|
||||
<ul>
|
||||
<li>Count of forwarded push notifications in last 7 days</li>
|
||||
<li>Includes today's current count</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>ARR</strong> (annual recurring revenue)
|
||||
<ul>
|
||||
<li>If plan has a fixed price, displays that value</li>
|
||||
<li>Otherwise, is an estimate based on the current users multiplied by annual price per user</li>
|
||||
<li>Currently, does not account for first year, flat $20 discount per month</li>
|
||||
<li>Plans with status of <strong>Free trial</strong> show estimated revenue for full year</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Links</strong>
|
||||
<ul>
|
||||
<li><strong><i class="fa fa-pie-chart"></i></strong> - remote server's stats page</li>
|
||||
|
|
Loading…
Reference in New Issue