use std::cmp::Reverse;
use std::fmt;
use serde_json::{Map, Value};
use super::EventObject;
use super::normalize::{MAX_BREADTH, MAX_DEPTH, MAX_STRING_LENGTH, normalize};
use crate::wire;
#[cfg(test)]
mod tests;
pub(crate) const MAX_EVENT_BYTES: usize = 102_400;
const LIMITED_FIELDS: [(&str, usize); 6] = [
("userIntent", 2_048),
("resourceName", 256),
("serverName", 256),
("serverVersion", 256),
("clientName", 256),
("clientVersion", 256),
];
const MAX_ERROR_MESSAGE_LENGTH: usize = 2_048;
const MAX_STACK_FRAMES: usize = 50;
const NORMALIZED_FIELDS: [&str; 4] = ["parameters", "response", "identifyActorData", "error"];
const TRUNCATION_SUFFIX: &str = "...";
const MAX_ROUNDS: usize = 10;
const MIN_CUT_LENGTH: usize = 100;
const BUFFER_BYTES: usize = 200;
const MIN_REDUCTION: usize = 10;
const NOT_A_STRING: &str = "a length-limited field is a number, a boolean or an object";
const NOT_FRAMES: &str = "error.frames is true, a number other than 0, or an object";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TruncateError {
message: String,
}
impl TruncateError {
fn new(message: &str) -> Self {
Self {
message: message.to_owned(),
}
}
}
impl fmt::Display for TruncateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
pub(crate) fn truncate_event(mut event: EventObject) -> Result<EventObject, TruncateError> {
limit_fields(&mut event)?;
let limited = take_normalized_fields(&mut event);
let mut depth = MAX_DEPTH;
loop {
normalize_fields(&mut event, &limited, depth);
if byte_size(&event) <= MAX_EVENT_BYTES {
return Ok(event);
}
if depth == 1 {
return Ok(cut_longest_strings(event));
}
depth -= 1;
}
}
fn limit_fields(event: &mut EventObject) -> Result<(), TruncateError> {
for (key, limit) in LIMITED_FIELDS {
if let Some(value) = event.get_mut(key)
&& let Some(limited) = limit_string(value, limit)?
{
*value = limited;
}
}
if let Some(error) = event.get_mut("error") {
limit_error(error)?;
}
Ok(())
}
fn limit_string(value: &Value, limit: usize) -> Result<Option<Value>, TruncateError> {
match value {
Value::Null => Ok(None),
Value::String(text) => {
Ok((wire::utf16_len(text) > limit).then(|| Value::String(cut(text, limit))))
}
Value::Array(items) => Ok((items.len() > limit).then(|| {
let head = Value::Array(items.iter().take(limit).cloned().collect());
let mut joined = wire::coerce_to_string(Some(&head));
joined.push_str(TRUNCATION_SUFFIX);
Value::String(joined)
})),
Value::Bool(_) | Value::Number(_) | Value::Object(_) => {
Err(TruncateError::new(NOT_A_STRING))
}
}
}
fn cut(text: &str, units: usize) -> String {
let mut cut = wire::cut_utf16(text, units).to_owned();
cut.push_str(TRUNCATION_SUFFIX);
cut
}
fn limit_error(error: &mut Value) -> Result<(), TruncateError> {
match error {
Value::Object(fields) => {
if let Some(message) = fields.get_mut("message")
&& let Some(limited) = limit_string(message, MAX_ERROR_MESSAGE_LENGTH)?
{
*message = limited;
}
if let Some(frames) = fields.get_mut("frames")
&& let Some(limited) = limit_frames(frames)?
{
*frames = limited;
}
}
Value::Array(items) => {
let spread: Map<String, Value> = std::mem::take(items)
.into_iter()
.enumerate()
.map(|(index, item)| (index.to_string(), item))
.collect();
*error = Value::Object(spread);
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
Ok(())
}
fn limit_frames(frames: &Value) -> Result<Option<Value>, TruncateError> {
let half = MAX_STACK_FRAMES / 2;
match frames {
Value::Array(items) => Ok((items.len() > MAX_STACK_FRAMES).then(|| {
let tail = items.len() - half;
let kept = items.iter().take(half).chain(items.iter().skip(tail));
Value::Array(kept.cloned().collect())
})),
Value::String(text) => {
Ok((wire::utf16_len(text) > MAX_STACK_FRAMES).then(|| spread_ends(text, half)))
}
Value::Null | Value::Bool(false) => Ok(None),
Value::Number(number) if number.as_f64() == Some(0.0) => Ok(None),
Value::Bool(true) | Value::Number(_) | Value::Object(_) => {
Err(TruncateError::new(NOT_FRAMES))
}
}
}
fn spread_ends(text: &str, half: usize) -> Value {
let tail = wire::utf16_len(text) - half;
let mut items = Vec::new();
let mut start = 0;
for c in text.chars() {
let end = start + c.len_utf16();
if end <= half || start >= tail {
items.push(Value::String(c.to_string()));
} else if start < half || end > tail {
items.push(Value::String(String::new()));
}
start = end;
}
Value::Array(items)
}
fn take_normalized_fields(event: &mut EventObject) -> Vec<(&'static str, Value)> {
NORMALIZED_FIELDS
.into_iter()
.filter_map(|key| {
let value = event.get_mut(key).filter(|value| !value.is_null())?;
Some((key, std::mem::take(value)))
})
.collect()
}
fn normalize_fields(event: &mut EventObject, limited: &[(&str, Value)], depth: usize) {
for (key, value) in limited {
if let Some(field) = event.get_mut(*key) {
*field = normalize(value, depth, MAX_BREADTH, MAX_STRING_LENGTH);
}
}
}
#[derive(Debug, Default)]
struct ByteCounter(usize);
impl fmt::Write for ByteCounter {
fn write_str(&mut self, text: &str) -> fmt::Result {
self.0 += text.len();
Ok(())
}
}
fn byte_size(event: &EventObject) -> usize {
let mut counter = ByteCounter::default();
for (key, value) in event {
let _ = wire::write_quoted(&mut counter, key);
let _ = wire::write_stringify(value, &mut counter);
}
2 + counter.0 + event.len() + event.len().saturating_sub(1)
}
#[derive(Debug, Clone)]
enum Step {
Key(String),
Index(usize),
}
#[derive(Debug)]
struct Candidate {
key: String,
path: Vec<Step>,
length: usize,
}
fn cut_longest_strings(mut event: EventObject) -> EventObject {
for _ in 0..MAX_ROUNDS {
let size = byte_size(&event);
if size <= MAX_EVENT_BYTES {
return event;
}
let mut candidates = Vec::new();
let mut path = Vec::new();
for (key, value) in wire::own_entries(&event) {
collect_long_strings(key, value, &mut path, &mut candidates);
}
candidates.sort_by_key(|candidate| Reverse(candidate.length));
let mut remaining = size - MAX_EVENT_BYTES + BUFFER_BYTES;
let mut cut_any = false;
for Candidate { key, path, length } in candidates {
if remaining == 0 {
break;
}
let reduction = remaining.min(length / 2);
if reduction < MIN_REDUCTION {
continue;
}
if let Some(Value::String(text)) = value_at(&mut event, &key, &path) {
*text = cut(text, length - reduction);
remaining -= reduction;
cut_any = true;
}
}
if !cut_any {
break;
}
}
event
}
fn collect_long_strings(key: &str, value: &Value, path: &mut Vec<Step>, out: &mut Vec<Candidate>) {
match value {
Value::String(text) => {
let length = wire::utf16_len(text);
if length > MIN_CUT_LENGTH {
out.push(Candidate {
key: key.to_owned(),
path: path.clone(),
length,
});
}
}
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
path.push(Step::Index(index));
collect_long_strings(key, item, path, out);
path.pop();
}
}
Value::Object(map) => {
for (name, item) in wire::own_entries(map) {
path.push(Step::Key(name.clone()));
collect_long_strings(key, item, path, out);
path.pop();
}
}
Value::Null | Value::Bool(_) | Value::Number(_) => {}
}
}
fn value_at<'a>(event: &'a mut EventObject, key: &str, path: &[Step]) -> Option<&'a mut Value> {
let mut at = event.get_mut(key)?;
for step in path {
at = match step {
Step::Key(name) => at.get_mut(name.as_str())?,
Step::Index(index) => at.get_mut(*index)?,
};
}
Some(at)
}