chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
knotless-serve: The delegation funnel follows the owner the gateway writes for
crates/knotless/src/usage.rs+2−1
| @@ -31,7 +31,8 @@ pub struct Report { | ||
/// What the handlers saw, by name and subject: `mcp.tool` by tool, | ||
/// `mcp.signin`, `git.fetch` by repository (a clone or a fetch, as | ||
/// asked for: `<handle or did>/<name>`), `cli.version` by version, | ||
/// `gateway.delegated`, `gateway.push` and `gateway.commit`. | ||
/// `gateway.delegated`, `gateway.push`, `gateway.commit`, and | ||
/// `gateway.served` by the owner the gateway wrote for. | ||
#[serde(default)] | ||
pub events: Vec<Event>, | ||
/// Of the accounts that did one thing in the days asked, not having | ||
crates/knotless-serve/src/gateway.rs+10−0
| @@ -661,6 +661,12 @@ pub async fn push( | ||
.map_err(written)?; | ||
report.extend(refused); | ||
app.event("gateway.push", "", true, started.elapsed(), Some(&caller)); | ||
// By the owner it wrote for, whoever asked: what delegating led to, | ||
// once a ref moved. | ||
if report.iter().any(|(_, res)| res.is_ok()) { | ||
let owner = Some(remote.did()); | ||
app.event("gateway.served", "", true, started.elapsed(), owner); | ||
} | ||
Ok(Json(PushResults { | ||
results: report | ||
.into_iter() | ||
| @@ -721,6 +727,10 @@ pub async fn commit( | ||
let answered = answer.await.expect("every queued commit is answered"); | ||
let ok = answered.is_ok(); | ||
app.event("gateway.commit", "", ok, started.elapsed(), Some(&caller)); | ||
if ok { | ||
let owner = Some(remote.did()); | ||
app.event("gateway.served", "", true, started.elapsed(), owner); | ||
} | ||
answered.map(Json) | ||
} | ||
| ||
crates/knotless-serve/src/usage.rs+4−4
| @@ -77,7 +77,7 @@ pub struct Usage { | ||
/// The funnels the report follows: of the accounts that first did the one | ||
/// in the days asked, how many went on to the other. | ||
const FUNNELS: [(&str, &str); 2] = [ | ||
("gateway.delegated", "gateway.push"), | ||
("gateway.delegated", "gateway.served"), | ||
("mcp.signin", "mcp.tool"), | ||
]; | ||
| ||
| @@ -904,7 +904,7 @@ mod tests { | ||
for (d, name, did) in [ | ||
(ago(40), "gateway.delegated", "did:a"), | ||
(ago(40), "gateway.delegated", "did:b"), | ||
(ago(39), "gateway.push", "did:a"), | ||
(ago(39), "gateway.served", "did:a"), | ||
(ago(35), "gateway.delegated", "did:a"), | ||
(ago(29), "gateway.delegated", "did:c"), | ||
] { | ||
| @@ -919,7 +919,7 @@ mod tests { | ||
// Opened again, as each deploy does: nothing moves twice. | ||
drop(usage); | ||
let usage = Usage::open(&path).unwrap(); | ||
usage.event("gateway.push", "", true, Duration::ZERO, Some("did:c")); | ||
usage.event("gateway.served", "", true, Duration::ZERO, Some("did:c")); | ||
usage.event("gateway.delegated", "", true, Duration::ZERO, Some("did:c")); | ||
usage.event("gateway.delegated", "", true, Duration::ZERO, Some("did:d")); | ||
usage.flush().unwrap(); | ||
| @@ -956,7 +956,7 @@ mod tests { | ||
[( | ||
ago(40), | ||
"gateway.delegated".into(), | ||
"gateway.push".into(), | ||
"gateway.served".into(), | ||
2, | ||
1 | ||
)] | ||
crates/knotless-serve/tests/gateway.rs+19−1
| @@ -6,6 +6,7 @@ mod common; | ||
use common::*; | ||
| ||
use knotless_serve::gateway::Gateway; | ||
use knotless_serve::usage::Usage; | ||
use knotless_serve::{App, router}; | ||
use serde_json::{Value, json}; | ||
use std::sync::atomic::Ordering; | ||
| @@ -33,6 +34,8 @@ struct Server { | ||
bots: std::path::PathBuf, | ||
did: String, | ||
app: Arc<App>, | ||
/// Where it counts its usage. | ||
usage: std::path::PathBuf, | ||
_rt: tokio::runtime::Runtime, | ||
_proxy: TlsProxy, | ||
_sandbox: Sandbox, | ||
| @@ -91,7 +94,9 @@ fn gateway_run(bot: Option<&Account>, indexed: bool, ttl: Duration, admins: &str | ||
s.set("KNOTLESS_APPVIEW", &base); | ||
} | ||
let index = indexed.then(|| knotless_serve::index::Index::open(&s.path("index.db")).unwrap()); | ||
let app = App::build(ctx(&s), ttl, Some(gw), index); | ||
let mut app = App::build(ctx(&s), ttl, Some(gw), index); | ||
let usage = Usage::open(&s.path("usage.db")).unwrap(); | ||
Arc::get_mut(&mut app).unwrap().usage = Some(Arc::new(usage)); | ||
let router = router(app.clone()); | ||
rt.spawn(async move { axum::serve(listener, router).await.unwrap() }); | ||
// As `knotless serve` counts what repositories are written in: at once | ||
| @@ -106,12 +111,21 @@ fn gateway_run(bot: Option<&Account>, indexed: bool, ttl: Duration, admins: &str | ||
bots, | ||
did, | ||
app, | ||
usage: s.path("usage.db"), | ||
_rt: rt, | ||
_proxy: proxy, | ||
_sandbox: s, | ||
} | ||
} | ||
| ||
/// Whether the gateway has told `gateway.served` for the owner `did`. | ||
fn served(server: &Server, did: &str) -> bool { | ||
server.app.usage.as_ref().unwrap().flush().unwrap(); | ||
let db = rusqlite::Connection::open(&server.usage).unwrap(); | ||
let query = "SELECT count(*) FROM account WHERE name = 'gateway.served' AND did = ?1"; | ||
db.query_row(query, [did], |r| r.get::<_, u64>(0)).unwrap() > 0 | ||
} | ||
| ||
fn web(mut s: Sandbox) -> Sandbox { | ||
s.set("KNOTLESS_DID_WEB_SCHEME", "http"); | ||
s | ||
| @@ -262,8 +276,12 @@ fn protected_branches_hold_collaborators() { | ||
); | ||
let err = refused(&collab, &clone, "main").unwrap(); | ||
assert!(err.contains("no merge commits"), "{err}"); | ||
// A push that moved nothing served the owner nothing. | ||
assert!(!served(&gw, &owner.account.did)); | ||
// Nor by way of a branch that took it: what counts is what main gains. | ||
assert_eq!(refused(&collab, &clone, "main:refs/heads/feature"), None); | ||
// One that moved a ref did, though a collaborator pushed it. | ||
assert!(served(&gw, &owner.account.did)); | ||
let err = refused(&collab, &clone, "main").unwrap(); | ||
assert!(err.contains("no merge commits"), "{err}"); | ||
// A new protected branch takes none in all its history. | ||
crates/knotless-serve/tests/usage.rs+2−1
| @@ -159,6 +159,7 @@ fn hits_requests_and_the_network() { | ||
} | ||
app.event("gateway.delegated", "", true, Duration::ZERO, Some("did:x")); | ||
app.event("gateway.push", "", true, ms(40), Some("did:x")); | ||
app.event("gateway.served", "", true, ms(40), Some("did:x")); | ||
let (base, mut operator) = appview.serve(&rt, app); | ||
| ||
let http = reqwest::Client::new(); | ||
| @@ -285,7 +286,7 @@ fn hits_requests_and_the_network() { | ||
assert_eq!( | ||
funnels, | ||
[ | ||
("gateway.delegated", "gateway.push", 1, 1), | ||
("gateway.delegated", "gateway.served", 1, 1), | ||
("mcp.signin", "mcp.tool", 2, 1) | ||
] | ||
); | ||
deploy/README.md+7−6
| @@ -201,12 +201,13 @@ KNOTLESS_APPVIEW=https://knotless.dev knotless admin stats | ||
last 30: `mcp.tool` by tool and `mcp.signin`; `web.login`; | ||
`git.fetch` by repository, once per clone or fetch, as asked for | ||
(`<handle or did>/<name>`); `cli.version` by the CLI's version; | ||
`gateway.delegated`, `gateway.push` and `gateway.commit`. | ||
`gateway.delegated`, `gateway.push`, `gateway.commit`, and | ||
`gateway.served`, by the owner the gateway pushed or committed for. | ||
- **Funnels**: of the accounts that delegated to the gateway in the days | ||
asked, not having in the 30 days before, how many have pushed through | ||
it then or since; of those that signed in to MCP so, how many have | ||
called a tool. A day older than 30 is counted as it stood once 30 days | ||
had passed. | ||
asked, not having in the 30 days before, how many the gateway has | ||
written for then or since, by anyone's push or commit that moved a | ||
ref; of those that signed in to MCP so, how many have called a tool. | ||
A day older than 30 is counted as it stood once 30 days had passed. | ||
- **Hours**: each hour's requests, how many failed and how many took over | ||
two seconds, to know when to read the [logs](#logs) of. The lines show | ||
only the hours with either; `--json` has every hour. | ||
| @@ -247,7 +248,7 @@ event cli.version 0.1.0 2210 calls, 0 failed, 0ms mean, 0 accounts | ||
event git.fetch alice.test/proj 140 calls, 0 failed, 0ms mean, 0 accounts | ||
event mcp.tool read_file 612 calls, 9 failed, 85ms mean, 6 accounts | ||
hour 2026-10-23T14 1022 requests, 3 failed, 2 over 2s | ||
funnel gateway.delegated -> gateway.push 4 started, 3 reached | ||
funnel gateway.delegated -> gateway.served 4 started, 3 reached | ||
funnel mcp.signin -> mcp.tool 7 started, 6 reached | ||
page /[authority]/[repo] 388 | ||
referrer bsky.app 52 | ||
lexicons/dev/knotless/getUsage.json+2−1
| @@ -307,7 +307,8 @@ | ||
"cli.version", | ||
"gateway.delegated", | ||
"gateway.push", | ||
"gateway.commit" | ||
"gateway.commit", | ||
"gateway.served" | ||
] | ||
}, | ||
"subject": { | ||