Skip to content

Commit 642aa3e

Browse files
committed
feat: add telemetry for cli
1 parent 3e21ef4 commit 642aa3e

21 files changed

+460
-4
lines changed

.github/workflows/cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
rsync -a --no-whole-file --ignore-existing "$originalfile" "$tmpfile"
5858
envsubst '$CONNECTION_STRING' < "$originalfile" > "$tmpfile" && mv "$tmpfile" "$originalfile"
5959
env:
60-
CONNECTION_STRING: ${{ secrets.APPINS_CONNECTION_STRING }}
60+
CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }}
6161

6262
- name: Build
6363
run: uv build

.github/workflows/publish-dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
rsync -a --no-whole-file --ignore-existing "$originalfile" "$tmpfile"
4141
envsubst '$CONNECTION_STRING' < "$originalfile" > "$tmpfile" && mv "$tmpfile" "$originalfile"
4242
env:
43-
CONNECTION_STRING: ${{ secrets.APPINS_CONNECTION_STRING }}
43+
CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }}
4444

4545
- name: Set development version
4646
shell: pwsh

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.8.46"
3+
version = "2.8.47"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/_cli/_telemetry.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import logging
2+
import os
3+
import time
4+
from functools import wraps
5+
from importlib.metadata import version
6+
from typing import Any, Callable, Dict, Optional
7+
8+
from uipath._cli._utils._common import get_claim_from_token
9+
from uipath.telemetry._track import (
10+
_get_project_key,
11+
is_telemetry_enabled,
12+
track_cli_event,
13+
)
14+
15+
logger = logging.getLogger(__name__)
16+
17+
# Telemetry event name template for Application Insights
18+
CLI_COMMAND_EVENT = "Cli.{command}"
19+
20+
21+
class CliTelemetryTracker:
22+
"""Tracks CLI command execution and sends telemetry to Application Insights.
23+
24+
Sends a single event per command execution at completion with:
25+
- Status: "Completed" or "Failed"
26+
- Success: Boolean indicating success/failure
27+
- Error details (if failed)
28+
"""
29+
30+
def __init__(self) -> None:
31+
self._start_times: Dict[str, float] = {}
32+
33+
@staticmethod
34+
def _get_event_name(command: str) -> str:
35+
return f"Cli.{command.capitalize()}"
36+
37+
def _enrich_properties(self, properties: Dict[str, Any]) -> None:
38+
"""Enrich properties with common context information.
39+
40+
Args:
41+
properties: The properties dictionary to enrich.
42+
"""
43+
# Add UiPath context
44+
project_key = _get_project_key()
45+
if project_key:
46+
properties["AgentId"] = project_key
47+
48+
# Get organization ID
49+
organization_id = os.getenv("UIPATH_ORGANIZATION_ID")
50+
if organization_id:
51+
properties["CloudOrganizationId"] = organization_id
52+
53+
# Get tenant ID
54+
tenant_id = os.getenv("UIPATH_TENANT_ID")
55+
if tenant_id:
56+
properties["CloudTenantId"] = tenant_id
57+
58+
# Get CloudUserId from JWT token
59+
try:
60+
cloud_user_id = get_claim_from_token("sub")
61+
if cloud_user_id:
62+
properties["CloudUserId"] = cloud_user_id
63+
except Exception:
64+
pass
65+
66+
properties["SessionId"] = "nosession" # Placeholder for session ID
67+
68+
try:
69+
properties["SDKVersion"] = version("uipath")
70+
except Exception:
71+
pass
72+
73+
properties["IsGithubCI"] = bool(os.getenv("GITHUB_ACTIONS"))
74+
75+
# Add source identifier
76+
properties["Source"] = "uipath-python-cli"
77+
properties["ApplicationName"] = "UiPath.AgentCli"
78+
79+
def track_command_start(self, command: str) -> None:
80+
"""Record the start time for duration calculation."""
81+
try:
82+
self._start_times[command] = time.time()
83+
logger.debug(f"Started tracking CLI command: {command}")
84+
85+
except Exception as e:
86+
logger.debug(f"Error recording CLI command start time: {e}")
87+
88+
def track_command_end(
89+
self,
90+
command: str,
91+
duration_ms: Optional[int] = None,
92+
) -> None:
93+
try:
94+
if duration_ms is None:
95+
start_time = self._start_times.pop(command, None)
96+
if start_time:
97+
duration_ms = int((time.time() - start_time) * 1000)
98+
99+
properties: Dict[str, Any] = {
100+
"Command": command,
101+
"Status": "Completed",
102+
"Success": True,
103+
}
104+
105+
if duration_ms is not None:
106+
properties["DurationMs"] = duration_ms
107+
108+
self._enrich_properties(properties)
109+
110+
track_cli_event(self._get_event_name(command), properties)
111+
logger.debug(f"Tracked CLI command completed: {command}")
112+
113+
except Exception as e:
114+
logger.debug(f"Error tracking CLI command end: {e}")
115+
116+
def track_command_failed(
117+
self,
118+
command: str,
119+
duration_ms: Optional[int] = None,
120+
exception: Optional[Exception] = None,
121+
) -> None:
122+
try:
123+
if duration_ms is None:
124+
start_time = self._start_times.pop(command, None)
125+
if start_time:
126+
duration_ms = int((time.time() - start_time) * 1000)
127+
128+
properties: Dict[str, Any] = {
129+
"Command": command,
130+
"Status": "Failed",
131+
"Success": False,
132+
}
133+
134+
if duration_ms is not None:
135+
properties["DurationMs"] = duration_ms
136+
137+
if exception is not None:
138+
properties["ErrorType"] = type(exception).__name__
139+
properties["ErrorMessage"] = str(exception)[:500]
140+
141+
self._enrich_properties(properties)
142+
143+
track_cli_event(self._get_event_name(command), properties)
144+
logger.debug(f"Tracked CLI command failed: {command}")
145+
146+
except Exception as e:
147+
logger.debug(f"Error tracking CLI command failed: {e}")
148+
149+
150+
def track_command(command: str) -> Callable[..., Any]:
151+
"""Decorator to track CLI command execution.
152+
153+
Sends an event (Cli.<Command>) to Application Insights at command
154+
completion with the execution outcome.
155+
156+
Properties tracked include:
157+
- Command: The command name
158+
- Status: Execution outcome ("Completed" or "Failed")
159+
- Success: Whether the command succeeded (true/false)
160+
- DurationMs: Execution time in milliseconds
161+
- ErrorType: Exception type name (on failure)
162+
- ErrorMessage: Exception message (on failure, truncated to 500 chars)
163+
- AgentId: Project key from .uipath/.telemetry.json (GUID)
164+
- Version: Package version (uipath package)
165+
- ProjectId, CloudOrganizationId, etc. (if available)
166+
167+
Telemetry failures are silently ignored to ensure CLI execution
168+
is never blocked by telemetry issues.
169+
170+
Args:
171+
command: The CLI command name (e.g., "pack", "publish", "run").
172+
173+
Returns:
174+
A decorator function that wraps the CLI command.
175+
176+
Example:
177+
@click.command()
178+
@track_command("pack")
179+
def pack(root, nolock):
180+
...
181+
"""
182+
183+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
184+
@wraps(func)
185+
def wrapper(*args: Any, **kwargs: Any) -> Any:
186+
if not is_telemetry_enabled() or os.getenv("UIPATH_JOB_KEY"):
187+
return func(*args, **kwargs)
188+
189+
tracker = CliTelemetryTracker()
190+
tracker.track_command_start(command)
191+
192+
try:
193+
result = func(*args, **kwargs)
194+
tracker.track_command_end(command)
195+
return result
196+
197+
except Exception as e:
198+
tracker.track_command_failed(command, exception=e)
199+
raise
200+
201+
return wrapper
202+
203+
return decorator

src/uipath/_cli/cli_add.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import click
88

99
from .._utils.constants import EVALS_FOLDER
10+
from ._telemetry import track_command
1011
from ._utils._console import ConsoleLogger
1112
from ._utils._resources import Resources
1213

@@ -84,6 +85,7 @@ def create_evaluator(evaluator_name):
8485
@click.command()
8586
@click.argument("resource", required=True)
8687
@click.argument("args", nargs=-1)
88+
@track_command("add")
8789
def add(resource: str, args: tuple[str]) -> None:
8890
"""Create a local resource.
8991

src/uipath/_cli/cli_debug.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from uipath.platform.common import UiPathConfig
3636
from uipath.tracing import LiveTrackingSpanProcessor, LlmOpsHttpExporter
3737

38+
from ._telemetry import track_command
3839
from ._utils._console import ConsoleLogger
3940
from .middlewares import Middlewares
4041

@@ -127,6 +128,7 @@ def load_simulation_config() -> MockingContext | None:
127128
default=5678,
128129
help="Port for the debug server (default: 5678)",
129130
)
131+
@track_command("debug")
130132
def debug(
131133
entrypoint: str | None,
132134
input: str | None,

src/uipath/_cli/cli_deploy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import click
22

3+
from ._telemetry import track_command
34
from .cli_pack import pack
45
from .cli_publish import publish
56

@@ -27,6 +28,7 @@
2728
help="Folder name to publish to (skips interactive selection)",
2829
)
2930
@click.argument("root", type=str, default="./")
31+
@track_command("deploy")
3032
def deploy(root, feed, folder):
3133
"""Pack and publish the project."""
3234
ctx = click.get_current_context()

src/uipath/_cli/cli_dev.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from uipath._cli._utils._debug import setup_debugging
1010
from uipath._cli.middlewares import Middlewares
1111

12+
from ._telemetry import track_command
13+
1214
console = ConsoleLogger()
1315

1416

@@ -44,6 +46,7 @@ def _check_dev_dependency(interface: str) -> None:
4446
default=5678,
4547
help="Port for the debug server (default: 5678)",
4648
)
49+
@track_command("dev")
4750
def dev(interface: str, debug: bool, debug_port: int) -> None:
4851
"""Launch UiPath Developer Console.
4952

src/uipath/_cli/cli_init.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
from .._utils.constants import ENV_TELEMETRY_ENABLED
3333
from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
34+
from ._telemetry import track_command
3435
from ._utils._console import ConsoleLogger
3536
from .middlewares import Middlewares
3637
from .models.runtime_schema import Bindings
@@ -326,6 +327,7 @@ def _display_entrypoint_graphs(entry_point_schemas: list[UiPathRuntimeSchema]) -
326327
default=False,
327328
help="Won't override existing .agent files and AGENTS.md file.",
328329
)
330+
@track_command("initialize")
329331
def init(no_agents_md_override: bool) -> None:
330332
"""Initialize the project."""
331333
with console.spinner("Initializing UiPath project ..."):

src/uipath/_cli/cli_invoke.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import httpx
88

99
from .._utils._ssl_context import get_httpx_client_kwargs
10+
from ._telemetry import track_command
1011
from ._utils._common import get_env_vars
1112
from ._utils._console import ConsoleLogger
1213
from ._utils._folders import get_personal_workspace_info_async
@@ -43,6 +44,7 @@ def _read_project_details() -> tuple[str, str]:
4344
type=click.Path(exists=True),
4445
help="File path for the .json input",
4546
)
47+
@track_command("invoke")
4648
def invoke(entrypoint: str | None, input: str | None, file: str | None) -> None:
4749
"""Invoke an agent published in my workspace."""
4850
if file:

0 commit comments

Comments
 (0)