[feat] Extend events beyond deployments#4335
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR extends Agenta's webhook event system from a single deployment-related event to a comprehensive set of events covering traces, testcases, and revision lifecycles (retrieve/fetch/query/log/committed) for applications, queries, testsets, evaluators, and environments. It introduces shared helpers in core/events/utils.py with a deliberate read-at-router / write-at-service split, instruments the relevant routers and service-layer commit methods, and regenerates the OpenAPI/Fern client artifacts plus docs.
Changes:
- Adds 28 new
EventType/WebhookEventTypevalues and sharedpublish_*helpers (with 1000-item caps and zero-count suppression). - Instruments router read-paths (
TracesRouter,TestcasesRouter, and the four revision routers) and refactorsEnvironmentsServiceplus adds emission to four newcommit_*_revisionservices. - Regenerates docs (OpenAPI status/request schemas, MDX pages migrating to Docusaurus
:::caution[deprecated]and bare<ParamsDetails>) and Python clientWebhookEventTypeliteral, plus design docs.
Reviewed changes
Copilot reviewed 201 out of 203 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
api/oss/src/core/events/types.py, core/webhooks/types.py |
Adds the 28 new event-type enum members. |
api/oss/src/core/events/utils.py (context) |
New shared publish_* helpers documenting the router/service split. |
api/oss/src/core/{applications,queries,testsets,evaluators,environments}/service.py |
Emits *.revisions.committed from each commit_*_revision; environments refactored to use the shared helper. |
api/oss/src/apis/fastapi/{traces,testcases,applications,queries,testsets,evaluators,environments}/router.py |
Emits read events (fetched/queried/retrieved/log) after responses are materialized. |
clients/python/agenta_client/types/webhook_event_type.py |
Updates the Literal[...] union to include the new event names. |
docs/docs/reference/api/*.{api.mdx,StatusCodes.json,RequestSchema.json} |
Regenerated OpenAPI docs: new event enums, :::caution[deprecated] syntax, <ParamsDetails> cleanup. |
docs/docs/prompt-engineering/integrating-prompts/04-webhooks.mdx |
Documents the new event taxonomy. |
docs/designs/extend-events-beyond-deployments/{tasks,gap,findings}.md |
Design notes capturing the implementation decisions and deferred acceptance tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Railway Preview Environment
Updated at 2026-05-19T06:49:31.717Z |
…events-beyond-deployments
…events-beyond-deployments
What this unlocks
This PR turns the events pipeline from "one event type per deployment commit" into a general read/commit/log surface across the major Git-style entities, with a real quota meter and a real feature flag wired all the way through.
Twenty-nine new event types ship in
EventTypeand (subscribably) inWebhookEventType: trace reads (traces.fetched,traces.queried), testcase reads (testcases.fetched,testcases.queried), and the fullretrieved/fetched/queried/logged/committedlifecycle forapplications,queries,testsets,evaluators, andenvironmentsat the revision level. Every type is webhook-subscribable. Webhook subscribers and thePOST /events/queryaudit log now see real-user activity, not just the pre-existingenvironments.revisions.committeddeploy precedent.Counter.EVENTS_INGESTEDis now a real, enforced meter — same two-layer pattern asCounter.TRACES_INGESTED. L1 lives insidecore/events/utils.py::_safe_publish: acache=Truesoft check that silently drops the publish when the org is over quota (read/commit responses are never 429'd by event metering). L2 lives inEventsWorker.process_batch: an authoritativeadjust()per org with the full per-org delta in the batch, mirroring the tracing worker. Plans declare the quota inline (Quota(retention=..., period=Period.MONTHLY)for Hobby/Pro/Business, unlimited for Agenta and Self-hosted Enterprise) and operators can flip alimit=...per plan without any code change.Flag.AUDITis a new entitlement flag that gatesPOST /events/query— the human-facing audit-log query surface. Hobby and Pro returnNOT_ENTITLED_RESPONSE(Tracker.FLAGS)(Business / Agenta / Self-hosted are entitled). Ingest and webhook delivery are not gated byFlag.AUDIT— events keep flowing into theeventstable and to webhook subscribers regardless of audit-log entitlement, so an upgrade makes historical events queryable immediately and webhook integrations work for every paid plan. The flag is inCONSTRAINTS[BLOCKED]so orgs can't promote themselves.The new helper layer in
core/events/utils.pyenforces the read=router / write=service split: read events emit at the router boundary after the response is materialized (so internal service-to-service fetches that resolve refs or hydrate diff state stay silent), and commit events emit from insidecommit_*_revision(...)at the service layer (so direct commit routes, simple-service create/edit, deploy paths, fork, and defaults seeding all produce exactly one event without double-publishing). The split is documented in the module docstring with the specific double-counting cases it prevents (commit_environment_revisioncallingquery_environment_revisionsto compute the diff;TracingService.query_tracescallingqueries_service.fetch_query_revisionto resolve the saved query; evaluation runs fetching hundreds of revisions per scenario).The publisher side now reads scope from the ambient
AuthScope(ContextVarset by the auth middleware) withrequest.stateas legacy fallback. That single change closes a quiet pre-existing gap: service-layer commit emissions used to shiporganization_id=nullon the Redis envelope because the service had noRequest. Today AuthScope propagates through nested async work, so every emittedEventMessage— read or commit, router or service — carries the full(organization_id, workspace_id, project_id, user_id).What changed under the hood
Event type surface.
EventType(api/oss/src/core/events/types.py) grows by 29 members across four groups:traces.{fetched,queried},testcases.{fetched,queried}, and{applications,queries,testsets,evaluators,environments}.revisions.{retrieved,fetched,queried,logged,committed}.WebhookEventType(api/oss/src/core/webhooks/types.py) mirrors the new event types as a strict subset ofEventType, derived by.valuere-export so the two enums stay in lockstep. The pre-existingenvironments.revisions.committedprecedent is preserved end-to-end (state/diff/references/messageattributes intact). Workflow revision events are intentionally deferred until workflows are confirmed as a durable exposed entity;SpansRouterand the legacyTracingRouterare intentionally not instrumented.Helper layer (
core/events/utils.py). Five keyword-only publishers:publish_trace_fetched,publish_trace_queried,publish_testcase_fetched,publish_testcase_queried, and a genericpublish_revision_event(domain, action, ...)that fans out to the 25 revision events. Each helper resolves scope (request_scope()→ AuthScope first, thenrequest.state), short-circuits oncount == 0/ missing revision / empty list, builds theEventenvelope with a generatedrequest_id/event_idandRequestType.UNKNOWN, and calls_safe_publishwhich runs L1 then publishes. Attribute builders are exposed separately (build_trace_fetched_attributes,build_revision_event_attributes, etc.) so call sites can produce attributes without publishing — used by the existing unit tests. List-shaped events capreferences/trace_ids/testcase_idsatMAX_REFERENCES = 1000whilecountstays uncapped (so consumers can detect truncation viacount > len(references)). Commit events deliberately omitcountand may carry an optionalmessage. The singleextra=kwarg preservesenvironments.revisions.committed's historicalstate/diffattributes through the shared helper.Router-layer read emission. Eight router classes emit reads at the handler boundary after the response is materialized:
TracesRouter.{fetch_trace,fetch_traces,query_traces},TestcasesRouter.{fetch_testcase,fetch_testcases,query_testcases}, and theretrieve/fetch/query/loghandlers on each ofApplicationsRouter/QueriesRouter/TestsetsRouter/EvaluatorsRouter/EnvironmentsRouter. Cached paths (theretrieve_*_revisionflows in queries / testsets, which useget_cache/set_cache) emit on cache hit too — the helper call sits after the cache check so the count of "the user asked for this revision" is unaffected by whether the response was served from cache. Trace endpoints that internally fetch spans to materialize traces emit only the trace event (no span event). Query-revision endpoints emit query-revision events, not trace events, even when they internally drive a trace fetch.Service-layer commit emission. Five
commit_*_revision(...)methods publish exactly one commit event each:ApplicationsService,QueriesService,TestsetsService,EvaluatorsService,EnvironmentsService. The delta-commit flow incommit_environment_revisionandcommit_testset_revisionearly-returns into a private_commit_*_revision_delta(...)helper that resolves the delta into full data and then re-enterscommit_*_revisiononce, so the emission line at the bottom of each method fires exactly once even on delta commits. The existingenvironments.revisions.committedproducer was normalized to usepublish_revision_event(extra={"state": ..., "diff": ...}, message=...)instead of constructing theEventenvelope inline, preserving every historical attribute. Higher-level compatibility paths (simple-service create / edit, deploy paths from the workflows and evaluators routers, defaults seeding, fork) keep flowing through the service-layer commit method without code change and automatically emit once per call.L1 quota (publisher side, silent drop).
_check_l1_events_quotainside_safe_publishrunscheck_entitlements(key=Counter.EVENTS_INGESTED, delta=1, cache=True, scope=scope_from(organization_id=...))whenis_ee()and the scope carries anorganization_id. Over-quota orgs log[EVENTS] L1 quota exceeded, dropping <event_type>and return without publishing — no HTTP error, no 429. The user's read or commit response is unaffected by event metering. On any other exception the helper fails open (return True) and the publish proceeds; the authoritative check in the worker is the source of truth. OSS skips L1 entirely (the EE imports are deferred underif is_ee():).L2 quota (worker side, authoritative).
EventsWorker.process_batchregroups the per-project batches by org, sums the per-org delta, and runs onecheck_entitlements(key=Counter.EVENTS_INGESTED, delta=delta, scope=scope_from(organization_id=org_id))per org (cache=False, the default — atomic DB upsert). Over-quota orgs drop their batch but messages are still ACKed so the consumer-group PEL doesn't back up. On adapter error (Redis / DB glitch) the worker drops the org's batch conservatively, mirroring the tracing worker's stance. The pre-existing per-projectFlag.ACCESScheck was removed: that flag isFalseon Hobby and Pro by design (it gates org-flag mutation inorganization_router, not events), so the old check would have silently dropped events for every Hobby/Pro customer.Flag.AUDITat the query side.POST /events/query(api/oss/src/apis/fastapi/events/router.py) now runscheck, _, _ = await check_entitlements(key=Flag.AUDIT)after the existingPermission.VIEW_SPANSaccess check; onnot checkthe handler returnsNOT_ENTITLED_RESPONSE(Tracker.FLAGS). Default plan map: Hobby=False, Pro=False, Business=True, Agenta=True, Self-hosted Enterprise=True. The flag is included inCONSTRAINTS[BLOCKED]so an org can't promote itself. Webhook subscription delivery is not gated by this flag; the audit log and the webhook delivery surface are independent consumers of the samestreams:eventstopic.AuthScope-first scope resolution.
_Scopewidened to carryworkspace_idin addition toorganization_id/project_id/user_id.request_scope()now reads the ambientAuthScopefirst (viaget_auth_scope()inoss/src/utils/context.py) and falls back torequest.stateonly when no auth context is published (admin endpoints, public endpoints, unit tests that hand-craftSimpleNamespacestate).publish_revision_eventfollows the same order: explicit kwargs → AuthScope →request.state. Service-layer commits no longer need to threadorganization_idthrough service signatures — they pick it up from the AuthScope set by the auth middleware on the originating HTTP request, propagated through nested async work via the standard ContextVar mechanism.Plan map.
Counter.EVENTS_INGESTEDdefaults across the five default plans: HobbyQuota(retention=Retention.MONTHLY, period=Period.MONTHLY), ProQUARTERLY, BusinessYEARLY, Agenta and Self-hosted Enterpriseperiod=Period.MONTHLYwith no retention (unlimited).Flag.AUDITdefaults: HobbyFalse, ProFalse, BusinessTrue, AgentaTrue, Self-hosted EnterpriseTrue. Both new gates are wired intoDEFAULT_ENTITLEMENTSso operator overrides viaAGENTA_ACCESS_PLANS/AGENTA_ACCESS_DEFAULT_PLAN_OVERLAYflow through unchanged.Tests
Unit additions (
api/oss/tests/pytest/unit/events/):test_events_utils.py(+~30 new cases on top of the 30 originally shipped): EventType / WebhookEventType parity, attribute builders + capping, revision attribute construction across all five domains,request_scopesemantics, AuthScope-first precedence, L1 quota allow / drop / fail-open / skip-on-OSS / skip-when-org-unknown.test_service_commit_emission.py(existing): one commit-event-per-call across all five services, environment-specificstate/diffpreservation, DAO-returns-None suppression, and delta-commit exactly-once coverage for environments and testsets.test_events_worker_l2.py(new): per-org delta aggregation across projects, allow / deny / skip-on-OSS / check-failure-drop semantics.test_events_router_audit.py(new): directPOST /events/queryrouter coverage for theFlag.AUDITallow and deny branches.One edit in
api/oss/tests/pytest/unit/test_environments_service.pyrepoints apatch("oss.src.core.environments.service.publish_event")topatch("oss.src.core.events.utils.publish_event")to match the normalized emission seam. Full OSS unit suite green: 575 passing.Migrations
No migration in this PR. The
eventstable existed already, themeters_typePostgres enum was extended withevents_ingestedon the meters reshape (9d3e8f0a1b2c_reshape_meters_tablefrom thefeat/clean-up-metersbranch, merged into this branch but not part of this PR's design scope), andFlag.AUDITis a code-level Pydantic enum — no DB representation, no schema change. Plan-map changes ship as code defaults; operators consume them via the existingAGENTA_ACCESS_PLANS/AGENTA_ACCESS_DEFAULT_PLAN_OVERLAYenv-driven config layer.Env vars
No env vars added, edited, or removed. Operators have nothing to change in their
.envfor this PR. The newFlag.AUDITand theCounter.EVENTS_INGESTEDquota are wired intoDEFAULT_ENTITLEMENTSand flow through the existing env-overlay surface; operators who want to override the per-plan defaults useAGENTA_ACCESS_PLANS/AGENTA_ACCESS_DEFAULT_PLAN_OVERLAYas they would for any other entitlement.Tests and docs
docs/designs/extend-events-beyond-deployments/carries the design story:research.mdandgap.mdas historical baselines,proposal.mdfor the as-shipped read/commit split and AuthScope-first scope resolution,events.mdas the per-event reference (29 events with example payloads, capping rules, and delta-commit semantics),tasks.mdas the execution checklist, and thissummary.md. User-facing docs:docs/docs/prompt-engineering/integrating-prompts/04-webhooks.mdxlists every subscribable event type and now distinguishes read/log payloads (count) from commit payloads (nocount, optionalmessage); the generated Fern TS client (web/packages/agenta-api-client/src/generated/api/types/WebhookEventType.ts) anddocs/docs/reference/openapi.jsoninclude all 29 new types. Internal API guidance (AGENTS.md) is unchanged — the helper layer follows the existing domain-folder + DTO-mapping conventions.