Skip to content

Commit a748aac

Browse files
committed
feat: add telemetry for cli
1 parent 46a7a35 commit a748aac

13 files changed

Lines changed: 701 additions & 6 deletions

File tree

.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.6.22"
3+
version = "2.6.23"
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: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import logging
2+
import os
3+
import time
4+
import uuid
5+
from functools import wraps
6+
from typing import Any, Callable, Dict, Optional
7+
8+
from uipath.telemetry._track import (
9+
is_telemetry_enabled,
10+
track_cli_event,
11+
)
12+
13+
logger = logging.getLogger(__name__)
14+
15+
# Telemetry event name templates for Application Insights
16+
CLI_COMMAND_STARTED = "Cli.{command}.Start.URT"
17+
CLI_COMMAND_COMPLETED = "Cli.{command}.End.URT"
18+
CLI_COMMAND_FAILED = "Cli.{command}.Failed.URT"
19+
20+
21+
class CliTelemetryTracker:
22+
"""Tracks CLI command execution and sends telemetry to Application Insights.
23+
24+
This class handles tracking of CLI command lifecycle events:
25+
- Command start events
26+
- Command completion events (success)
27+
- Command failure events (with error details)
28+
"""
29+
30+
def __init__(self) -> None:
31+
self._start_times: Dict[str, float] = {}
32+
self._event_ids: Dict[str, str] = {}
33+
34+
@staticmethod
35+
def _get_event_name(command: str, status: str) -> str:
36+
return f"Cli.{command.capitalize()}.{status}.URT"
37+
38+
def _enrich_properties(self, properties: Dict[str, Any]) -> None:
39+
"""Enrich properties with common context information.
40+
41+
Args:
42+
properties: The properties dictionary to enrich.
43+
"""
44+
# Add CI environment detection
45+
properties["IsCI"] = bool(os.getenv("GITHUB_ACTIONS"))
46+
47+
# Add UiPath context
48+
project_id = os.getenv("UIPATH_PROJECT_ID")
49+
if project_id:
50+
properties["ProjectId"] = project_id
51+
52+
org_id = os.getenv("UIPATH_CLOUD_ORGANIZATION_ID")
53+
if org_id:
54+
properties["CloudOrganizationId"] = org_id
55+
56+
user_id = os.getenv("UIPATH_CLOUD_USER_ID")
57+
if user_id:
58+
properties["CloudUserId"] = user_id
59+
60+
tenant_id = os.getenv("UIPATH_TENANT_ID")
61+
if tenant_id:
62+
properties["TenantId"] = tenant_id
63+
64+
# Add source identifier
65+
properties["Source"] = "uipath-python-cli"
66+
properties["ApplicationName"] = "UiPath.Cli"
67+
68+
def track_command_start(self, command: str) -> None:
69+
try:
70+
self._start_times[command] = time.time()
71+
self._event_ids[command] = str(uuid.uuid4())
72+
73+
properties: Dict[str, Any] = {
74+
"Command": command,
75+
"EventId": self._event_ids[command],
76+
}
77+
self._enrich_properties(properties)
78+
79+
track_cli_event(self._get_event_name(command, "Start"), properties)
80+
logger.debug(f"Tracked CLI command started: {command}")
81+
82+
except Exception as e:
83+
logger.debug(f"Error tracking CLI command start: {e}")
84+
85+
def track_command_end(
86+
self,
87+
command: str,
88+
duration_ms: Optional[int] = None,
89+
) -> None:
90+
try:
91+
if duration_ms is None:
92+
start_time = self._start_times.pop(command, None)
93+
if start_time:
94+
duration_ms = int((time.time() - start_time) * 1000)
95+
96+
event_id = self._event_ids.pop(command, None)
97+
98+
properties: Dict[str, Any] = {
99+
"Command": command,
100+
"Success": True,
101+
}
102+
103+
if event_id:
104+
properties["EventId"] = event_id
105+
106+
if duration_ms is not None:
107+
properties["DurationMs"] = duration_ms
108+
109+
self._enrich_properties(properties)
110+
111+
track_cli_event(self._get_event_name(command, "End"), properties)
112+
logger.debug(f"Tracked CLI command completed: {command}")
113+
114+
except Exception as e:
115+
logger.debug(f"Error tracking CLI command end: {e}")
116+
117+
def track_command_failed(
118+
self,
119+
command: str,
120+
duration_ms: Optional[int] = None,
121+
exception: Optional[Exception] = None,
122+
) -> None:
123+
try:
124+
if duration_ms is None:
125+
start_time = self._start_times.pop(command, None)
126+
if start_time:
127+
duration_ms = int((time.time() - start_time) * 1000)
128+
129+
event_id = self._event_ids.pop(command, None)
130+
131+
properties: Dict[str, Any] = {
132+
"Command": command,
133+
"Success": False,
134+
}
135+
136+
if event_id:
137+
properties["EventId"] = event_id
138+
139+
if duration_ms is not None:
140+
properties["DurationMs"] = duration_ms
141+
142+
if exception is not None:
143+
properties["ErrorType"] = type(exception).__name__
144+
properties["ErrorMessage"] = str(exception)[:500]
145+
146+
self._enrich_properties(properties)
147+
148+
track_cli_event(self._get_event_name(command, "Failed"), properties)
149+
logger.debug(f"Tracked CLI command failed: {command}")
150+
151+
except Exception as e:
152+
logger.debug(f"Error tracking CLI command failed: {e}")
153+
154+
155+
def track_command(command: str) -> Callable[..., Any]:
156+
"""Decorator to track CLI command execution.
157+
158+
Tracks the following events to Application Insights:
159+
- Cli.<Command>.Start.URT - when command begins
160+
- Cli.<Command>.End.URT - on successful completion
161+
- Cli.<Command>.Failed.URT - on exception
162+
163+
Properties tracked include:
164+
- Command: The command name
165+
- Success: Whether the command succeeded
166+
- DurationMs: Execution time in milliseconds
167+
- ErrorType: Exception type name (on failure)
168+
- ErrorMessage: Exception message (on failure, truncated to 500 chars)
169+
- ProjectId, CloudOrganizationId, etc. (if available)
170+
171+
Telemetry failures are silently ignored to ensure CLI execution
172+
is never blocked by telemetry issues.
173+
174+
Args:
175+
command: The CLI command name (e.g., "pack", "publish", "run").
176+
177+
Returns:
178+
A decorator function that wraps the CLI command.
179+
180+
Example:
181+
@click.command()
182+
@track_command("pack")
183+
def pack(root, nolock):
184+
...
185+
"""
186+
187+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
188+
@wraps(func)
189+
def wrapper(*args: Any, **kwargs: Any) -> Any:
190+
if not is_telemetry_enabled():
191+
return func(*args, **kwargs)
192+
193+
tracker = CliTelemetryTracker()
194+
tracker.track_command_start(command)
195+
196+
try:
197+
result = func(*args, **kwargs)
198+
tracker.track_command_end(command)
199+
return result
200+
201+
except Exception as e:
202+
tracker.track_command_failed(command, exception=e)
203+
raise
204+
205+
return wrapper
206+
207+
return decorator

src/uipath/_cli/cli_init.py

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

2828
from .._utils.constants import ENV_TELEMETRY_ENABLED
2929
from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
30+
from ._telemetry import track_command
3031
from ._utils._console import ConsoleLogger
3132
from .middlewares import Middlewares
3233
from .models.runtime_schema import Bindings
@@ -252,6 +253,7 @@ def _add_graph_to_chart(chart: Chart | Subgraph, graph: UiPathRuntimeGraph) -> N
252253
default=False,
253254
help="Won't override existing .agent files and AGENTS.md file.",
254255
)
256+
@track_command("init")
255257
def init(no_agents_md_override: bool) -> None:
256258
"""Initialize the project."""
257259
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:

src/uipath/_cli/cli_new.py

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

55
import click
66

7+
from ._telemetry import track_command
78
from ._utils._console import ConsoleLogger
89
from .middlewares import Middlewares
910

@@ -46,6 +47,7 @@ def generate_uipath_json(target_directory):
4647

4748
@click.command()
4849
@click.argument("name", type=str, default="")
50+
@track_command("new")
4951
def new(name: str):
5052
"""Generate a quick-start project."""
5153
directory = os.getcwd()

src/uipath/_cli/cli_pack.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from uipath.platform.common import UiPathConfig
1515

1616
from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
17+
from ._telemetry import track_command
1718
from ._utils._console import ConsoleLogger
1819
from ._utils._project_files import (
1920
ensure_config_file,
@@ -336,6 +337,7 @@ def display_project_info(config):
336337
is_flag=True,
337338
help="Skip running uv lock and exclude uv.lock from the package",
338339
)
340+
@track_command("pack")
339341
def pack(root, nolock):
340342
"""Pack the project."""
341343
version = get_project_version(root)

src/uipath/_cli/cli_publish.py

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

88
from .._utils._ssl_context import get_httpx_client_kwargs
9+
from ._telemetry import track_command
910
from ._utils._common import get_env_vars
1011
from ._utils._console import ConsoleLogger
1112
from ._utils._folders import get_personal_workspace_info_async
@@ -118,6 +119,7 @@ def find_feed_by_folder_name(
118119
type=str,
119120
help="Folder name to publish to (skips interactive selection)",
120121
)
122+
@track_command("publish")
121123
def publish(feed, folder):
122124
"""Publish the package."""
123125
[base_url, token] = get_env_vars()

src/uipath/_cli/cli_run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
LlmOpsHttpExporter,
2828
)
2929

30+
from ._telemetry import track_command
3031
from ._utils._console import ConsoleLogger
3132
from .middlewares import Middlewares
3233

@@ -84,6 +85,7 @@
8485
is_flag=True,
8586
help="Keep the temporary state file even when not resuming and no job id is provided",
8687
)
88+
@track_command("run")
8789
def run(
8890
entrypoint: str | None,
8991
input: str | None,

0 commit comments

Comments
 (0)