Skip to content

Commit 51ef6be

Browse files
authored
Merge pull request #4 from Floe-Labs/chore/gif-and-instrumentation
feat: demo GIF + adoption instrumentation
2 parents ef700ec + 92c5d1d commit 51ef6be

5 files changed

Lines changed: 178 additions & 10 deletions

File tree

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# floe-guard
22

3+
[![PyPI version](https://img.shields.io/pypi/v/floe-guard.svg)](https://pypi.org/project/floe-guard/)
4+
[![Downloads](https://static.pepy.tech/badge/floe-guard/month)](https://pepy.tech/project/floe-guard)
5+
[![Python versions](https://img.shields.io/pypi/pyversions/floe-guard.svg)](https://pypi.org/project/floe-guard/)
6+
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7+
38
**A local budget guardrail for AI agents.** It hard-stops your agent *before its
49
next LLM call* when it would cross a spend ceiling — so a runaway loop dies at
510
$0.10 instead of $4,000. No account, no signup, no network. Runs in your process.
@@ -26,8 +31,9 @@ BUDGET EXCEEDED — call blocked
2631
The next call would cross your budget; floe-guard stopped your agent before it ran.
2732
```
2833

29-
_Animated demo coming — run `python examples/runaway_loop.py` to watch it stop a loop live._
30-
<!-- TODO: record docs/stop-the-loop.gif and restore the embed: ![stop the loop](docs/stop-the-loop.gif) -->
34+
![floe-guard hard-stopping a runaway loop before it crosses a $0.10 ceiling](docs/stop-the-loop.gif)
35+
36+
_Run it yourself: `python examples/runaway_loop.py` — no API key, no account, no network._
3137

3238
## See it stop a loop (no API key needed)
3339

@@ -180,8 +186,18 @@ cross-vendor *enforcement* is the hosted Floe product running server-side — no
180186
this client. Use the number to inform a local ceiling; the server stays the
181187
source of truth.
182188

183-
**[dev-dashboard.floelabs.xyz](https://dev-dashboard.floelabs.xyz)** ·
184-
**[floelabs.xyz](https://floelabs.xyz)**
189+
**[dev-dashboard.floelabs.xyz](https://dev-dashboard.floelabs.xyz/?utm_source=floe-guard&utm_medium=readme&utm_campaign=oss)** ·
190+
**[floelabs.xyz](https://floelabs.xyz/?utm_source=floe-guard&utm_medium=readme&utm_campaign=oss)**
191+
192+
## Built with floe-guard
193+
194+
Using floe-guard in your project? Add the badge so others find it:
195+
196+
[![guarded by floe-guard](https://img.shields.io/badge/guarded%20by-floe--guard-2f81f7.svg)](https://github.com/Floe-Labs/floe-guard)
197+
198+
```markdown
199+
[![guarded by floe-guard](https://img.shields.io/badge/guarded%20by-floe--guard-2f81f7.svg)](https://github.com/Floe-Labs/floe-guard)
200+
```
185201

186202
## Development
187203

docs/INSTRUMENTATION.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Instrumentation
2+
3+
How we measure floe-guard's adoption — and the one thing we deliberately *don't* do.
4+
5+
## No runtime telemetry (deliberate)
6+
7+
floe-guard does **not** phone home. It sends no usage events, no install pings, no
8+
identifiers — nothing leaves your process at runtime except hosted-budget reads
9+
you explicitly opt into by setting `FLOE_API_KEY` (the *Upgrade to hosted Floe*
10+
path) — never otherwise.
11+
12+
This is a choice, not an oversight. A guardrail's whole value is trust: a library
13+
that silently exfiltrates usage from people's agents is the opposite of a tool you
14+
hand a budget to. Runtime telemetry in OSS dev tools reliably triggers backlash
15+
and forks. So we measure adoption only from **public, outside-in signals** — the
16+
ones below — and accept that they're coarser than telemetry. That trade is correct
17+
for this project.
18+
19+
## The signals we track
20+
21+
| Runbook KPI | How it's measured | Source |
22+
|---|---|---|
23+
| Star velocity | GitHub stargazers over time | `scripts/metrics.py` → GitHub API |
24+
| Fork count | GitHub forks | `scripts/metrics.py` |
25+
| Install velocity (`pip install`) | PyPI download counts (day/week/month) | `scripts/metrics.py` → pypistats.org |
26+
| README reach | repo views & unique clones | `scripts/metrics.py` (needs `GITHUB_TOKEN`) |
27+
| README → site click-through | UTM params on outbound links | Floe web analytics |
28+
| Install → hosted signup | UTM `utm_source=floe-guard` lands in the signup funnel | Floe web analytics |
29+
| "Built with floe-guard" usage | badge backlinks | GitHub code/badge search |
30+
31+
### Snapshot script
32+
33+
```bash
34+
python scripts/metrics.py # stars, forks, PyPI downloads
35+
GITHUB_TOKEN=ghp_xxx python scripts/metrics.py # + repo views/clones
36+
```
37+
38+
It prints real numbers only — if a source is unreachable it says so rather than
39+
guessing. Run it on a schedule and diff snapshots to get *velocity* (the runbook's
40+
actual target), not just totals.
41+
42+
### UTM scheme
43+
44+
Outbound links in the README carry:
45+
46+
```
47+
?utm_source=floe-guard&utm_medium=readme&utm_campaign=oss
48+
```
49+
50+
So clicks from this repo into `dev-dashboard.floelabs.xyz` / `floelabs.xyz` are
51+
attributable in Floe's web analytics, and any session that converts to a hosted
52+
signup keeps `utm_source=floe-guard` — that's the **install → signup** handoff the
53+
runbook calls the critical metric. The conversion side lives in the Floe web app's
54+
funnel, not in this repo.
55+
56+
### "Built with floe-guard" badge
57+
58+
Downstream projects can advertise usage with the badge in the README's *Built with
59+
floe-guard* section. Each embed is a backlink — searchable on GitHub — and is the
60+
honest version of a "powered by" usage signal (opt-in, public, no tracking).
61+
62+
## What's still manual
63+
64+
- Posting the launch content and seeding (the runbook's Week-1/2 GTM motion).
65+
- Wiring the UTM-tagged sessions into a dashboard on the Floe web side.

docs/stop-the-loop.gif

110 KB
Loading

docs/stop-the-loop.gif.PLACEHOLDER.txt

Lines changed: 0 additions & 6 deletions
This file was deleted.

scripts/metrics.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Pull floe-guard's public adoption metrics into a single snapshot.
2+
3+
No dependencies — stdlib only. Reads public APIs:
4+
- GitHub repo stats (stars, forks, watchers) — no auth needed
5+
- GitHub traffic (views, clones) — needs a token with push access
6+
- PyPI downloads (last day / week / month) — no auth needed (pypistats.org)
7+
8+
Usage:
9+
python scripts/metrics.py
10+
GITHUB_TOKEN=ghp_xxx python scripts/metrics.py # also fetch traffic (views/clones)
11+
12+
These are the runbook KPIs (star velocity, forks, install velocity). Run it on a
13+
schedule and diff snapshots to get velocity. It only reports real numbers — if a
14+
source is unreachable it says so rather than guessing.
15+
"""
16+
from __future__ import annotations
17+
18+
import json
19+
import os
20+
import urllib.error
21+
import urllib.request
22+
23+
REPO = "Floe-Labs/floe-guard"
24+
PACKAGE = "floe-guard"
25+
26+
27+
def _get(
28+
url: str, headers: dict[str, str] | None = None, timeout: float = 15.0
29+
) -> dict[str, object]:
30+
req = urllib.request.Request(url, headers=headers or {})
31+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (trusted hosts)
32+
return json.loads(resp.read().decode())
33+
34+
35+
def github_repo() -> dict[str, object]:
36+
try:
37+
d = _get(
38+
f"https://api.github.com/repos/{REPO}",
39+
headers={"Accept": "application/vnd.github+json", "User-Agent": "floe-guard-metrics"},
40+
)
41+
return {
42+
"stars": d.get("stargazers_count"),
43+
"forks": d.get("forks_count"),
44+
"watchers": d.get("subscribers_count"),
45+
"open_issues": d.get("open_issues_count"),
46+
}
47+
except (urllib.error.URLError, ValueError) as e:
48+
return {"error": f"github repo stats unavailable: {e}"}
49+
50+
51+
def github_traffic() -> dict[str, object]:
52+
token = os.environ.get("GITHUB_TOKEN", "").strip()
53+
if not token:
54+
return {"note": "set GITHUB_TOKEN (push access) to fetch views/clones"}
55+
hdr = {
56+
"Authorization": f"Bearer {token}",
57+
"Accept": "application/vnd.github+json",
58+
"User-Agent": "floe-guard-metrics",
59+
}
60+
out: dict[str, object] = {}
61+
for kind in ("views", "clones"):
62+
try:
63+
d = _get(f"https://api.github.com/repos/{REPO}/traffic/{kind}", headers=hdr)
64+
out[kind] = {"count": d.get("count"), "uniques": d.get("uniques")}
65+
except (urllib.error.URLError, ValueError) as e:
66+
out[kind] = {"error": str(e)}
67+
return out
68+
69+
70+
def pypi_downloads() -> dict[str, object]:
71+
try:
72+
d = _get(
73+
f"https://pypistats.org/api/packages/{PACKAGE}/recent",
74+
headers={"User-Agent": "floe-guard-metrics"},
75+
)
76+
return d.get("data", {"error": "no data field"})
77+
except (urllib.error.URLError, ValueError) as e:
78+
return {"error": f"pypistats unavailable (new packages take ~1 day to appear): {e}"}
79+
80+
81+
def main() -> None:
82+
snapshot = {
83+
"repo": REPO,
84+
"package": PACKAGE,
85+
"github": github_repo(),
86+
"traffic": github_traffic(),
87+
"pypi_downloads_recent": pypi_downloads(),
88+
}
89+
print(json.dumps(snapshot, indent=2))
90+
91+
92+
if __name__ == "__main__":
93+
main()

0 commit comments

Comments
 (0)