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:
Lauryn Menard 2024-01-17 22:27:14 +01:00 committed by Tim Abbott
parent 8310d1f0be
commit 536aef854c
4 changed files with 168 additions and 38 deletions

View File

@ -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)

View File

@ -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("")

View File

@ -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

View File

@ -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>