diff --git a/.gitignore b/.gitignore index ef3f5e4..599dac8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ +# Python virtual environment +.venv/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# VS Code settings +.vscode/ + +# macOS system files +.DS_Store # Build build/ diff --git a/README.md b/README.md index 2012473..a101e45 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ To demonstrate the value DevRel programs provide we need metrics, measurements, * [Requirements and Use Cases](https://github.com/DevRel-Foundation/wg-resource-aggregation/discussions/114) +## Using the Metrics Index + +- [How to run scripts](./docs/how-to-run-scripts.md) + ## About the Metrics Index This repository is maintained with support of the [Resource and Asset Aggregation Working Group](https://github.com/DevRel-Foundation/wg-resource-aggregation) and Developer Relations Foundation. @@ -19,3 +23,4 @@ This repository is maintained with support of the [Resource and Asset Aggregatio The DevRel Foundation is on a mission to elevate the professional practice of Developer Relations and increase awareness of it as a driver of business value. Visit https://dev-rel.org/ to learn more. + diff --git a/docs/how-to-run-scripts.md b/docs/how-to-run-scripts.md new file mode 100644 index 0000000..8e897ea --- /dev/null +++ b/docs/how-to-run-scripts.md @@ -0,0 +1,23 @@ + +# Environment Setup + +Some metrics in this project include scripts to demonstrate how to collect data. + +## Python Scripts + +Use a local Python virtual environment for dependencies. To create and activate it: + + +```sh +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +``` + +If you see no prompt change after activation, verify with `which python`—it should point to `.venv/bin/python`. + +**Note:** The `.venv` folder is excluded from version control via `.gitignore`. Each developer should create and activate their own local environment. + +## Node Scripts + +To be determined. diff --git a/metrics/activity/activity.md b/metrics/activity/activity.md new file mode 100644 index 0000000..8c72285 --- /dev/null +++ b/metrics/activity/activity.md @@ -0,0 +1,11 @@ + +# Activity Metrics + +Activity metrics measure team output and effort. This is an excellent place to start a measurement program because activities your team owns and executes on are within your control. It is reasonable and expected to be able to gather analytics and reports from the tools your team depends. + +## Content Production + +- [Blog Post Publish Efficiency](./blog-post-publish-rate.md) +- [Blog Post Publish Rate](./blog-post-publish-rate.md) + + diff --git a/metrics/activity/blog-post-publish-rate/benchmarks.md b/metrics/activity/blog-post-publish-rate/benchmarks.md new file mode 100644 index 0000000..e52d47c --- /dev/null +++ b/metrics/activity/blog-post-publish-rate/benchmarks.md @@ -0,0 +1,6 @@ +org, blog url, feed url, start, end, effort %, total blog posts published, authors, blog post publish rate, blog post efficiency rate +DevRel Foundation, https://dev-rel.org/blog, https://dev-rel.org/blog/feed.xml, 2025-08-01, 2025-11-01, 2.5, 4, 3, 0.30, 4.0143 + + + + diff --git a/metrics/activity/blog-post-publish-rate/blog-post-publish-rate.md b/metrics/activity/blog-post-publish-rate/blog-post-publish-rate.md new file mode 100644 index 0000000..646e4f9 --- /dev/null +++ b/metrics/activity/blog-post-publish-rate/blog-post-publish-rate.md @@ -0,0 +1,113 @@ + +# Blog Post Publish Rate + +The number of blog posts published over a period of time (weekly). + +This is an impactful metric because blog posts as an activity can: +- increase brand awareness +- increase discoverability in organic search (inbound growth) +- deliver value to your ideal customer to solve one of their problems (retention) + +You may additionally want to capture **Blog Post Publish Efficiency** which is the publish rate relative to the amount of effort expended (weekly). + +## Pre-Conditions + +A few things will be needed for successful collection of these metrics. + +- you need to have a public blog +- you need to have editorial control to publish at will +- normalize values to 40 hours per work week (5 days) for efficiency calculation + +## Measurement + +You'll need to gather: +- number of blog posts published between two dates +- amount of time spent producing published posts (estimate) + +**Blog Post Production Rate**: +``` +number of blog posts published inclusive / number of weeks between first and last publish post +``` + +**Blog Post Production Rate Efficiency**: +``` +number of blog posts published inclusive / hours of effort per post +``` + +### Example + +- Duration: 1 week +- Published posts: 2 +- Blog Post Publish Rate: 2.00 (2 / 1) +- Author effort: 5 days +- Blog Post Publish Efficiency: 2.00 (2 / (5 / 5)) + +### Example + +- Duration: 4 weeks +- Published Posts: 9 +- Blog Post Publish Rate: 2.25 (9 / 4) +- Total effort by authors (2 authors): 30 days +- Blog Post Publish Efficiency: 1.50 (9 / (30 / 5)) + +## Analysis + +You'll want to combine this activity metric with other outcomes. The objective is to increase the blog publish rate while impacting secondary outcome measures. + +- blog attention hours - maintaining or increasing attention hours indicates content is relevant for audience +- blog conversion rate - increasing conversion rate indicates clear CTA + +You could also look for trends: +- is blog post publish rate increasing over time +- does blog post publish efficiency vary substantially between two sources of content + +### Guardrails + +You'll want to combine this activity metric with other outcome metrics that ensure you are achieving a desirable outcome. It would be easy to write shorter articles, low quality articles, or take other short-cuts to increase blog post publish rate at the expense of these desired outcomes. + +Good guardrail metrics to compensate: +- blog attention hours - decreasing trend in attention hours indicates content does not engage audience +- blog conversion rate - decreasing conversion rate indicates CTA is unclear + +## Obstacles + +### What if I don't have a blog? +Free public blogging platforms exist that many teams can leverage. See the DevRel Foundation Tools Catalog for examples. The best time to start a blog if you don't have one for your program is today. + +### What If I don't have a blog feed? +If your blog does not support feeds for automatic data collection, you can manually visit the blog and collect the data. Just scroll through the feed and count the number of posts and number of authors for the time period. + +### What if I don't have outcome measures? +Collecting activity measures can still be valuable to optimize your workflows around sourcing content. + +### What if I don't have enough content being produced? +Many programs will increase their publish rate by sub-contracting out to freelance writers or starting a community writing program. + +### What if my team multi-tasks and isn't always focused on content production? +It is common to make a rough approximation. As long as you apply this consistently you can make adjustments in relative terms. You may be able to manage your team in a way to encourage these dedicated periods of time are available. + +For example, you may estimate a team of five spends 20% of their time (1 day per week) on blog content production. This is effectively one week of total effort across the team. + +### What if my publish rate dropped by half? +It is not unusual for there to be frequent volatility. When there are new product launches or other activities there are many more topics to produce content on. When teams are engaged with technical support, events, or product launches this can pull attention away from content production for a brief time. The measure is a rate, so extend the period of time to a larger frame to smooth the value out if this becomes a concern. It also gives you framing to share with stakeholders on the impact competing programs may have on your content production deliverables. + +### What if other teams contribute content? +It is very common to leverage product, engineering, and other departments in the production of content. The blog post publish rate does not account for whom is creating the content. When calculating blog post publish rate efficiency you would count these other contributors efforts into the cumulative total. + +## Automation + +Many blogging platforms support RSS or Atom Feeds. In this repository is a python script you can run that will fetch the feed and calculate the number of authors and number of blog posts delivered over a period of time. + +``` +$ python3 blog-post-publish-rate.py --start 2025-07-01 --end 2025-09-30 --feed https://dev-rel.org/blog/feed.xml +...analyzing 12 weeks +...found 10 articles +blog post publish rate = 0.833 +...found 3 authors +...default to 50% time effort +blog post publish efficiency = 0.555 + +$ python3 blog-post-publish-rate.py --start 2025-07-01 --end 2025-09-30 --effort 45 --feed https://dev-rel.org/blog/feed.xml +blog post publish rate = 0.833 +blog post publish efficiency = 1.111 +``` diff --git a/metrics/activity/blog-post-publish-rate/blog-post-publish-rate.py b/metrics/activity/blog-post-publish-rate/blog-post-publish-rate.py new file mode 100755 index 0000000..599523b --- /dev/null +++ b/metrics/activity/blog-post-publish-rate/blog-post-publish-rate.py @@ -0,0 +1,116 @@ + +""" +Quick and dirty script to calculate blog post publish rate and efficiency from an RSS/Atom feed. + +$ python3 blog-post-publish-rate.py --start 2025-08-01 --end 2025-11-01 --feed https://dev-rel.org/blog/feed.xml --effort 2.5 +Articles: 4 +Authors: 3 +Days: 93 +Weeks: 13.29 +Publish Rate (articles/week): 0.30 +Publish Efficiency: 4.0143 +""" + +import sys +import argparse +import requests +import xml.etree.ElementTree as ET +from datetime import datetime + +def parse_args(): + parser = argparse.ArgumentParser(description="Calculate blog post publish rate and efficiency.") + parser.add_argument('--start', required=True, help='Start date (YYYY-MM-DD)') + parser.add_argument('--end', required=True, help='End date (YYYY-MM-DD)') + parser.add_argument('--feed', required=True, help='Blog feed URL (RSS/Atom)') + parser.add_argument('--effort', type=float, default=100.0, help='Effort percentage (default: 100)') + parser.add_argument('--info', action='store_true', help='Print extra info') + return parser.parse_args() + +def fetch_feed(url): + try: + response = requests.get(url) + response.raise_for_status() + return response.text + except Exception as e: + print(f"Error fetching feed: {e}", file=sys.stderr) + sys.exit(1) + +def parse_date(date_str): + # Try common date formats + fmts = [ + "%Y-%m-%dT%H:%M:%S%z", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d", + "%a, %d %b %Y %H:%M:%S %Z", + "%a, %d %b %Y %H:%M:%S %z", + "%a, %d %b %Y %H:%M:%S GMT", + "%a, %d %b %Y %H:%M:%S" + ] + for fmt in fmts: + try: + return datetime.strptime(date_str, fmt) + except Exception: + continue + return None + +def parse_feed(feed_xml, start, end): + root = ET.fromstring(feed_xml) + ns = {'atom': 'http://www.w3.org/2005/Atom', 'rss': 'http://purl.org/rss/1.0/'} + articles = [] + authors = set() + # Try Atom first + for entry in root.findall('.//{http://www.w3.org/2005/Atom}entry'): + date_el = entry.find('{http://www.w3.org/2005/Atom}published') or entry.find('{http://www.w3.org/2005/Atom}updated') + author_el = entry.find('{http://www.w3.org/2005/Atom}author/{http://www.w3.org/2005/Atom}name') + if date_el is not None: + pub_date = parse_date(date_el.text) + if pub_date and start <= pub_date <= end: + articles.append(pub_date) + if author_el is not None: + authors.add(author_el.text) + # Try RSS + for item in root.findall('.//item'): + date_el = item.find('pubDate') + # RSS author can be or + author_el = item.find('author') + if author_el is None: + # Try Dublin Core creator + for child in item: + if child.tag.endswith('creator'): + author_el = child + break + if date_el is not None: + pub_date = parse_date(date_el.text) + if pub_date and start <= pub_date <= end: + articles.append(pub_date) + if author_el is not None and author_el.text: + authors.add(author_el.text.strip()) + return articles, authors + +def main(): + args = parse_args() + start = datetime.strptime(args.start, "%Y-%m-%d") + end = datetime.strptime(args.end, "%Y-%m-%d") + feed_xml = fetch_feed(args.feed) + articles, authors = parse_feed(feed_xml, start, end) + num_articles = len(articles) + num_authors = len(authors) + num_days = (end - start).days + 1 + num_weeks = num_days / 7.0 + publish_rate = num_articles / num_weeks if num_weeks > 0 else 0 + efficiency = publish_rate / (num_authors * (args.effort / 100.0)) if num_authors > 0 and args.effort > 0 else 0 + print(f"Articles: {num_articles}") + print(f"Authors: {num_authors}") + print(f"Days: {num_days}") + print(f"Weeks: {num_weeks:.2f}") + print(f"Publish Rate (articles/week): {publish_rate:.2f}") + print(f"Publish Efficiency: {efficiency:.4f}") + if args.info: + print(f"Authors: {', '.join(authors)}") + +if __name__ == "__main__": + main() + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests