Skip to content

Commit 8bc81bb

Browse files
authored
Add comments to metrics code and add error handling (#413)
* Update write-metrics-md.py with comments * Update get-metrics.py with comments and error handling
1 parent a801315 commit 8bc81bb

File tree

2 files changed

+71
-14
lines changed

2 files changed

+71
-14
lines changed

.github/workflows/get-metrics.py

+61-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44

55
import cartopy
6+
import google
67
import matplotlib
78
import matplotlib.cm as cm
89
import matplotlib.colors as colors
@@ -11,10 +12,13 @@
1112
from google.analytics.data_v1beta import BetaAnalyticsDataClient
1213
from google.analytics.data_v1beta.types import DateRange, Dimension, Metric, RunReportRequest
1314

15+
# Project ID Numbers
1416
PORTAL_ID = '266784902'
1517
FOUNDATIONS_ID = '281776420'
1618
COOKBOOKS_ID = '324070631'
1719

20+
21+
# Access Secrets
1822
PRIVATE_KEY_ID = os.environ.get('PRIVATE_KEY_ID')
1923
PRIVATE_KEY = os.environ.get('PRIVATE_KEY')
2024

@@ -32,16 +36,27 @@
3236
'universe_domain': 'googleapis.com',
3337
}
3438

35-
client = BetaAnalyticsDataClient.from_service_account_info(credentials_dict)
39+
try:
40+
client = BetaAnalyticsDataClient.from_service_account_info(credentials_dict)
41+
except google.auth.exceptions.MalformedError as e:
42+
print('Malformed Error:', repr(e))
43+
# Insight into reason for failure without exposing secret key
44+
print('Length of PRIVATE_KEY:', len(PRIVATE_KEY)) # 0: Secret not found, ~734: Secret malformed
3645

37-
pre_project_date = '2020-03-31' # random date before project start
46+
pre_project_date = '2020-03-31' # Random date before project start
3847

3948

4049
def _format_rounding(value):
50+
"""
51+
Helper function for rounding string displays. 1,232 -> 1.2K
52+
"""
4153
return f'{round(value / 1000, 1):.1f}K'
4254

4355

4456
def _run_total_users_report(property_id):
57+
"""
58+
Function for requesting cumulative active users from a project since project start.
59+
"""
4560
request = RunReportRequest(
4661
property=f'properties/{property_id}',
4762
dimensions=[],
@@ -58,16 +73,24 @@ def _run_total_users_report(property_id):
5873

5974

6075
def get_total_users(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
76+
"""
77+
Function for taking cumulative active users from each project and dumping it into a JSON with the current datetime.
78+
"""
6179
metrics_dict = {}
6280
metrics_dict['Now'] = str(datetime.datetime.now())
6381
metrics_dict['Portal'] = _run_total_users_report(PORTAL_ID)
6482
metrics_dict['Foundations'] = _run_total_users_report(FOUNDATIONS_ID)
6583
metrics_dict['Cookbooks'] = _run_total_users_report(COOKBOOKS_ID)
84+
85+
# Save to JSON, Remember Action is called from root directory
6686
with open('portal/metrics/user_metrics.json', 'w') as outfile:
6787
json.dump(metrics_dict, outfile)
6888

6989

7090
def _run_active_users_this_year(property_id):
91+
"""
92+
Function for requesting active users by day from a project since year start.
93+
"""
7194
current_year = datetime.datetime.now().year
7295
start_date = f'{current_year}-01-01'
7396

@@ -87,14 +110,19 @@ def _run_active_users_this_year(property_id):
87110
dates.append(date)
88111
user_counts.append(int(row.metric_values[0].value))
89112

113+
# Days need to be sorted chronologically
90114
return zip(*sorted(zip(dates, user_counts), key=lambda x: x[0]))
91115

92116

93117
def plot_projects_this_year(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
118+
"""
119+
Function for taking year-to-date active users by day and plotting it for each project.
120+
"""
94121
portal_dates, portal_users = _run_active_users_this_year(PORTAL_ID)
95122
foundations_dates, foundations_users = _run_active_users_this_year(FOUNDATIONS_ID)
96123
cookbooks_dates, cookbooks_users = _run_active_users_this_year(COOKBOOKS_ID)
97124

125+
# Plotting code
98126
plt.figure(figsize=(10, 5.5))
99127
plt.title('Year-to-Date Pythia Active Users', fontsize=15)
100128

@@ -109,6 +137,9 @@ def plot_projects_this_year(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
109137

110138

111139
def _run_top_pages_report(property_id):
140+
"""
141+
Function for requesting top 5 pages from a project.
142+
"""
112143
request = RunReportRequest(
113144
property=f'properties/{property_id}',
114145
dimensions=[Dimension(name='pageTitle')],
@@ -123,48 +154,58 @@ def _run_top_pages_report(property_id):
123154
views = int(row.metric_values[0].value)
124155
views_dict[page] = views
125156

157+
# Sort by views and grab the top 5
126158
top_pages = sorted(views_dict.items(), key=lambda item: item[1], reverse=True)[:5]
159+
# String manipulation on page titles "Cartopy - Pythia Foundations" -> "Cartopy"
127160
pages = [page.split('—')[0] for page, _ in top_pages]
128161
views = [views for _, views in top_pages]
129162

163+
# Reverse order of lists, so they'll plot with most visited page on top (i.e. last)
130164
return pages[::-1], views[::-1]
131165

132166

133167
def plot_top_pages(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
168+
"""
169+
Function that takes the top 5 viewed pages for all 3 projects and plot them on a histogram.
170+
"""
134171
portal_pages, portal_views = _run_top_pages_report(PORTAL_ID)
135172
foundations_pages, foundations_views = _run_top_pages_report(FOUNDATIONS_ID)
136173
cookbooks_pages, cookbooks_views = _run_top_pages_report(COOKBOOKS_ID)
137174

138-
pages = cookbooks_pages + foundations_pages + portal_pages
139-
175+
# Plotting code
140176
fig, ax = plt.subplots(figsize=(10, 5.5))
141177
plt.title('All-Time Top Pages', fontsize=15)
142178

143-
y = np.arange(5)
144-
y2 = np.arange(6, 11)
145-
y3 = np.arange(12, 17)
146-
y4 = np.append(y, y2)
147-
y4 = np.append(y4, y3)
179+
y = np.arange(5) # 0-4 for Cookbooks
180+
y2 = np.arange(6, 11) # 6-10 for Foundations
181+
y3 = np.arange(12, 17) # 12-16 for Portal
148182

149183
bar1 = ax.barh(y3, portal_views, align='center', label='Portal', color='purple')
150184
bar2 = ax.barh(y2, foundations_views, align='center', label='Foundations', color='royalblue')
151185
bar3 = ax.barh(y, cookbooks_views, align='center', label='Cookbooks', color='indianred')
152186

187+
y4 = np.append(y, y2)
188+
y4 = np.append(y4, y3) # 0-4,6-19,12-6 for page labels to have a gap between projects
189+
pages = cookbooks_pages + foundations_pages + portal_pages # List of all pages
153190
ax.set_yticks(y4, labels=pages, fontsize=12)
154191

192+
# Adds round-formatted views label to end of each bar
155193
ax.bar_label(bar1, fmt=_format_rounding, padding=5, fontsize=10)
156194
ax.bar_label(bar2, fmt=_format_rounding, padding=5, fontsize=10)
157195
ax.bar_label(bar3, fmt=_format_rounding, padding=5, fontsize=10)
158196

159197
ax.set_xscale('log')
160-
ax.set_xlim([10, 10**5])
198+
ax.set_xlim([10, 10**5]) # set_xlim must be after setting xscale to log
161199
ax.set_xlabel('Page Views', fontsize=12)
162200

163201
plt.legend(fontsize=12, loc='lower right')
164202
plt.savefig('portal/metrics/toppages.png', bbox_inches='tight')
165203

166204

167205
def _run_usersXcountry_report(property_id):
206+
"""
207+
Function for requesting users by country for a project.
208+
"""
168209
request = RunReportRequest(
169210
property=f'properties/{property_id}',
170211
dimensions=[Dimension(name='country')],
@@ -183,6 +224,9 @@ def _run_usersXcountry_report(property_id):
183224

184225

185226
def plot_usersXcountry(FOUNDATIONS_ID):
227+
"""
228+
Function for taking users by country for Pythia Foundations and plotting them on a map.
229+
"""
186230
users_by_country = _run_usersXcountry_report(FOUNDATIONS_ID)
187231

188232
# Google API Country names do not match Cartopy Country Shapefile names
@@ -201,11 +245,13 @@ def plot_usersXcountry(FOUNDATIONS_ID):
201245
for key in dict_api2cartopy:
202246
users_by_country[dict_api2cartopy[key]] = users_by_country.pop(key)
203247

248+
# Sort by views and grab the top 10 countries for a text box
204249
top_10_countries = sorted(users_by_country.items(), key=lambda item: item[1], reverse=True)[:10]
205250
top_10_text = '\n'.join(
206251
f'{country}: {_format_rounding(value)}' for i, (country, value) in enumerate(top_10_countries)
207252
)
208253

254+
# Plotting code
209255
fig = plt.figure(figsize=(10, 4))
210256
ax = plt.axes(projection=cartopy.crs.PlateCarree(), frameon=False)
211257
ax.set_title('Pythia Foundations Users by Country', fontsize=15)
@@ -215,12 +261,13 @@ def plot_usersXcountry(FOUNDATIONS_ID):
215261
countries = reader.records()
216262

217263
colormap = plt.get_cmap('Blues')
218-
newcmp = colors.ListedColormap(colormap(np.linspace(0.2, 1, 128)))
264+
newcmp = colors.ListedColormap(colormap(np.linspace(0.2, 1, 128))) # Truncate colormap to remove white hues
219265
newcmp.set_extremes(under='grey')
220266

221-
norm = colors.LogNorm(vmin=1, vmax=max(users_by_country.values()))
267+
norm = colors.LogNorm(vmin=1, vmax=max(users_by_country.values())) # Plot on log scale
222268
mappable = cm.ScalarMappable(norm=norm, cmap=newcmp)
223269

270+
# Loop through countries and plot their color
224271
for country in countries:
225272
country_name = country.attributes['SOVEREIGNT']
226273
if country_name in users_by_country.keys():
@@ -238,10 +285,12 @@ def plot_usersXcountry(FOUNDATIONS_ID):
238285
[country.geometry], cartopy.crs.PlateCarree(), facecolor='grey', edgecolor='white', linewidth=0.7
239286
)
240287

288+
# Add colorbar
241289
cax = fig.add_axes([0.1, -0.015, 0.67, 0.03])
242290
cbar = fig.colorbar(mappable=mappable, cax=cax, spacing='uniform', orientation='horizontal', extend='min')
243291
cbar.set_label('Unique Users')
244292

293+
# Add top 10 countries text
245294
props = dict(boxstyle='round', facecolor='white', edgecolor='white')
246295
ax.text(1.01, 0.5, top_10_text, transform=ax.transAxes, fontsize=12, verticalalignment='center', bbox=props)
247296

.github/workflows/write-metrics-md.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33

44
def process_user_data(json_file, top_pages, this_year, map, md_file):
5+
"""
6+
Function for writing portal/metrics.md from saved files output by get-metrics.py
7+
"""
58
with open(json_file, 'r') as f:
69
user_data = json.load(f)
710

@@ -12,6 +15,7 @@ def process_user_data(json_file, top_pages, this_year, map, md_file):
1215
user_data.pop('Now')
1316
f.write('\n\n')
1417

18+
# Markdown table
1519
headers = '| Project | Users |'
1620
separator = '| ' + ' | '.join(['-----'] * 2) + ' |'
1721
rows = []
@@ -21,6 +25,7 @@ def process_user_data(json_file, top_pages, this_year, map, md_file):
2125
f.write(table)
2226
f.write('\n\n')
2327

28+
# Add plots
2429
f.write(f'![Users this Year]({this_year})\n\n')
2530
f.write(f'![Top Pages]({top_pages})\n\n')
2631
f.write(f'![Users by Country]({map})\n\n')
@@ -29,9 +34,12 @@ def process_user_data(json_file, top_pages, this_year, map, md_file):
2934

3035

3136
if __name__ == '__main__':
32-
json_file = 'portal/metrics/user_metrics.json'
37+
json_file = 'portal/metrics/user_metrics.json' # Accessed from root repository
38+
39+
# HTML is built from within `portal/` directory, so paths differ from json file.
3340
top_pages = 'metrics/toppages.png'
3441
this_year = 'metrics/thisyear.png'
3542
map = 'metrics/bycountry.png'
36-
md_file = 'portal/metrics.md'
43+
44+
md_file = 'portal/metrics.md' # Written from root repository
3745
process_user_data(json_file, top_pages, this_year, map, md_file)

0 commit comments

Comments
 (0)