Module: PostHog::MCP::Truncation Private
- Defined in:
- lib/posthog/mcp/truncation.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
Layered truncation so an event fits within a byte budget before capture:
- Field-level string limits (intent, resource name, metadata fields).
- Stack-frame limiting and message caps on the
$exception_listshape. - Response content text limits (32KB per text block).
- Recursive normalization of user-controlled fields (depth/breadth/string caps).
- Size-targeted truncation: progressive depth reduction, then trimming the largest strings until under MAX_EVENT_BYTES.
Pure functions; the input event is never mutated. Hash keys are strings.
Constant Summary collapse
- MAX_DEPTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
10- MAX_BREADTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
100- MAX_STRING_LENGTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
32_768- MAX_EVENT_BYTES =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
The core client drops any single message larger than
Defaults::Message::MAX_BYTES(32KB) at batch time, so the internal event is budgeted to leave headroom for the envelope ($lib, timestamp, uuid, distinct_id) the client adds around it. PostHog::Defaults::Message::MAX_BYTES - 2048
- MAX_USER_INTENT_LENGTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
2048- MAX_ERROR_MESSAGE_LENGTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
2048- MAX_RESOURCE_NAME_LENGTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
256- MAX_METADATA_LENGTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
256- MAX_STACK_FRAMES =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
50- MAX_CONTENT_TEXT_LENGTH =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
32_768- TRUNCATION_SUFFIX =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
'...'- METADATA_FIELDS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
[ ['user_intent', MAX_USER_INTENT_LENGTH], ['resource_name', MAX_RESOURCE_NAME_LENGTH], ['server_name', MAX_METADATA_LENGTH], ['server_version', MAX_METADATA_LENGTH], ['client_name', MAX_METADATA_LENGTH], ['client_version', MAX_METADATA_LENGTH], ['error_type', MAX_METADATA_LENGTH], ['client_user_agent', MAX_METADATA_LENGTH], ['vendor_client', MAX_METADATA_LENGTH], ['llm_model', MAX_METADATA_LENGTH] ].freeze
- NORMALIZED_FIELDS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Includes user-supplied
properties(custom events,event_properties,capture_tool_call): a large numeric array cannot be shrunk by string trimming, so it must take part in depth/breadth reduction or the core client drops the whole message at batch time. %w[parameters response identify_actor_data error properties].freeze
Class Method Summary collapse
- .collect_string_paths(obj, current_path, results) ⇒ Object private
- .deep_copy(obj) ⇒ Object private
- .get_nested_value(obj, path) ⇒ Object private
-
.json_byte_size(value) ⇒ Object
private
Byte size of the compact JSON encoding, coercing non-JSON values like the transport would.
- .jsonable(value) ⇒ Object private
-
.normalize(value, depth = MAX_DEPTH, max_breadth = MAX_BREADTH, max_string_length = MAX_STRING_LENGTH) ⇒ Object
private
Recursively normalize a value: cap strings, coerce non-serializable values, convert times, detect cycles, and bound depth/breadth.
- .set_nested_value(obj, path, value) ⇒ Object private
-
.truncate_event(event) ⇒ Hash
private
New event within the byte budget.
- .truncate_exception_list(error) ⇒ Object private
- .truncate_largest_fields(obj, max_bytes) ⇒ Object private
-
.truncate_payload(payload) ⇒ Hash
private
Re-apply the byte budget to a built payload a
before_sendhook returned. - .truncate_response_content(response) ⇒ Object private
- .truncate_stack_frames(frames) ⇒ Object private
- .truncate_string(value, max_length) ⇒ Object private
- .truncate_to_size(event, fields = NORMALIZED_FIELDS) ⇒ Object private
- .visit(value, remaining_depth, max_breadth, max_string_length, memo) ⇒ Object private
- .visit_array(array, remaining_depth, max_breadth, max_string_length, memo) ⇒ Object private
- .visit_object(hash, remaining_depth, max_breadth, max_string_length, memo) ⇒ Object private
Class Method Details
.collect_string_paths(obj, current_path, results) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
194 195 196 197 198 199 200 201 202 203 |
# File 'lib/posthog/mcp/truncation.rb', line 194 def collect_string_paths(obj, current_path, results) case obj when String results << { path: current_path.dup, length: obj.length } if obj.length > 100 when Array obj.each_with_index { |item, i| collect_string_paths(item, current_path + [i.to_s], results) } when Hash obj.each { |key, value| collect_string_paths(value, current_path + [key.to_s], results) } end end |
.deep_copy(obj) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
223 224 225 226 227 228 229 |
# File 'lib/posthog/mcp/truncation.rb', line 223 def deep_copy(obj) case obj when Hash then obj.to_h { |k, v| [k, deep_copy(v)] } when Array then obj.map { |v| deep_copy(v) } else obj end end |
.get_nested_value(obj, path) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
205 206 207 208 209 210 211 212 213 |
# File 'lib/posthog/mcp/truncation.rb', line 205 def get_nested_value(obj, path) path.reduce(obj) do |current, key| case current when Array then current[key.to_i] when Hash then current[key] else return nil end end end |
.json_byte_size(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Byte size of the compact JSON encoding, coercing non-JSON values like the transport would.
178 179 180 |
# File 'lib/posthog/mcp/truncation.rb', line 178 def json_byte_size(value) JSON.generate(jsonable(value)).bytesize end |
.jsonable(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/posthog/mcp/truncation.rb', line 182 def jsonable(value) case value when Hash then value.to_h { |k, v| [k.to_s, jsonable(v)] } when Array then value.map { |v| jsonable(v) } when String, Integer, true, false, nil then value when Float then value.finite? ? value : value.to_s when Time then value.utc.iso8601(3) else value.respond_to?(:iso8601) ? value.iso8601 : value.to_s end end |
.normalize(value, depth = MAX_DEPTH, max_breadth = MAX_BREADTH, max_string_length = MAX_STRING_LENGTH) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Recursively normalize a value: cap strings, coerce non-serializable values, convert times, detect cycles, and bound depth/breadth.
62 63 64 |
# File 'lib/posthog/mcp/truncation.rb', line 62 def normalize(value, depth = MAX_DEPTH, max_breadth = MAX_BREADTH, max_string_length = MAX_STRING_LENGTH) visit(value, depth, max_breadth, max_string_length, {}.compare_by_identity) end |
.set_nested_value(obj, path, value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
215 216 217 218 219 220 221 |
# File 'lib/posthog/mcp/truncation.rb', line 215 def set_nested_value(obj, path, value) parent = path.empty? ? nil : get_nested_value(obj, path[0...-1]) case parent when Array then parent[path.last.to_i] = value when Hash then parent[path.last] = value end end |
.truncate_event(event) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns new event within the byte budget.
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/posthog/mcp/truncation.rb', line 308 def truncate_event(event) result = event.dup METADATA_FIELDS.each do |key, max_length| result[key] = truncate_string(result[key], max_length) if result[key].is_a?(String) end result['error'] = truncate_exception_list(result['error']) if result['error'].is_a?(Hash) result['response'] = truncate_response_content(result['response']) unless result['response'].nil? NORMALIZED_FIELDS.each do |field| result[field] = normalize(result[field]) unless result[field].nil? end truncate_to_size(result) end |
.truncate_exception_list(error) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/posthog/mcp/truncation.rb', line 141 def truncate_exception_list(error) list = error['$exception_list'] return error unless list.is_a?(Array) truncated = list.map do |exception| next exception unless exception.is_a?(Hash) nxt = exception.dup nxt['value'] = truncate_string(nxt['value'], MAX_ERROR_MESSAGE_LENGTH) if nxt['value'].is_a?(String) stacktrace = nxt['stacktrace'] if stacktrace.is_a?(Hash) && stacktrace['frames'].is_a?(Array) && !stacktrace['frames'].empty? nxt['stacktrace'] = stacktrace.merge('frames' => truncate_stack_frames(stacktrace['frames'])) end nxt end error.merge('$exception_list' => truncated) end |
.truncate_largest_fields(obj, max_bytes) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/posthog/mcp/truncation.rb', line 231 def truncate_largest_fields(obj, max_bytes) result = deep_copy(obj) 10.times do current_size = json_byte_size(result) return result if current_size <= max_bytes excess = current_size - max_bytes string_paths = [] collect_string_paths(result, [], string_paths) string_paths.sort_by! { |entry| -entry[:length] } break if string_paths.empty? remaining = excess + 200 truncated = false string_paths.each do |entry| break if remaining <= 0 length = entry[:length] reduction = [remaining, length / 2].min next if reduction < 10 new_length = length - reduction current_value = get_nested_value(result, entry[:path]) next unless current_value.is_a?(String) set_nested_value(result, entry[:path], current_value[0, new_length] + TRUNCATION_SUFFIX) remaining -= reduction truncated = true end break unless truncated end result end |
.truncate_payload(payload) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Re-apply the byte budget to a built payload a before_send hook returned.
The hook runs after truncate_event, so it can grow an event back over
the transport's per-message limit, where the batch would drop it whole;
trimming here costs the enrichment its bulk instead of the whole event.
299 300 301 302 303 304 |
# File 'lib/posthog/mcp/truncation.rb', line 299 def truncate_payload(payload) return payload unless payload.is_a?(Hash) key = payload.key?(:properties) && !payload.key?('properties') ? :properties : 'properties' truncate_to_size(payload, [key]) end |
.truncate_response_content(response) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/posthog/mcp/truncation.rb', line 159 def truncate_response_content(response) return response unless response.is_a?(Hash) content = response['content'] return response unless content.is_a?(Array) new_content = content.map do |block| if block.is_a?(Hash) && block['type'] == 'text' && block['text'].is_a?(String) && block['text'].length > MAX_CONTENT_TEXT_LENGTH block.merge('text' => block['text'][0, MAX_CONTENT_TEXT_LENGTH] + TRUNCATION_SUFFIX) else block end end response.merge('content' => new_content) end |
.truncate_stack_frames(frames) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
134 135 136 137 138 139 |
# File 'lib/posthog/mcp/truncation.rb', line 134 def truncate_stack_frames(frames) return frames unless frames.is_a?(Array) && frames.length > MAX_STACK_FRAMES half = MAX_STACK_FRAMES / 2 frames[0, half] + frames[-half, half] end |
.truncate_string(value, max_length) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
128 129 130 131 132 |
# File 'lib/posthog/mcp/truncation.rb', line 128 def truncate_string(value, max_length) return value unless value.is_a?(String) && value.length > max_length value[0, max_length] + TRUNCATION_SUFFIX end |
.truncate_to_size(event, fields = NORMALIZED_FIELDS) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/posthog/mcp/truncation.rb', line 268 def truncate_to_size(event, fields = NORMALIZED_FIELDS) return event if json_byte_size(event) <= MAX_EVENT_BYTES # Trim the largest strings first so a big tool response keeps its shape # (the budget here is tight enough that depth reduction alone would turn # a `content` array into "[Array]"). trimmed = truncate_largest_fields(event, MAX_EVENT_BYTES) return trimmed if json_byte_size(trimmed) <= MAX_EVENT_BYTES (MAX_DEPTH - 1).downto(1) do |depth| reduced = event.dup fields.each do |field| reduced[field] = normalize(reduced[field], depth) unless reduced[field].nil? end return reduced if json_byte_size(reduced) <= MAX_EVENT_BYTES end minimal = event.dup fields.each do |field| minimal[field] = normalize(minimal[field], 1) unless minimal[field].nil? end truncate_largest_fields(minimal, MAX_EVENT_BYTES) end |
.visit(value, remaining_depth, max_breadth, max_string_length, memo) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/posthog/mcp/truncation.rb', line 66 def visit(value, remaining_depth, max_breadth, max_string_length, memo) case value when nil, true, false, Integer then value when Float return '[NaN]' if value.nan? return (value.positive? ? '[Infinity]' : '[-Infinity]') if value.infinite? value when String value.length > max_string_length ? value[0, max_string_length] + TRUNCATION_SUFFIX : value when Symbol then value.to_s when Time then value.utc.iso8601(3) when Proc, Method name = value.respond_to?(:name) ? value.name : nil "[Function: #{name || '<anonymous>'}]" when Array return '[Circular ~]' if memo.key?(value) return '[Array]' if remaining_depth <= 0 memo[value] = true result = visit_array(value, remaining_depth - 1, max_breadth, max_string_length, memo) memo.delete(value) result when Hash return '[Circular ~]' if memo.key?(value) return '[Object]' if remaining_depth <= 0 memo[value] = true result = visit_object(value, remaining_depth - 1, max_breadth, max_string_length, memo) memo.delete(value) result else value.respond_to?(:iso8601) ? value.iso8601 : value.to_s end end |
.visit_array(array, remaining_depth, max_breadth, max_string_length, memo) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/posthog/mcp/truncation.rb', line 102 def visit_array(array, remaining_depth, max_breadth, max_string_length, memo) result = [] array.each_with_index do |item, index| if index >= max_breadth result << '[MaxProperties ~]' break end result << visit(item, remaining_depth, max_breadth, max_string_length, memo) end result end |
.visit_object(hash, remaining_depth, max_breadth, max_string_length, memo) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/posthog/mcp/truncation.rb', line 114 def visit_object(hash, remaining_depth, max_breadth, max_string_length, memo) result = {} count = 0 hash.each do |key, val| if count >= max_breadth result['...'] = '[MaxProperties ~]' break end result[key.to_s] = visit(val, remaining_depth, max_breadth, max_string_length, memo) count += 1 end result end |