app-server-test-client/src/plugin_analytics_capture.rs
102 lines
use anyhow::Context;use anyhow::Result;use anyhow::bail;use serde_json::Value;use std::fs;use std::io;use std::path::Path;pub(super) fn read_events_for_remote_plugin( path: &Path, remote_plugin_id: &str,) -> Result<Vec<Value>> { let contents = match fs::read_to_string(path) { Ok(contents) => contents, Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), Err(err) => { return Err(err).with_context(|| format!("read capture file {}", path.display())); } }; let mut matching = Vec::new(); for (index, line) in contents.lines().enumerate() { if line.trim().is_empty() { continue; } let payload: Value = serde_json::from_str(line).with_context(|| { format!( "parse analytics capture line {} from {}", index + 1, path.display() ) })?; let events = payload["events"] .as_array() .context("analytics capture payload is missing events")?; matching.extend( events .iter() .filter(|event| event["event_params"]["plugin_id"] == remote_plugin_id) .cloned(), ); } Ok(matching)}pub(super) struct PluginEventIdentity<'a> { pub(super) plugin_id: &'a str, pub(super) plugin_name: &'a str, pub(super) marketplace_name: &'a str,}pub(super) fn validate_mutation_events( events: Vec<Value>, expected: PluginEventIdentity<'_>,) -> Result<Vec<Value>> { let event_type = "codex_plugin_installed"; let matching = events .iter() .filter(|event| event["event_type"] == event_type) .collect::<Vec<_>>(); let [event] = matching.as_slice() else { bail!( "expected exactly one `{event_type}` event for `{}`, found {}", expected.plugin_id, matching.len() ); }; validate_event(event, &expected)?; Ok(vec![(*event).clone()])}fn validate_event(event: &Value, expected: &PluginEventIdentity<'_>) -> Result<()> { let params = &event["event_params"]; require_string(params, "plugin_id", expected.plugin_id)?; require_string(params, "plugin_name", expected.plugin_name)?; require_string(params, "marketplace_name", expected.marketplace_name)?; for field in [ "has_skills", "mcp_server_count", "connector_ids", "product_client_id", ] { if params.get(field).is_none_or(Value::is_null) { bail!( "{} event has null or missing `{field}`", event["event_type"] ); } } Ok(())}fn require_string(params: &Value, field: &str, expected: &str) -> Result<()> { let actual = params.get(field).and_then(Value::as_str); if actual != Some(expected) { bail!("expected `{field}` to be `{expected}`, got {actual:?}"); } Ok(())}#[cfg(test)]#[path = "plugin_analytics_capture_tests.rs"]mod tests;