Custom event tracking#286
Conversation
0018654 to
17f3482
Compare
17f3482 to
23ffc5f
Compare
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
23ffc5f to
29ebef3
Compare
for port in PostgreSQL tests
for port in MySQL tests
To detect whether the update method completed.
An updater method returns early if the updater is already updating.
By mocking Aikido::Zen::APIStream to prevent real HTTP request to https://runtime.aikido.dev/api/runtime/stream.
By scaling down durations by 10x.
in API stream tests
In case the server-side event stream ever contains sensitive data.
To emphasize the mutual exclusion property of defined updater methods.
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.
29ebef3 to
381c5a7
Compare
| end | ||
| end | ||
|
|
||
| # @param event [Aikido::Zen::Tracked] |
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| 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") |
There was a problem hiding this comment.
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
| 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
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
🚀 New Features
⚡ Enhancements
More info