Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
rsync -a --no-whole-file --ignore-existing "$originalfile" "$tmpfile"
envsubst '$CONNECTION_STRING' < "$originalfile" > "$tmpfile" && mv "$tmpfile" "$originalfile"
env:
CONNECTION_STRING: ${{ secrets.APPINS_CONNECTION_STRING }}
CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }}

- name: Build
run: uv build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
rsync -a --no-whole-file --ignore-existing "$originalfile" "$tmpfile"
envsubst '$CONNECTION_STRING' < "$originalfile" > "$tmpfile" && mv "$tmpfile" "$originalfile"
env:
CONNECTION_STRING: ${{ secrets.APPINS_CONNECTION_STRING }}
CONNECTION_STRING: ${{ secrets.APPLICATIONINSIGHTS_CONNECTION_STRING }}

- name: Set development version
shell: pwsh
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.6.22"
version = "2.6.23"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
207 changes: 207 additions & 0 deletions src/uipath/_cli/_telemetry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import logging
import os
import time
import uuid
from functools import wraps
from typing import Any, Callable, Dict, Optional

from uipath.telemetry._track import (
is_telemetry_enabled,
track_cli_event,
)

logger = logging.getLogger(__name__)

# Telemetry event name templates for Application Insights
CLI_COMMAND_STARTED = "Cli.{command}.Start.URT"
CLI_COMMAND_COMPLETED = "Cli.{command}.End.URT"
CLI_COMMAND_FAILED = "Cli.{command}.Failed.URT"


class CliTelemetryTracker:
"""Tracks CLI command execution and sends telemetry to Application Insights.

This class handles tracking of CLI command lifecycle events:
- Command start events
- Command completion events (success)
- Command failure events (with error details)
"""

def __init__(self) -> None:
self._start_times: Dict[str, float] = {}
self._event_ids: Dict[str, str] = {}

@staticmethod
def _get_event_name(command: str, status: str) -> str:
return f"Cli.{command.capitalize()}.{status}.URT"

def _enrich_properties(self, properties: Dict[str, Any]) -> None:
"""Enrich properties with common context information.

Args:
properties: The properties dictionary to enrich.
"""
# Add CI environment detection
properties["IsCI"] = bool(os.getenv("GITHUB_ACTIONS"))

# Add UiPath context
project_id = os.getenv("UIPATH_PROJECT_ID")
if project_id:
properties["ProjectId"] = project_id

org_id = os.getenv("UIPATH_CLOUD_ORGANIZATION_ID")
if org_id:
properties["CloudOrganizationId"] = org_id

user_id = os.getenv("UIPATH_CLOUD_USER_ID")
if user_id:
properties["CloudUserId"] = user_id

tenant_id = os.getenv("UIPATH_TENANT_ID")
if tenant_id:
properties["TenantId"] = tenant_id

# Add source identifier
properties["Source"] = "uipath-python-cli"
properties["ApplicationName"] = "UiPath.Cli"

def track_command_start(self, command: str) -> None:
try:
self._start_times[command] = time.time()
self._event_ids[command] = str(uuid.uuid4())

properties: Dict[str, Any] = {
"Command": command,
"EventId": self._event_ids[command],
}
self._enrich_properties(properties)

track_cli_event(self._get_event_name(command, "Start"), properties)
logger.debug(f"Tracked CLI command started: {command}")

except Exception as e:
logger.debug(f"Error tracking CLI command start: {e}")

def track_command_end(
self,
command: str,
duration_ms: Optional[int] = None,
) -> None:
try:
if duration_ms is None:
start_time = self._start_times.pop(command, None)
if start_time:
duration_ms = int((time.time() - start_time) * 1000)

event_id = self._event_ids.pop(command, None)

properties: Dict[str, Any] = {
"Command": command,
"Success": True,
}

if event_id:
properties["EventId"] = event_id

if duration_ms is not None:
properties["DurationMs"] = duration_ms

self._enrich_properties(properties)

track_cli_event(self._get_event_name(command, "End"), properties)
logger.debug(f"Tracked CLI command completed: {command}")

except Exception as e:
logger.debug(f"Error tracking CLI command end: {e}")

def track_command_failed(
self,
command: str,
duration_ms: Optional[int] = None,
exception: Optional[Exception] = None,
) -> None:
try:
if duration_ms is None:
start_time = self._start_times.pop(command, None)
if start_time:
duration_ms = int((time.time() - start_time) * 1000)

event_id = self._event_ids.pop(command, None)

properties: Dict[str, Any] = {
"Command": command,
"Success": False,
}

if event_id:
properties["EventId"] = event_id

if duration_ms is not None:
properties["DurationMs"] = duration_ms

if exception is not None:
properties["ErrorType"] = type(exception).__name__
properties["ErrorMessage"] = str(exception)[:500]

self._enrich_properties(properties)

track_cli_event(self._get_event_name(command, "Failed"), properties)
logger.debug(f"Tracked CLI command failed: {command}")

except Exception as e:
logger.debug(f"Error tracking CLI command failed: {e}")


def track_command(command: str) -> Callable[..., Any]:
"""Decorator to track CLI command execution.

Tracks the following events to Application Insights:
- Cli.<Command>.Start.URT - when command begins
- Cli.<Command>.End.URT - on successful completion
- Cli.<Command>.Failed.URT - on exception

Properties tracked include:
- Command: The command name
- Success: Whether the command succeeded
- DurationMs: Execution time in milliseconds
- ErrorType: Exception type name (on failure)
- ErrorMessage: Exception message (on failure, truncated to 500 chars)
- ProjectId, CloudOrganizationId, etc. (if available)

Telemetry failures are silently ignored to ensure CLI execution
is never blocked by telemetry issues.

Args:
command: The CLI command name (e.g., "pack", "publish", "run").

Returns:
A decorator function that wraps the CLI command.

Example:
@click.command()
@track_command("pack")
def pack(root, nolock):
...
"""

def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if not is_telemetry_enabled():
return func(*args, **kwargs)

tracker = CliTelemetryTracker()
tracker.track_command_start(command)

try:
result = func(*args, **kwargs)
tracker.track_command_end(command)
return result

except Exception as e:
tracker.track_command_failed(command, exception=e)
raise

return wrapper

return decorator
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import click

from .._utils.constants import EVALS_FOLDER
from ._telemetry import track_command
from ._utils._console import ConsoleLogger
from ._utils._resources import Resources

Expand Down Expand Up @@ -84,6 +85,7 @@ def create_evaluator(evaluator_name):
@click.command()
@click.argument("resource", required=True)
@click.argument("args", nargs=-1)
@track_command("add")
def add(resource: str, args: tuple[str]) -> None:
"""Create a local resource.

Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click

from ._auth._auth_service import AuthService
from ._telemetry import track_command
from ._utils._common import environment_options
from ._utils._console import ConsoleLogger

Expand Down Expand Up @@ -43,6 +44,7 @@
default="OR.Execution",
help="Space-separated list of OAuth scopes to request (e.g., 'OR.Execution OR.Queues'). Defaults to 'OR.Execution'",
)
@track_command("auth")
def auth(
environment: str,
force: bool = False,
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_deploy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import click

from ._telemetry import track_command
from .cli_pack import pack
from .cli_publish import publish

Expand Down Expand Up @@ -27,6 +28,7 @@
help="Folder name to publish to (skips interactive selection)",
)
@click.argument("root", type=str, default="./")
@track_command("deploy")
def deploy(root, feed, folder):
"""Pack and publish the project."""
ctx = click.get_current_context()
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from uipath.core.tracing import UiPathTraceManager
from uipath.runtime import UiPathRuntimeContext, UiPathRuntimeFactoryRegistry

from uipath._cli._telemetry import track_command
from uipath._cli._utils._console import ConsoleLogger
from uipath._cli._utils._debug import setup_debugging
from uipath._cli.middlewares import Middlewares
Expand Down Expand Up @@ -39,6 +40,7 @@ def _check_dev_dependency() -> None:
default=5678,
help="Port for the debug server (default: 5678)",
)
@track_command("dev")
def dev(interface: str | None, debug: bool, debug_port: int) -> None:
"""Launch UiPath Developer Console."""
try:
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from .._utils.constants import ENV_TELEMETRY_ENABLED
from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
from ._telemetry import track_command
from ._utils._console import ConsoleLogger
from .middlewares import Middlewares
from .models.runtime_schema import Bindings
Expand Down Expand Up @@ -252,6 +253,7 @@ def _add_graph_to_chart(chart: Chart | Subgraph, graph: UiPathRuntimeGraph) -> N
default=False,
help="Won't override existing .agent files and AGENTS.md file.",
)
@track_command("init")
def init(no_agents_md_override: bool) -> None:
"""Initialize the project."""
with console.spinner("Initializing UiPath project ..."):
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import httpx

from .._utils._ssl_context import get_httpx_client_kwargs
from ._telemetry import track_command
from ._utils._common import get_env_vars
from ._utils._console import ConsoleLogger
from ._utils._folders import get_personal_workspace_info_async
Expand Down Expand Up @@ -43,6 +44,7 @@ def _read_project_details() -> tuple[str, str]:
type=click.Path(exists=True),
help="File path for the .json input",
)
@track_command("invoke")
def invoke(entrypoint: str | None, input: str | None, file: str | None) -> None:
"""Invoke an agent published in my workspace."""
if file:
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import click

from ._telemetry import track_command
from ._utils._console import ConsoleLogger
from .middlewares import Middlewares

Expand Down Expand Up @@ -46,6 +47,7 @@ def generate_uipath_json(target_directory):

@click.command()
@click.argument("name", type=str, default="")
@track_command("new")
def new(name: str):
"""Generate a quick-start project."""
directory = os.getcwd()
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from uipath.platform.common import UiPathConfig

from ..telemetry._constants import _PROJECT_KEY, _TELEMETRY_CONFIG_FILE
from ._telemetry import track_command
from ._utils._console import ConsoleLogger
from ._utils._project_files import (
ensure_config_file,
Expand Down Expand Up @@ -336,6 +337,7 @@ def display_project_info(config):
is_flag=True,
help="Skip running uv lock and exclude uv.lock from the package",
)
@track_command("pack")
def pack(root, nolock):
"""Pack the project."""
version = get_project_version(root)
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import httpx

from .._utils._ssl_context import get_httpx_client_kwargs
from ._telemetry import track_command
from ._utils._common import get_env_vars
from ._utils._console import ConsoleLogger
from ._utils._folders import get_personal_workspace_info_async
Expand Down Expand Up @@ -118,6 +119,7 @@ def find_feed_by_folder_name(
type=str,
help="Folder name to publish to (skips interactive selection)",
)
@track_command("publish")
def publish(feed, folder):
"""Publish the package."""
[base_url, token] = get_env_vars()
Expand Down
2 changes: 2 additions & 0 deletions src/uipath/_cli/cli_pull.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from uipath.platform.common import UiPathConfig

from ._telemetry import track_command
from ._utils._common import ensure_coded_agent_project, may_override_files
from ._utils._console import ConsoleLogger
from ._utils._project_files import (
Expand All @@ -30,6 +31,7 @@
is_flag=True,
help="Automatically overwrite local files without prompts",
)
@track_command("pull")
def pull(root: Path, overwrite: bool) -> None:
"""Pull remote project files from Studio Web.

Expand Down
Loading