Skip to content

Commit e25dc7d

Browse files
CM-61376: Track CLI/IDE activation events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 71f3690 commit e25dc7d

File tree

7 files changed

+130
-0
lines changed

7 files changed

+130
-0
lines changed

cycode/cli/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ def app_callback(
166166
if user_agent:
167167
user_agent_option = UserAgentOptionScheme().loads(user_agent)
168168
CycodeClientBase.enrich_user_agent(user_agent_option.user_agent_suffix)
169+
ctx.obj['plugin_app_name'] = user_agent_option.app_name
170+
ctx.obj['plugin_app_version'] = user_agent_option.app_version
169171

170172
if not no_update_notifier:
171173
ctx.call_on_close(lambda: check_latest_version_on_close(ctx))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import TYPE_CHECKING, Optional
2+
3+
from cycode import __version__
4+
from cycode.cli.config import configuration_manager
5+
from cycode.cyclient.cli_activation_client import CliActivationClient
6+
from cycode.logger import get_logger
7+
8+
if TYPE_CHECKING:
9+
from cycode.cyclient.cycode_client_base import CycodeClientBase
10+
11+
logger = get_logger('Activation Manager')
12+
13+
_CLI_CLIENT_NAME = 'cli'
14+
15+
16+
def try_report_activation(
17+
cycode_client: 'CycodeClientBase',
18+
plugin_app_name: Optional[str] = None,
19+
plugin_app_version: Optional[str] = None,
20+
) -> None:
21+
"""Report CLI/IDE activation to the backend if the (client, version) pair is new.
22+
23+
Failures are swallowed — activation tracking is non-critical.
24+
"""
25+
try:
26+
client = plugin_app_name or _CLI_CLIENT_NAME
27+
version = plugin_app_version or __version__
28+
29+
if configuration_manager.get_last_reported_activation_version(client) == version:
30+
return
31+
32+
CliActivationClient(cycode_client).report_activation()
33+
configuration_manager.update_last_reported_activation_version(client, version)
34+
except Exception:
35+
logger.debug('Failed to report CLI activation', exc_info=True)

cycode/cli/apps/scan/scan_command.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import click
66
import typer
77

8+
from cycode.cli.apps.activation_manager import try_report_activation
89
from cycode.cli.apps.sca_options import (
910
GradleAllSubProjectsOption,
1011
MavenSettingsFileOption,
@@ -140,6 +141,10 @@ def scan_command(
140141
scan_client = get_scan_cycode_client(ctx)
141142
ctx.obj['client'] = scan_client
142143

144+
plugin_app_name = ctx.obj.get('plugin_app_name')
145+
plugin_app_version = ctx.obj.get('plugin_app_version')
146+
try_report_activation(scan_client.scan_cycode_client, plugin_app_name, plugin_app_version)
147+
143148
# Get remote URL from current working directory
144149
remote_url = _try_get_git_remote_url(os.getcwd())
145150

cycode/cli/user_settings/config_file_manager.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class ConfigFileManager(BaseFileManager):
1818
SCAN_SECTION_NAME: str = 'scan'
1919

2020
INSTALLATION_ID_FIELD_NAME: str = 'installation_id'
21+
LAST_REPORTED_ACTIVATION_VERSIONS_FIELD_NAME: str = 'last_reported_activation_versions'
2122
API_URL_FIELD_NAME: str = 'cycode_api_url'
2223
APP_URL_FIELD_NAME: str = 'cycode_app_url'
2324
VERBOSE_FIELD_NAME: str = 'verbose'
@@ -68,6 +69,16 @@ def update_installation_id(self, installation_id: str) -> None:
6869
update_data = {self.ENVIRONMENT_SECTION_NAME: {self.INSTALLATION_ID_FIELD_NAME: installation_id}}
6970
self.write_content_to_file(update_data)
7071

72+
def get_last_reported_activation_versions(self) -> dict[str, str]:
73+
value = self._get_value_from_environment_section(self.LAST_REPORTED_ACTIVATION_VERSIONS_FIELD_NAME)
74+
return value if isinstance(value, dict) else {}
75+
76+
def update_last_reported_activation_version(self, client: str, version: str) -> None:
77+
versions = self.get_last_reported_activation_versions()
78+
versions[client] = version
79+
update_data = {self.ENVIRONMENT_SECTION_NAME: {self.LAST_REPORTED_ACTIVATION_VERSIONS_FIELD_NAME: versions}}
80+
self.write_content_to_file(update_data)
81+
7182
def add_exclusion(self, scan_type: str, exclusion_type: str, new_exclusion: str) -> None:
7283
exclusions = self._get_exclusions_by_exclusion_type(scan_type, exclusion_type)
7384
if new_exclusion in exclusions:

cycode/cli/user_settings/configuration_manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ def get_or_create_installation_id(self) -> str:
9494

9595
return installation_id
9696

97+
def get_last_reported_activation_version(self, client: str) -> Optional[str]:
98+
return self.global_config_file_manager.get_last_reported_activation_versions().get(client)
99+
100+
def update_last_reported_activation_version(self, client: str, version: str) -> None:
101+
self.global_config_file_manager.update_last_reported_activation_version(client, version)
102+
97103
def get_config_file_manager(self, scope: Optional[str] = None) -> ConfigFileManager:
98104
if scope == 'local':
99105
return self.local_config_file_manager
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import TYPE_CHECKING
2+
3+
if TYPE_CHECKING:
4+
from cycode.cyclient.cycode_client_base import CycodeClientBase
5+
6+
_CLI_ACTIVATION_PATH = 'scans/api/v4/cli-activation'
7+
8+
9+
class CliActivationClient:
10+
def __init__(self, cycode_client: 'CycodeClientBase') -> None:
11+
self._cycode_client = cycode_client
12+
13+
def report_activation(self) -> None:
14+
self._cycode_client.put(url_path=_CLI_ACTIVATION_PATH)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from cycode.cli.user_settings.config_file_manager import ConfigFileManager
6+
7+
8+
@pytest.fixture()
9+
def config_manager(tmp_path: Path) -> ConfigFileManager:
10+
return ConfigFileManager(tmp_path)
11+
12+
13+
def test_get_last_reported_activation_versions_returns_empty_when_not_set(
14+
config_manager: ConfigFileManager,
15+
) -> None:
16+
assert config_manager.get_last_reported_activation_versions() == {}
17+
18+
19+
def test_update_and_get_last_reported_activation_version_cli(config_manager: ConfigFileManager) -> None:
20+
config_manager.update_last_reported_activation_version('cli', '1.10.7')
21+
22+
assert config_manager.get_last_reported_activation_versions() == {'cli': '1.10.7'}
23+
24+
25+
def test_update_and_get_last_reported_activation_version_plugin(config_manager: ConfigFileManager) -> None:
26+
config_manager.update_last_reported_activation_version('vscode_extension', '2.0.0')
27+
28+
assert config_manager.get_last_reported_activation_versions() == {'vscode_extension': '2.0.0'}
29+
30+
31+
def test_update_last_reported_activation_version_multiple_clients(config_manager: ConfigFileManager) -> None:
32+
config_manager.update_last_reported_activation_version('cli', '1.10.7')
33+
config_manager.update_last_reported_activation_version('vscode_extension', '2.0.0')
34+
config_manager.update_last_reported_activation_version('jetbrains_extension', '1.5.0')
35+
36+
assert config_manager.get_last_reported_activation_versions() == {
37+
'cli': '1.10.7',
38+
'vscode_extension': '2.0.0',
39+
'jetbrains_extension': '1.5.0',
40+
}
41+
42+
43+
def test_update_last_reported_activation_version_overwrites_existing(config_manager: ConfigFileManager) -> None:
44+
config_manager.update_last_reported_activation_version('cli', '1.10.7')
45+
config_manager.update_last_reported_activation_version('cli', '1.10.8')
46+
47+
assert config_manager.get_last_reported_activation_versions() == {'cli': '1.10.8'}
48+
49+
50+
def test_update_last_reported_activation_version_does_not_affect_other_clients(
51+
config_manager: ConfigFileManager,
52+
) -> None:
53+
config_manager.update_last_reported_activation_version('cli', '1.10.7')
54+
config_manager.update_last_reported_activation_version('vscode_extension', '2.0.0')
55+
config_manager.update_last_reported_activation_version('cli', '1.10.8')
56+
57+
assert config_manager.get_last_reported_activation_versions()['vscode_extension'] == '2.0.0'

0 commit comments

Comments
 (0)