use std::collections::HashMap;
use serde_json::{json, Map, Value};
use crate::config::{RumConfig, SDK_VERSION};
use crate::context::RumContextSnapshot;
use crate::event::RumEvent;
pub(crate) fn build_payloads(config: &RumConfig, events: Vec<RumEvent>) -> Vec<Value> {
let mut groups: HashMap<RumContextSnapshot, Vec<Value>> = HashMap::new();
for event in events {
groups
.entry(event.context().clone())
.or_default()
.push(event.to_json());
}
groups
.into_iter()
.map(|(context, events)| build_payload(config, context, events))
.collect()
}
fn build_payload(config: &RumConfig, context: RumContextSnapshot, events: Vec<Value>) -> Value {
let events = events
.into_iter()
.map(|event| add_event_context(config, &context, event))
.collect::<Vec<_>>();
json!({
"app": {
"id": config.app_id,
"env": config.env.as_str(),
"version": config.app_version.as_deref().unwrap_or("-"),
"name": config.app_name.as_deref().unwrap_or("-"),
"type": config.app_type,
},
"device": {
"id": config.utdid.as_deref().unwrap_or("-"),
"type": config.device_type,
"name": config.device_name.as_deref().unwrap_or("unknown"),
"model": config.device_model.as_deref().unwrap_or("unknown"),
"brand": config.device_brand.as_deref().unwrap_or("unknown"),
},
"os": {
"type": config.os_type,
"user_agent": format!("alibabacloud_rum/{SDK_VERSION}"),
"version": config.os_version.as_deref().unwrap_or("unknown"),
},
"_v": SDK_VERSION,
"events": events,
})
}
fn add_event_context(config: &RumConfig, context: &RumContextSnapshot, mut event: Value) -> Value {
let Value::Object(event_object) = &mut event else {
return event;
};
let mut user = Map::new();
user.insert(
"id".to_string(),
json!(config
.user_id
.as_deref()
.or(config.utdid.as_deref())
.unwrap_or("-")),
);
if let Some(value) = &config.user_name {
user.insert("name".to_string(), json!(value));
}
if let Some(value) = &config.user_tags {
user.insert("tags".to_string(), json!(value));
}
event_object.insert("user".to_string(), Value::Object(user));
event_object.insert(
"session".to_string(),
json!({
"id": context.session_id,
}),
);
event_object.insert(
"view".to_string(),
json!({
"id": context.view_id,
"name": context.view_name,
}),
);
event_object.insert(
"net".to_string(),
json!({
"device_model": config.net_device_model,
}),
);
let properties = properties(config, event_object);
event_object.insert("properties".to_string(), properties);
event
}
fn properties(config: &RumConfig, event_object: &Map<String, Value>) -> Value {
let mut properties = Map::new();
for (key, value) in &config.properties {
properties.insert(key.clone(), json!(value));
}
if let Some(Value::Object(event_properties)) = event_object.get("properties") {
for (key, value) in event_properties {
properties.insert(key.clone(), value.clone());
}
}
Value::Object(properties)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::default_platform_type;
use crate::event::ViewEvent;
#[test]
fn payload_uses_context_snapshot() {
let config = RumConfig::builder()
.config_address("http://127.0.0.1/rum")
.app_id("app")
.property("channel", "test")
.build()
.unwrap();
let context = RumContextSnapshot {
session_id: "s1".to_string(),
view_id: "v1".to_string(),
view_name: "main-view".to_string(),
};
let payloads = build_payloads(&config, vec![RumEvent::View(ViewEvent::initial(context))]);
assert_eq!(payloads.len(), 1);
assert_eq!(payloads[0]["app"]["id"], "app");
assert_eq!(payloads[0]["app"]["type"], default_platform_type());
assert_eq!(payloads[0]["device"]["type"], default_platform_type());
assert_eq!(payloads[0]["os"]["type"], default_platform_type());
assert_eq!(payloads[0]["events"][0]["session"]["id"], "s1");
assert_eq!(payloads[0]["events"][0]["view"]["id"], "v1");
assert_eq!(payloads[0]["events"][0]["properties"]["channel"], "test");
}
#[test]
fn payloads_are_grouped_by_context_snapshot() {
let config = RumConfig::builder()
.config_address("http://127.0.0.1/rum")
.app_id("app")
.build()
.unwrap();
let first_context = RumContextSnapshot {
session_id: "s1".to_string(),
view_id: "v1".to_string(),
view_name: "main-view".to_string(),
};
let second_context = RumContextSnapshot {
session_id: "s2".to_string(),
view_id: "v1".to_string(),
view_name: "main-view".to_string(),
};
let payloads = build_payloads(
&config,
vec![
RumEvent::View(ViewEvent::initial(first_context.clone())),
RumEvent::View(ViewEvent::initial(second_context.clone())),
RumEvent::View(ViewEvent::initial(first_context)),
],
);
assert_eq!(payloads.len(), 2);
for payload in payloads {
let events = payload["events"].as_array().unwrap();
let session_id = events[0]["session"]["id"].as_str().unwrap();
let event_count = events.len();
match session_id {
"s1" => assert_eq!(event_count, 2),
"s2" => assert_eq!(event_count, 1),
other => panic!("unexpected session id: {other}"),
}
}
}
#[test]
fn event_properties_are_merged_with_config_properties() {
let config = RumConfig::builder()
.config_address("http://127.0.0.1/rum")
.app_id("app")
.property("channel", "config")
.property("shared", "config")
.build()
.unwrap();
let context = RumContextSnapshot {
session_id: "s1".to_string(),
view_id: "v1".to_string(),
view_name: "main-view".to_string(),
};
let event = json!({
"event_type": "custom",
"properties": {
"sku": "123",
"shared": "event"
}
});
let event = add_event_context(&config, &context, event);
assert_eq!(event["properties"]["channel"], "config");
assert_eq!(event["properties"]["sku"], "123");
assert_eq!(event["properties"]["shared"], "event");
}
#[test]
fn non_object_event_properties_are_ignored() {
let config = RumConfig::builder()
.config_address("http://127.0.0.1/rum")
.app_id("app")
.property("channel", "config")
.build()
.unwrap();
let context = RumContextSnapshot {
session_id: "s1".to_string(),
view_id: "v1".to_string(),
view_name: "main-view".to_string(),
};
let event = json!({
"event_type": "custom",
"properties": "invalid"
});
let event = add_event_context(&config, &context, event);
assert_eq!(event["properties"]["channel"], "config");
assert_eq!(event["properties"].as_object().unwrap().len(), 1);
}
}