Skip to content
Merged
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
70 changes: 67 additions & 3 deletions solutions/ess-maker-skills/scripts/adk_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@
# 1.4.0: added ``toolkit_git_sha`` + ``toolkit_git_branch`` common
# dimensions for precise upgrade-posture reporting and CA-vs-DA
# attribution — ADO 7943642.
SCHEMA_VERSION = "1.4.0"
# 1.5.0: added derived ``connector`` (workday|servicenow|"") on
# adk.flightcheck.run/result + adk.capability.use — ADO 7943641.
SCHEMA_VERSION = "1.5.0"

# Surfaces the ADK emits from (spec enum: sdk | cli | studio | docs). The
# Python skill scripts are the CLI surface.
Expand Down Expand Up @@ -236,6 +238,55 @@ def normalize_capability(capability: str) -> str:
return c if c in _CAPABILITY_SET else CAPABILITY_UNKNOWN


# --- Connector taxonomy (ADO 7943641) -------------------------------------
# Attribute Connect + FlightCheck usage to the specific backend HR system so
# Workday vs ServiceNow adoption / reliability can be reported separately
# instead of collapsed under a single generic "connect" wedge. Bounded enum
# keeps the dashboard dimension controlled (cardinality never grows).
#
# Values:
# workday -> Workday connect flow, Workday-scope FlightCheck runs,
# checks in the Workday / Workday Tenant / Workday Extension
# categories.
# servicenow -> ServiceNow connect flow, ServiceNow-scope FlightCheck runs,
# checks in the ServiceNow category.
# legacy -> Explicit label for older events that emitted the generic
# "connect" capability WITHOUT a connector arg. Emitted by
# emit_capability.py when the caller passed no --connector
# flag AND the capability is one that a future maker MIGHT
# have connector context for (today: "connect"). Keeps the
# pre-attribution corpus queryable as its own bucket rather
# than double-counted against a real connector.
# unknown -> Out-of-taxonomy value provided by the caller (typo,
# future-connector name not yet in the enum).
# "" -> Legitimately not connector-scoped (most capabilities,
# checks in Environment / Authentication / Prerequisites /
# Local Files categories, "full"-scope FlightCheck runs
# that span multiple connectors).
CONNECTORS = ("workday", "servicenow")
_CONNECTOR_SET = frozenset(CONNECTORS)
CONNECTOR_LEGACY = "legacy"
CONNECTOR_UNKNOWN = "unknown"


def normalize_connector(connector: str) -> str:
"""Normalize a ``connector`` value to the canonical enum.

Empty stays empty (event is not connector-scoped).
Non-empty inputs are lower-cased / stripped and mapped to themselves if
they are in :data:`CONNECTORS`, to :data:`CONNECTOR_LEGACY` if the caller
explicitly passed that sentinel, else to :data:`CONNECTOR_UNKNOWN`.
"""
if not connector:
return ""
c = str(connector).strip().lower()
if c in _CONNECTOR_SET:
return c
if c == CONNECTOR_LEGACY:
return CONNECTOR_LEGACY
return CONNECTOR_UNKNOWN


# Outcomes the spec treats as errors (must carry error_* fields).
_ERROR_OUTCOMES = frozenset(
{"client_error", "server_error", "timeout", "abandoned", "failure", "fail"}
Expand Down Expand Up @@ -1271,18 +1322,24 @@ def emit_api_call(


def emit_capability_use(
adk_capability: str, *, surface: str = SURFACE_CLI, block: bool = False
adk_capability: str,
*,
connector: str = "",
surface: str = SURFACE_CLI,
block: bool = False,
) -> dict[str, Any]:
sid, _ = get_session(surface)
data = common_dimensions(surface, session_id=sid)
data["adk_capability"] = normalize_capability(adk_capability)
data["connector"] = normalize_connector(connector)
return _emit(EVENT_CAPABILITY_USE, data, block=block)


def emit_flightcheck_run(
*,
agent_id: str = "",
adk_capability: str = "flightcheck",
connector: str = "",
run_index: int = 0,
surface: str = SURFACE_CLI,
block: bool = False,
Expand All @@ -1292,6 +1349,7 @@ def emit_flightcheck_run(
data.update({
"agent_id": agent_id,
"adk_capability": normalize_capability(adk_capability),
"connector": normalize_connector(connector),
"run_index": int(run_index),
})
return _emit(EVENT_FLIGHTCHECK_RUN, data, block=block)
Expand All @@ -1301,6 +1359,7 @@ def emit_flightcheck_result(
*,
agent_id: str = "",
adk_capability: str = "flightcheck",
connector: str = "",
run_index: int = 0,
result: str = "pass",
duration_ms: int = 0,
Expand All @@ -1312,6 +1371,7 @@ def emit_flightcheck_result(
data.update({
"agent_id": agent_id,
"adk_capability": normalize_capability(adk_capability),
"connector": normalize_connector(connector),
"run_index": int(run_index),
"result": result,
"duration_ms": int(duration_ms),
Expand All @@ -1322,6 +1382,7 @@ def emit_flightcheck_result(
def emit_flightcheck_error(
*,
agent_id: str = "",
connector: str = "",
error_code: str = "",
error_category: str = "runtime",
error_message: str = "",
Expand All @@ -1330,7 +1391,10 @@ def emit_flightcheck_error(
) -> dict[str, Any]:
sid, _ = get_session(surface)
data = common_dimensions(surface, session_id=sid)
data.update({"agent_id": agent_id})
data.update({
"agent_id": agent_id,
"connector": normalize_connector(connector),
})
_apply_error_fields(data, "server_error", error_code, error_message, error_category)
return _emit(EVENT_FLIGHTCHECK_ERROR, data, block=block)

Expand Down
69 changes: 59 additions & 10 deletions solutions/ess-maker-skills/scripts/emit_capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))


def _parse_connector(args: list[str]) -> tuple[str, list[str]]:
"""Extract ``--connector <value>`` (or ``--connector=<value>``) from args.

Returns (connector, remaining_args). Missing / malformed flag returns
("", args) — the caller passes an empty string through, which
``normalize_connector`` maps to "" (not connector-scoped).
"""
out = []
connector = ""
i = 0
while i < len(args):
a = args[i]
if a == "--connector":
if i + 1 < len(args):
connector = args[i + 1]
i += 2
continue
i += 1
continue
if a.startswith("--connector="):
connector = a.split("=", 1)[1]
i += 1
continue
out.append(a)
i += 1
return connector, out


def main(argv: list[str]) -> int:
args = argv[1:]

Expand All @@ -57,9 +85,14 @@ def main(argv: list[str]) -> int:

if not args or args[0] in ("-h", "--help"):
print(
"Usage: python scripts/emit_capability.py <capability>\n"
"Usage: python scripts/emit_capability.py <capability> "
"[--connector <workday|servicenow|legacy>]\n"
"Records that a maker used an ADK capability (best-effort, "
"non-blocking).\n\n"
"The optional --connector flag attributes Connect + FlightCheck "
"usage to the specific backend HR system (Workday vs ServiceNow) "
"so PMs can report adoption / reliability by connector. Pass "
"'legacy' for older events that predate connector attribution.\n\n"
"Valid capabilities:\n "
+ "\n ".join(adk_telemetry.ADK_CAPABILITIES)
)
Expand All @@ -70,22 +103,33 @@ def main(argv: list[str]) -> int:
print(cap)
return 0

if args[0] == "--worker":
if len(args) < 2:
# Extract --connector from anywhere in the argv tail so callers can put it
# before or after the capability positional. Worker mode uses the same
# parser so the subprocess round-trip preserves the flag.
connector, rest = _parse_connector(args)

if rest and rest[0] == "--worker":
if len(rest) < 2:
return 0
try:
adk_telemetry.emit_capability_use(args[1].strip(), block=True)
adk_telemetry.emit_capability_use(
rest[1].strip(), connector=connector, block=True
)
except Exception: # noqa: BLE001 — telemetry must never break a skill
pass
return 0

capability = args[0].strip()
if not rest:
return 0
capability = rest[0].strip()
if not adk_telemetry.telemetry_enabled():
return 0

if adk_telemetry._SYNC:
try:
adk_telemetry.emit_capability_use(capability, block=True)
adk_telemetry.emit_capability_use(
capability, connector=connector, block=True
)
except Exception: # noqa: BLE001 — telemetry must never break a skill
pass
return 0
Expand All @@ -105,10 +149,15 @@ def main(argv: list[str]) -> int:
)
else:
kwargs["start_new_session"] = True
worker = subprocess.Popen(
[sys.executable, os.path.abspath(__file__), "--worker", capability],
**kwargs,
)
worker_argv = [
sys.executable,
os.path.abspath(__file__),
"--worker",
capability,
]
if connector:
worker_argv.extend(["--connector", connector])
worker = subprocess.Popen(worker_argv, **kwargs)
threading.Thread(
target=worker.wait,
name="adk-capability-worker-reaper",
Expand Down
28 changes: 26 additions & 2 deletions solutions/ess-maker-skills/scripts/flightcheck/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1135,12 +1135,26 @@ def _run_single_checkpoint(args):
# --scope emit so checkpoint runs also count toward the adk.* cubes.
try:
import adk_telemetry as _adk
from flightcheck.telemetry import derive_connector_from_category

_agent_id = _active_agent.get("botId", "")
if tenant_id or tenant_name:
_adk.set_identity(tenant_id=tenant_id or "", tenant_name=tenant_name)
_ridx = _adk.next_run_index(_agent_id)
_adk.emit_flightcheck_run(agent_id=_agent_id, run_index=_ridx)
# Single-checkpoint runs execute exactly one owning check, so the
# first result row's category is the run's connector (or "" for
# cross-cutting checkpoints like Environment / Authentication).
# Derived here rather than passed by the caller so the CLI runtime
# path matches the same connector attribution as the legacy
# ESSMakerKit.FlightCheck.* events (ADO 7943641 review).
_connector = ""
if result.results:
_connector = derive_connector_from_category(
getattr(result.results[0], "category", "") or ""
)
_adk.emit_flightcheck_run(
agent_id=_agent_id, run_index=_ridx, connector=_connector
)
_result_map = {
"READY": "pass",
"READY_WITH_WARNINGS": "partial",
Expand All @@ -1151,6 +1165,7 @@ def _run_single_checkpoint(args):
run_index=_ridx,
result=_result_map.get(result.overall, "fail"),
duration_ms=int(getattr(result, "duration_secs", 0) * 1000),
connector=_connector,
)
_adk.flush(timeout=3)
except Exception: # noqa: BLE001 — adk telemetry must never break the run
Expand Down Expand Up @@ -1778,12 +1793,20 @@ def main():
# the legacy ESSMakerKit.FlightCheck.* events; never affects the run.
try:
import adk_telemetry as _adk
from flightcheck.telemetry import derive_connector_from_scope

_agent_id = active_agent.get("botId", "")
if tenant_id or tenant_name:
_adk.set_identity(tenant_id=tenant_id, tenant_name=tenant_name)
_ridx = _adk.next_run_index(_agent_id)
_adk.emit_flightcheck_run(agent_id=_agent_id, run_index=_ridx)
# Derive connector from the CLI scope so scope-based runs get the
# same attribution as the legacy flightcheck events (ADO 7943641
# review). "full" and cross-cutting scopes return "" — the finer
# per-check attribution lives on the check events, not run events.
_connector = derive_connector_from_scope(args.scope)
_adk.emit_flightcheck_run(
agent_id=_agent_id, run_index=_ridx, connector=_connector
)
_result_map = {
"READY": "pass",
"READY_WITH_WARNINGS": "partial",
Expand All @@ -1794,6 +1817,7 @@ def main():
run_index=_ridx,
result=_result_map.get(result.overall, "fail"),
duration_ms=int(getattr(result, "duration_secs", 0) * 1000),
connector=_connector,
)
_adk.flush(timeout=3)
except Exception: # noqa: BLE001 — adk telemetry must never break the run
Expand Down
Loading
Loading