codeboarding 0.14.1__tar.gz → 0.14.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeboarding-0.14.1/codeboarding.egg-info → codeboarding-0.14.2}/PKG-INFO +1 -1
- {codeboarding-0.14.1 → codeboarding-0.14.2}/README.md +5 -4
- {codeboarding-0.14.1 → codeboarding-0.14.2/codeboarding.egg-info}/PKG-INFO +1 -1
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding.egg-info/SOURCES.txt +1 -0
- codeboarding-0.14.2/health_main.py +152 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/pyproject.toml +1 -1
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/__init__.py +3 -2
- codeboarding-0.14.2/telemetry/__init__.py +11 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/telemetry/service.py +32 -18
- codeboarding-0.14.2/tests/test_telemetry_service.py +205 -0
- codeboarding-0.14.1/telemetry/__init__.py +0 -10
- codeboarding-0.14.1/tests/test_telemetry_service.py +0 -99
- {codeboarding-0.14.1 → codeboarding-0.14.2}/LICENSE +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/PYPI.md +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/agent_responses.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/change_status.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/component_ownership.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/constants.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/content_hash.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/file_index_models.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/incremental_results.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/llm_config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/llm_errors.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/llm_renderers/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/llm_renderers/call_graph.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/llm_renderers/scope.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/model_capabilities.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/planner_agent.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/prompts/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/prompts/profiles.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/prompts/shared.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/relation_edges.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/retry.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/scope_analysis_agent.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/scope_ids.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/tools/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/tools/base.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/tools/get_method_calls.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/tools/read_file.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/agents/tree_planner_agent.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/clustering_ids.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding.egg-info/dependency_links.txt +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding.egg-info/entry_points.txt +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding.egg-info/requires.txt +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding.egg-info/top_level.txt +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/bootstrap.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/commands/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/commands/full_analysis.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/commands/incremental_analysis.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/commands/partial_analysis.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/render.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_cli/view_instructions.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_workflows/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_workflows/analysis.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_workflows/orchestration.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_workflows/sources/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_workflows/sources/local.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/codeboarding_workflows/sources/remote.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/constants.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/core/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/core/plugin_loader.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/core/protocols.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/core/registry.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/analysis_json.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/diagram_generator.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/exceptions.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/file_coverage.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/file_index.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/incremental_changes.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/incremental_update.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/io_utils.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/run_context.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/run_mode.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/scope_assembly.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/scope_plan.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/diagram_analysis/tree_shape.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/github_action.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/checks/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/checks/circular_deps.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/checks/function_size.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/checks/unused_code_diagnostics.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/models.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/health/runner.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/install.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/logging_config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/main.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/callbacks.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/context.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/mixin.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/paths.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/stats.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/monitoring/writers.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/html.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/html_template.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/markdown.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/mdx.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/rendering.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/output_generators/sphinx.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/change_detector.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/errors.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/fingerprint_diff.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/git_ops.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/ignore.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/repo_utils/path_utils.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/setup.cfg +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/analysis_cache.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/analysis_result.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/cfg/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/cfg/call_graph.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/cfg/edge.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/cfg/location_key.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/cluster_relations.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/exceptions.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/models.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/draft.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/frontier.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/inventory.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/replay.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/spec.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/names/tokens.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/clustering/service.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/csharp_config_scanner.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/dotnet_sdk.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/csharp_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/go_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/java_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/php_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/python_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/rust_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/call_graph_builder.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/edge_build_context.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/edge_builder.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/hierarchy_builder.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/language_adapter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/lsp_client.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/lsp_constants.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/lsp_recycler.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/models.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/process_memory.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/progress.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/protocols.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/result_converter.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/source_inspector.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/symbol_table.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/engine/utils.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/incremental_orchestrator.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/internal_references.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/java_config_scanner.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/java_utils.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/language_results.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/lsp_client/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/lsp_client/diagnostics.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/node.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/programming_language.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/reference_resolver.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/scanner.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/static_analyzer/typescript_config_scanner.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/telemetry/device_id.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/telemetry/events.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/telemetry/schemas.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_cli_parser.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_github_action.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_install.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_logging_config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_main.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_persisted_analysis_artifacts.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_pyproject_packages.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_registry_coverage.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_telemetry_events.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_tool_registry.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_user_config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_view_instructions.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_vscode_constants.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_windows_compatibility.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tests/test_windows_encoding.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tool_registry/__init__.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tool_registry/installers.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tool_registry/manifest.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tool_registry/paths.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/tool_registry/registry.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/user_config.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/utils.py +0 -0
- {codeboarding-0.14.1 → codeboarding-0.14.2}/vscode_constants.py +0 -0
|
@@ -212,10 +212,11 @@ for the actual depth. Existing baseline loading behavior is unchanged: prefer
|
|
|
212
212
|
|
|
213
213
|
## Telemetry
|
|
214
214
|
|
|
215
|
-
CodeBoarding collects
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
CodeBoarding collects usage telemetry (which command ran, success/failure,
|
|
216
|
+
duration, token cost, repository size and languages, and the account the
|
|
217
|
+
repository belongs to) to help us improve the tool. It is on by default and
|
|
218
|
+
never collects source code, file names, repository names, paths, prompts, model
|
|
219
|
+
outputs, or API keys. Opt out anytime:
|
|
219
220
|
|
|
220
221
|
```bash
|
|
221
222
|
export CODEBOARDING_TELEMETRY=false # or: export DO_NOT_TRACK=1
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Standalone entry point for running health checks on a repository.
|
|
2
|
+
|
|
3
|
+
Runs static analysis and health checks only — no LLM agents or diagram generation.
|
|
4
|
+
Useful for testing health checks in isolation and for CI/CD health gates.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Local repository (output written to <repo>/.codeboarding/health/)
|
|
8
|
+
python health_main.py --local /path/to/repo
|
|
9
|
+
|
|
10
|
+
# Remote repository (cloned to cwd/repos/, output to cwd/<repo_name>/.codeboarding/health/)
|
|
11
|
+
python health_main.py https://github.com/user/repo
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from health.runner import run_health_checks
|
|
19
|
+
from health.config import initialize_health_dir, load_health_config
|
|
20
|
+
from logging_config import setup_logging
|
|
21
|
+
from repo_utils import clone_repository, get_repo_name
|
|
22
|
+
from static_analyzer import get_static_analysis
|
|
23
|
+
from utils import CODEBOARDING_DIR_NAME, get_artifact_dir
|
|
24
|
+
from vscode_constants import update_config
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_health_check_local(
|
|
30
|
+
repo_path: Path,
|
|
31
|
+
project_name: str | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Run health checks on a local repository.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
repo_path: Path to a local repository
|
|
37
|
+
project_name: Optional project name (default: repo directory name)
|
|
38
|
+
"""
|
|
39
|
+
resolved_repo_path = repo_path.resolve()
|
|
40
|
+
if not resolved_repo_path.is_dir():
|
|
41
|
+
raise ValueError(f"Repository path does not exist: {resolved_repo_path}")
|
|
42
|
+
|
|
43
|
+
resolved_project_name = project_name or resolved_repo_path.name
|
|
44
|
+
output_dir = resolved_repo_path / CODEBOARDING_DIR_NAME
|
|
45
|
+
|
|
46
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
setup_logging(log_dir=output_dir)
|
|
48
|
+
|
|
49
|
+
_run_health_checks(resolved_repo_path, resolved_project_name, output_dir)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_health_check_remote(
|
|
53
|
+
repo_url: str,
|
|
54
|
+
project_name: str | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Run health checks on a remote repository.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
repo_url: URL of a remote Git repository
|
|
60
|
+
project_name: Optional project name (default: extracted from URL)
|
|
61
|
+
"""
|
|
62
|
+
repo_root = Path("repos")
|
|
63
|
+
repo_name = clone_repository(repo_url, repo_root)
|
|
64
|
+
resolved_repo_path = repo_root / repo_name
|
|
65
|
+
resolved_project_name = project_name or get_repo_name(repo_url)
|
|
66
|
+
output_dir = Path.cwd() / repo_name / CODEBOARDING_DIR_NAME
|
|
67
|
+
|
|
68
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
setup_logging(log_dir=output_dir)
|
|
70
|
+
|
|
71
|
+
_run_health_checks(resolved_repo_path, resolved_project_name, output_dir)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _run_health_checks(repo_path: Path, project_name: str, output_dir: Path) -> None:
|
|
75
|
+
"""Core health check logic shared by local and remote paths."""
|
|
76
|
+
logger.info(f"Running health checks on '{project_name}' at {repo_path}")
|
|
77
|
+
|
|
78
|
+
static_analysis = get_static_analysis(repo_path, cache_dir=get_artifact_dir(repo_path))
|
|
79
|
+
|
|
80
|
+
# Load health check configuration and initialize health config dir
|
|
81
|
+
health_config_dir = output_dir / "health"
|
|
82
|
+
initialize_health_dir(health_config_dir)
|
|
83
|
+
health_config = load_health_config(health_config_dir)
|
|
84
|
+
|
|
85
|
+
report = run_health_checks(static_analysis, project_name, config=health_config, repo_path=repo_path)
|
|
86
|
+
|
|
87
|
+
if report is None:
|
|
88
|
+
logger.warning("Health checks skipped: no languages found in static analysis results")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
report_path = health_config_dir / "health_report.json"
|
|
92
|
+
report_path.write_text(report.model_dump_json(indent=2, exclude_none=True))
|
|
93
|
+
|
|
94
|
+
logger.info(f"Health report written to {report_path} (overall score: {report.overall_score:.3f})")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
"""Main entry point that parses CLI arguments and routes to subcommands."""
|
|
99
|
+
parser = argparse.ArgumentParser(
|
|
100
|
+
description="Run static analysis health checks on a repository (local or remote)",
|
|
101
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
102
|
+
epilog="""
|
|
103
|
+
Examples:
|
|
104
|
+
# Local repository (output written to <repo>/.codeboarding/health/)
|
|
105
|
+
python health_main.py --local /path/to/repo
|
|
106
|
+
|
|
107
|
+
# Remote repository (cloned to cwd/repos/, output to cwd/<repo_name>/.codeboarding/health/)
|
|
108
|
+
python health_main.py https://github.com/user/repo
|
|
109
|
+
|
|
110
|
+
""",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"repositories",
|
|
114
|
+
nargs="*",
|
|
115
|
+
help="One or more remote Git repository URLs to run health checks on",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument("--local", type=Path, help="Path to a local repository")
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--project-name",
|
|
120
|
+
type=str,
|
|
121
|
+
default=None,
|
|
122
|
+
help="Name of the project (default: extracted from repo path or URL)",
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--binary-location", type=Path, help="Path to the binary directory for language servers and tools"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
args = parser.parse_args()
|
|
129
|
+
|
|
130
|
+
# Validate: must provide either remote repos or --local, not both
|
|
131
|
+
has_remote = bool(args.repositories)
|
|
132
|
+
has_local = args.local is not None
|
|
133
|
+
|
|
134
|
+
if has_remote == has_local:
|
|
135
|
+
parser.error("Provide either one or more remote repositories or --local, but not both.")
|
|
136
|
+
|
|
137
|
+
# Resolve binary paths for tools (tokei, LSP servers, etc.)
|
|
138
|
+
if args.binary_location:
|
|
139
|
+
update_config(args.binary_location)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if has_local:
|
|
143
|
+
run_health_check_local(args.local, args.project_name)
|
|
144
|
+
else:
|
|
145
|
+
for repo_url in args.repositories:
|
|
146
|
+
run_health_check_remote(repo_url, args.project_name)
|
|
147
|
+
except ValueError as e:
|
|
148
|
+
parser.error(str(e))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
main()
|
|
@@ -66,6 +66,8 @@ def sanitize_repo_url(repo_url: str) -> str:
|
|
|
66
66
|
Normalizes Git URLs to ensure proper format for cloning.
|
|
67
67
|
Preserves HTTPS URLs for CI compatibility while supporting SSH URLs.
|
|
68
68
|
"""
|
|
69
|
+
repo_url = repo_url.rstrip("/") # else the suffix below yields ``.../repo/.git``
|
|
70
|
+
|
|
69
71
|
if repo_url.startswith("git@") or repo_url.startswith("ssh://"):
|
|
70
72
|
return repo_url # already in SSH format
|
|
71
73
|
elif repo_url.startswith("https://") or repo_url.startswith("http://"):
|
|
@@ -94,8 +96,7 @@ def remote_repo_exists(repo_url: str) -> bool:
|
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
def get_repo_name(repo_url: str):
|
|
97
|
-
|
|
98
|
-
base = repo_url.rstrip("/").split("/")[-1]
|
|
99
|
+
base = sanitize_repo_url(repo_url).split("/")[-1]
|
|
99
100
|
repo_name, _ = os.path.splitext(base)
|
|
100
101
|
return repo_name
|
|
101
102
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Opt-out usage telemetry.
|
|
2
|
+
|
|
3
|
+
One singleton (``telemetry``) sends events to PostHog. Disabled with
|
|
4
|
+
``CODEBOARDING_TELEMETRY=false`` (or ``DO_NOT_TRACK=1``). No repo content, paths,
|
|
5
|
+
or API keys are ever sent; the repository's owning account is, so events are
|
|
6
|
+
pseudonymous rather than anonymous. See ``service.py`` for the identity rules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from telemetry.service import ProductTelemetry, telemetry
|
|
10
|
+
|
|
11
|
+
__all__ = ["ProductTelemetry", "telemetry"]
|
|
@@ -30,16 +30,40 @@ def _telemetry_disabled() -> bool:
|
|
|
30
30
|
return os.getenv("CODEBOARDING_TELEMETRY", "true").strip().lower() == "false"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _org() -> str:
|
|
34
|
+
"""The account a run's repository belongs to, or '' when none is named.
|
|
35
|
+
|
|
36
|
+
Why: ``CODEBOARDING_ORG`` when a caller knows the owner, else the owner half
|
|
37
|
+
of ``GITHUB_REPOSITORY``; the repository's own name is never read.
|
|
38
|
+
"""
|
|
39
|
+
owner = os.getenv("CODEBOARDING_ORG", "").strip() or os.getenv("GITHUB_REPOSITORY", "").strip().partition("/")[0]
|
|
40
|
+
return owner.lower()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: Properties describing the process rather than the event. A payload that could
|
|
44
|
+
#: set one could hide automated traffic inside usage, or claim an owner it does
|
|
45
|
+
#: not have — so they are dropped from caller properties before the origin is
|
|
46
|
+
#: applied. Listed rather than derived from ``_origin()``: ``org`` is absent
|
|
47
|
+
#: there when no owner is known, which is exactly the case a caller could fill.
|
|
48
|
+
ORIGIN_KEYS = frozenset({"source", "internal", "org"})
|
|
49
|
+
|
|
50
|
+
|
|
33
51
|
def _origin() -> dict[str, object]:
|
|
34
|
-
"""Who invoked this run,
|
|
52
|
+
"""Who invoked this run, whether that is product usage, and which deployment.
|
|
35
53
|
|
|
36
|
-
``
|
|
37
|
-
|
|
38
|
-
engine's own suite, "evals" for the benchmark harness, "core" for any other
|
|
39
|
-
embedding. ``internal`` is derived from it so the two can never disagree.
|
|
54
|
+
Why: ``internal`` is derived from ``source`` so the two cannot disagree, and
|
|
55
|
+
``org`` is omitted rather than blank so an absent key reads as unknown.
|
|
40
56
|
"""
|
|
41
57
|
source = os.getenv("CODEBOARDING_SOURCE", "core")
|
|
42
|
-
|
|
58
|
+
origin: dict[str, object] = {"source": source, "internal": source in INTERNAL_SOURCES}
|
|
59
|
+
if org := _org():
|
|
60
|
+
origin["org"] = org
|
|
61
|
+
return origin
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _with_origin(properties: dict | None) -> dict:
|
|
65
|
+
"""Caller properties, unable to claim any of :data:`ORIGIN_KEYS`."""
|
|
66
|
+
return {k: v for k, v in (properties or {}).items() if k not in ORIGIN_KEYS} | _origin()
|
|
43
67
|
|
|
44
68
|
|
|
45
69
|
class ProductTelemetry:
|
|
@@ -88,12 +112,7 @@ class ProductTelemetry:
|
|
|
88
112
|
self._client.capture(
|
|
89
113
|
distinct_id=self.user_id,
|
|
90
114
|
event=event,
|
|
91
|
-
|
|
92
|
-
# the process, not a field an event model gets to claim — and
|
|
93
|
-
# `internal` is the one property a product metric filters on, so
|
|
94
|
-
# a payload that could overwrite it is a payload that could hide
|
|
95
|
-
# automated traffic inside usage.
|
|
96
|
-
properties={**(properties or {}), **_origin()},
|
|
115
|
+
properties=_with_origin(properties),
|
|
97
116
|
)
|
|
98
117
|
except Exception as e:
|
|
99
118
|
logger.debug("Telemetry capture failed: %s", e)
|
|
@@ -106,12 +125,7 @@ class ProductTelemetry:
|
|
|
106
125
|
self._client.capture_exception(
|
|
107
126
|
exc,
|
|
108
127
|
distinct_id=self.user_id,
|
|
109
|
-
|
|
110
|
-
# the process, not a field an event model gets to claim — and
|
|
111
|
-
# `internal` is the one property a product metric filters on, so
|
|
112
|
-
# a payload that could overwrite it is a payload that could hide
|
|
113
|
-
# automated traffic inside usage.
|
|
114
|
-
properties={**(properties or {}), **_origin()},
|
|
128
|
+
properties=_with_origin(properties),
|
|
115
129
|
)
|
|
116
130
|
except Exception as e:
|
|
117
131
|
logger.debug("Telemetry capture_exception failed: %s", e)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""What every event carries about who produced it.
|
|
2
|
+
|
|
3
|
+
``source`` has always said which invoker a run came from. What it could not say
|
|
4
|
+
is whether that invoker is a *person using the product*: a dashboard measuring
|
|
5
|
+
usage had to spell out ``source not in ('tests', 'evals')`` and was silently
|
|
6
|
+
wrong from the moment another internal source was added. ``internal`` is derived
|
|
7
|
+
from the source in one place so that filter is one condition and stays correct.
|
|
8
|
+
|
|
9
|
+
These exercise ``ProductTelemetry`` itself rather than ``telemetry.events``,
|
|
10
|
+
which stubs ``capture`` out — the stamping happens below that stub, so it is
|
|
11
|
+
invisible to every test in ``test_telemetry_events.py``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from telemetry.service import INTERNAL_SOURCES, ProductTelemetry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def client(monkeypatch):
|
|
23
|
+
"""A recorder in place of the PostHog SDK, on the live singleton."""
|
|
24
|
+
seen = SimpleNamespace(captures=[], exceptions=[])
|
|
25
|
+
service = ProductTelemetry()
|
|
26
|
+
monkeypatch.setattr(
|
|
27
|
+
service,
|
|
28
|
+
"_client",
|
|
29
|
+
SimpleNamespace(
|
|
30
|
+
capture=lambda **kw: seen.captures.append(kw),
|
|
31
|
+
capture_exception=lambda exc, **kw: seen.exceptions.append((exc, kw)),
|
|
32
|
+
flush=lambda: None,
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
seen.service = service
|
|
36
|
+
return seen
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_a_product_run_is_not_marked_internal(client, monkeypatch):
|
|
40
|
+
monkeypatch.setenv("CODEBOARDING_SOURCE", "oss")
|
|
41
|
+
|
|
42
|
+
client.service.capture("analysis_started", {"command": "run_full"})
|
|
43
|
+
|
|
44
|
+
props = client.captures[0]["properties"]
|
|
45
|
+
assert props["source"] == "oss"
|
|
46
|
+
assert props["internal"] is False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("source", sorted(INTERNAL_SOURCES))
|
|
50
|
+
def test_every_internal_source_is_marked_internal(client, monkeypatch, source):
|
|
51
|
+
"""The benchmark harness sets ``evals``; the engine's own suite sets ``tests``.
|
|
52
|
+
Neither is a person using the product, and neither may be counted as one."""
|
|
53
|
+
monkeypatch.setenv("CODEBOARDING_SOURCE", source)
|
|
54
|
+
|
|
55
|
+
client.service.capture("analysis_completed", {"status": "success"})
|
|
56
|
+
|
|
57
|
+
props = client.captures[0]["properties"]
|
|
58
|
+
assert props["source"] == source
|
|
59
|
+
assert props["internal"] is True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_an_unknown_source_defaults_to_product_traffic(client, monkeypatch):
|
|
63
|
+
"""A source nobody declared is an embedding of the library, which IS usage.
|
|
64
|
+
Defaulting the other way would quietly drop a real integration's runs."""
|
|
65
|
+
monkeypatch.setenv("CODEBOARDING_SOURCE", "some-new-wrapper")
|
|
66
|
+
|
|
67
|
+
client.service.capture("analysis_started", {})
|
|
68
|
+
|
|
69
|
+
assert client.captures[0]["properties"]["internal"] is False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_the_origin_travels_on_exceptions_too(client, monkeypatch):
|
|
73
|
+
"""``$exception`` is an event like any other, and error-rate dashboards are
|
|
74
|
+
exactly where a run of the benchmark's deliberate failure cases would hurt."""
|
|
75
|
+
monkeypatch.setenv("CODEBOARDING_SOURCE", "evals")
|
|
76
|
+
exc = RuntimeError("boom")
|
|
77
|
+
|
|
78
|
+
client.service.capture_exception(exc, properties={"command": "run_incremental"})
|
|
79
|
+
|
|
80
|
+
_, kwargs = client.exceptions[0]
|
|
81
|
+
assert kwargs["properties"]["internal"] is True
|
|
82
|
+
assert kwargs["properties"]["command"] == "run_incremental"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_a_caller_property_never_overwrites_the_origin(client, monkeypatch):
|
|
86
|
+
"""The origin merges LAST, so a caller cannot claim it.
|
|
87
|
+
|
|
88
|
+
It used to go first, which left an event model free to overwrite `source`
|
|
89
|
+
simply by having a field of that name. No schema does today — but `internal`
|
|
90
|
+
is the one property a product metric filters on, so a payload that could
|
|
91
|
+
overwrite it is a payload that could hide automated traffic inside usage, and
|
|
92
|
+
that is not a thing to leave resting on nobody having added the field yet."""
|
|
93
|
+
monkeypatch.setenv("CODEBOARDING_SOURCE", "evals")
|
|
94
|
+
|
|
95
|
+
client.service.capture("repo_scanned", {"source": "oss", "internal": False})
|
|
96
|
+
|
|
97
|
+
props = client.captures[0]["properties"]
|
|
98
|
+
assert props["source"] == "evals"
|
|
99
|
+
assert props["internal"] is True
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_ci_runs_report_the_repository_owner(client, monkeypatch):
|
|
103
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
104
|
+
monkeypatch.setenv("GITHUB_REPOSITORY", "Acme-Corp/widgets")
|
|
105
|
+
|
|
106
|
+
client.service.capture("analysis_started", {})
|
|
107
|
+
|
|
108
|
+
assert client.captures[0]["properties"]["org"] == "acme-corp"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_the_same_owner_always_reads_the_same_way(client, monkeypatch):
|
|
112
|
+
"""The owner is the join key across surfaces and across runs, so casing and
|
|
113
|
+
the two ways of naming one have to agree on a single value."""
|
|
114
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
115
|
+
monkeypatch.setenv("GITHUB_REPOSITORY", "Acme/widgets")
|
|
116
|
+
client.service.capture("analysis_started", {})
|
|
117
|
+
|
|
118
|
+
monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)
|
|
119
|
+
monkeypatch.setenv("CODEBOARDING_ORG", "acme")
|
|
120
|
+
client.service.capture("analysis_started", {})
|
|
121
|
+
|
|
122
|
+
assert client.captures[0]["properties"]["org"] == client.captures[1]["properties"]["org"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_only_the_owner_is_taken_from_the_repository_slug(client, monkeypatch):
|
|
126
|
+
"""Why: the name is the half that says something about the code."""
|
|
127
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
128
|
+
monkeypatch.setenv("GITHUB_REPOSITORY", "acme/secret-prototype")
|
|
129
|
+
|
|
130
|
+
client.service.capture("analysis_started", {})
|
|
131
|
+
|
|
132
|
+
props = client.captures[0]["properties"]
|
|
133
|
+
assert props["org"] == "acme"
|
|
134
|
+
assert "secret-prototype" not in str(props)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_an_embedding_can_name_the_owner_itself(client, monkeypatch):
|
|
138
|
+
"""The VSCode extension resolves the owner from the workspace's git remote
|
|
139
|
+
and sets ``CODEBOARDING_ORG``, which has to win: a run driven by the editor
|
|
140
|
+
inside a CI-like environment belongs to the workspace, not the runner."""
|
|
141
|
+
monkeypatch.setenv("CODEBOARDING_ORG", "Acme")
|
|
142
|
+
monkeypatch.setenv("GITHUB_REPOSITORY", "someone-else/runner")
|
|
143
|
+
|
|
144
|
+
client.service.capture("analysis_started", {})
|
|
145
|
+
|
|
146
|
+
assert client.captures[0]["properties"]["org"] == "acme"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_a_local_run_reports_no_owner_at_all(client, monkeypatch):
|
|
150
|
+
"""Nothing infers an owner. With neither variable set the key is absent
|
|
151
|
+
rather than empty, so a query can tell "no owner" from "owner is blank"."""
|
|
152
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
153
|
+
monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)
|
|
154
|
+
|
|
155
|
+
client.service.capture("analysis_started", {})
|
|
156
|
+
|
|
157
|
+
assert "org" not in client.captures[0]["properties"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_a_caller_property_never_overwrites_the_owner(client, monkeypatch):
|
|
161
|
+
"""``org`` merges with the rest of the origin, after the payload, for the
|
|
162
|
+
same reason ``internal`` does: it is a property of the process."""
|
|
163
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
164
|
+
monkeypatch.setenv("GITHUB_REPOSITORY", "acme/widgets")
|
|
165
|
+
|
|
166
|
+
client.service.capture("repo_scanned", {"org": "somebody-else"})
|
|
167
|
+
|
|
168
|
+
assert client.captures[0]["properties"]["org"] == "acme"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_a_caller_cannot_supply_an_owner_when_there_is_none(client, monkeypatch):
|
|
172
|
+
"""Merging the origin last stops a payload *overwriting* an owner, but not
|
|
173
|
+
inventing one: with no owner resolved the key is absent, so a caller's value
|
|
174
|
+
would have survived into it and attributed the run to an account it has
|
|
175
|
+
nothing to do with. The reserved keys come off the payload first."""
|
|
176
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
177
|
+
monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)
|
|
178
|
+
|
|
179
|
+
client.service.capture("repo_scanned", {"org": "somebody-else", "total_loc": 10})
|
|
180
|
+
|
|
181
|
+
props = client.captures[0]["properties"]
|
|
182
|
+
assert "org" not in props
|
|
183
|
+
assert props["total_loc"] == 10
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_a_caller_cannot_supply_an_owner_on_exceptions_either(client, monkeypatch):
|
|
187
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
188
|
+
monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)
|
|
189
|
+
|
|
190
|
+
client.service.capture_exception(RuntimeError("boom"), properties={"org": "somebody-else"})
|
|
191
|
+
|
|
192
|
+
_, kwargs = client.exceptions[0]
|
|
193
|
+
assert "org" not in kwargs["properties"]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_the_owner_travels_on_exceptions_too(client, monkeypatch):
|
|
197
|
+
"""Why: exceptions take a different code path, and error-rate dashboards are
|
|
198
|
+
where knowing whether one deployment or all of them is hitting a bug pays."""
|
|
199
|
+
monkeypatch.delenv("CODEBOARDING_ORG", raising=False)
|
|
200
|
+
monkeypatch.setenv("GITHUB_REPOSITORY", "acme/widgets")
|
|
201
|
+
|
|
202
|
+
client.service.capture_exception(RuntimeError("boom"), properties={"command": "run_full"})
|
|
203
|
+
|
|
204
|
+
_, kwargs = client.exceptions[0]
|
|
205
|
+
assert kwargs["properties"]["org"] == "acme"
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"""Anonymous, opt-out usage telemetry.
|
|
2
|
-
|
|
3
|
-
One singleton (``telemetry``) sends anonymous events to PostHog. Disabled with
|
|
4
|
-
``CODEBOARDING_TELEMETRY=false`` (or ``DO_NOT_TRACK=1``). No repo content, paths,
|
|
5
|
-
or API keys are ever sent. See ``service.py`` for the identity rules.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from telemetry.service import ProductTelemetry, telemetry
|
|
9
|
-
|
|
10
|
-
__all__ = ["ProductTelemetry", "telemetry"]
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"""What every event carries about who produced it.
|
|
2
|
-
|
|
3
|
-
``source`` has always said which invoker a run came from. What it could not say
|
|
4
|
-
is whether that invoker is a *person using the product*: a dashboard measuring
|
|
5
|
-
usage had to spell out ``source not in ('tests', 'evals')`` and was silently
|
|
6
|
-
wrong from the moment another internal source was added. ``internal`` is derived
|
|
7
|
-
from the source in one place so that filter is one condition and stays correct.
|
|
8
|
-
|
|
9
|
-
These exercise ``ProductTelemetry`` itself rather than ``telemetry.events``,
|
|
10
|
-
which stubs ``capture`` out — the stamping happens below that stub, so it is
|
|
11
|
-
invisible to every test in ``test_telemetry_events.py``.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from types import SimpleNamespace
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
from telemetry.service import INTERNAL_SOURCES, ProductTelemetry
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@pytest.fixture
|
|
22
|
-
def client(monkeypatch):
|
|
23
|
-
"""A recorder in place of the PostHog SDK, on the live singleton."""
|
|
24
|
-
seen = SimpleNamespace(captures=[], exceptions=[])
|
|
25
|
-
service = ProductTelemetry()
|
|
26
|
-
monkeypatch.setattr(
|
|
27
|
-
service,
|
|
28
|
-
"_client",
|
|
29
|
-
SimpleNamespace(
|
|
30
|
-
capture=lambda **kw: seen.captures.append(kw),
|
|
31
|
-
capture_exception=lambda exc, **kw: seen.exceptions.append((exc, kw)),
|
|
32
|
-
flush=lambda: None,
|
|
33
|
-
),
|
|
34
|
-
)
|
|
35
|
-
seen.service = service
|
|
36
|
-
return seen
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_a_product_run_is_not_marked_internal(client, monkeypatch):
|
|
40
|
-
monkeypatch.setenv("CODEBOARDING_SOURCE", "oss")
|
|
41
|
-
|
|
42
|
-
client.service.capture("analysis_started", {"command": "run_full"})
|
|
43
|
-
|
|
44
|
-
props = client.captures[0]["properties"]
|
|
45
|
-
assert props["source"] == "oss"
|
|
46
|
-
assert props["internal"] is False
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@pytest.mark.parametrize("source", sorted(INTERNAL_SOURCES))
|
|
50
|
-
def test_every_internal_source_is_marked_internal(client, monkeypatch, source):
|
|
51
|
-
"""The benchmark harness sets ``evals``; the engine's own suite sets ``tests``.
|
|
52
|
-
Neither is a person using the product, and neither may be counted as one."""
|
|
53
|
-
monkeypatch.setenv("CODEBOARDING_SOURCE", source)
|
|
54
|
-
|
|
55
|
-
client.service.capture("analysis_completed", {"status": "success"})
|
|
56
|
-
|
|
57
|
-
props = client.captures[0]["properties"]
|
|
58
|
-
assert props["source"] == source
|
|
59
|
-
assert props["internal"] is True
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def test_an_unknown_source_defaults_to_product_traffic(client, monkeypatch):
|
|
63
|
-
"""A source nobody declared is an embedding of the library, which IS usage.
|
|
64
|
-
Defaulting the other way would quietly drop a real integration's runs."""
|
|
65
|
-
monkeypatch.setenv("CODEBOARDING_SOURCE", "some-new-wrapper")
|
|
66
|
-
|
|
67
|
-
client.service.capture("analysis_started", {})
|
|
68
|
-
|
|
69
|
-
assert client.captures[0]["properties"]["internal"] is False
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def test_the_origin_travels_on_exceptions_too(client, monkeypatch):
|
|
73
|
-
"""``$exception`` is an event like any other, and error-rate dashboards are
|
|
74
|
-
exactly where a run of the benchmark's deliberate failure cases would hurt."""
|
|
75
|
-
monkeypatch.setenv("CODEBOARDING_SOURCE", "evals")
|
|
76
|
-
exc = RuntimeError("boom")
|
|
77
|
-
|
|
78
|
-
client.service.capture_exception(exc, properties={"command": "run_incremental"})
|
|
79
|
-
|
|
80
|
-
_, kwargs = client.exceptions[0]
|
|
81
|
-
assert kwargs["properties"]["internal"] is True
|
|
82
|
-
assert kwargs["properties"]["command"] == "run_incremental"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_a_caller_property_never_overwrites_the_origin(client, monkeypatch):
|
|
86
|
-
"""The origin merges LAST, so a caller cannot claim it.
|
|
87
|
-
|
|
88
|
-
It used to go first, which left an event model free to overwrite `source`
|
|
89
|
-
simply by having a field of that name. No schema does today — but `internal`
|
|
90
|
-
is the one property a product metric filters on, so a payload that could
|
|
91
|
-
overwrite it is a payload that could hide automated traffic inside usage, and
|
|
92
|
-
that is not a thing to leave resting on nobody having added the field yet."""
|
|
93
|
-
monkeypatch.setenv("CODEBOARDING_SOURCE", "evals")
|
|
94
|
-
|
|
95
|
-
client.service.capture("repo_scanned", {"source": "oss", "internal": False})
|
|
96
|
-
|
|
97
|
-
props = client.captures[0]["properties"]
|
|
98
|
-
assert props["source"] == "evals"
|
|
99
|
-
assert props["internal"] is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|