Skip to content

Commit a801315

Browse files
Automation of Metrics (#412)
* Create automate-metrics.yaml * outline for automate-metrics.yaml * Create get-metrics.py * Update automate-metrics.yaml * Update automate-metrics.yaml * Create metrics.md * Update footer-menu.html with metrics * Update footer-menu.html * Update footer-menu.html with metrics * Delete portal/metrics.md * Create metrics.md * add json_extract to extensions * Update get-metrics.py to also write file * Update get-metrics.py with returned condition * Update automate-metrics.yaml * pre-commit * use os to access passed in keys * fix double quoted strings * add jsonextract to environment.yml * write metrics.md in a python file * rm from config * use env files * update comment * state with a table * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * rm table because it won't format and add to toc tree * add dispatch * starting metrics file, and - vs _ * pre-commit * fix write-metrics * Metrics API test (#408) * Create automate-metrics.yaml * outline for automate-metrics.yaml * Create get-metrics.py * Update automate-metrics.yaml * Update automate-metrics.yaml * Update footer-menu.html with metrics * Create metrics.md * add json_extract to extensions * Update get-metrics.py to also write file * Update get-metrics.py with returned condition * Update automate-metrics.yaml * pre-commit * use os to access passed in keys * fix double quoted strings * add jsonextract to environment.yml * write metrics.md in a python file * rm from config * use env files * update comment * state with a table * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * rm table because it won't format and add to toc tree * add dispatch * starting metrics file, and - vs _ * pre-commit * fix write-metrics --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * make a metrics folder for holding future graphs * missed a path herr * add metrics automation as a step before site building * rm automate-metrics schedule * Add last updated time to write-metrics * pre-commit * Update automate-metrics.yaml to use wget * Update automate-metrics.yaml * use curl * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update automate-metrics.yaml * Update automate-metrics.yaml fix file name * Update automate-metrics.yaml * Update automate-metrics.yaml * Update automate-metrics.yaml * Update get-metrics.py * Update get-metrics.py * Update automate-metrics.yaml * Update get-metrics.py * Update automate-metrics.yaml * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py to build dict * Update automate-metrics.yaml * Update get-metrics.py * Update get-metrics.py * Update automate-metrics.yaml * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update automate-metrics.yaml * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * Update automate-metrics.yaml * Update write-metrics-md.py * Update get-metrics.py * Update automate-metrics.yaml * Update nightly-build.yaml * Update publish-site.yaml * Update trigger-preview.yaml * Update trigger-site-build.yaml * change from workflow_dispatch to workflow_call * Update get-metrics.py * Update get-metrics.py * add files to gitignore * add scripts for more plotting and nest automate-metrics better * print small portion of key * use pip * testing * testing * print stmts * more prints * don't save * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * try -u before python call * Try no quotes no $, move env files, combine pip * try printing in main * Update get-metrics.py * Update get-metrics.py * Update get-metrics.py * duplicate needs * try with linebreaks * try except * valueerror * change the error * import google * change print move imports * isolate to part that fails * working from main now * rm action from PR triggered workflow * oops re add jobs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix file paths * fix filepaths * opps make it work on actions, not local again * save stale versions of images, image path to python files from root * fix lowercase and make metrics folder * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * format md table, add portal to top pages plot * Delete portal/metrics/bypage.png * add 3rd plot type and edit fontsizes etc * remove map for now * some comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e10a744 commit a801315

10 files changed

+360
-0
lines changed

.github/workflows/get-metrics.py

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import datetime
2+
import json
3+
import os
4+
5+
import cartopy
6+
import matplotlib
7+
import matplotlib.cm as cm
8+
import matplotlib.colors as colors
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
from google.analytics.data_v1beta import BetaAnalyticsDataClient
12+
from google.analytics.data_v1beta.types import DateRange, Dimension, Metric, RunReportRequest
13+
14+
PORTAL_ID = '266784902'
15+
FOUNDATIONS_ID = '281776420'
16+
COOKBOOKS_ID = '324070631'
17+
18+
PRIVATE_KEY_ID = os.environ.get('PRIVATE_KEY_ID')
19+
PRIVATE_KEY = os.environ.get('PRIVATE_KEY')
20+
21+
credentials_dict = {
22+
'type': 'service_account',
23+
'project_id': 'cisl-vast-pythia',
24+
'private_key_id': PRIVATE_KEY_ID,
25+
'private_key': PRIVATE_KEY,
26+
'client_email': '[email protected]',
27+
'client_id': '113402578114110723940',
28+
'auth_uri': 'https://accounts.google.com/o/oauth2/auth',
29+
'token_uri': 'https://oauth2.googleapis.com/token',
30+
'auth_provider_x509_cert_url': 'https://www.googleapis.com/oauth2/v1/certs',
31+
'client_x509_cert_url': 'https://www.googleapis.com/robot/v1/metadata/x509/pythia-metrics-api%40cisl-vast-pythia.iam.gserviceaccount.com',
32+
'universe_domain': 'googleapis.com',
33+
}
34+
35+
client = BetaAnalyticsDataClient.from_service_account_info(credentials_dict)
36+
37+
pre_project_date = '2020-03-31' # random date before project start
38+
39+
40+
def _format_rounding(value):
41+
return f'{round(value / 1000, 1):.1f}K'
42+
43+
44+
def _run_total_users_report(property_id):
45+
request = RunReportRequest(
46+
property=f'properties/{property_id}',
47+
dimensions=[],
48+
metrics=[Metric(name='activeUsers')],
49+
date_ranges=[DateRange(start_date=pre_project_date, end_date='today')],
50+
)
51+
response = client.run_report(request)
52+
53+
total_users = 0
54+
for row in response.rows:
55+
total_users += int(row.metric_values[0].value)
56+
57+
return _format_rounding(total_users)
58+
59+
60+
def get_total_users(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
61+
metrics_dict = {}
62+
metrics_dict['Now'] = str(datetime.datetime.now())
63+
metrics_dict['Portal'] = _run_total_users_report(PORTAL_ID)
64+
metrics_dict['Foundations'] = _run_total_users_report(FOUNDATIONS_ID)
65+
metrics_dict['Cookbooks'] = _run_total_users_report(COOKBOOKS_ID)
66+
with open('portal/metrics/user_metrics.json', 'w') as outfile:
67+
json.dump(metrics_dict, outfile)
68+
69+
70+
def _run_active_users_this_year(property_id):
71+
current_year = datetime.datetime.now().year
72+
start_date = f'{current_year}-01-01'
73+
74+
request = RunReportRequest(
75+
property=f'properties/{property_id}',
76+
dimensions=[Dimension(name='date')],
77+
metrics=[Metric(name='activeUsers')],
78+
date_ranges=[DateRange(start_date=start_date, end_date='today')],
79+
)
80+
response = client.run_report(request)
81+
82+
dates = []
83+
user_counts = []
84+
for row in response.rows:
85+
date_str = row.dimension_values[0].value
86+
date = datetime.datetime.strptime(date_str, '%Y%m%d')
87+
dates.append(date)
88+
user_counts.append(int(row.metric_values[0].value))
89+
90+
return zip(*sorted(zip(dates, user_counts), key=lambda x: x[0]))
91+
92+
93+
def plot_projects_this_year(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
94+
portal_dates, portal_users = _run_active_users_this_year(PORTAL_ID)
95+
foundations_dates, foundations_users = _run_active_users_this_year(FOUNDATIONS_ID)
96+
cookbooks_dates, cookbooks_users = _run_active_users_this_year(COOKBOOKS_ID)
97+
98+
plt.figure(figsize=(10, 5.5))
99+
plt.title('Year-to-Date Pythia Active Users', fontsize=15)
100+
101+
plt.plot(portal_dates, portal_users, color='purple', label='Portal')
102+
plt.plot(foundations_dates, foundations_users, color='royalblue', label='Foundations')
103+
plt.plot(cookbooks_dates, cookbooks_users, color='indianred', label='Cookbooks')
104+
105+
plt.legend(fontsize=12, loc='upper right')
106+
107+
plt.xlabel('Date', fontsize=12)
108+
plt.savefig('portal/metrics/thisyear.png', bbox_inches='tight')
109+
110+
111+
def _run_top_pages_report(property_id):
112+
request = RunReportRequest(
113+
property=f'properties/{property_id}',
114+
dimensions=[Dimension(name='pageTitle')],
115+
date_ranges=[DateRange(start_date=pre_project_date, end_date='today')],
116+
metrics=[Metric(name='screenPageViews')],
117+
)
118+
response = client.run_report(request)
119+
120+
views_dict = {}
121+
for row in response.rows:
122+
page = row.dimension_values[0].value
123+
views = int(row.metric_values[0].value)
124+
views_dict[page] = views
125+
126+
top_pages = sorted(views_dict.items(), key=lambda item: item[1], reverse=True)[:5]
127+
pages = [page.split('—')[0] for page, _ in top_pages]
128+
views = [views for _, views in top_pages]
129+
130+
return pages[::-1], views[::-1]
131+
132+
133+
def plot_top_pages(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID):
134+
portal_pages, portal_views = _run_top_pages_report(PORTAL_ID)
135+
foundations_pages, foundations_views = _run_top_pages_report(FOUNDATIONS_ID)
136+
cookbooks_pages, cookbooks_views = _run_top_pages_report(COOKBOOKS_ID)
137+
138+
pages = cookbooks_pages + foundations_pages + portal_pages
139+
140+
fig, ax = plt.subplots(figsize=(10, 5.5))
141+
plt.title('All-Time Top Pages', fontsize=15)
142+
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)
148+
149+
bar1 = ax.barh(y3, portal_views, align='center', label='Portal', color='purple')
150+
bar2 = ax.barh(y2, foundations_views, align='center', label='Foundations', color='royalblue')
151+
bar3 = ax.barh(y, cookbooks_views, align='center', label='Cookbooks', color='indianred')
152+
153+
ax.set_yticks(y4, labels=pages, fontsize=12)
154+
155+
ax.bar_label(bar1, fmt=_format_rounding, padding=5, fontsize=10)
156+
ax.bar_label(bar2, fmt=_format_rounding, padding=5, fontsize=10)
157+
ax.bar_label(bar3, fmt=_format_rounding, padding=5, fontsize=10)
158+
159+
ax.set_xscale('log')
160+
ax.set_xlim([10, 10**5])
161+
ax.set_xlabel('Page Views', fontsize=12)
162+
163+
plt.legend(fontsize=12, loc='lower right')
164+
plt.savefig('portal/metrics/toppages.png', bbox_inches='tight')
165+
166+
167+
def _run_usersXcountry_report(property_id):
168+
request = RunReportRequest(
169+
property=f'properties/{property_id}',
170+
dimensions=[Dimension(name='country')],
171+
metrics=[Metric(name='activeUsers')],
172+
date_ranges=[DateRange(start_date=pre_project_date, end_date='today')],
173+
)
174+
response = client.run_report(request)
175+
176+
user_by_country = {}
177+
for row in response.rows:
178+
country = row.dimension_values[0].value
179+
users = int(row.metric_values[0].value)
180+
user_by_country[country] = user_by_country.get(country, 0) + users
181+
182+
return user_by_country
183+
184+
185+
def plot_usersXcountry(FOUNDATIONS_ID):
186+
users_by_country = _run_usersXcountry_report(FOUNDATIONS_ID)
187+
188+
# Google API Country names do not match Cartopy Country Shapefile names
189+
dict_api2cartopy = {
190+
'Tanzania': 'United Republic of Tanzania',
191+
'United States': 'United States of America',
192+
'Congo - Kinshasa': 'Democratic Republic of the Congo',
193+
'Bahamas': 'The Bahamas',
194+
'Timor-Leste': 'East Timor',
195+
'C\u00f4te d\u2019Ivoire': 'Ivory Coast',
196+
'Bosnia & Herzegovina': 'Bosnia and Herzegovina',
197+
'Serbia': 'Republic of Serbia',
198+
'Trinidad & Tobago': 'Trinidad and Tobago',
199+
}
200+
201+
for key in dict_api2cartopy:
202+
users_by_country[dict_api2cartopy[key]] = users_by_country.pop(key)
203+
204+
top_10_countries = sorted(users_by_country.items(), key=lambda item: item[1], reverse=True)[:10]
205+
top_10_text = '\n'.join(
206+
f'{country}: {_format_rounding(value)}' for i, (country, value) in enumerate(top_10_countries)
207+
)
208+
209+
fig = plt.figure(figsize=(10, 4))
210+
ax = plt.axes(projection=cartopy.crs.PlateCarree(), frameon=False)
211+
ax.set_title('Pythia Foundations Users by Country', fontsize=15)
212+
213+
shapefile = cartopy.io.shapereader.natural_earth(category='cultural', resolution='110m', name='admin_0_countries')
214+
reader = cartopy.io.shapereader.Reader(shapefile)
215+
countries = reader.records()
216+
217+
colormap = plt.get_cmap('Blues')
218+
newcmp = colors.ListedColormap(colormap(np.linspace(0.2, 1, 128)))
219+
newcmp.set_extremes(under='grey')
220+
221+
norm = colors.LogNorm(vmin=1, vmax=max(users_by_country.values()))
222+
mappable = cm.ScalarMappable(norm=norm, cmap=newcmp)
223+
224+
for country in countries:
225+
country_name = country.attributes['SOVEREIGNT']
226+
if country_name in users_by_country.keys():
227+
facecolor = newcmp(norm(users_by_country[country_name]))
228+
ax.add_geometries(
229+
[country.geometry],
230+
cartopy.crs.PlateCarree(),
231+
facecolor=facecolor,
232+
edgecolor='white',
233+
linewidth=0.7,
234+
norm=matplotlib.colors.LogNorm(),
235+
)
236+
else:
237+
ax.add_geometries(
238+
[country.geometry], cartopy.crs.PlateCarree(), facecolor='grey', edgecolor='white', linewidth=0.7
239+
)
240+
241+
cax = fig.add_axes([0.1, -0.015, 0.67, 0.03])
242+
cbar = fig.colorbar(mappable=mappable, cax=cax, spacing='uniform', orientation='horizontal', extend='min')
243+
cbar.set_label('Unique Users')
244+
245+
props = dict(boxstyle='round', facecolor='white', edgecolor='white')
246+
ax.text(1.01, 0.5, top_10_text, transform=ax.transAxes, fontsize=12, verticalalignment='center', bbox=props)
247+
248+
plt.tight_layout()
249+
plt.savefig('portal/metrics/bycountry.png', bbox_inches='tight')
250+
251+
252+
if __name__ == '__main__':
253+
get_total_users(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID)
254+
plot_projects_this_year(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID)
255+
plot_top_pages(PORTAL_ID, FOUNDATIONS_ID, COOKBOOKS_ID)
256+
plot_usersXcountry(FOUNDATIONS_ID)

.github/workflows/nightly-build.yaml

+24
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@ on:
66
- cron: '0 0 * * *' # Daily “At 00:00”
77

88
jobs:
9+
automate-metrics:
10+
runs-on: macos-latest
11+
steps:
12+
- uses: actions/checkout@v3
13+
- name: Automate Metrics
14+
env:
15+
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
16+
PRIVATE_KEY_ID: ${{ secrets.PRIVATE_KEY_ID }}
17+
run: |
18+
python -m venv analytics-api
19+
source analytics-api/bin/activate
20+
pip install google-analytics-data cartopy matplotlib
21+
22+
python .github/workflows/get-metrics.py
23+
python .github/workflows/write-metrics-md.py
24+
925
build:
26+
needs: automate-metrics
1027
if: ${{ github.repository_owner == 'ProjectPythia' }}
1128
uses: ProjectPythia/cookbook-actions/.github/workflows/build-book.yaml@main
1229
with:
@@ -20,3 +37,10 @@ jobs:
2037
uses: ./.github/workflows/sphinx-link-checker.yaml
2138
with:
2239
path_to_source: 'portal'
40+
41+
deploy:
42+
needs: build
43+
uses: ProjectPythia/cookbook-actions/.github/workflows/deploy-book.yaml@main
44+
with:
45+
cname: projectpythia.org
46+
publish_dir: 'portal/_build/html'

.github/workflows/publish-site.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,24 @@ on:
88
workflow_dispatch:
99

1010
jobs:
11+
automate-metrics:
12+
runs-on: macos-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
- name: Automate Metrics
16+
env:
17+
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
18+
PRIVATE_KEY_ID: ${{ secrets.PRIVATE_KEY_ID }}
19+
run: |
20+
python -m venv analytics-api
21+
source analytics-api/bin/activate
22+
pip install google-analytics-data cartopy matplotlib
23+
24+
python .github/workflows/get-metrics.py
25+
python .github/workflows/write-metrics-md.py
26+
1127
build:
28+
needs: automate-metrics
1229
uses: ProjectPythia/cookbook-actions/.github/workflows/build-book.yaml@main
1330
with:
1431
environment_file: 'environment.yml'

.github/workflows/trigger-preview.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,24 @@ on:
88
- completed
99

1010
jobs:
11+
automate-metrics:
12+
runs-on: macos-latest
13+
steps:
14+
- uses: actions/checkout@v3
15+
- name: Automate Metrics
16+
env:
17+
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
18+
PRIVATE_KEY_ID: ${{ secrets.PRIVATE_KEY_ID }}
19+
run: |
20+
python -m venv analytics-api
21+
source analytics-api/bin/activate
22+
pip install google-analytics-data cartopy matplotlib
23+
24+
python .github/workflows/get-metrics.py
25+
python .github/workflows/write-metrics-md.py
26+
1127
find-pull-request:
28+
needs: automate-metrics
1229
uses: ProjectPythia/cookbook-actions/.github/workflows/find-pull-request.yaml@main
1330
deploy-preview:
1431
needs: find-pull-request

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

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import json
2+
3+
4+
def process_user_data(json_file, top_pages, this_year, map, md_file):
5+
with open(json_file, 'r') as f:
6+
user_data = json.load(f)
7+
8+
with open(md_file, 'w') as f:
9+
f.write('# Metrics \n\n')
10+
now = user_data['Now']
11+
f.write(f'Last Updated: {now}')
12+
user_data.pop('Now')
13+
f.write('\n\n')
14+
15+
headers = '| Project | Users |'
16+
separator = '| ' + ' | '.join(['-----'] * 2) + ' |'
17+
rows = []
18+
for key in user_data.keys():
19+
rows.append('| ' + key + ' | ' + user_data[key] + ' |')
20+
table = '\n'.join([headers, separator] + rows)
21+
f.write(table)
22+
f.write('\n\n')
23+
24+
f.write(f'![Users this Year]({this_year})\n\n')
25+
f.write(f'![Top Pages]({top_pages})\n\n')
26+
f.write(f'![Users by Country]({map})\n\n')
27+
28+
f.close()
29+
30+
31+
if __name__ == '__main__':
32+
json_file = 'portal/metrics/user_metrics.json'
33+
top_pages = 'metrics/toppages.png'
34+
this_year = 'metrics/thisyear.png'
35+
map = 'metrics/bycountry.png'
36+
md_file = 'portal/metrics.md'
37+
process_user_data(json_file, top_pages, this_year, map, md_file)

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,8 @@ portal/code_of_conduct.md
138138
portal/resource-gallery.md
139139
portal/resource-gallery/*.md
140140
resource-gallery-submission-input.json
141+
142+
# Analytics
143+
.github/workflows/analytics-api/
144+
.github/workflows/*.json
145+
.github/workflows/*.png

portal/_templates/footer-menu.html

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ <h1>{{ _("About") }}</h1>
88
<li><a href="https://projectpythia.org/about.html">{{ _("About Project Pythia") }}</a></li>
99
<li><a href="https://foundations.projectpythia.org/preamble/how-to-use.html">{{ _("How to use Pythia Foundations") }}</a></li>
1010
<li><a href="https://projectpythia.org/#how-to-cite">{{ _("How to Cite") }}</a></li>
11+
<li><a href="https://projectpythia.org/metrics.html">{{ _("Metrics") }}</a></li>
1112
</ul>
1213
</div>
1314
<div class="col-9 col-sm-4 col-md-4 col-lg-3 footer-menu-col">

0 commit comments

Comments
 (0)