Skip to main content

roder_usage_analytics/
sink.rs

1//! Passive runtime recorder: an `EventSink` extension that projects
2//! emitted runtime events into the analytics store.
3//!
4//! Registered through the standard extension registry, it rides the
5//! runtime's bounded per-sink dispatch (roadmap phase 64): recording can
6//! never block tool execution, provider requests, or turn progress, and a
7//! broken store surfaces as redacted `extension.event_sink_failed` events.
8//! Disabling analytics simply means not registering this extension.
9
10use std::sync::Arc;
11
12use roder_api::capabilities::CapabilityRequest;
13use roder_api::events::EventEnvelope;
14use roder_api::extension::{
15    EventSink, EventSinkId, ExtensionManifest, ExtensionRegistryBuilder, ProvidedService,
16    RoderExtension,
17};
18use semver::Version;
19
20use crate::ingest::AnalyticsIngestor;
21use crate::store::AnalyticsStore;
22
23pub const ANALYTICS_EXTENSION_ID: &str = "roder-usage-analytics";
24pub const ANALYTICS_SINK_ID: &str = "usage-analytics";
25
26pub struct UsageAnalyticsSink {
27    store: Arc<AnalyticsStore>,
28}
29
30impl UsageAnalyticsSink {
31    pub fn new(store: Arc<AnalyticsStore>) -> Self {
32        Self { store }
33    }
34}
35
36#[async_trait::async_trait]
37impl EventSink for UsageAnalyticsSink {
38    fn id(&self) -> EventSinkId {
39        ANALYTICS_SINK_ID.to_string()
40    }
41
42    async fn handle_event(&self, envelope: &EventEnvelope) -> anyhow::Result<()> {
43        AnalyticsIngestor::new(&self.store).ingest_event(envelope)
44    }
45}
46
47/// Extension wrapper so hosts can install analytics via
48/// `DefaultRegistryConfig::extra_extensions` or a plain registry builder.
49pub struct UsageAnalyticsExtension {
50    store: Arc<AnalyticsStore>,
51}
52
53impl UsageAnalyticsExtension {
54    pub fn new(store: Arc<AnalyticsStore>) -> Self {
55        Self { store }
56    }
57}
58
59impl RoderExtension for UsageAnalyticsExtension {
60    fn manifest(&self) -> ExtensionManifest {
61        ExtensionManifest {
62            id: ANALYTICS_EXTENSION_ID.to_string(),
63            name: "Local Usage Analytics".to_string(),
64            version: Version::new(0, 1, 0),
65            api_version: "0.1.0".to_string(),
66            description: Some(
67                "Projects local runtime events into the SQLite usage-analytics store.".to_string(),
68            ),
69            provides: vec![ProvidedService::EventSink(ANALYTICS_SINK_ID.to_string())],
70            required_capabilities: vec![CapabilityRequest::new("events.read.all")],
71        }
72    }
73
74    fn install(&self, registry: &mut ExtensionRegistryBuilder) -> anyhow::Result<()> {
75        registry.event_sink(Arc::new(UsageAnalyticsSink::new(self.store.clone())));
76        Ok(())
77    }
78}