Skip to content

Alert when completion drops

Goal: a Slack message when a quiz’s completion rate this week is well below last week’s, and a short morning digest of yesterday’s numbers.

Uses: Analytics. Scope: analytics:read (quizzes:read too if you list the quizzes with GET /v1/quizzes). One timezone, America/New_York below, decides “today” and goes as tz on every request, so both weeks and the digest describe the same days.

The alert

GET /v1/quizzes/{quiz_id}/analytics/overview answers totals.completion_rate for an inclusive date range. Ask for two disjoint weeks and compare:

import datetime as dt
import os
from zoneinfo import ZoneInfo
import httpx
HEADERS = {"Authorization": f"Bearer {os.environ['OCTANE_API_KEY']}"}
QUIZ = "https://api.octaneai.com/v1/quizzes/quiz_7c9e6679742540de944be07fc1f90ae7"
SLACK = os.environ["SLACK_WEBHOOK_URL"]
TZ = "America/New_York" # one timezone for "today" and for every request
def totals(start, end):
r = httpx.get(f"{QUIZ}/analytics/overview", params={"from": start, "to": end, "tz": TZ}, headers=HEADERS, timeout=30)
r.raise_for_status()
return r.json()["totals"]
today = dt.datetime.now(ZoneInfo(TZ)).date()
this_week = totals(today - dt.timedelta(days=7), today - dt.timedelta(days=1))
last_week = totals(today - dt.timedelta(days=14), today - dt.timedelta(days=8))
now, before = this_week["completion_rate"], last_week["completion_rate"] # percentages, 62.5
print(this_week["starts"], now, before)
if this_week["starts"] >= 50 and now is not None and before and (before - now) / before > 0.2:
httpx.post(SLACK, json={"text": (
f"Quiz completion is down {(before - now) / before:.0%} week over week: "
f"{now:.1f}% of {this_week['starts']} starts, was {before:.1f}%."
)}, timeout=30).raise_for_status()
  • Ranges are inclusive on both ends, so keep the two weeks disjoint.
  • completion_rate is a percentage with two decimals (62.5); it is null when nothing started.
  • Require a minimum number of starts before alerting; a quiz with ten starts swings wildly.

The digest

Yesterday’s numbers are one call with from and to set to the same day, plus the top products, both in the same tz:

import datetime as dt
import os
from zoneinfo import ZoneInfo
import httpx
HEADERS = {"Authorization": f"Bearer {os.environ['OCTANE_API_KEY']}"}
QUIZ = "https://api.octaneai.com/v1/quizzes/quiz_7c9e6679742540de944be07fc1f90ae7"
SLACK = os.environ["SLACK_WEBHOOK_URL"]
TZ = "America/New_York"
yesterday = (dt.datetime.now(ZoneInfo(TZ)).date() - dt.timedelta(days=1)).isoformat()
r = httpx.get(f"{QUIZ}/analytics/overview", params={"from": yesterday, "to": yesterday, "tz": TZ}, headers=HEADERS, timeout=30)
r.raise_for_status()
t = r.json()["totals"]
r = httpx.get(f"{QUIZ}/analytics/products", params={"from": yesterday, "to": yesterday, "tz": TZ, "sort": "revenue", "limit": 5}, headers=HEADERS, timeout=30)
r.raise_for_status()
products = r.json()
revenue = f"{t['revenue']['amount']} {t['revenue']['currency']}" if t["revenue"] else "no revenue" # money can be null
lines = [f"*{yesterday}*: {t['views']} views, {t['starts']} starts, {t['completions']} completions, {t['orders']} orders, {revenue}"]
for p in products["rows"]:
lines.append(f"- {p['title']}: shown {p['times_shown']}x, {p['line_revenue']['amount']} {p['line_revenue']['currency']}")
print("\n".join(lines))
httpx.post(SLACK, json={"text": "\n".join(lines)}, timeout=30).raise_for_status()

Money is {"amount": "12.50", "currency": "USD"} with a decimal string, never a float; format it, do not add it up as a float.

Things to know

  • Cost. An analytics body costs 3 units; these few calls a day are nothing against a Plus allowance of 72 with 12 refilled per second. A burst over many quizzes should read remaining in the RateLimit header and sleep on a 429 for Retry-After seconds.
  • Cache. Answers are cached up to 300 seconds on our side and carry an ETag; a job that re-reads the same range can send If-None-Match and pay 1 unit for a 304.
  • Freshness. freshness.closed_days_through says the last day that will not change any more; yesterday is closed by the time a morning job runs, today is read live.
  • Every quiz. To watch all published quizzes, walk GET /v1/quizzes?status=published (needs quizzes:read) and run the alert per id, or read GET /v1/analytics/quizzes?sort=completion_rate&dir=asc for the whole store (25 quizzes a page by default, limit up to 1000).