recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__py3-none-any.whl
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.
Potentially problematic release.
This version of recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +188 -143
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +76 -0
- recce_cloud/api/gitlab.py +82 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/event/__init__.py
CHANGED
|
@@ -10,17 +10,21 @@ from typing import Dict
|
|
|
10
10
|
|
|
11
11
|
import sentry_sdk
|
|
12
12
|
|
|
13
|
-
from recce import
|
|
13
|
+
from recce import get_runner, get_version, is_ci_env, is_recce_cloud_instance
|
|
14
14
|
from recce import yaml as pyml
|
|
15
15
|
from recce.event.collector import Collector
|
|
16
16
|
from recce.git import current_branch, hosting_repo
|
|
17
|
-
from recce.github import
|
|
18
|
-
get_github_codespace_available_at
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
from recce.github import (
|
|
18
|
+
get_github_codespace_available_at,
|
|
19
|
+
get_github_codespace_info,
|
|
20
|
+
get_github_codespace_name,
|
|
21
|
+
is_github_codespace,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
USER_HOME = os.path.expanduser("~")
|
|
25
|
+
RECCE_USER_HOME = os.path.join(USER_HOME, ".recce")
|
|
26
|
+
RECCE_USER_PROFILE = os.path.join(RECCE_USER_HOME, "profile.yml")
|
|
27
|
+
RECCE_USER_EVENT_PATH = os.path.join(RECCE_USER_HOME, ".unsend_events.json")
|
|
24
28
|
|
|
25
29
|
__version__ = get_version()
|
|
26
30
|
_collector = Collector()
|
|
@@ -33,13 +37,13 @@ def init():
|
|
|
33
37
|
|
|
34
38
|
# Amplitude init
|
|
35
39
|
_collector.set_api_key(api_key)
|
|
36
|
-
_collector.set_user_id(user_profile.get(
|
|
40
|
+
_collector.set_user_id(user_profile.get("user_id"))
|
|
37
41
|
_collector.set_unsend_events_file(RECCE_USER_EVENT_PATH)
|
|
38
42
|
|
|
39
43
|
# Sentry init
|
|
40
44
|
sentry_env = _get_sentry_env()
|
|
41
45
|
sentry_dns = _get_sentry_dns()
|
|
42
|
-
release_version = __version__ if sentry_env !=
|
|
46
|
+
release_version = __version__ if sentry_env != "development" else None
|
|
43
47
|
sentry_sdk.init(
|
|
44
48
|
dsn=sentry_dns,
|
|
45
49
|
environment=sentry_env,
|
|
@@ -47,58 +51,63 @@ def init():
|
|
|
47
51
|
# Set traces_sample_rate to 1.0 to capture 100%
|
|
48
52
|
# of transactions for performance monitoring.
|
|
49
53
|
# We recommend adjusting this value in production.
|
|
50
|
-
traces_sample_rate=1.0
|
|
54
|
+
traces_sample_rate=1.0,
|
|
51
55
|
)
|
|
52
|
-
sentry_sdk.set_tag(
|
|
53
|
-
sentry_sdk.set_tag(
|
|
54
|
-
sentry_sdk.set_tag(
|
|
55
|
-
sentry_sdk.set_tag(
|
|
56
|
-
sentry_sdk.set_tag(
|
|
56
|
+
sentry_sdk.set_tag("recce.version", __version__)
|
|
57
|
+
sentry_sdk.set_tag("platform", sys.platform)
|
|
58
|
+
sentry_sdk.set_tag("is_ci_env", is_ci_env())
|
|
59
|
+
sentry_sdk.set_tag("is_github_codespace", is_github_codespace())
|
|
60
|
+
sentry_sdk.set_tag("is_recce_cloud_instance", is_recce_cloud_instance())
|
|
61
|
+
sentry_sdk.set_tag("system_timezone", get_system_timezone())
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
def get_user_id():
|
|
60
|
-
return load_user_profile().get(
|
|
65
|
+
return load_user_profile().get("user_id")
|
|
61
66
|
|
|
62
67
|
|
|
63
68
|
def get_recce_api_token():
|
|
64
|
-
return load_user_profile().get(
|
|
69
|
+
return load_user_profile().get("api_token")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def update_recce_api_token(token):
|
|
73
|
+
return update_user_profile({"api_token": token})
|
|
65
74
|
|
|
66
75
|
|
|
67
76
|
def is_anonymous_tracking():
|
|
68
|
-
return load_user_profile().get(
|
|
77
|
+
return load_user_profile().get("anonymous_tracking", False)
|
|
69
78
|
|
|
70
79
|
|
|
71
80
|
def _get_sentry_dns():
|
|
72
|
-
dns_file = os.path.normpath(os.path.join(os.path.dirname(__file__),
|
|
73
|
-
with open(dns_file) as f:
|
|
81
|
+
dns_file = os.path.normpath(os.path.join(os.path.dirname(__file__), "SENTRY_DNS"))
|
|
82
|
+
with open(dns_file, encoding="utf-8") as f:
|
|
74
83
|
dns = f.read().strip()
|
|
75
84
|
return dns
|
|
76
85
|
|
|
77
86
|
|
|
78
87
|
def _get_sentry_env():
|
|
79
|
-
if
|
|
80
|
-
return
|
|
81
|
-
elif re.match(r
|
|
82
|
-
return
|
|
83
|
-
elif
|
|
84
|
-
return
|
|
85
|
-
elif
|
|
86
|
-
return
|
|
87
|
-
elif
|
|
88
|
-
return
|
|
89
|
-
return
|
|
88
|
+
if ".dev" in __version__:
|
|
89
|
+
return "development"
|
|
90
|
+
elif re.match(r"^\d+\.\d+\.\d+\.\d{8}[a|b|rc]?.*$", __version__):
|
|
91
|
+
return "nightly"
|
|
92
|
+
elif "a" in __version__:
|
|
93
|
+
return "alpha"
|
|
94
|
+
elif "b" in __version__:
|
|
95
|
+
return "beta"
|
|
96
|
+
elif "rc" in __version__:
|
|
97
|
+
return "release-candidate"
|
|
98
|
+
return "production"
|
|
90
99
|
|
|
91
100
|
|
|
92
101
|
def _get_api_key():
|
|
93
|
-
if os.getenv(
|
|
102
|
+
if os.getenv("RECCE_EVENT_API_KEY"):
|
|
94
103
|
# For local testing purpose
|
|
95
|
-
return os.getenv(
|
|
104
|
+
return os.getenv("RECCE_EVENT_API_KEY")
|
|
96
105
|
|
|
97
|
-
config_file = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
|
106
|
+
config_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "CONFIG"))
|
|
98
107
|
try:
|
|
99
|
-
with open(config_file) as fh:
|
|
108
|
+
with open(config_file, encoding="utf-8") as fh:
|
|
100
109
|
config = pyml.load(fh)
|
|
101
|
-
return config.get(
|
|
110
|
+
return config.get("event_api_key")
|
|
102
111
|
except Exception:
|
|
103
112
|
return None
|
|
104
113
|
|
|
@@ -108,15 +117,15 @@ def _generate_user_profile():
|
|
|
108
117
|
os.makedirs(RECCE_USER_HOME, exist_ok=True)
|
|
109
118
|
except Exception:
|
|
110
119
|
# TODO: should show warning message but not raise exception
|
|
111
|
-
print(
|
|
120
|
+
print("Please disable command tracking to continue.")
|
|
112
121
|
exit(1)
|
|
113
122
|
if is_github_codespace() is True:
|
|
114
|
-
salted_name = f
|
|
123
|
+
salted_name = f"codespace-{get_github_codespace_name()}"
|
|
115
124
|
user_id = hashlib.sha256(salted_name.encode()).hexdigest()
|
|
116
125
|
else:
|
|
117
126
|
user_id = uuid.uuid4().hex
|
|
118
|
-
with open(RECCE_USER_PROFILE,
|
|
119
|
-
pyml.dump({
|
|
127
|
+
with open(RECCE_USER_PROFILE, "w+", encoding="utf-8") as f:
|
|
128
|
+
pyml.dump({"user_id": user_id, "anonymous_tracking": True}, f)
|
|
120
129
|
return dict(user_id=user_id, anonymous_tracking=True)
|
|
121
130
|
|
|
122
131
|
|
|
@@ -125,9 +134,9 @@ def load_user_profile():
|
|
|
125
134
|
if not os.path.exists(RECCE_USER_PROFILE):
|
|
126
135
|
user_profile = _generate_user_profile()
|
|
127
136
|
else:
|
|
128
|
-
with open(RECCE_USER_PROFILE,
|
|
137
|
+
with open(RECCE_USER_PROFILE, "r", encoding="utf-8") as f:
|
|
129
138
|
user_profile = pyml.load(f)
|
|
130
|
-
if user_profile.get(
|
|
139
|
+
if user_profile is None or user_profile.get("user_id") is None:
|
|
131
140
|
user_profile = _generate_user_profile()
|
|
132
141
|
|
|
133
142
|
return user_profile
|
|
@@ -136,7 +145,7 @@ def load_user_profile():
|
|
|
136
145
|
def update_user_profile(update_values):
|
|
137
146
|
original = load_user_profile()
|
|
138
147
|
original.update(update_values)
|
|
139
|
-
with open(RECCE_USER_PROFILE,
|
|
148
|
+
with open(RECCE_USER_PROFILE, "w+", encoding="utf-8") as f:
|
|
140
149
|
pyml.dump(original, f)
|
|
141
150
|
return original
|
|
142
151
|
|
|
@@ -146,10 +155,10 @@ def flush_events(command=None):
|
|
|
146
155
|
|
|
147
156
|
|
|
148
157
|
def should_log_event():
|
|
149
|
-
with open(RECCE_USER_PROFILE,
|
|
158
|
+
with open(RECCE_USER_PROFILE, "r", encoding="utf-8") as f:
|
|
150
159
|
user_profile = pyml.load(f)
|
|
151
160
|
# TODO: default anonymous_tracking to false if field is not present
|
|
152
|
-
tracking = user_profile.get(
|
|
161
|
+
tracking = user_profile.get("anonymous_tracking", False)
|
|
153
162
|
tracking = tracking and isinstance(tracking, bool)
|
|
154
163
|
if not tracking:
|
|
155
164
|
return False
|
|
@@ -166,18 +175,18 @@ def log_event(prop, event_type, **kwargs):
|
|
|
166
175
|
|
|
167
176
|
repo = hosting_repo()
|
|
168
177
|
if repo is not None:
|
|
169
|
-
prop[
|
|
178
|
+
prop["repository"] = sha256(repo.encode()).hexdigest()
|
|
170
179
|
|
|
171
180
|
branch = current_branch()
|
|
172
181
|
if branch is not None:
|
|
173
|
-
prop[
|
|
182
|
+
prop["branch"] = sha256(branch.encode()).hexdigest()
|
|
174
183
|
|
|
175
184
|
runner = get_runner()
|
|
176
185
|
if runner is not None:
|
|
177
|
-
prop[
|
|
186
|
+
prop["runner_type"] = runner
|
|
178
187
|
|
|
179
|
-
if runner ==
|
|
180
|
-
prop[
|
|
188
|
+
if runner == "github codespaces":
|
|
189
|
+
prop["codespaces_name"] = get_github_codespace_name()
|
|
181
190
|
|
|
182
191
|
payload = dict(
|
|
183
192
|
**prop,
|
|
@@ -191,11 +200,11 @@ def log_api_event(endpoint_name, prop):
|
|
|
191
200
|
**prop,
|
|
192
201
|
endpoint_name=endpoint_name,
|
|
193
202
|
)
|
|
194
|
-
log_event(prop,
|
|
203
|
+
log_event(prop, "api_event")
|
|
195
204
|
_collector.schedule_flush()
|
|
196
205
|
|
|
197
206
|
|
|
198
|
-
def log_load_state(command=
|
|
207
|
+
def log_load_state(command="server", single_env=False):
|
|
199
208
|
from recce.models import CheckDAO
|
|
200
209
|
|
|
201
210
|
checks = 0
|
|
@@ -213,10 +222,10 @@ def log_load_state(command='server', single_env=False):
|
|
|
213
222
|
)
|
|
214
223
|
|
|
215
224
|
if command == "server":
|
|
216
|
-
prop[
|
|
225
|
+
prop["single_env"] = single_env
|
|
217
226
|
|
|
218
|
-
log_event(prop,
|
|
219
|
-
if command ==
|
|
227
|
+
log_event(prop, "load_state")
|
|
228
|
+
if command == "server":
|
|
220
229
|
_collector.schedule_flush()
|
|
221
230
|
|
|
222
231
|
|
|
@@ -227,31 +236,29 @@ def log_codespaces_events(command):
|
|
|
227
236
|
return
|
|
228
237
|
|
|
229
238
|
user_prop = dict(
|
|
230
|
-
location=codespace.get(
|
|
231
|
-
is_prebuild=codespace.get(
|
|
239
|
+
location=codespace.get("location"),
|
|
240
|
+
is_prebuild=codespace.get("prebuild", False),
|
|
232
241
|
)
|
|
233
242
|
|
|
234
243
|
prop = dict(
|
|
235
|
-
machine=codespace.get(
|
|
244
|
+
machine=codespace.get("machine", {}).get("display_name"),
|
|
236
245
|
codespaces_name=get_github_codespace_name(),
|
|
237
246
|
)
|
|
238
247
|
|
|
239
248
|
# Codespace created event, send once
|
|
240
|
-
codespace_created_at = load_user_profile().get(
|
|
249
|
+
codespace_created_at = load_user_profile().get("codespace_created_at")
|
|
241
250
|
if codespace_created_at is None:
|
|
242
|
-
created_at = datetime.fromisoformat(codespace.get(
|
|
243
|
-
prop[
|
|
244
|
-
_collector.log_event(prop,
|
|
245
|
-
|
|
246
|
-
update_user_profile({'codespace_created_at': codespace.get('created_at')})
|
|
251
|
+
created_at = datetime.fromisoformat(codespace.get("created_at"))
|
|
252
|
+
prop["state"] = "created"
|
|
253
|
+
_collector.log_event(prop, "codespace_instance", event_triggered_at=created_at, user_properties=user_prop)
|
|
254
|
+
update_user_profile({"codespace_created_at": codespace.get("created_at")})
|
|
247
255
|
|
|
248
256
|
# Codespace available event, send multiple times as start/stop it
|
|
249
257
|
available_at = get_github_codespace_available_at(codespace)
|
|
250
|
-
if available_at and available_at.isoformat() != load_user_profile().get(
|
|
251
|
-
prop[
|
|
252
|
-
_collector.log_event(prop,
|
|
253
|
-
|
|
254
|
-
update_user_profile({'codespace_available_at': available_at.isoformat()})
|
|
258
|
+
if available_at and available_at.isoformat() != load_user_profile().get("codespace_available_at"):
|
|
259
|
+
prop["state"] = "available"
|
|
260
|
+
_collector.log_event(prop, "codespace_instance", event_triggered_at=available_at, user_properties=user_prop)
|
|
261
|
+
update_user_profile({"codespace_available_at": available_at.isoformat()})
|
|
255
262
|
|
|
256
263
|
# Codespace instance event should be flushed immediately
|
|
257
264
|
_collector.send_events()
|
|
@@ -259,20 +266,25 @@ def log_codespaces_events(command):
|
|
|
259
266
|
|
|
260
267
|
def log_single_env_event():
|
|
261
268
|
prop = dict(
|
|
262
|
-
action=
|
|
269
|
+
action="launch_server",
|
|
263
270
|
)
|
|
264
|
-
log_event(prop,
|
|
271
|
+
log_event(prop, "[Experiment] single_environment")
|
|
265
272
|
_collector.schedule_flush()
|
|
266
273
|
|
|
267
274
|
|
|
268
275
|
def log_performance(feature_name: str, metrics: Dict):
|
|
269
276
|
prop = metrics
|
|
270
|
-
log_event(prop, f
|
|
277
|
+
log_event(prop, f"[Performance] {feature_name}")
|
|
278
|
+
_collector.schedule_flush()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def log_connected_to_cloud():
|
|
282
|
+
log_event({"action": "connected_to_cloud"}, "Connect OSS to Cloud")
|
|
271
283
|
_collector.schedule_flush()
|
|
272
284
|
|
|
273
285
|
|
|
274
286
|
def capture_exception(e):
|
|
275
|
-
user_id = load_user_profile().get(
|
|
287
|
+
user_id = load_user_profile().get("user_id")
|
|
276
288
|
if is_ci_env() is True:
|
|
277
289
|
user_id = f"{user_id}_CI"
|
|
278
290
|
|
recce/event/collector.py
CHANGED
|
@@ -4,20 +4,19 @@ import platform
|
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
-
from datetime import datetime
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
8
|
from json import JSONDecodeError
|
|
9
|
-
from datetime import timezone
|
|
10
9
|
|
|
11
10
|
import portalocker
|
|
12
11
|
import requests
|
|
13
12
|
|
|
14
|
-
from recce import __version__, is_ci_env
|
|
13
|
+
from recce import __version__, is_ci_env, is_recce_cloud_instance
|
|
15
14
|
from recce.github import is_github_codespace
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class Collector:
|
|
19
18
|
def __init__(self):
|
|
20
|
-
self._api_endpoint =
|
|
19
|
+
self._api_endpoint = "https://api.amplitude.com/2/httpapi"
|
|
21
20
|
self._api_key = None
|
|
22
21
|
self._user_id = None
|
|
23
22
|
|
|
@@ -26,6 +25,7 @@ class Collector:
|
|
|
26
25
|
self._upload_threshold = 10
|
|
27
26
|
self._is_ci: bool = is_ci_env()
|
|
28
27
|
self._is_github_codespace: bool = is_github_codespace()
|
|
28
|
+
self._is_recce_cloud_instance: bool = is_recce_cloud_instance()
|
|
29
29
|
self._flush_timer = None
|
|
30
30
|
|
|
31
31
|
def schedule_flush(self):
|
|
@@ -35,6 +35,7 @@ class Collector:
|
|
|
35
35
|
|
|
36
36
|
# send async thread
|
|
37
37
|
import threading
|
|
38
|
+
|
|
38
39
|
if self._flush_timer:
|
|
39
40
|
try:
|
|
40
41
|
self._flush_timer.cancel()
|
|
@@ -59,11 +60,18 @@ class Collector:
|
|
|
59
60
|
self._unsend_events_file = unsend_events_file
|
|
60
61
|
self._check_required_files()
|
|
61
62
|
|
|
62
|
-
def _log_event(
|
|
63
|
+
def _log_event(
|
|
64
|
+
self,
|
|
65
|
+
user_id,
|
|
66
|
+
event_type,
|
|
67
|
+
created_at,
|
|
68
|
+
user_properties,
|
|
69
|
+
event_properties,
|
|
70
|
+
):
|
|
63
71
|
event = dict(
|
|
64
72
|
user_id=user_id,
|
|
65
73
|
event_type=event_type,
|
|
66
|
-
ip=
|
|
74
|
+
ip="$remote",
|
|
67
75
|
time=int(time.mktime(created_at.timetuple())),
|
|
68
76
|
user_properties=user_properties,
|
|
69
77
|
event_properties=event_properties,
|
|
@@ -91,7 +99,7 @@ class Collector:
|
|
|
91
99
|
else:
|
|
92
100
|
# Convert to UTC timezone
|
|
93
101
|
created_at = event_triggered_at.astimezone(timezone.utc)
|
|
94
|
-
python_version = f
|
|
102
|
+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
95
103
|
|
|
96
104
|
# when the recce is running in automation use cases
|
|
97
105
|
# replace the user id with project_id to avoid so many unique user id
|
|
@@ -102,6 +110,7 @@ class Collector:
|
|
|
102
110
|
python_version=python_version,
|
|
103
111
|
is_ci=self._is_ci,
|
|
104
112
|
is_github_codespace=self._is_github_codespace,
|
|
113
|
+
is_recce_cloud_instance=self._is_recce_cloud_instance,
|
|
105
114
|
)
|
|
106
115
|
|
|
107
116
|
if user_properties is not None:
|
|
@@ -120,17 +129,18 @@ class Collector:
|
|
|
120
129
|
if not os.path.exists(user_home):
|
|
121
130
|
os.makedirs(user_home, exist_ok=True)
|
|
122
131
|
if not os.path.exists(self._unsend_events_file):
|
|
123
|
-
with portalocker.Lock(self._unsend_events_file,
|
|
124
|
-
f.write(json.dumps({
|
|
132
|
+
with portalocker.Lock(self._unsend_events_file, "w+", timeout=5) as f:
|
|
133
|
+
f.write(json.dumps({"unsend_events": []}))
|
|
125
134
|
|
|
126
135
|
def _is_full(self):
|
|
127
|
-
with portalocker.Lock(self._unsend_events_file,
|
|
136
|
+
with portalocker.Lock(self._unsend_events_file, "r+", timeout=5) as f:
|
|
128
137
|
o = json.loads(f.read())
|
|
129
|
-
return len(o.get(
|
|
138
|
+
return len(o.get("unsend_events", [])) >= self._upload_threshold
|
|
130
139
|
|
|
131
140
|
@contextmanager
|
|
132
141
|
def load_json(self):
|
|
133
|
-
with portalocker.Lock(self._unsend_events_file,
|
|
142
|
+
with portalocker.Lock(self._unsend_events_file, "r+", timeout=5) as f:
|
|
143
|
+
o = None
|
|
134
144
|
try:
|
|
135
145
|
o = json.loads(f.read())
|
|
136
146
|
yield o
|
|
@@ -140,15 +150,16 @@ class Collector:
|
|
|
140
150
|
finally:
|
|
141
151
|
f.seek(0)
|
|
142
152
|
f.truncate()
|
|
143
|
-
|
|
153
|
+
if o is not None:
|
|
154
|
+
f.write(json.dumps(o))
|
|
144
155
|
|
|
145
156
|
def send_events(self):
|
|
146
157
|
with self.load_json() as o:
|
|
147
158
|
payload = dict(
|
|
148
159
|
api_key=self._api_key,
|
|
149
|
-
events=o[
|
|
160
|
+
events=o["unsend_events"],
|
|
150
161
|
)
|
|
151
|
-
o[
|
|
162
|
+
o["unsend_events"] = []
|
|
152
163
|
try:
|
|
153
164
|
requests.post(self._api_endpoint, json=payload)
|
|
154
165
|
except Exception:
|
|
@@ -157,17 +168,17 @@ class Collector:
|
|
|
157
168
|
|
|
158
169
|
def _store_to_file(self, event):
|
|
159
170
|
with self.load_json() as o:
|
|
160
|
-
events = o.get(
|
|
171
|
+
events = o.get("unsend_events", None)
|
|
161
172
|
if events is None:
|
|
162
|
-
o[
|
|
173
|
+
o["unsend_events"] = []
|
|
163
174
|
|
|
164
|
-
o[
|
|
175
|
+
o["unsend_events"].append(event)
|
|
165
176
|
|
|
166
177
|
def _cleanup_unsend_events(self):
|
|
167
178
|
with self.load_json() as o:
|
|
168
|
-
events = o.get(
|
|
179
|
+
events = o.get("unsend_events", None)
|
|
169
180
|
if events is None:
|
|
170
|
-
o[
|
|
181
|
+
o["unsend_events"] = []
|
|
171
182
|
|
|
172
|
-
while len(o[
|
|
173
|
-
o[
|
|
183
|
+
while len(o["unsend_events"]) > self._delete_threshold:
|
|
184
|
+
o["unsend_events"].pop(0)
|
recce/event/track.py
CHANGED
|
@@ -18,8 +18,8 @@ from recce.git import current_branch, hosting_repo
|
|
|
18
18
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
21
|
-
_enable_traceback: bool = os.environ.get(
|
|
22
|
-
logger = logging.getLogger(
|
|
21
|
+
_enable_traceback: bool = os.environ.get("RECCE_PRINT_TRACEBACK") == "1"
|
|
22
|
+
logger = logging.getLogger("uvicorn")
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class TrackCommand(Command):
|
|
@@ -39,11 +39,23 @@ class TrackCommand(Command):
|
|
|
39
39
|
deprecated: bool = False,
|
|
40
40
|
beta: bool = False,
|
|
41
41
|
) -> None:
|
|
42
|
-
super(TrackCommand, self).__init__(
|
|
43
|
-
|
|
42
|
+
super(TrackCommand, self).__init__(
|
|
43
|
+
name,
|
|
44
|
+
context_settings,
|
|
45
|
+
callback,
|
|
46
|
+
params,
|
|
47
|
+
help,
|
|
48
|
+
epilog,
|
|
49
|
+
short_help,
|
|
50
|
+
options_metavar,
|
|
51
|
+
add_help_option,
|
|
52
|
+
no_args_is_help,
|
|
53
|
+
hidden,
|
|
54
|
+
deprecated,
|
|
55
|
+
)
|
|
44
56
|
|
|
45
57
|
def _show_error_message(self, msg, params):
|
|
46
|
-
if params.get(
|
|
58
|
+
if params.get("debug"):
|
|
47
59
|
console.print_exception(show_locals=True)
|
|
48
60
|
else:
|
|
49
61
|
print(traceback.format_exc())
|
|
@@ -51,39 +63,39 @@ class TrackCommand(Command):
|
|
|
51
63
|
# console.out(msg, highlight=False)
|
|
52
64
|
|
|
53
65
|
def _show_hint_message(self, hint):
|
|
54
|
-
console.print(f
|
|
66
|
+
console.print(f"[bold yellow]Hint[/bold yellow]:\n {escape(hint)}")
|
|
55
67
|
|
|
56
68
|
def invoke(self, ctx: Context) -> t.Any:
|
|
57
69
|
status = False
|
|
58
70
|
start_time = time.time()
|
|
59
|
-
reason =
|
|
60
|
-
event.set_exception_tag(
|
|
71
|
+
reason = "error"
|
|
72
|
+
event.set_exception_tag("command", ctx.command.name)
|
|
61
73
|
event.log_codespaces_events(ctx.command.name)
|
|
62
74
|
|
|
63
75
|
try:
|
|
64
76
|
ret = super(TrackCommand, self).invoke(ctx)
|
|
65
77
|
if ret is None or ret == 0:
|
|
66
78
|
status = True
|
|
67
|
-
reason =
|
|
79
|
+
reason = "ok"
|
|
68
80
|
else:
|
|
69
|
-
reason =
|
|
81
|
+
reason = "error"
|
|
70
82
|
sys.exit(ret)
|
|
71
83
|
return ret
|
|
72
84
|
except RecceException as e:
|
|
73
85
|
logger.debug(traceback.format_exc())
|
|
74
86
|
console.log("[Error] " + str(e))
|
|
75
|
-
reason =
|
|
87
|
+
reason = "error"
|
|
76
88
|
sys.exit(1)
|
|
77
89
|
except SystemExit as e:
|
|
78
|
-
reason =
|
|
90
|
+
reason = "error"
|
|
79
91
|
raise e
|
|
80
92
|
except KeyboardInterrupt as e:
|
|
81
|
-
reason =
|
|
93
|
+
reason = "aborted"
|
|
82
94
|
raise e
|
|
83
95
|
except Exception as e:
|
|
84
96
|
self._show_error_message(str(e), ctx.params)
|
|
85
97
|
event.capture_exception(e)
|
|
86
|
-
reason =
|
|
98
|
+
reason = "fatal"
|
|
87
99
|
event.flush_exceptions()
|
|
88
100
|
sys.exit(1)
|
|
89
101
|
finally:
|
|
@@ -93,32 +105,32 @@ class TrackCommand(Command):
|
|
|
93
105
|
branch = current_branch()
|
|
94
106
|
command = ctx.command.name
|
|
95
107
|
duration = end_time - start_time
|
|
96
|
-
target_path = ctx.params.get(
|
|
97
|
-
target_base_path = ctx.params.get(
|
|
108
|
+
target_path = ctx.params.get("target_path", None)
|
|
109
|
+
target_base_path = ctx.params.get("target_base_path", None)
|
|
98
110
|
props = dict(
|
|
99
111
|
command=command,
|
|
100
112
|
status=status,
|
|
101
113
|
reason=reason,
|
|
102
114
|
duration=duration,
|
|
103
|
-
cloud=ctx.params.get(
|
|
104
|
-
review=ctx.params.get(
|
|
105
|
-
debug=ctx.params.get(
|
|
115
|
+
cloud=ctx.params.get("cloud", False),
|
|
116
|
+
review=ctx.params.get("review", False),
|
|
117
|
+
debug=ctx.params.get("debug", False),
|
|
106
118
|
)
|
|
107
119
|
|
|
108
120
|
if runner is not None:
|
|
109
|
-
props[
|
|
121
|
+
props["runner_type"] = runner
|
|
110
122
|
|
|
111
123
|
if repo is not None:
|
|
112
|
-
props[
|
|
124
|
+
props["repository"] = sha256(repo.encode()).hexdigest()
|
|
113
125
|
|
|
114
126
|
if branch is not None:
|
|
115
|
-
props[
|
|
127
|
+
props["branch"] = sha256(branch.encode()).hexdigest()
|
|
116
128
|
|
|
117
129
|
if target_path is not None:
|
|
118
|
-
props[
|
|
130
|
+
props["target_path"] = sha256(target_path.encode()).hexdigest()
|
|
119
131
|
|
|
120
132
|
if target_base_path is not None:
|
|
121
|
-
props[
|
|
133
|
+
props["target_base_path"] = sha256(target_base_path.encode()).hexdigest()
|
|
122
134
|
|
|
123
135
|
try:
|
|
124
136
|
recce_context = load_context()
|
|
@@ -128,9 +140,19 @@ class TrackCommand(Command):
|
|
|
128
140
|
|
|
129
141
|
if recce_context is not None:
|
|
130
142
|
if recce_context.adapter_type == "dbt":
|
|
131
|
-
props[
|
|
143
|
+
props["adapter_type"] = "DBT"
|
|
144
|
+
# Add dbt warehouse type only for dbt adapter
|
|
145
|
+
try:
|
|
146
|
+
from recce.adapter.dbt_adapter import DbtAdapter
|
|
147
|
+
|
|
148
|
+
dbt_adapter: DbtAdapter = recce_context.adapter
|
|
149
|
+
warehouse_type = dbt_adapter.adapter.type()
|
|
150
|
+
props["warehouse_type"] = warehouse_type
|
|
151
|
+
except Exception:
|
|
152
|
+
# If we can't get the warehouse type, skip it
|
|
153
|
+
pass
|
|
132
154
|
elif recce_context.adapter_type == "sqlmesh":
|
|
133
|
-
props[
|
|
155
|
+
props["adapter_type"] = "SQLMesh"
|
|
134
156
|
|
|
135
|
-
event.log_event(props,
|
|
157
|
+
event.log_event(props, "command", params=ctx.params)
|
|
136
158
|
event.flush_events()
|
recce/exceptions.py
CHANGED
recce/git.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from git import
|
|
3
|
+
from git import InvalidGitRepositoryError, Repo
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def current_default_branch():
|
|
7
7
|
try:
|
|
8
8
|
repo = Repo(search_parent_directories=True)
|
|
9
|
-
return repo.remotes.origin.refs[
|
|
9
|
+
return repo.remotes.origin.refs["HEAD"].reference.remote_head
|
|
10
10
|
except Exception:
|
|
11
11
|
return None
|
|
12
12
|
|
|
@@ -53,19 +53,19 @@ def commit_hash_from_branch(branch: str, short_length: int = 7, short: bool = Fa
|
|
|
53
53
|
return None
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def hosting_repo(remote: str =
|
|
56
|
+
def hosting_repo(remote: str = "origin"):
|
|
57
57
|
try:
|
|
58
58
|
repo = Repo(search_parent_directories=True)
|
|
59
59
|
origin_url = repo.remote(name=remote).url
|
|
60
60
|
remote_repo = None
|
|
61
61
|
|
|
62
|
-
if origin_url.startswith(
|
|
62
|
+
if origin_url.startswith("git@"):
|
|
63
63
|
# Handle git@github.com:user/repo.git
|
|
64
|
-
remote_repo = origin_url.split(
|
|
64
|
+
remote_repo = origin_url.split(":")[1].replace(".git", "")
|
|
65
65
|
|
|
66
|
-
elif origin_url.startswith(
|
|
66
|
+
elif origin_url.startswith("https://") or origin_url.startswith("http://"):
|
|
67
67
|
# Handle https://github.com/user/repo.git or http://github.com/user/repo.git
|
|
68
|
-
remote_repo =
|
|
68
|
+
remote_repo = "/".join(origin_url.split("/")[-2:]).replace(".git", "")
|
|
69
69
|
|
|
70
70
|
return remote_repo
|
|
71
71
|
except ValueError:
|