feat: contributor capture middleware - #703
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
6ca8d35 to
bd204ee
Compare
5fd0317 to
cd645df
Compare
bd204ee to
b90ffab
Compare
cd645df to
43fe465
Compare
d4b0610 to
9080fcf
Compare
43fe465 to
51d128f
Compare
f7d1c06 to
14c221d
Compare
This comment has been minimized.
This comment has been minimized.
| sink := m.sinkProvider() | ||
| for _, entityID := range ids { | ||
| sink.RecordEntity(contributors.EntityTypeProject, entityID, interactionID) | ||
| } |
There was a problem hiding this comment.
IaC drops extra projects
Medium Severity
recordProjects calls RecordEntity for every parsed project ID under the same interaction ID, but the production sink keeps only the first entity per interaction and ignores later calls. IaC share responses with multiple projects therefore lose all but one captured ID, and which project survives depends on map iteration order.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 14c221d. Configure here.
There was a problem hiding this comment.
This is intentional, the project is used a reference to find the target, and since all the projects share the same target, we can pick any of them.
51d128f to
2812906
Compare
14c221d to
0b76b0b
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| return false | ||
| } | ||
|
|
||
| return doc.Data.Attributes.Configuration.Output.Report |
There was a problem hiding this comment.
Wrong create-test report JSON path
High Severity
parseCreateTestPublishReport now reads data.attributes.configuration.output.report, but create-test bodies in this repo use data.attributes.config.publish_report (see TestAttributesCreate / TestConfiguration in the generated test API client). With --report, the flag stays false, pending tests are never registered, and contributor capture never runs for the components polling flow.
Reviewed by Cursor Bugbot for commit 4316392. Configure here.
There was a problem hiding this comment.
I have tested this. As of today, the requests sent by the commands we are capturing use the hidden endpoint, and this JSON path.
371af1f to
2a12cc2
Compare
4316392 to
b71b173
Compare
This comment has been minimized.
This comment has been minimized.
b71b173 to
a8d9f89
Compare
| return | ||
| } | ||
| m.clearPendingTest(testID) | ||
| m.recordProjects(entityIDs, state.interactionID) |
There was a problem hiding this comment.
Pending cleared before capture
Medium Severity
In the test components flow, captureIfPendingTest removes the test ID from pendingTestsSingleton before calling recordProjects. If recording panics or otherwise fails, the pending flag is already gone, so later component polls will not capture the project ID even though the response was successful.
Reviewed by Cursor Bugbot for commit a8d9f89. Configure here.
There was a problem hiding this comment.
There will be no later "component polls" because it's not a polling API - the call happens exactly once after test status polling finishes.
There was a problem hiding this comment.
I'd clean it up. Right now, IDEs may be out of scope, but they would run into exactly this kind of error, where a long running, re-used process needs to be tidy.
This comment has been minimized.
This comment has been minimized.
a8d9f89 to
6dea7c5
Compare
This comment has been minimized.
This comment has been minimized.
|
In response to review bot: Not true, it never broke it, it just made it potentially take a less optimized path, and it appears the bot is looking at stale data, there's now extra work to restore seeking, with a test. It's fine that the middleware isn't usable outside this module. We can move it to internal, but it separates it from all the other middleware. Regardless, small change, happy to do it if a maintainer agrees with 🤖. Memory leak is a dramatic word for this, even if it is technically accurate. You'd need thousands of tests before you got to 1MB of memory - it's a non-issue, and implementing a TTL is overengineering. This happens once for every HTTP request and adds ns, which will be statistically insignificant compared to request durations. Again, this is rather dramatic. |
| } | ||
| entityIDs := extract(body) | ||
| if len(entityIDs) == 0 { | ||
| return |
There was a problem hiding this comment.
[Should Fix] pendingTestsSingleton.ids is only cleared on the success path. captureIfPendingTest returns early at line 212 when extract(body) finds no matching entity IDs, so clearPendingTest (line 215) is never reached for a create-test flow that never gets a matching components response. Since this is process-global state (var pendingTestsSingleton, line 31) with no eviction on that path, entries accumulate for the life of the process. It's harmless for a short-lived CLI run, but this repo's AGENTS.md explicitly calls out avoiding package-level mutable state for hosts that reuse a process across many invocations (IDE/MCP), so it's worth closing before this middleware is wired up more broadly.
Fix: clear the pending-test entry on any outcome for a tracked test ID (not just a successful capture), or replace the singleton with a bounded/expiring cache.
— AI review
There was a problem hiding this comment.
Clearing it on any outcome is not correct, because we don't know when we're observing the final call related to that test, so we may clear it early and miss the one containing the project ID.
As I describe above, I think adding a TTL is over-engineering and it's not possible to replace the singleton because there is no workflow scope to attach to in the middleware.
There was a problem hiding this comment.
TTL wouldn't be good, I agree. How about attaching to the context?
There was a problem hiding this comment.
It's possible, but only by making changes to set this up in engineImpl - and relies on code outside of this codebase not changing how it handles contexts, which isn't great.
There was a problem hiding this comment.
Also, the changes actually require attaching it to a context that hangs around for the lifetime of the process anyway, so it doesn't solve test IDs sticking around after they're not required.
| } | ||
|
|
||
| // parseMonitorProjectIDs extracts project IDs from a monitor/monitor-dependencies response. | ||
| func parseMonitorProjectIDs(body []byte) []string { |
There was a problem hiding this comment.
[Suggestion] None of the parse functions in this file (parseMonitorProjectIDs, parseIaCShareProjectIDs, parseComponentsProjectIDs, etc.) log anything when parsing fails — they just return an empty result. If one of these API response shapes changes, capture stops silently with no signal anywhere that something broke.
Probably better as a follow-up than expanding this PR: a debug-level log with the endpoint kind on parse failure would make a schema drift visible.
— AI review
There was a problem hiding this comment.
This mechanism is essentially invisible to users of the CLI, so I don't think a debug log is the right call - we won't see it anyway. What I plan to do (in the final PR for this work) is introduce data for analytics to use which indicates whether a capture occurred, because that is reported and can be analyzed.
There was a problem hiding this comment.
Could also be an error log - this is the only way to analyze things in the CLI, so error logging is essential. In every single support case, we require the debug level logs for this reason.
There was a problem hiding this comment.
The point is that this is invisible to users of the CLI, and so it's very unlikely for them to ever bring us the logs. Nevertheless, I can include this in a follow up which wires this up and adds the analytics.
|
|
||
| // Sink is the mechanism by which this middleware reports captured entities. | ||
| type Sink interface { | ||
| RecordEntity(entityType contributors.EntityType, entityID, interactionID string) |
There was a problem hiding this comment.
[Suggestion] RecordEntity is called synchronously from RoundTrip, so a slow Sink implementation would add latency to every captured request. The current implementation (internal/contributors/sink.go) is just a mutex-protected in-memory map write, so this is harmless today, but the interface itself doesn't say implementations must be fast/non-blocking.
Probably better as a follow-up than expanding this PR: a doc comment on the interface stating the non-blocking expectation would prevent a future implementation from violating it silently.
— AI review
There was a problem hiding this comment.
Added the comment.
6dea7c5 to
aac9a35
Compare
PR Reviewer Guide 🔍
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 5 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit aac9a35. Configure here.
| state, needsResponse := m.beginRequestCapture(req) | ||
|
|
||
| if !needsResponse { | ||
| return m.next.RoundTrip(req) |
There was a problem hiding this comment.
AI BOM captures before success
Medium Severity
For EndpointAIBomUpload, beginRequestCapture writes the revision ID to the sink before RoundTrip runs. Monitor, IaC share, and test flows only record after a successful 2xx response. Failed uploads, HTTP errors, and transport failures can still emit contributor data for uploads that never completed.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit aac9a35. Configure here.
There was a problem hiding this comment.
This is intentional, the data is only used later when corresponding entities exist in the platform which happens on success. If a response failure occurs, we don't know if a test started or not.
| } | ||
| projectID := parseComponentsProjectID(bodyBytes) | ||
| if projectID == "" { | ||
| return // the test is still running; stay pending for the next poll |
There was a problem hiding this comment.
Pending tests map never evicted
Low Severity
Process-global pendingTestsSingleton.ids is only removed in clearPendingTest after a components response yields a project ID. Early returns on non-2xx components responses, successful responses with no parseable project ID, or a create-test flow that never completes components leave entries in the map for the process lifetime.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit aac9a35. Configure here.


Description
This PR implements contributor capturing middleware which captures project IDs, associated them with interaction IDs and passes them to a singleton sink. A future PR will implement a post invoke hook to collect data from the sync and call the contributor emitter, as well as checking the feature flag included in a constant in this PR before enabling the sink.
Checklist
make test)make generate)make lint)feat/contributor-capture-wiring(in draft, enables capture and wires the middleware output to the contributor emitter), and have run against the real commands we're intending to capture data forNote
Medium Risk
Adds new network middleware that inspects request/response bodies, which could affect CLI network behavior if body handling or panic recovery has bugs; however it is gated by a nil sink provider and includes safeguards like body restoration and panic recovery.
Overview
Adds an HTTP round-trip middleware that captures Snyk entity IDs from product API traffic and stores them in a per-interaction sink for later contributor publishing.
The middleware recognizes monitor, IaC share, test-create/test-components, and AI BOM upload endpoints. It extracts project IDs from responses (and revision IDs from AI BOM upload requests), associates them with the
snyk-interaction-idheader, and records them via aSink. A newinternal/contributorspackage provides the singleton sink and feature-flag/config constants; capture is currently disabled untilcontributors.Enable()is called.The middleware is inserted into the default network round-tripper chain in
pkg/networking/networking.go. It peeks request/response bodies with a 64 KiB limit and restores them so normal request flow is unaffected, and it recovers from panics to avoid breaking CLI commands.Reviewed by Cursor Bugbot for commit aac9a35. Bugbot is set up for automated code reviews on this repo. Configure here.