-
Notifications
You must be signed in to change notification settings - Fork 15
Track CLI invocation source and full command #1293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7df316f
bc0f093
ea01dcb
30af022
c120b15
f8a0937
a24628e
8930132
8001fce
1c6855c
228ca35
3525a35
34ebbf6
b118b6f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,74 @@ | ||
| import os | ||
| from enum import Enum | ||
| from itertools import takewhile | ||
| from typing import Any, Dict, Union | ||
|
|
||
| from requests import Session | ||
|
|
||
| from smart_tests.app import Application | ||
| from smart_tests.utils.authentication import get_org_workspace | ||
| from smart_tests.utils.env_keys import CALLER_KEY, detect_ci_provider | ||
| from smart_tests.utils.http_client import _HttpClient, _join_paths | ||
| from smart_tests.version import __version__ | ||
|
|
||
| from .commands import Command | ||
|
|
||
| # Map CLI subcommand tokens to Command enum values. | ||
| # Longer matches are tried first so "record build" matches before "record". | ||
| _COMMAND_MAP = { | ||
|
ItIsUday marked this conversation as resolved.
|
||
| ("verify",): Command.VERIFY, | ||
| ("record", "build"): Command.RECORD_BUILD, | ||
| ("record", "session"): Command.RECORD_SESSION, | ||
|
ItIsUday marked this conversation as resolved.
|
||
| ("record", "tests"): Command.RECORD_TESTS, | ||
| ("record", "commit"): Command.COMMIT, | ||
| ("record", "attachment"): Command.RECORD_ATTACHMENT, | ||
| ("record", "deployment"): Command.RECORD_DEPLOYMENT, | ||
| ("subset",): Command.SUBSET, | ||
| ("detect-flakes",): Command.DETECT_FLAKE, | ||
| ("gate",): Command.GATE, | ||
| ("update", "alias"): Command.UPDATE_ALIAS, | ||
| ("inspect", "model"): Command.INSPECT_MODEL, | ||
| ("inspect", "subset"): Command.INSPECT_SUBSET, | ||
| ("stats", "test_sessions"): Command.STATS_TEST_SESSIONS, | ||
| ("compare", "subsets"): Command.COMPARE_SUBSETS, | ||
| ("get", "docs"): Command.GET_DOCS, | ||
| } | ||
|
|
||
|
|
||
| def _detect_command(argv: list[str]) -> Command: | ||
|
psakthivel04 marked this conversation as resolved.
|
||
| """Best-effort detection of the Command from argv. Returns UNKNOWN for typos.""" | ||
| command_tokens = list(takewhile(lambda a: not a.startswith("-"), argv[1:])) | ||
|
|
||
| for tokens, command in sorted(_COMMAND_MAP.items(), key=lambda x: -len(x[0])): | ||
| if tuple(command_tokens[:len(tokens)]) == tokens: | ||
| return command | ||
| return Command.UNKNOWN | ||
|
|
||
|
|
||
| def send_command_tracking(argv: list[str], exit_code: int): | ||
| """Send a single COMMAND_INVOCATION event with the full command string. Fire-and-forget.""" | ||
| client = TrackingClient(_detect_command(argv)) | ||
| metadata = { | ||
| "exitCode": str(exit_code), | ||
| } | ||
|
|
||
| raw_command = " ".join(argv)[:2000] | ||
|
|
||
| payload = client.construct_payload( | ||
| event_name=Tracking.Event.COMMAND_INVOCATION, | ||
| metadata=metadata, | ||
| raw_command=raw_command, | ||
| ) | ||
|
|
||
| client.post_payload(payload=payload) | ||
|
|
||
|
|
||
| class Tracking: | ||
| # General events | ||
| class Event(Enum): | ||
| SHALLOW_CLONE = 'SHALLOW_CLONE' # this event is an example | ||
| PERFORMANCE = 'PERFORMANCE' | ||
| COMMAND_INVOCATION = 'COMMAND_INVOCATION' | ||
|
|
||
| # Error events | ||
| class ErrorEvent(Enum): | ||
|
|
@@ -45,14 +98,8 @@ def send_event( | |
| event_name: Tracking.Event, | ||
| metadata: Dict[str, Any] | None = None | ||
| ): | ||
| org, workspace = get_org_workspace() | ||
| if metadata is None: | ||
| metadata = {} | ||
| metadata["organization"] = org or "" | ||
| metadata["workspace"] = workspace or "" | ||
| self._post_payload( | ||
| event_name=event_name, | ||
| metadata=metadata, | ||
| self.post_payload( | ||
| payload=self.construct_payload(event_name=event_name, metadata=metadata), | ||
| ) | ||
|
|
||
| def send_error_event( | ||
|
|
@@ -62,34 +109,49 @@ def send_error_event( | |
| api: str = "", | ||
| metadata: Dict[str, Any] | None = None | ||
| ): | ||
| org, workspace = get_org_workspace() | ||
| if metadata is None: | ||
| metadata = {} | ||
| metadata["stackTrace"] = stack_trace | ||
| metadata["organization"] = org or "" | ||
| metadata["workspace"] = workspace or "" | ||
| metadata["api"] = api | ||
|
Comment on lines
114
to
115
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you leave stackTrace and api in this method instead of putting them in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That was intentional because the |
||
| self._post_payload( | ||
| event_name=event_name, | ||
| metadata=metadata, | ||
| ) | ||
|
|
||
| def _post_payload( | ||
| payload = self.construct_payload(event_name=event_name, metadata=metadata) | ||
| self.post_payload(payload=payload) | ||
|
|
||
| def post_payload( | ||
| self, | ||
| event_name: Union[Tracking.Event, Tracking.ErrorEvent], | ||
| metadata: Dict[str, Any] | ||
| payload: dict, | ||
| ): | ||
| payload = { | ||
| "command": self.command.value, | ||
| "eventName": event_name.value, | ||
| "cliVersion": __version__, | ||
| "metadata": metadata, | ||
| } | ||
| path = _join_paths( | ||
| '/intake', | ||
| 'cli_tracking' | ||
| ) | ||
| try: | ||
| self.http_client.request('post', payload=payload, path=path) | ||
| self.http_client.request('post', payload=payload, path=path, timeout=(2, 2)) | ||
| except Exception: | ||
| pass | ||
|
|
||
| def construct_payload( | ||
| self, | ||
| event_name: Union[Tracking.Event, Tracking.ErrorEvent], | ||
| metadata: Dict[str, Any] | None = None, | ||
| raw_command: str | None = None | ||
| ) -> dict: | ||
| org, workspace = get_org_workspace() | ||
|
|
||
| if metadata is None: | ||
| metadata = {} | ||
|
|
||
| metadata["organization"] = org or "" | ||
| metadata["workspace"] = workspace or "" | ||
| metadata["caller"] = os.environ.get(CALLER_KEY) or "cli" | ||
| metadata["ciProvider"] = detect_ci_provider() | ||
|
|
||
| payload = { | ||
| "command": self.command.value, | ||
| "eventName": event_name.value, | ||
| "cliVersion": __version__, | ||
| "metadata": metadata, | ||
| "rawCommand": raw_command, | ||
| } | ||
|
|
||
| return payload | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we invoke the CLI tracking asynchronously instead of blocking the execution?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is invoked when all the command is processed already, and the CLI is about to exit. Asynchronous execution here might not be needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it's true that tracking happens in the
finallyblock after the command completes,the HTTP request still blocks the CLI exit. This means every user waits an extra
50-200ms (or more with slow networks) for the tracking call to complete
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But since the CLI is about to exit, we have these options
sys.exit()is calledBut these are not helpful unless we change the shutdown process some how.
I will add a short timeout, because if the server is unreachable, the cli will hang, this is more of an issue.