mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
"door" stood for several different things across this fork — the ingest
endpoint cloud serves at POST /v1/event, the replay endpoint that produces the
session-replay topic, a code path that is not an address at all, the importer
that pulls a heavy package onto the startup path — so a reader had to work out
which one each comment meant.
Every metaphor site now says what it is. Measured over the worktree:
before 93 occurrences of (?i)\bdoors?\b (89 of \bdoor\b)
after 3
90 uses replaced, by sense:
endpoint/endpoints 49 an HTTP address that accepts requests — cloud's
/v1/event and /v1/replay, this deployment's /v1/flags,
the capture routes at /v1/e, /v1/s and /v1/ai
path/paths 13 a code path that is not an address: the two ways a
report reaches PENDING_INPUT, the three ways a team
gets created, a second route around an access check
ingest 6 where the referent is the ingest tier itself
importer 4 the module that pulls a package onto the Django
startup path
entry point, starting point, registration point, nav entry, landing page,
"on entry" and "another way" carry the remaining one-off senses
Three sites keep the word, deliberately:
- insights/api/test/test_flags.py — test_the_door_is_not_under_api is a test
function name, so it is left alone.
- insights/temporal/README.md — a blockquote of upstream prose. Editing a
quotation misrepresents its source.
- products/warehouse_sources/.../pretix/canonical_descriptions.py — a Pretix
check-in list admits attendees at a literal door. Not a metaphor.
Verification:
python3 -m py_compile the 30 changed .py files, exit 0
yaml.safe_load .hanzo/workflows/deploy.yml parses
every changed .rs/.ts/.tsx line is a //, ///, //! or /** comment, bar three
balanced string literals — a jest description, a setFlagsUnavailable reason,
and a Rust assert message
The pre-commit formatters did not run: bin/insightscli needs a project venv this
checkout has none of, so it exits 127 on `python: not found` with nothing staged
at all. Neither ruff nor prettier nor rustfmt rewraps comment text, and ruff
ignores E501, so no changed line is one they would have altered.
225 lines
6.9 KiB
Rust
225 lines
6.9 KiB
Rust
//! The ingest endpoint's refusal contract: a caller can tell success from oblivion, and an
|
|
//! operator can see who is being turned away.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use async_trait::async_trait;
|
|
use axum_test_helper::TestClient;
|
|
use capture::{
|
|
config::{AiRouting, CaptureMode},
|
|
prometheus::setup_metrics_recorder,
|
|
quota_limiters::CaptureQuotaLimiter,
|
|
router::router,
|
|
sinks::print::PrintSink,
|
|
team::{StaticTeamResolver, TeamResolveError, TeamResolver},
|
|
time::SystemTime,
|
|
warnings::Warnings,
|
|
};
|
|
use common_redis::MockRedisClient;
|
|
use common_types::TeamId;
|
|
use limiters::token_dropper::TokenDropper;
|
|
use metrics_exporter_prometheus::PrometheusHandle;
|
|
use once_cell::sync::Lazy;
|
|
use serde_json::Value;
|
|
|
|
#[path = "./common/utils.rs"]
|
|
mod test_utils;
|
|
use test_utils::DEFAULT_CONFIG;
|
|
|
|
/// One recorder per test binary; `render()` is a cumulative scrape.
|
|
static METRICS: Lazy<PrometheusHandle> =
|
|
Lazy::new(|| setup_metrics_recorder(String::from("test"), "events"));
|
|
|
|
/// Resolves nothing: every key is forged, unknown, or revoked.
|
|
struct NoTeamResolver;
|
|
|
|
#[async_trait]
|
|
impl TeamResolver for NoTeamResolver {
|
|
async fn resolve(&self, _token: &str) -> Result<TeamId, TeamResolveError> {
|
|
Err(TeamResolveError::Unknown)
|
|
}
|
|
}
|
|
|
|
fn client(mode: CaptureMode, team_resolver: Option<Arc<dyn TeamResolver>>) -> TestClient {
|
|
let redis = Arc::new(MockRedisClient::new());
|
|
let mut cfg = DEFAULT_CONFIG.clone();
|
|
cfg.capture_mode = mode;
|
|
let quota_limiter = CaptureQuotaLimiter::new(&cfg, redis.clone(), Duration::from_secs(60));
|
|
|
|
TestClient::new(router(
|
|
SystemTime {},
|
|
HealthRegistry::new("refusal"),
|
|
PrintSink {},
|
|
redis,
|
|
None,
|
|
quota_limiter,
|
|
TokenDropper::default(),
|
|
None,
|
|
false, // metrics: the recorder is installed once by METRICS
|
|
mode,
|
|
String::from("capture"),
|
|
None,
|
|
25 * 1024 * 1024,
|
|
false,
|
|
1_i64,
|
|
false,
|
|
0.0_f32,
|
|
26_214_400,
|
|
None,
|
|
Some(10),
|
|
None,
|
|
256,
|
|
team_resolver,
|
|
Arc::new(Warnings::default()),
|
|
))
|
|
}
|
|
|
|
async fn post(client: &TestClient, path: &str, body: &str) -> (u16, Option<String>, String) {
|
|
let res = client
|
|
.post(path)
|
|
.body(body.to_owned())
|
|
.header("Content-Type", "application/json")
|
|
.header("X-Forwarded-For", "127.0.0.1")
|
|
.send()
|
|
.await;
|
|
let status = res.status().as_u16();
|
|
let content_type = res
|
|
.headers()
|
|
.get("content-type")
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(str::to_owned);
|
|
(status, content_type, res.text().await)
|
|
}
|
|
|
|
const KEYLESS: &str = r#"{"event":"$pageview","distinct_id":"d1"}"#;
|
|
const KEYED: &str = r#"{"event":"$pageview","distinct_id":"d1","api_key":"pk-real"}"#;
|
|
|
|
#[tokio::test]
|
|
async fn keyless_analytics_is_refused_by_name() {
|
|
Lazy::force(&METRICS);
|
|
let c = client(
|
|
CaptureMode::Events,
|
|
Some(Arc::new(StaticTeamResolver::new(1))),
|
|
);
|
|
|
|
let (status, content_type, body) = post(&c, "/v1/e", KEYLESS).await;
|
|
|
|
// Not a 200 with a silent drop: a named refusal a client can branch on.
|
|
assert_eq!(status, 401);
|
|
assert_eq!(content_type.as_deref(), Some("application/json"));
|
|
|
|
let body: Value = serde_json::from_str(&body).expect("refusal must be JSON");
|
|
assert_eq!(body["code"], "ingest_key_required");
|
|
assert_eq!(body["status"], 401);
|
|
assert!(body["error"].is_string());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn keyless_recordings_is_refused_by_the_same_name() {
|
|
Lazy::force(&METRICS);
|
|
let c = client(
|
|
CaptureMode::Recordings,
|
|
Some(Arc::new(StaticTeamResolver::new(1))),
|
|
);
|
|
|
|
let (status, _, body) = post(&c, "/v1/s", KEYLESS).await;
|
|
|
|
assert_eq!(status, 401);
|
|
let body: Value = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(body["code"], "ingest_key_required");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_key_that_names_no_project_is_refused_apart_from_a_missing_one() {
|
|
Lazy::force(&METRICS);
|
|
let c = client(CaptureMode::Events, Some(Arc::new(NoTeamResolver)));
|
|
|
|
let (status, _, body) = post(&c, "/v1/e", KEYED).await;
|
|
|
|
// 403, not 401: this key will never work, so retrying with it is pointless.
|
|
assert_eq!(status, 403);
|
|
let body: Value = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(body["code"], "ingest_key_unknown");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_key_that_resolves_still_passes() {
|
|
Lazy::force(&METRICS);
|
|
let c = client(
|
|
CaptureMode::Events,
|
|
Some(Arc::new(StaticTeamResolver::new(42))),
|
|
);
|
|
|
|
let (status, _, body) = post(&c, "/v1/e", KEYED).await;
|
|
|
|
assert_eq!(status, 200, "a resolvable key must not be refused: {body}");
|
|
assert!(
|
|
!body.contains("ingest_key"),
|
|
"success carries no refusal code"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disabling_the_allowlist_admits_an_unresolvable_key_but_still_refuses_a_keyless_one() {
|
|
Lazy::force(&METRICS);
|
|
// `None` is what config `enforce_token_allowlist=false` wires in server.rs.
|
|
let c = client(CaptureMode::Events, None);
|
|
|
|
let (status, _, _) = post(&c, "/v1/e", KEYED).await;
|
|
assert_eq!(status, 200, "allow-list off admits a shape-valid key");
|
|
|
|
let (status, _, body) = post(&c, "/v1/e", KEYLESS).await;
|
|
assert_eq!(status, 401, "the switch never re-opens the keyless lane");
|
|
let body: Value = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(body["code"], "ingest_key_required");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn refusals_are_visible_to_an_operator() {
|
|
Lazy::force(&METRICS);
|
|
|
|
let keyless = client(
|
|
CaptureMode::Events,
|
|
Some(Arc::new(StaticTeamResolver::new(1))),
|
|
);
|
|
post(&keyless, "/v1/e", KEYLESS).await;
|
|
|
|
let unknown = client(CaptureMode::Events, Some(Arc::new(NoTeamResolver)));
|
|
post(&unknown, "/v1/e", KEYED).await;
|
|
|
|
let scrape = METRICS.render();
|
|
|
|
// Both refusal kinds are counted, attributed to the endpoint that refused.
|
|
assert!(
|
|
scrape.contains(r#"capture_ingestion_warnings_total{"#)
|
|
&& scrape.contains(r#"type="ingest_key_required""#),
|
|
"a keyless refusal must be countable:\n{scrape}"
|
|
);
|
|
assert!(
|
|
scrape.contains(r#"type="ingest_key_unknown""#),
|
|
"an unknown-key refusal must be countable:\n{scrape}"
|
|
);
|
|
assert!(
|
|
scrape.contains(r#"source="analytics""#),
|
|
"the refusing endpoint must be attributed:\n{scrape}"
|
|
);
|
|
assert!(
|
|
scrape.contains(r#"outcome="emitted""#),
|
|
"the first refusal of a key must not be throttled away:\n{scrape}"
|
|
);
|
|
|
|
// Volume is counted separately from the throttled per-key warning, so a
|
|
// throttled log never hides how much traffic is being turned away.
|
|
assert!(
|
|
scrape.contains("capture_events_dropped_total"),
|
|
"refused events must still be counted:\n{scrape}"
|
|
);
|
|
|
|
// The credential itself is never scraped.
|
|
assert!(
|
|
!scrape.contains("pk-real"),
|
|
"a refused credential must not reach the metrics plane:\n{scrape}"
|
|
);
|
|
}
|