feat(governance): add track_event for custom telemetry to /runtime/log#1745
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a platform-specific telemetry hook to the uipath-platform governance surface so runtime consumers can emit custom events to the agentic governance ingress (POST .../api/v1/runtime/log), with optional correlation via x-uipath-operation-id.
Changes:
- Add
GovernanceService.track_event()/track_event_async()to POST{eventName, data?}to/runtime/log, withoperation_idfalling back toresolve_trace_id(). - Add
UiPathPlatformGovernanceProvider.track_event()/track_event_async()delegation methods. - Bump
uipath-platformversion0.1.73 → 0.1.74(and lockfile).
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/uipath-platform/src/uipath/platform/governance/_governance_service.py | Adds the new /runtime/log client methods and operation-id header behavior. |
| packages/uipath-platform/src/uipath/platform/governance/_governance_provider.py | Exposes track_event through the provider adapter via thin delegation. |
| packages/uipath-platform/tests/services/test_governance_service.py | Adds sync/async tests covering payload shape, operation-id behavior, override routing, and error paths. |
| packages/uipath-platform/tests/services/test_governance_provider.py | Adds delegation tests for the provider’s sync/async track_event methods. |
| packages/uipath-platform/pyproject.toml | Version bump to 0.1.74. |
| packages/uipath-platform/uv.lock | Lockfile update reflecting the version bump. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ockfile track_event / track_event_async now reject empty or whitespace-only event_name with a ValueError at call time instead of round-tripping the platform's own 4xx (per Copilot review on #1745). Documented in the Raises section and covered by parametrized tests on both the sync and async variants. Also regenerates packages/uipath/uv.lock so it tracks the bumped uipath-platform 0.1.74 — the prior CI run failed `uv sync --locked` because the lockfile still referenced 0.1.73. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f702886 to
73908ab
Compare
73908ab to
8a03bce
Compare
…it empty Lets the runtime layer (uipath-runtime-python) stop carrying ``trace_id`` through ``UiPathGovernedRuntime`` / ``GuardrailCompensator``. The runtime emits compensation requests with ``trace_id=""`` and the platform fills in the canonical agent trace id at HTTP-call time via the existing ``resolve_trace_id()`` helper — same fallback ``track_event`` (PR #1745) already uses. uipath-core - ``GovernRequest.trace_id`` relaxes from required ``str`` to ``str = ""`` default. Docstring documents the platform-side self-resolve contract so wire callers know an empty value is legitimate. uipath-platform - ``GovernanceService._compensate`` / ``_compensate_async`` now call a new ``_resolve_request_trace_id()`` helper before the POST. When ``request.trace_id`` is empty the helper resolves via ``resolve_trace_id()`` (env → LLMOps external span → OTel current span). Caller-supplied values win — the runtime captures live OTel context across its background-pool hop via ``contextvars.copy_context()``, so when the worker calls ``provider.compensate(...)`` the platform-side resolver sees the agent's live span and returns the same canonical id. Caller-supplied non-empty trace ids continue to pass through unchanged. Tests - uipath-core governance suite: 32 passed. - uipath-platform governance service suite: 27 passed. - ruff + mypy clean on ``src/uipath/core/governance`` and ``src/uipath/platform/governance``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cb498ce to
903c35b
Compare
…it empty Lets the runtime layer (uipath-runtime-python) stop carrying ``trace_id`` through ``UiPathGovernedRuntime`` / ``GuardrailCompensator``. The runtime emits compensation requests with ``trace_id=""`` and the platform fills in the canonical agent trace id at HTTP-call time via the existing ``resolve_trace_id()`` helper — same fallback ``track_event`` (PR #1745) already uses. uipath-core - ``GovernRequest.trace_id`` relaxes from required ``str`` to ``str = ""`` default. Docstring documents the platform-side self-resolve contract so wire callers know an empty value is legitimate. uipath-platform - ``GovernanceService._compensate`` / ``_compensate_async`` now call a new ``_resolve_request_trace_id()`` helper before the POST. When ``request.trace_id`` is empty the helper resolves via ``resolve_trace_id()`` (env → LLMOps external span → OTel current span). Caller-supplied values win — the runtime captures live OTel context across its background-pool hop via ``contextvars.copy_context()``, so when the worker calls ``provider.compensate(...)`` the platform-side resolver sees the agent's live span and returns the same canonical id. Caller-supplied non-empty trace ids continue to pass through unchanged. Tests - uipath-core governance suite: 32 passed. - uipath-platform governance service suite: 27 passed. - ruff + mypy clean on ``src/uipath/core/governance`` and ``src/uipath/platform/governance``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
296d587 to
c1ae046
Compare
8a03bce to
62befd6
Compare
62befd6 to
c51c0c6
Compare
ef22945 to
b523a6d
Compare
850ef57 to
f8d75d4
Compare
🚨 Heads up:
|
|
Please squash this PR under one commit. |
There was a problem hiding this comment.
track_event and track_event_async are public service methods that are automatically going to be include in our docs here https://uipath.github.io/uipath-python/core/assets/.
they are internal SDK concerns.
|
Clarifying the changes-requested comment above after checking the docs config:
They are internal SDK/runtime concerns. Runtime PR #135 can still wire this through the internal platform provider without exposing it on Suggested shape: # GovernanceService: private, not client-facing/docs-facing
def _track_event(
self,
*,
event_name: str,
data: dict[str, Any] | None = None,
operation_id: str | None = None,
) -> None:
...
# UiPathPlatformGovernanceProvider: internal runtime adapter
def track_event(
self,
*,
event_name: str,
data: dict[str, Any] | None = None,
operation_id: str | None = None,
) -> None:
self._service._track_event(
event_name=event_name,
data=data,
operation_id=operation_id,
) |
9b8bc44 to
e4810f6
Compare
e4810f6 to
fa403c1
Compare
244a1b9 to
aff4723
Compare
GovernanceService._track_event(*, event_name, data=None, operation_id=None) posts to /runtime/log, which the server forwards to App Insights as a customEvents row. Both sync and async variants are provided. The method is underscore-prefixed because it is an internal runtime seam, not a client-facing SDK call — kept off the public service surface (and off ``@traced``) so the auto-generated mkdocs stays focused on retrieve_policy / compensate and tracing the telemetry-emit path itself can't recursively create governance spans. event_name is validated client-side — empty or whitespace-only values raise ValueError before any URL/header work, so callers fail fast with a clear error instead of round-tripping the platform's 4xx. The optional operation_id becomes the x-uipath-operation-id header that the server stamps as App Insights operation_Id on every emitted event — events sharing an id are queryable together in KQL. When the caller omits operation_id, it falls back to resolve_trace_id() so events from the same agent trace correlate automatically; when no source resolves, the header is omitted and App Insights generates its own id per event. UiPathPlatformGovernanceProvider exposes thin track_event / track_event_async delegates (the provider is the runtime-facing adapter; it isn't included in the customer-facing docs) so runtime consumers emit events through the protocol-adapter surface without importing the platform service directly. The delegates forward to the service's underscore-prefixed methods. GovernRequest gains the per-evaluation telemetry fields needed by the audit-event payload (trace_id is optional now via str | None default, plus the agentFramework / agentType / runtimeVersion runtime-identity fields the governance server stamps onto rule-denied telemetry). This is a public contract change, so uipath-core is bumped to 0.5.27 and uipath-platform's lower bound is raised to ``uipath-core>=0.5.27`` so installations cannot resolve a core package that lacks the new fields. Bumps: - uipath-core 0.5.26 → 0.5.27 (new GovernRequest fields) - uipath-platform 0.1.82 → 0.1.83 (republish required because the new uipath-core pin needs a fresh wheel — 0.1.82 is already on PyPI with the old ``uipath-core>=0.5.26`` pin) Co-Authored-By: Aditi Kumari <aditi.kumari@uipath.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aff4723 to
303c4ae
Compare
|
…ports Drop the dispatcher from ``governance/__init__.py``'s ``__all__`` and remove the top-level re-import. The class is host-wiring glue for the runtime sink's non-blocking ``track_event`` adapter, not a customer- facing API — its docstring even tags the audience as "the host". Same principle radu applied to ``_track_event`` on ``GovernanceService`` (PR #1745): internal seams shouldn't sit on the docs-facing ``uipath.platform.governance`` import path that ``mkdocstrings`` auto-discovers. Internal callers import via the explicit private path: from uipath.platform.governance._live_track_event_dispatcher import ( LiveTrackEventDispatcher, ) Test imports updated accordingly. No public-surface change beyond the ``__init__.py`` re-export — the class itself stays where it is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime's TrackEventAuditSink invokes its injected ``track_event`` callable synchronously on the agent's hook thread. Wiring the platform provider's method directly would block agent execution on governance telemetry's HTTP round-trip (the ``POST /runtime/log`` write to App Insights). LiveTrackEventDispatcher wraps ``provider.track_event`` in a bounded ``ThreadPoolExecutor`` (default 10 workers, matching ``LiveTrackingSpanProcessor`` and sized for the bursty-but-not- sustained shape of governance events). The host wires ``dispatcher.dispatch`` into ``AuditManager(track_event=...)`` instead of the provider's method — same kwargs, but the HTTP work runs on a background worker. ``dispatch`` returns immediately; worker exceptions are caught and logged at debug per the fire-and-forget contract. ``shutdown(wait=True)`` drains in-flight submissions so process teardown doesn't lose telemetry. Idempotent — safe to call from multiple atexit hooks. Public-surface discipline ------------------------- The class is intentionally **not** re-exported from ``governance/__init__.py``. It's host-wiring glue for the runtime sink, not a customer-facing API — same principle radu applied to ``_track_event`` on ``GovernanceService`` (PR #1745): internal seams shouldn't sit on the docs-facing ``uipath.platform.governance`` import path that ``mkdocstrings`` auto-discovers. Internal callers use the explicit private path: from uipath.platform.governance._live_track_event_dispatcher import ( LiveTrackEventDispatcher, ) Tests cover the boundary contract (dispatch non-blocking via a ``threading.Event`` sync proof), kwarg forwarding, default flow- through, exception swallowing, concurrency under burst, and shutdown drain. Bumps: - uipath-platform 0.1.84 → 0.1.85 (new class needs a fresh wheel; ``0.1.84`` is already on PyPI) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The runtime's TrackEventAuditSink invokes its injected ``track_event`` callable synchronously on the agent's hook thread. Wiring the platform provider's method directly would block agent execution on governance telemetry's HTTP round-trip (the ``POST /runtime/log`` write to App Insights). LiveTrackEventDispatcher wraps ``provider.track_event`` in a bounded ``ThreadPoolExecutor`` (default 10 workers, matching ``LiveTrackingSpanProcessor`` and sized for the bursty-but-not- sustained shape of governance events). The host wires ``dispatcher.dispatch`` into ``AuditManager(track_event=...)`` instead of the provider's method — same kwargs, but the HTTP work runs on a background worker. Failure modes — all silent on the hook thread --------------------------------------------- - **Saturated pool**: ThreadPoolExecutor's own queue is unbounded, so the dispatcher uses a ``threading.BoundedSemaphore`` capped at ``max_workers * inflight_oversubscription`` (default 10 × 4 = 40). When the cap is reached, ``dispatch`` drops with a warning rather than queueing forever. Mirrors GuardrailCompensator so both governance dispatchers share one shape. - **Pool shut down**: ``executor.submit`` raises ``RuntimeError`` after shutdown — caught, semaphore slot released, logged at debug. A late dispatch (e.g. atexit hook after the pool drained) never crashes the hook thread. - **Worker exception**: provider HTTP call may raise (serialisation, 5xx, transport). Worker catches, logs at debug with ``exc_info=True`` for diagnostics, releases the in-flight slot. ``shutdown(wait=True)`` drains in-flight submissions so process teardown doesn't lose telemetry. Idempotent — safe to call from multiple atexit hooks. Public-surface discipline ------------------------- The class is intentionally **not** re-exported from ``governance/__init__.py``. It's host-wiring glue for the runtime sink, not a customer-facing API — same principle radu applied to ``_track_event`` on ``GovernanceService`` (PR #1745): internal seams shouldn't sit on the docs-facing ``uipath.platform.governance`` import path that ``mkdocstrings`` auto-discovers. Internal callers use the explicit private path: from uipath.platform.governance._live_track_event_dispatcher import ( LiveTrackEventDispatcher, ) Tests cover the boundary contract (dispatch non-blocking via a ``threading.Event`` sync proof), kwarg forwarding, default flow-through, exception swallowing, concurrency under burst, shutdown drain, shutdown idempotence, post-shutdown silent dispatch, and saturated-pool drop semantics. Bumps: - uipath-platform 0.1.84 → 0.1.85 (new class needs a fresh wheel; ``0.1.84`` is already on PyPI) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🚨 Heads up:
|



Summary
Adds
track_eventtoGovernanceServiceandUiPathPlatformGovernanceProviderfor posting custom telemetry events toPOST /agenticgovernance_/api/v1/runtime/log. The server forwards each event to App Insights as acustomEventsrow.GovernanceService.track_event(*, event_name, data=None, operation_id=None)+ async — POSTs{ eventName, data? }to/runtime/log.eventNamemust be non-empty;datais included only when provided.x-uipath-operation-idheader — caller can supplyoperation_idto correlate events from one logical request (becomes App Insightsoperation_Id). When omitted, falls back toresolve_trace_id()so events from the same agent trace correlate automatically. When no source resolves, the header is omitted and App Insights generates its own id per event.UiPathPlatformGovernanceProvider.track_event(*, ...)+ async — thin delegate so runtime consumers can emit events through the protocol-adapter surface without importing the platform service directly.Bumps:
uipath-platform0.1.73 → 0.1.74.Design choices
compensate(*, ...)—track_event(*, event_name, data=None, operation_id=None)follows the same kwarg-only pattern already established on the service; no request-object wrapper.operation_idfallback toresolve_trace_id(), not required — events emitted from inside an OTel-traced agent flow automatically get the canonical trace id as theiroperation_Id. Callers in background pools (where OTel context is thread-local and lost) passoperation_idexplicitly; callers with neither leave App Insights to assign one per event._build_org_scoped_request—UIPATH_SERVICE_URL_AGENTICGOVERNANCEoverride + routing-header injection work for/runtime/logwithout extra plumbing. Override redirects to{override}/api/v1/runtime/logand replaces the platform router's tenant/account headers.track_eventis platform-specific telemetry, not a runtime contract. Adding a protocol can be done later ifuipath-runtimeconsumers need to swap in fakes.@traced(name="governance_track_event", ...)on both sync/async — matches the trace span names already used forretrieve_policyandcompensate.Out of scope (follow-ups)
track_eventif/when a runtime consumer needs constructor-injected fakes.Test plan
ruff check ./ruff format --check .— cleanmypy src tests—Success: no issues found in 201 source filesuipath-platformsuitetrack_event:TestTrackEvent(8): name-only payload;dataincluded when provided; calleroperation_idheader; trace-id fallback viaresolve_trace_id(); caller value wins over fallback; header omitted when no source resolves;UIPATH_ORGANIZATION_IDmissing raisesValueError; HTTP error raisesEnrichedException.TestTrackEventAsync(1): async variant — payload shape + trace-id fallback.TestServiceUrlOverride(+1):UIPATH_SERVICE_URL_AGENTICGOVERNANCEredirects/runtime/logand preservesx-uipath-operation-id.TestDelegation(+2): provider delegation for sync + async.🤖 Generated with Claude Code
Development Packages
uipath-platform
uipath-core
uipath