feat: add auto-event emission to CapiscioMiddleware#33
Conversation
Add opt-in automatic event emission to the FastAPI/Starlette middleware. When an EventEmitter is provided, the middleware automatically emits: - request.received: on every inbound request - verification.success/failed: after badge verification - request.completed: after response with status_code and duration_ms Design: - Fully opt-in: no emitter = no events (backward compatible) - Safe: emitter errors never break request handling - Standardized fields: method, path, caller_did, duration_ms, status_code
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
There was a problem hiding this comment.
Pull request overview
Adds opt-in automatic observability event emission to the FastAPI/Starlette CapiscioMiddleware so request lifecycle and verification outcomes can be tracked without manual instrumentation.
Changes:
- Extend
CapiscioMiddlewareto accept an optionalEventEmitterand emit standardized middleware lifecycle events. - Add new middleware-related event type constants to
EventEmitter. - Add unit tests validating auto-event emission across success/failure/excluded-path scenarios.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
capiscio_sdk/integrations/fastapi.py |
Adds emitter support and emits request.received, verification.*, and request.completed events around middleware execution. |
capiscio_sdk/events.py |
Introduces middleware auto-event type constants on EventEmitter. |
tests/unit/test_fastapi_integration.py |
Adds TestAutoEvents coverage for auto-emission behavior using a mocked EventEmitter. |
| def test_auto_events_on_missing_badge_block(self): | ||
| """Emitter receives request.received and verification.failed when badge missing.""" | ||
| guard = MagicMock() | ||
| guard.agent_id = "test-agent" | ||
|
|
||
| emitter = MagicMock(spec=EventEmitter) | ||
|
|
||
| app = self._make_app(guard, emitter) | ||
| client = TestClient(app) | ||
|
|
||
| response = client.post("/tasks/send", json={"msg": "hi"}) |
There was a problem hiding this comment.
Auto-events are tested for missing-badge in block mode and for verification failure in log mode, but there’s no test covering missing-badge behavior in fail_mode="log"/"monitor" with an emitter. Add a test to lock in whether verification.failed should be emitted (per PR description) while still allowing the request.
| import json | ||
| import pytest | ||
| from unittest.mock import MagicMock, patch | ||
| from unittest.mock import MagicMock, patch, call |
There was a problem hiding this comment.
call is imported from unittest.mock but never used in this test module; this will be flagged by ruff (F401) and should be removed (or actually used if you intended to assert call ordering).
| from unittest.mock import MagicMock, patch, call | |
| from unittest.mock import MagicMock, patch |
| if self.fail_mode in ("log", "monitor"): | ||
| logger.warning(f"Missing X-Capiscio-Badge header for {request.url.path} ({self.fail_mode} mode)") | ||
| request.state.agent = None | ||
| request.state.agent_id = None | ||
| return await call_next(request) | ||
| response = await call_next(request) |
There was a problem hiding this comment.
In the missing-badge branch for fail_mode = "log"/"monitor", the middleware currently emits request.received and request.completed but does not emit verification.failed. This conflicts with the PR description that verification.failed should be emitted when the badge is missing; consider emitting it here as well (without blocking).
| self._auto_emit(EventEmitter.EVENT_VERIFICATION_FAILED, { | ||
| "method": request.method, | ||
| "path": path, | ||
| "reason": "missing_badge", | ||
| }) |
There was a problem hiding this comment.
The verification.failed auto-event emitted for a blocked missing-badge request does not include duration_ms, but the PR description lists duration_ms as a key field for verification failures. Consider including a duration (e.g., from request_start).
capiscio_sdk/integrations/fastapi.py
Outdated
| the middleware automatically emits request.received, request.completed, | ||
| verification.success, and verification.failed events. |
There was a problem hiding this comment.
Docstring says the middleware "automatically emits request.completed" when an emitter is provided, but blocked requests (401/403 paths) return before _auto_emit_completed is called. Update the docstring to clarify when request.completed is (and is not) emitted, or emit it for blocked responses too.
| the middleware automatically emits request.received, request.completed, | |
| verification.success, and verification.failed events. | |
| the middleware automatically emits request.received, verification.success, | |
| and verification.failed events, and emits request.completed for requests | |
| that reach the downstream ASGI app (blocked 4xx responses may not emit | |
| request.completed). |
capiscio_sdk/events.py
Outdated
| # Middleware auto-event types (emitted automatically by CapiscioMiddleware) | ||
| EVENT_REQUEST_RECEIVED = "request.received" | ||
| EVENT_REQUEST_COMPLETED = "request.completed" | ||
| EVENT_REQUEST_FAILED = "request.failed" |
There was a problem hiding this comment.
EVENT_REQUEST_FAILED is introduced but not referenced anywhere in the repository. If it’s not part of the current auto-event contract, consider removing it to avoid confusing API consumers, or wire it into the middleware and add coverage.
| EVENT_REQUEST_FAILED = "request.failed" |
- CHANGELOG.md: document middleware auto-events feature - README.md: add middleware observability section - docs/guides/configuration.md: add auto-events configuration guide - docs/api-reference.md: add EventEmitter and event constants
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
- Remove unused 'call' import from test module - Remove dead EVENT_REQUEST_FAILED constant (not wired into middleware) - Emit verification.failed for missing badge in log/monitor mode - Add duration_ms to blocked missing-badge verification.failed event - Fix docstring to clarify request.completed not emitted for blocked responses - Add test for missing-badge in log mode with emitter
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
| def _auto_emit(self, event_type: str, data: Dict[str, Any]) -> None: | ||
| """Emit an auto-event if emitter is configured.""" | ||
| if self._emitter: | ||
| try: | ||
| self._emitter.emit(event_type, data) | ||
| except Exception: | ||
| logger.debug(f"Failed to emit auto-event {event_type}", exc_info=True) |
There was a problem hiding this comment.
_auto_emit() calls EventEmitter.emit() synchronously from the async middleware. EventEmitter.emit() can trigger a flush, which performs a blocking httpx.Client.post() and can therefore block the event loop and add latency to requests. Consider offloading emission/flush to a background thread/queue (or using an async emitter) so middleware request handling isn’t impacted by network I/O.
|
|
||
| # Middleware auto-event types (emitted automatically by CapiscioMiddleware) | ||
| EVENT_REQUEST_RECEIVED = "request.received" | ||
| EVENT_REQUEST_COMPLETED = "request.completed" |
There was a problem hiding this comment.
Docs/PR metadata refer to a EVENT_REQUEST_FAILED constant, but the code only defines EVENT_REQUEST_RECEIVED, EVENT_REQUEST_COMPLETED, EVENT_VERIFICATION_SUCCESS, and EVENT_VERIFICATION_FAILED. Either add the missing constant (and ensure it’s actually emitted where appropriate) or remove references to it from docs/changelog to avoid advertising a nonexistent API surface.
| EVENT_REQUEST_COMPLETED = "request.completed" | |
| EVENT_REQUEST_COMPLETED = "request.completed" | |
| EVENT_REQUEST_FAILED = "request.failed" |
| emitter = EventEmitter( | ||
| agent_id="your-agent-id", | ||
| api_key="sk_live_...", | ||
| registry_url="https://registry.capisc.io" |
There was a problem hiding this comment.
The example constructs EventEmitter with registry_url=..., but EventEmitter.__init__ expects server_url (there’s no registry_url kwarg). This snippet will raise TypeError if copied. Update the parameter name (or add a backwards-compatible alias in EventEmitter).
| registry_url="https://registry.capisc.io" | |
| server_url="https://registry.capisc.io" |
| If you're using `CapiscIO.connect()`, the agent identity already has an emitter: | ||
|
|
||
| ```python | ||
| from capiscio_sdk import CapiscIO | ||
| from capiscio_sdk.integrations.fastapi import CapiscioMiddleware | ||
|
|
||
| agent = CapiscIO.connect(api_key="sk_live_...") | ||
|
|
||
| app.add_middleware( | ||
| CapiscioMiddleware, | ||
| guard=guard, | ||
| emitter=agent.emitter # Use the agent's emitter | ||
| ) |
There was a problem hiding this comment.
This section says CapiscIO.connect() returns an object with agent.emitter, but AgentIdentity currently only exposes emit(...) and keeps the emitter as a private _emitter field. Either add a public emitter property/attribute on AgentIdentity, or update the docs to show the supported pattern (e.g., construct an EventEmitter from agent.server_url/agent.api_key/agent.agent_id, or just call agent.emit(...)).
| from capiscio_sdk.events import EventEmitter | ||
| from capiscio_sdk.integrations.fastapi import CapiscioMiddleware | ||
|
|
||
| emitter = EventEmitter(agent_id="...", api_key="...", registry_url="...") |
There was a problem hiding this comment.
The README example uses EventEmitter(..., registry_url="..."), but the initializer parameter is server_url. As written, this snippet will raise TypeError when run; please update the argument name (or support registry_url as an alias).
| emitter = EventEmitter(agent_id="...", api_key="...", registry_url="...") | |
| emitter = EventEmitter(agent_id="...", api_key="...", server_url="...") |
| - Standardized fields: `method`, `path`, `caller_did`, `duration_ms`, `status_code` | ||
| - Safe by design: emitter errors never break request handling | ||
| - Excluded paths emit no events | ||
| - New event type constants: `EVENT_REQUEST_RECEIVED`, `EVENT_REQUEST_COMPLETED`, `EVENT_REQUEST_FAILED`, `EVENT_VERIFICATION_SUCCESS`, `EVENT_VERIFICATION_FAILED` |
There was a problem hiding this comment.
The changelog lists EVENT_REQUEST_FAILED as a new constant, but it isn’t defined in capiscio_sdk.events (and no corresponding middleware event type appears to be emitted). Please either add/emit this event type or remove it from the changelog entry to keep release notes accurate.
| - New event type constants: `EVENT_REQUEST_RECEIVED`, `EVENT_REQUEST_COMPLETED`, `EVENT_REQUEST_FAILED`, `EVENT_VERIFICATION_SUCCESS`, `EVENT_VERIFICATION_FAILED` | |
| - New event type constants: `EVENT_REQUEST_RECEIVED`, `EVENT_REQUEST_COMPLETED`, `EVENT_VERIFICATION_SUCCESS`, `EVENT_VERIFICATION_FAILED` |
| - EVENT_TASK_STARTED | ||
| - EVENT_TASK_COMPLETED | ||
| - EVENT_TASK_FAILED | ||
| - EVENT_TOOL_CALL | ||
| - EVENT_TOOL_RESULT | ||
| - EVENT_REQUEST_RECEIVED | ||
| - EVENT_REQUEST_COMPLETED | ||
| - EVENT_REQUEST_FAILED | ||
| - EVENT_VERIFICATION_SUCCESS | ||
| - EVENT_VERIFICATION_FAILED |
There was a problem hiding this comment.
docs/api-reference.md lists EVENT_TASK_STARTED, EVENT_TOOL_CALL, EVENT_REQUEST_FAILED, etc. as module members of capiscio_sdk.events, but these are currently class attributes on EventEmitter (and EVENT_REQUEST_FAILED isn’t defined at all). This will produce incorrect API docs (and may break doc builds depending on mkdocstrings settings). Consider documenting only EventEmitter here, or exporting module-level constants (e.g., EVENT_TASK_STARTED = EventEmitter.EVENT_TASK_STARTED, etc.) and ensuring all listed symbols exist.
| - EVENT_TASK_STARTED | |
| - EVENT_TASK_COMPLETED | |
| - EVENT_TASK_FAILED | |
| - EVENT_TOOL_CALL | |
| - EVENT_TOOL_RESULT | |
| - EVENT_REQUEST_RECEIVED | |
| - EVENT_REQUEST_COMPLETED | |
| - EVENT_REQUEST_FAILED | |
| - EVENT_VERIFICATION_SUCCESS | |
| - EVENT_VERIFICATION_FAILED |
Summary
Adds opt-in automatic event emission to the FastAPI/Starlette middleware, closing the gap where all observability events had to be manually emitted.
Problem
The middleware handled badge verification but emitted zero events. Developers had to manually instrument every request lifecycle event, which meant most deployments had no observability into request patterns, verification outcomes, or latency.
Solution
When an
EventEmitteris provided toCapiscioMiddleware, it automatically emits standardized events at key middleware lifecycle points:request.receivedverification.successverification.failedrequest.completedDesign Principles
exclude_pathsemit nothing (no noise from health checks).Usage
New Event Type Constants
Added to
capiscio_sdk/events.py:EVENT_REQUEST_RECEIVEDEVENT_REQUEST_COMPLETEDEVENT_REQUEST_FAILEDEVENT_VERIFICATION_SUCCESSEVENT_VERIFICATION_FAILEDTests
7 new tests in
TestAutoEventscovering:All 50 unit tests pass (22 fastapi + 28 events).