~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

knotless-serve: The delegation funnel follows the owner the gateway writes for

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:gateway-served-funnel
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": {