Skip to content

Commit e0836d1

Browse files
feat: [SNOW-2330065] collect telemetry about flags from parent contexts (#2651)
* feat: [SNOW-2330065] collect telemetry about flags from parent contexts * feat: [SNOW-2330065] review fixes
1 parent 234ea0b commit e0836d1

File tree

3 files changed

+92
-9
lines changed

3 files changed

+92
-9
lines changed

src/snowflake/cli/_app/cli_app.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import inspect
1718
import logging
1819
import os
1920
import platform
@@ -47,6 +48,20 @@
4748

4849
log = logging.getLogger(__name__)
4950

51+
INTERNAL_CLI_FLAGS = {
52+
"custom_help",
53+
"version",
54+
"docs",
55+
"structure",
56+
"info",
57+
"configuration_file",
58+
"pycharm_debug_library_path",
59+
"pycharm_debug_server_host",
60+
"pycharm_debug_server_port",
61+
"disable_external_command_plugins",
62+
"commands_registration",
63+
}
64+
5065

5166
def _do_not_execute_on_completion(callback):
5267
def enriched_callback(value):
@@ -270,8 +285,36 @@ def default(
270285
pycharm_debug_server_port=pycharm_debug_server_port,
271286
)
272287

288+
self._validate_internal_flags_excluded_from_telemetry(default)
289+
273290
self._app = app
274291
return app
275292

293+
@staticmethod
294+
def _validate_internal_flags_excluded_from_telemetry(callback_function):
295+
"""
296+
We have not been interested in collecting telemetry data about root
297+
command flags (most of which are internal flags). This method validates
298+
that all new flags should be added to INTERNAL_CLI_FLAGS and thus
299+
excluded from telemetry as well.
300+
"""
301+
sig = inspect.signature(callback_function)
302+
actual_params = {name for name in sig.parameters.keys() if name != "ctx"}
303+
if actual_params != INTERNAL_CLI_FLAGS:
304+
missing = actual_params - INTERNAL_CLI_FLAGS
305+
extra = INTERNAL_CLI_FLAGS - actual_params
306+
error_parts = []
307+
if missing:
308+
error_parts.append(
309+
f"Parameters in default() but not in INTERNAL_CLI_FLAGS: {missing}"
310+
)
311+
if extra:
312+
error_parts.append(
313+
f"Flags in INTERNAL_CLI_FLAGS but not in default(): {extra}"
314+
)
315+
raise AssertionError(
316+
"INTERNAL_CLI_FLAGS mismatch! " + ". ".join(error_parts)
317+
)
318+
276319
def get_click_context(self):
277320
return self._click_context

src/snowflake/cli/_app/telemetry.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
import platform
1919
import sys
2020
from enum import Enum, unique
21-
from typing import Any, Dict, Union
21+
from typing import Any, Dict, Optional, Union
2222

2323
import click
2424
import typer
2525
from snowflake.cli import __about__
26+
from snowflake.cli._app.cli_app import INTERNAL_CLI_FLAGS
2627
from snowflake.cli._app.constants import PARAM_APPLICATION_NAME
2728
from snowflake.cli.api.cli_global_context import (
2829
_CliGlobalContextAccess,
@@ -38,6 +39,7 @@
3839
TelemetryField,
3940
)
4041
from snowflake.connector.time_util import get_time_millis
42+
from typer import Context
4143

4244

4345
@unique
@@ -140,17 +142,30 @@ def _get_command_metrics() -> TelemetryDict:
140142
def _find_command_info() -> TelemetryDict:
141143
ctx = click.get_current_context()
142144
command_path = ctx.command_path.split(" ")[1:]
145+
146+
command_flags = {}
147+
format_value = None
148+
current_ctx: Optional[Context] = ctx
149+
while current_ctx:
150+
for flag, flag_value in current_ctx.params.items():
151+
if (
152+
flag_value
153+
and flag not in command_flags
154+
and flag not in INTERNAL_CLI_FLAGS
155+
):
156+
command_flags[flag] = current_ctx.get_parameter_source(flag).name # type: ignore[attr-defined]
157+
if format_value is None and "format" in current_ctx.params:
158+
format_value = current_ctx.params["format"]
159+
current_ctx = current_ctx.parent
160+
161+
if format_value is None:
162+
format_value = OutputFormat.TABLE
163+
143164
return {
144165
CLITelemetryField.COMMAND: command_path,
145166
CLITelemetryField.COMMAND_GROUP: command_path[0],
146-
CLITelemetryField.COMMAND_FLAGS: {
147-
k: ctx.get_parameter_source(k).name # type: ignore[attr-defined]
148-
for k, v in ctx.params.items()
149-
if v # noqa
150-
},
151-
CLITelemetryField.COMMAND_OUTPUT_TYPE: ctx.params.get(
152-
"format", OutputFormat.TABLE
153-
).value,
167+
CLITelemetryField.COMMAND_FLAGS: command_flags,
168+
CLITelemetryField.COMMAND_OUTPUT_TYPE: format_value.value,
154169
CLITelemetryField.PROJECT_DEFINITION_VERSION: str(_get_definition_version()),
155170
CLITelemetryField.MODE: _get_cli_running_mode(),
156171
}

tests/app/test_telemetry.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,28 @@ def test_cli_exception_classification(error: Exception, is_cli: bool):
215215
from snowflake.cli._app.telemetry import _is_cli_exception
216216

217217
assert _is_cli_exception(error) == is_cli
218+
219+
220+
@mock.patch("uuid.uuid4")
221+
def test_flags_from_parent_contexts_are_captured(mock_uuid4, mock_connect, runner):
222+
mock_uuid4.return_value = uuid.UUID("8a2225b3800c4017a4a9eab941db58fa")
223+
224+
result = runner.invoke(
225+
["dbt", "execute", "--run-async", "pipeline_name", "run", "--debug"]
226+
)
227+
228+
assert result.exit_code == 0, result.output
229+
230+
usage_command_event = (
231+
mock_connect.return_value._telemetry.try_add_log_to_batch.call_args_list[ # noqa: SLF001
232+
0
233+
]
234+
.args[0]
235+
.to_dict()
236+
)
237+
238+
command_flags = usage_command_event["message"]["command_flags"]
239+
assert "run_async" in command_flags, (
240+
f"run_async flag should be captured in telemetry. "
241+
f"Found flags: {command_flags}"
242+
)

0 commit comments

Comments
 (0)