Skip to content

feat(governance): add track_event for custom telemetry to /runtime/log#1745

Merged
viswa-uipath merged 1 commit into
mainfrom
feat/traces-api
Jun 30, 2026
Merged

feat(governance): add track_event for custom telemetry to /runtime/log#1745
viswa-uipath merged 1 commit into
mainfrom
feat/traces-api

Conversation

@viswa-uipath

@viswa-uipath viswa-uipath commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds track_event to GovernanceService and UiPathPlatformGovernanceProvider for posting custom telemetry events to POST /agenticgovernance_/api/v1/runtime/log. The server forwards each event to App Insights as a customEvents row.

  • GovernanceService.track_event(*, event_name, data=None, operation_id=None) + async — POSTs { eventName, data? } to /runtime/log. eventName must be non-empty; data is included only when provided.
  • x-uipath-operation-id header — caller can supply operation_id to correlate events from one logical request (becomes App Insights operation_Id). When omitted, 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.track_event(*, ...) + async — thin delegate so runtime consumers can emit events through the protocol-adapter surface without importing the platform service directly.

Bumps: uipath-platform 0.1.73 → 0.1.74.

Design choices

  • Public ergonomic signature, matching 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_id fallback to resolve_trace_id(), not required — events emitted from inside an OTel-traced agent flow automatically get the canonical trace id as their operation_Id. Callers in background pools (where OTel context is thread-local and lost) pass operation_id explicitly; callers with neither leave App Insights to assign one per event.
  • Reuses _build_org_scoped_requestUIPATH_SERVICE_URL_AGENTICGOVERNANCE override + routing-header injection work for /runtime/log without extra plumbing. Override redirects to {override}/api/v1/runtime/log and replaces the platform router's tenant/account headers.
  • No new core protocoltrack_event is platform-specific telemetry, not a runtime contract. Adding a protocol can be done later if uipath-runtime consumers need to swap in fakes.
  • @traced(name="governance_track_event", ...) on both sync/async — matches the trace span names already used for retrieve_policy and compensate.

Out of scope (follow-ups)

  • A core protocol for track_event if/when a runtime consumer needs constructor-injected fakes.

Test plan

  • ruff check . / ruff format --check . — clean
  • mypy src testsSuccess: no issues found in 201 source files
  • 1301 passed, 7 skipped (LLM creds), 4 deselected — full uipath-platform suite
  • 12 new tests specifically for track_event:
    • TestTrackEvent (8): name-only payload; data included when provided; caller operation_id header; trace-id fallback via resolve_trace_id(); caller value wins over fallback; header omitted when no source resolves; UIPATH_ORGANIZATION_ID missing raises ValueError; HTTP error raises EnrichedException.
    • TestTrackEventAsync (1): async variant — payload shape + trace-id fallback.
    • TestServiceUrlOverride (+1): UIPATH_SERVICE_URL_AGENTICGOVERNANCE redirects /runtime/log and preserves x-uipath-operation-id.
    • TestDelegation (+2): provider delegation for sync + async.

🤖 Generated with Claude Code

Development Packages

uipath-platform

[project]
dependencies = [
  # Exact version (copy-paste ready):
  "uipath-platform==0.1.83.dev1017457037",

  # Any version from this PR (uncomment to use a range instead):
  # "uipath-platform>=0.1.83.dev1017450000,<0.1.83.dev1017460000",
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-platform = { index = "testpypi" }
uipath-core = { index = "testpypi" }

[tool.uv]
override-dependencies = ["uipath-core==0.5.28.dev1017457037"]

uipath-core

[project]
dependencies = [
  # Exact version (copy-paste ready):
  "uipath-core==0.5.27.dev1017457026",

  # Any version from this PR (uncomment to use a range instead):
  # "uipath-core>=0.5.27.dev1017450000,<0.5.27.dev1017460000",
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath-core = { index = "testpypi" }

uipath

[project]
dependencies = [
  # Exact version (copy-paste ready):
  "uipath==2.11.16.dev1017457034",

  # Any version from this PR (uncomment to use a range instead):
  # "uipath>=2.11.16.dev1017450000,<2.11.16.dev1017460000",
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath = { index = "testpypi" }
uipath-platform = { index = "testpypi" }
uipath-core = { index = "testpypi" }

[tool.uv]
override-dependencies = ["uipath-platform==0.1.83.dev1017457034", "uipath-core==0.5.28.dev1017457034"]

Copilot AI review requested due to automatic review settings June 24, 2026 06:57
@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels Jun 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, with operation_id falling back to resolve_trace_id().
  • Add UiPathPlatformGovernanceProvider.track_event() / track_event_async() delegation methods.
  • Bump uipath-platform version 0.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.

viswa-uipath added a commit that referenced this pull request Jun 24, 2026
…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>
@viswa-uipath viswa-uipath added the build:dev Create a dev build from the pr label Jun 24, 2026
@viswa-uipath viswa-uipath added build:dev Create a dev build from the pr and removed build:dev Create a dev build from the pr labels Jun 25, 2026
@github-actions github-actions Bot added test:uipath-runtime and removed build:dev Create a dev build from the pr labels Jun 25, 2026
@viswa-uipath viswa-uipath changed the base branch from main to feat/adapter-fix June 25, 2026 14:23
@viswa-uipath viswa-uipath added the build:dev Create a dev build from the pr label Jun 25, 2026
viswa-uipath added a commit that referenced this pull request Jun 26, 2026
…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>
viswa-uipath added a commit that referenced this pull request Jun 27, 2026
…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>
@viswa-uipath viswa-uipath force-pushed the feat/adapter-fix branch 2 times, most recently from 296d587 to c1ae046 Compare June 27, 2026 09:57
@viswa-uipath viswa-uipath removed the build:dev Create a dev build from the pr label Jun 28, 2026
@viswa-uipath viswa-uipath added the build:dev Create a dev build from the pr label Jun 28, 2026
@viswa-uipath viswa-uipath force-pushed the feat/adapter-fix branch 4 times, most recently from ef22945 to b523a6d Compare June 29, 2026 09:44
Base automatically changed from feat/adapter-fix to main June 29, 2026 10:00
@viswa-uipath viswa-uipath force-pushed the feat/traces-api branch 3 times, most recently from 850ef57 to f8d75d4 Compare June 29, 2026 16:34
@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-integrations cross-tests are FAILING 🚨

Your changes may break one or more integrations in uipath-integrations-python:

  • uipath-openai-agents
  • uipath-google-adk
  • uipath-agent-framework
  • uipath-llamaindex
  • uipath-pydantic-ai

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

Comment thread packages/uipath-core/src/uipath/core/governance/providers.py
Comment thread packages/uipath-platform/pyproject.toml Outdated
@radu-mocanu

Copy link
Copy Markdown
Collaborator

Please squash this PR under one commit.

@radu-mocanu radu-mocanu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@radu-mocanu

radu-mocanu commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Clarifying the changes-requested comment above after checking the docs config:

track_event and track_event_async are public service methods that will become client-facing if/when governance docs are generated from GovernanceService, the same way the current mkdocs pages document public service classes via mkdocstrings. The provider is not currently referenced by the docs, so the provider method should not be documented unless a docs page explicitly includes it.

They are internal SDK/runtime concerns.

Runtime PR #135 can still wire this through the internal platform provider without exposing it on UiPath().governance.

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,
    )

@viswa-uipath viswa-uipath force-pushed the feat/traces-api branch 2 times, most recently from 9b8bc44 to e4810f6 Compare June 30, 2026 11:13
@viswa-uipath viswa-uipath requested a review from radu-mocanu June 30, 2026 11:15
Comment thread packages/uipath-platform/src/uipath/platform/governance/_governance_service.py Outdated
Comment thread packages/uipath-platform/src/uipath/platform/governance/_governance_service.py Outdated
@viswa-uipath viswa-uipath force-pushed the feat/traces-api branch 2 times, most recently from 244a1b9 to aff4723 Compare June 30, 2026 13:38
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>
@viswa-uipath viswa-uipath enabled auto-merge (squash) June 30, 2026 14:02
@viswa-uipath viswa-uipath merged commit fe490f7 into main Jun 30, 2026
125 of 126 checks passed
@viswa-uipath viswa-uipath deleted the feat/traces-api branch June 30, 2026 14:04
@sonarqubecloud

Copy link
Copy Markdown

viswa-uipath added a commit that referenced this pull request Jun 30, 2026
…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>
viswa-uipath added a commit that referenced this pull request Jun 30, 2026
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>
viswa-uipath added a commit that referenced this pull request Jun 30, 2026
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>
@github-actions

Copy link
Copy Markdown

🚨 Heads up: uipath-langchain cross-tests are FAILING 🚨

Your changes may break the uipath-langchain-python integration.

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build:dev Create a dev build from the pr test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-runtime

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants