Skip to content

Custom event tracking#286

Open
marksmith wants to merge 26 commits into
mainfrom
custom-event-tracking
Open

Custom event tracking#286
marksmith wants to merge 26 commits into
mainfrom
custom-event-tracking

Conversation

@marksmith

@marksmith marksmith commented May 27, 2026

Copy link
Copy Markdown
Collaborator

This change adds support for tracking named user events, sent from application code using Aikido::Zen.track_user_event. User events include the event name, user ID, and IP address.

This change extends and should be reviewed after #285.

Summary by Aikido

Security Issues: 0 🔍 Quality Issues: 5 Resolved Issues: 0

🚀 New Features

  • Added user event tracking and reporting via Agent and APIClient
  • Implemented realtime settings updates stream with APIStream and handlers

⚡ Enhancements

  • Corrected runtime settings timestamp parsing and methods to return Boolean

More info

@marksmith marksmith requested review from hansott and tomaisthorpe May 27, 2026 15:06
Comment thread lib/aikido/zen/agent.rb
@marksmith marksmith force-pushed the custom-event-tracking branch from 0018654 to 17f3482 Compare May 28, 2026 08:19
Comment thread lib/aikido/zen/api_client.rb
@marksmith marksmith force-pushed the custom-event-tracking branch from 17f3482 to 23ffc5f Compare May 28, 2026 08:21
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.69951% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/aikido/zen/agent.rb 70.58% 9 Missing and 6 partials ⚠️
lib/aikido/zen/api_client.rb 58.82% 6 Missing and 1 partial ⚠️
lib/aikido/zen/api_stream.rb 97.27% 2 Missing and 1 partial ⚠️
lib/aikido/zen.rb 75.00% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@marksmith marksmith force-pushed the custom-event-tracking branch from 23ffc5f to 29ebef3 Compare May 29, 2026 11:22
The endpoint for realtime settings updates can be set using the
realtime_settings_updates_endpoint configuration option or the
AIKIDO_REALTIME_SETTINGS_UPDATES_ENDPOINT environment variable.
Defaults to https://zen.aikido.dev.
@marksmith marksmith force-pushed the custom-event-tracking branch from 29ebef3 to 381c5a7 Compare June 15, 2026 10:53
Comment thread lib/aikido/zen/agent.rb
end
end

# @param event [Aikido::Zen::Tracked]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send_user_event duplicates Agent#report's enqueue/call/rescue pattern. Consolidate to reuse report or extract shared worker/api/error-handling logic.

Details

✨ AI Reasoning
​The Agent already implements a generic report(event) helper that enqueues work with @worker.perform, calls an API client method, and rescues Aikido::Zen::APIError and Aikido::Zen::NetworkError. The newly added send_user_event method repeats that same pattern: checking @api_client.can_make_requests?, using @worker.perform, invoking @api_client.send_user_event, yielding response, and rescuing the same exceptions. This duplicates non-trivial control flow and error handling logic within the same class, increasing maintenance burden because any change to the worker/enqueue/error-handling behavior would need to be applied in multiple places.

🔧 How do I fix it?
Delete extra code. Extract repeated code sequences into reusable functions or methods. Use loops or data structures to eliminate repetitive patterns.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

event = {}

begin
event_str.each_line do |line|

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APIStream#work parsing loop is deeply nested (>=6 levels). Extract parsing stages (frame extraction, per-event parsing, handler dispatch) into helper methods to reduce nesting and improve readability.

Details

✨ AI Reasoning
​The APIStream#work method contains multiple nested control-flow blocks for handling the HTTP stream, body chunks, event framing, JSON parsing, and per-line parsing. This chain reaches six meaningful nesting levels, which harms readability and maintainability of the streaming parser. The nesting was introduced in this PR (new file/method). Extracting parsing steps into smaller methods would reduce nesting and improve readability and testability.

🔧 How do I fix it?
Keep nesting levels under 4. Extract complex logic into separate functions when indentation exceeds 4 levels.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

end
end

private def work

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private method 'work' performs connection setup, stream reading, SSE parsing, and event dispatch; its name is too vague—use a descriptive name or add documentation.

Details

✨ AI Reasoning
​The change adds a private method APIStream#work that encapsulates the entire streaming connection lifecycle: creating HTTP client, opening connection, reading chunks, parsing server-sent events, and dispatching events to handlers. The single-word name 'work' does not convey these responsibilities; it's unclear without reading the implementation what 'work' does, and it mixes multiple concerns. A clearer name or documentation would make intent and responsibilities obvious to future maintainers.

🔧 How do I fix it?
Use descriptive verb-noun function names, add docstrings explaining the function's purpose, or provide meaningful return type hints.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Comment on lines +141 to +192
http.request(request) do |response|
case response.code.to_i
when 200
# empty
when 401, 403
@running.make_false
return nil
else
return nil
end

buffer = +""

response.read_body do |chunk|
return nil unless running?

@config.logger.debug("API stream received chunk of #{chunk.bytesize} bytes")

buffer << chunk

while (index = buffer.index("\n\n"))
event_str = buffer.slice!(0..index + 1)
buffer = buffer.lstrip

event = {}

begin
event_str.each_line do |line|
case line
when /^event:\s*(.+)/
event[:type] = $1.strip
when /^data:\s*(.+)/
event[:data] = JSON.parse($1.strip)
end
end
rescue => err
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
next
end

@handlers.each do |handler|
handler.call(event)
rescue => err
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
end
end
end
end
ensure
@config.logger.debug("API stream disconnecting")
http.finish
@config.logger.debug("API stream disconnected")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block passed to http.request is complex (parsing, buffering, dispatch); extract to a named method to clarify and isolate the streaming parse logic.

Show fix
Suggested change
http.request(request) do |response|
case response.code.to_i
when 200
# empty
when 401, 403
@running.make_false
return nil
else
return nil
end
buffer = +""
response.read_body do |chunk|
return nil unless running?
@config.logger.debug("API stream received chunk of #{chunk.bytesize} bytes")
buffer << chunk
while (index = buffer.index("\n\n"))
event_str = buffer.slice!(0..index + 1)
buffer = buffer.lstrip
event = {}
begin
event_str.each_line do |line|
case line
when /^event:\s*(.+)/
event[:type] = $1.strip
when /^data:\s*(.+)/
event[:data] = JSON.parse($1.strip)
end
end
rescue => err
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
next
end
@handlers.each do |handler|
handler.call(event)
rescue => err
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
end
end
end
end
ensure
@config.logger.debug("API stream disconnecting")
http.finish
@config.logger.debug("API stream disconnected")
http.request(request, &method(:handle_streaming_response))
ensure
@config.logger.debug("API stream disconnecting")
http.finish
@config.logger.debug("API stream disconnected")
end
end
private def handle_streaming_response(response)
case response.code.to_i
when 200
# empty
when 401, 403
@running.make_false
return nil
else
return nil
end
buffer = +""
response.read_body do |chunk|
return nil unless running?
@config.logger.debug("API stream received chunk of #{chunk.bytesize} bytes")
buffer << chunk
while (index = buffer.index("\n\n"))
event_str = buffer.slice!(0..index + 1)
buffer = buffer.lstrip
event = {}
begin
event_str.each_line do |line|
case line
when /^event:\s*(.+)/
event[:type] = $1.strip
when /^data:\s*(.+)/
event[:data] = JSON.parse($1.strip)
end
end
rescue => err
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
next
end
@handlers.each do |handler|
handler.call(event)
rescue => err
@config.logger.error("Error in API stream: #{err.class}: #{err.message}")
end
end
Details

✨ AI Reasoning
​A new complex anonymous block handles the HTTP response lifecycle: verifies status codes, manages a buffer, splits SSE chunks, parses lines, JSON-decodes data, constructs event hashes, and dispatches to handlers while handling errors. This is substantial business logic placed inside an unnamed block; extracting it to a clearly named method would make the parsing and dispatch behavior easier to understand and test.

Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant