use std::fmt;
use std::sync::{Mutex, PoisonError};
use std::time::Duration;
use futures::future::join4;
use serde_json::{Map, Value};
use tokio::task::JoinError;
use super::tags::validate_tags;
use super::{EventObject, set_field};
use crate::capture::panic_message;
use crate::config::{HookError, UserIdentity};
use crate::constants::AGENTCAT_TAG_SESSION_SOURCE;
use crate::handles::session_from_hook_value;
use crate::hooks::{AbortOnDrop, PendingHooks};
#[cfg(test)]
mod tests;
pub(crate) const HOOK_BUDGET: Duration = Duration::from_secs(30);
struct Label {
name: &'static str,
error_prefix: &'static str,
}
const RESOLVE_SESSION_ID: Label = Label {
name: "resolve_session_id",
error_prefix: "resolve_session_id hook error: ",
};
const IDENTIFY: Label = Label {
name: "identify",
error_prefix: "Error: identify hook returned an error - ",
};
const EVENT_TAGS: Label = Label {
name: "event_tags",
error_prefix: "event_tags callback error: ",
};
const EVENT_PROPERTIES: Label = Label {
name: "event_properties",
error_prefix: "event_properties callback error: ",
};
#[derive(Debug, Default)]
pub(crate) struct HookResults {
pub(crate) session_value: Option<String>,
pub(crate) identity: Option<UserIdentity>,
pub(crate) tags: Option<Map<String, Value>>,
pub(crate) properties: Option<Map<String, Value>>,
}
#[derive(Debug)]
pub(crate) struct SessionMintFailed;
impl fmt::Display for SessionMintFailed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("could not generate a session ID")
}
}
pub(crate) async fn settle(
mut hooks: PendingHooks,
budget: Duration,
log: &mut Vec<String>,
) -> HookResults {
let session_task = hooks.resolve_session_id.take().map(AbortOnDrop::new);
let identify_task = hooks.identify.take().map(AbortOnDrop::new);
let tags_task = hooks.event_tags.take().map(AbortOnDrop::new);
let properties_task = hooks.event_properties.take().map(AbortOnDrop::new);
let lines = Mutex::new(Vec::new());
let tags = async {
let raw = settle_one(tags_task, &EVENT_TAGS, budget, &lines).await?;
let mut dropped = Vec::new();
let valid = validate_tags(&raw, &mut dropped);
push(&lines, dropped);
valid
};
let (session_value, identity, tags, properties) = join4(
settle_one(session_task, &RESOLVE_SESSION_ID, budget, &lines),
settle_one(identify_task, &IDENTIFY, budget, &lines),
tags,
settle_one(properties_task, &EVENT_PROPERTIES, budget, &lines),
)
.await;
log.extend(lines.into_inner().unwrap_or_else(PoisonError::into_inner));
HookResults {
session_value,
identity,
tags,
properties,
}
}
async fn settle_one<T>(
task: Option<AbortOnDrop<Result<Option<T>, HookError>>>,
hook: &Label,
budget: Duration,
lines: &Mutex<Vec<String>>,
) -> Option<T> {
let task = task?;
let (value, line) = match tokio::time::timeout(budget, task).await {
Ok(Ok(Ok(value))) => (value, None),
Ok(Ok(Err(error))) => (None, Some(format!("{}{error}", hook.error_prefix))),
Ok(Err(error)) => {
let message = failure_message(error);
let line = format!(
"Warning: {} hook failed in background - {message}",
hook.name
);
(None, Some(line))
}
Err(_elapsed) => {
let line = format!(
"Warning: {} hook did not settle within {}ms; publishing event without it",
hook.name,
HOOK_BUDGET.as_millis()
);
(None, Some(line))
}
};
push(lines, line);
value
}
pub(super) fn failure_message(error: JoinError) -> String {
match error.try_into_panic() {
Ok(payload) => panic_message(&*payload),
Err(error) => error.to_string(),
}
}
fn push(lines: &Mutex<Vec<String>>, new: impl IntoIterator<Item = String>) {
lines
.lock()
.unwrap_or_else(PoisonError::into_inner)
.extend(new);
}
pub(crate) fn apply(
event: &mut EventObject,
results: HookResults,
hook_mode: bool,
) -> Result<(), SessionMintFailed> {
let HookResults {
session_value,
identity,
tags,
properties,
} = results;
if hook_mode {
let project = event
.get("projectId")
.and_then(Value::as_str)
.filter(|project| !project.is_empty());
let (session_id, source) =
session_from_hook_value(session_value.as_deref(), project).ok_or(SessionMintFailed)?;
set_field(event, "sessionId", Value::String(session_id));
let mut sdk_tags = event_tags(event);
sdk_tags.insert(
AGENTCAT_TAG_SESSION_SOURCE.to_owned(),
Value::String(source.as_str().to_owned()),
);
set_field(event, "tags", Value::Object(sdk_tags));
}
if let Some(identity) = identity {
set_field(
event,
"identifyActorGivenId",
Value::String(identity.user_id),
);
match identity.user_name {
Some(name) => set_field(event, "identifyActorName", Value::String(name)),
None => {
event.shift_remove("identifyActorName");
}
}
let data = identity.user_data.unwrap_or_default();
set_field(event, "identifyActorData", Value::Object(data));
}
if let Some(mut merged) = tags {
merged.extend(event_tags(event));
set_field(event, "tags", Value::Object(merged));
}
if let Some(properties) = properties {
set_field(event, "properties", Value::Object(properties));
}
Ok(())
}
fn event_tags(event: &EventObject) -> Map<String, Value> {
match event.get("tags") {
Some(Value::Object(tags)) => tags.clone(),
_ => Map::new(),
}
}