airbyte-internal-ops 0.4.1__py3-none-any.whl → 0.5.0__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.
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/METADATA +1 -1
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/RECORD +13 -52
- airbyte_ops_mcp/cli/cloud.py +42 -3
- airbyte_ops_mcp/cloud_admin/api_client.py +473 -0
- airbyte_ops_mcp/cloud_admin/models.py +56 -0
- airbyte_ops_mcp/mcp/cloud_connector_versions.py +460 -0
- airbyte_ops_mcp/mcp/prerelease.py +6 -46
- airbyte_ops_mcp/regression_tests/ci_output.py +151 -71
- airbyte_ops_mcp/regression_tests/http_metrics.py +21 -2
- airbyte_ops_mcp/regression_tests/models.py +6 -0
- airbyte_ops_mcp/telemetry.py +162 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/.gitignore +0 -1
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/README.md +0 -420
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/__init__.py +0 -2
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/__init__.py +0 -1
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/__init__.py +0 -8
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/base_backend.py +0 -16
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/duckdb_backend.py +0 -87
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/file_backend.py +0 -165
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connection_objects_retrieval.py +0 -377
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connector_runner.py +0 -247
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/errors.py +0 -7
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/evaluation_modes.py +0 -25
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/hacks.py +0 -23
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/json_schema_helper.py +0 -384
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/mitm_addons.py +0 -37
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/models.py +0 -595
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/proxy.py +0 -207
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/secret_access.py +0 -47
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/segment_tracking.py +0 -45
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/utils.py +0 -214
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/conftest.py.disabled +0 -751
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/consts.py +0 -4
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/poetry.lock +0 -4480
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/pytest.ini +0 -9
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/__init__.py +0 -1
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_check.py +0 -61
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_discover.py +0 -117
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_read.py +0 -627
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_spec.py +0 -43
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/report.py +0 -542
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/stash_keys.py +0 -38
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/__init__.py +0 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/private_details.html.j2 +0 -305
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/report.html.j2 +0 -515
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/utils.py +0 -187
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/__init__.py +0 -0
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_check.py +0 -61
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_discover.py +0 -217
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_read.py +0 -177
- airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_spec.py +0 -631
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/entry_points.txt +0 -0
|
@@ -171,7 +171,7 @@ def _get_github_artifacts_url() -> str | None:
|
|
|
171
171
|
return f"{run_url}#artifacts"
|
|
172
172
|
|
|
173
173
|
|
|
174
|
-
def
|
|
174
|
+
def generate_action_test_comparison_report(
|
|
175
175
|
target_image: str,
|
|
176
176
|
control_image: str,
|
|
177
177
|
command: str,
|
|
@@ -179,16 +179,17 @@ def generate_regression_report(
|
|
|
179
179
|
control_result: dict[str, Any],
|
|
180
180
|
output_dir: Path,
|
|
181
181
|
) -> Path:
|
|
182
|
-
"""Generate a markdown
|
|
182
|
+
"""Generate a markdown comparison report for a single action (command).
|
|
183
183
|
|
|
184
184
|
This creates a comprehensive report with context, message counts comparison,
|
|
185
|
-
and record counts per stream (for read commands). The
|
|
186
|
-
|
|
185
|
+
and record counts per stream (for read commands). The report starts with an
|
|
186
|
+
L2 header containing the command name, making it easy to consolidate multiple
|
|
187
|
+
command reports into a single document.
|
|
187
188
|
|
|
188
189
|
Args:
|
|
189
190
|
target_image: The target (new version) connector image.
|
|
190
191
|
control_image: The control (baseline version) connector image.
|
|
191
|
-
command: The Airbyte command that was run.
|
|
192
|
+
command: The Airbyte command that was run (e.g., "spec", "check", "discover", "read").
|
|
192
193
|
target_result: Results dict from running target connector.
|
|
193
194
|
control_result: Results dict from running control connector.
|
|
194
195
|
output_dir: Directory to write the report to.
|
|
@@ -204,51 +205,22 @@ def generate_regression_report(
|
|
|
204
205
|
target_record_counts = target_result.get("record_counts_per_stream", {})
|
|
205
206
|
control_record_counts = control_result.get("record_counts_per_stream", {})
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
artifact_name = (
|
|
209
|
-
f"regression-test-artifacts-{run_id}" if run_id else "regression-test-artifacts"
|
|
210
|
-
)
|
|
211
|
-
|
|
208
|
+
# Extract version tags for the summary table
|
|
212
209
|
target_version = (
|
|
213
210
|
target_image.rsplit(":", 1)[-1] if ":" in target_image else "unknown"
|
|
214
211
|
)
|
|
215
212
|
control_version = (
|
|
216
213
|
control_image.rsplit(":", 1)[-1] if ":" in control_image else "unknown"
|
|
217
214
|
)
|
|
218
|
-
connector_name = (
|
|
219
|
-
target_image.rsplit(":", 1)[0] if ":" in target_image else target_image
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
run_url = _get_github_run_url()
|
|
223
|
-
artifacts_url = _get_github_artifacts_url()
|
|
224
215
|
|
|
216
|
+
# Start with L2 header containing the command name (no L1 header)
|
|
217
|
+
# This allows multiple command reports to be concatenated into a single document
|
|
218
|
+
# Note: Context block (connector, versions, workflow links) is added at the workflow level
|
|
225
219
|
lines: list[str] = [
|
|
226
|
-
"
|
|
227
|
-
"",
|
|
228
|
-
"## Context",
|
|
220
|
+
f"## `{command.upper()}` Test Results",
|
|
229
221
|
"",
|
|
230
|
-
f"- **Test Date:** {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
231
|
-
f"- **Connector:** `{connector_name}`",
|
|
232
|
-
f"- **Control Version:** `{control_version}`",
|
|
233
|
-
f"- **Target Version:** `{target_version}`",
|
|
234
|
-
f"- **Command:** `{command.upper()}`",
|
|
235
222
|
]
|
|
236
223
|
|
|
237
|
-
if run_url:
|
|
238
|
-
lines.append(f"- **Workflow Run:** [View Execution]({run_url})")
|
|
239
|
-
if artifacts_url:
|
|
240
|
-
lines.append(f"- **Artifacts:** [Download `{artifact_name}`]({artifacts_url})")
|
|
241
|
-
else:
|
|
242
|
-
lines.append(f"- **Artifacts:** `{artifact_name}`")
|
|
243
|
-
|
|
244
|
-
lines.extend(
|
|
245
|
-
[
|
|
246
|
-
"",
|
|
247
|
-
"## Summary",
|
|
248
|
-
"",
|
|
249
|
-
]
|
|
250
|
-
)
|
|
251
|
-
|
|
252
224
|
if regression_detected:
|
|
253
225
|
if target_result["success"] and not control_result["success"]:
|
|
254
226
|
lines.append("**Result:** Target succeeded, control failed (improvement)")
|
|
@@ -261,20 +233,24 @@ def generate_regression_report(
|
|
|
261
233
|
else:
|
|
262
234
|
lines.append("**Result:** Both versions failed")
|
|
263
235
|
|
|
236
|
+
# Use emojis for better scanability
|
|
237
|
+
control_emoji = "✅" if control_result["success"] else "❌"
|
|
238
|
+
target_emoji = "✅" if target_result["success"] else "❌"
|
|
239
|
+
|
|
264
240
|
lines.extend(
|
|
265
241
|
[
|
|
266
242
|
"",
|
|
267
|
-
"| Version | Exit Code |
|
|
268
|
-
"
|
|
269
|
-
f"| Control ({control_version}) | {control_result['exit_code']} | {
|
|
270
|
-
f"| Target ({target_version}) | {target_result['exit_code']} | {
|
|
243
|
+
"| Version | Exit Code | Result |",
|
|
244
|
+
"|---------|-----------|--------|",
|
|
245
|
+
f"| Control (`{control_version}`) | {control_result['exit_code']} | {control_emoji} |",
|
|
246
|
+
f"| Target (`{target_version}`) | {target_result['exit_code']} | {target_emoji} |",
|
|
271
247
|
"",
|
|
272
248
|
]
|
|
273
249
|
)
|
|
274
250
|
|
|
275
251
|
lines.extend(
|
|
276
252
|
[
|
|
277
|
-
"
|
|
253
|
+
"### Command Execution Metrics",
|
|
278
254
|
"",
|
|
279
255
|
]
|
|
280
256
|
)
|
|
@@ -282,7 +258,7 @@ def generate_regression_report(
|
|
|
282
258
|
if target_counts or control_counts:
|
|
283
259
|
lines.extend(
|
|
284
260
|
[
|
|
285
|
-
"
|
|
261
|
+
"#### Message Types",
|
|
286
262
|
"",
|
|
287
263
|
"| Type | Control | Target | Delta |",
|
|
288
264
|
"|------|---------|--------|-------|",
|
|
@@ -294,14 +270,14 @@ def generate_regression_report(
|
|
|
294
270
|
target_count = target_counts.get(msg_type, 0)
|
|
295
271
|
delta = target_count - control_count
|
|
296
272
|
lines.append(
|
|
297
|
-
f"| {msg_type} | {control_count} | {target_count} | {_format_delta(delta)} |"
|
|
273
|
+
f"| `{msg_type}` | {control_count} | {target_count} | {_format_delta(delta)} |"
|
|
298
274
|
)
|
|
299
275
|
lines.append("")
|
|
300
276
|
|
|
301
277
|
if target_record_counts or control_record_counts:
|
|
302
278
|
lines.extend(
|
|
303
279
|
[
|
|
304
|
-
"
|
|
280
|
+
"#### Record Count per Stream",
|
|
305
281
|
"",
|
|
306
282
|
"| Stream | Control | Target | Delta |",
|
|
307
283
|
"|--------|---------|--------|-------|",
|
|
@@ -330,35 +306,144 @@ def generate_regression_report(
|
|
|
330
306
|
if control_http or target_http:
|
|
331
307
|
lines.extend(
|
|
332
308
|
[
|
|
333
|
-
"
|
|
309
|
+
"#### HTTP Metrics",
|
|
334
310
|
"",
|
|
335
|
-
"| Version | Flow Count | Duplicate Flows |",
|
|
336
|
-
"
|
|
337
|
-
f"| Control | {control_http.get('flow_count', 0)} | {control_http.get('duplicate_flow_count', 0)} |",
|
|
338
|
-
f"| Target | {target_http.get('flow_count', 0)} | {target_http.get('duplicate_flow_count', 0)} |",
|
|
311
|
+
"| Version | Flow Count | Duplicate Flows | Cache Hit Ratio |",
|
|
312
|
+
"|---------|------------|-----------------|-----------------|",
|
|
313
|
+
f"| Control | {control_http.get('flow_count', 0)} | {control_http.get('duplicate_flow_count', 0)} | {control_http.get('cache_hit_ratio', 'N/A')} |",
|
|
314
|
+
f"| Target | {target_http.get('flow_count', 0)} | {target_http.get('duplicate_flow_count', 0)} | {target_http.get('cache_hit_ratio', 'N/A')} |",
|
|
339
315
|
"",
|
|
340
316
|
]
|
|
341
317
|
)
|
|
342
318
|
|
|
319
|
+
# Note: Execution Details section removed as redundant with Summary table
|
|
320
|
+
|
|
321
|
+
report_content = "\n".join(lines)
|
|
322
|
+
report_path = output_dir / "report.md"
|
|
323
|
+
report_path.write_text(report_content)
|
|
324
|
+
|
|
325
|
+
return report_path
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Backwards-compatible alias for the old function name
|
|
329
|
+
generate_regression_report = generate_action_test_comparison_report
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def generate_single_version_report(
|
|
333
|
+
connector_image: str,
|
|
334
|
+
command: str,
|
|
335
|
+
result: dict[str, Any],
|
|
336
|
+
output_dir: Path,
|
|
337
|
+
) -> Path:
|
|
338
|
+
"""Generate a markdown report for a single-version regression test.
|
|
339
|
+
|
|
340
|
+
This creates a report with message counts and record counts per stream for a single
|
|
341
|
+
connector run. The report starts with an L2 header containing the command name,
|
|
342
|
+
making it easy to consolidate multiple command reports.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
connector_image: The connector image that was tested.
|
|
346
|
+
command: The Airbyte command that was run (e.g., "spec", "check", "discover", "read").
|
|
347
|
+
result: Results dict from running the connector.
|
|
348
|
+
output_dir: Directory to write the report to.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Path to the generated report.md file.
|
|
352
|
+
"""
|
|
353
|
+
message_counts = result.get("message_counts", {})
|
|
354
|
+
record_counts = result.get("record_counts_per_stream", {})
|
|
355
|
+
|
|
356
|
+
run_id = os.getenv("GITHUB_RUN_ID", "")
|
|
357
|
+
artifact_name = (
|
|
358
|
+
f"regression-test-artifacts-{command}-{run_id}"
|
|
359
|
+
if run_id
|
|
360
|
+
else f"regression-test-artifacts-{command}"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
version = (
|
|
364
|
+
connector_image.rsplit(":", 1)[-1] if ":" in connector_image else "unknown"
|
|
365
|
+
)
|
|
366
|
+
connector_name = (
|
|
367
|
+
connector_image.rsplit(":", 1)[0] if ":" in connector_image else connector_image
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
run_url = _get_github_run_url()
|
|
371
|
+
artifacts_url = _get_github_artifacts_url()
|
|
372
|
+
|
|
373
|
+
# Get tester identity from environment (GitHub Actions sets GITHUB_ACTOR)
|
|
374
|
+
tester = os.getenv("GITHUB_ACTOR") or os.getenv("USER") or "unknown"
|
|
375
|
+
|
|
376
|
+
# Start with L2 header containing the command name (no L1 header)
|
|
377
|
+
lines: list[str] = [
|
|
378
|
+
f"## `{command.upper()}` Test Results",
|
|
379
|
+
"",
|
|
380
|
+
"### Context",
|
|
381
|
+
"",
|
|
382
|
+
f"- **Test Date:** {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
383
|
+
f"- **Tester:** `{tester}`",
|
|
384
|
+
f"- **Connector:** `{connector_name}`",
|
|
385
|
+
f"- **Version:** `{version}`",
|
|
386
|
+
f"- **Command:** `{command.upper()}`",
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
if run_url:
|
|
390
|
+
lines.append(f"- **Workflow Run:** [View Execution]({run_url})")
|
|
391
|
+
if artifacts_url:
|
|
392
|
+
lines.append(f"- **Artifacts:** [Download `{artifact_name}`]({artifacts_url})")
|
|
393
|
+
else:
|
|
394
|
+
lines.append(f"- **Artifacts:** `{artifact_name}`")
|
|
395
|
+
|
|
343
396
|
lines.extend(
|
|
344
397
|
[
|
|
345
|
-
"## Execution Details",
|
|
346
398
|
"",
|
|
347
|
-
"###
|
|
399
|
+
"### Summary",
|
|
400
|
+
"",
|
|
401
|
+
f"**Result:** {'PASS' if result['success'] else 'FAIL'}",
|
|
348
402
|
"",
|
|
349
|
-
f"- **
|
|
350
|
-
f"- **
|
|
351
|
-
f"- **Success:** {control_result['success']}",
|
|
352
|
-
f"- **Stdout:** `{control_result.get('stdout_file', 'N/A')}`",
|
|
353
|
-
f"- **Stderr:** `{control_result.get('stderr_file', 'N/A')}`",
|
|
403
|
+
f"- **Exit Code:** {result['exit_code']}",
|
|
404
|
+
f"- **Success:** {result['success']}",
|
|
354
405
|
"",
|
|
355
|
-
|
|
406
|
+
]
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if message_counts:
|
|
410
|
+
lines.extend(
|
|
411
|
+
[
|
|
412
|
+
"### Message Types",
|
|
413
|
+
"",
|
|
414
|
+
"| Type | Count |",
|
|
415
|
+
"|------|-------|",
|
|
416
|
+
]
|
|
417
|
+
)
|
|
418
|
+
for msg_type in sorted(message_counts.keys()):
|
|
419
|
+
count = message_counts[msg_type]
|
|
420
|
+
lines.append(f"| `{msg_type}` | {count} |")
|
|
421
|
+
lines.append("")
|
|
422
|
+
|
|
423
|
+
if record_counts:
|
|
424
|
+
lines.extend(
|
|
425
|
+
[
|
|
426
|
+
"### Record Count per Stream",
|
|
427
|
+
"",
|
|
428
|
+
"| Stream | Count |",
|
|
429
|
+
"|--------|-------|",
|
|
430
|
+
]
|
|
431
|
+
)
|
|
432
|
+
total = 0
|
|
433
|
+
for stream in sorted(record_counts.keys()):
|
|
434
|
+
count = record_counts[stream]
|
|
435
|
+
total += count
|
|
436
|
+
lines.append(f"| {stream} | {count} |")
|
|
437
|
+
lines.append(f"| **Total** | **{total}** |")
|
|
438
|
+
lines.append("")
|
|
439
|
+
|
|
440
|
+
lines.extend(
|
|
441
|
+
[
|
|
442
|
+
"### Execution Details",
|
|
356
443
|
"",
|
|
357
|
-
f"- **Image:** `{
|
|
358
|
-
f"- **
|
|
359
|
-
f"- **
|
|
360
|
-
f"- **Stdout:** `{target_result.get('stdout_file', 'N/A')}`",
|
|
361
|
-
f"- **Stderr:** `{target_result.get('stderr_file', 'N/A')}`",
|
|
444
|
+
f"- **Image:** `{connector_image}`",
|
|
445
|
+
f"- **Stdout:** `{result.get('stdout_file', 'N/A')}`",
|
|
446
|
+
f"- **Stderr:** `{result.get('stderr_file', 'N/A')}`",
|
|
362
447
|
"",
|
|
363
448
|
]
|
|
364
449
|
)
|
|
@@ -373,9 +458,6 @@ def generate_regression_report(
|
|
|
373
458
|
def get_report_summary(report_path: Path) -> str:
|
|
374
459
|
"""Get a brief summary pointing to the full report.
|
|
375
460
|
|
|
376
|
-
Args:
|
|
377
|
-
report_path: Path to the full report.md file.
|
|
378
|
-
|
|
379
461
|
Returns:
|
|
380
462
|
Brief markdown summary for GITHUB_STEP_SUMMARY.
|
|
381
463
|
"""
|
|
@@ -393,7 +475,5 @@ def get_report_summary(report_path: Path) -> str:
|
|
|
393
475
|
|
|
394
476
|
return f"""## Regression Test Report
|
|
395
477
|
|
|
396
|
-
Full report available in
|
|
397
|
-
|
|
398
|
-
See the Checks tab for the complete report with message counts and execution details.
|
|
478
|
+
Full report available in artifact {artifact_link}.
|
|
399
479
|
"""
|
|
@@ -47,11 +47,21 @@ class HttpMetrics:
|
|
|
47
47
|
flow_count: int
|
|
48
48
|
duplicate_flow_count: int
|
|
49
49
|
unique_urls: list[str]
|
|
50
|
+
cache_hits_count: int = 0
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def cache_hit_ratio(self) -> str:
|
|
54
|
+
"""Calculate cache hit ratio as a percentage string."""
|
|
55
|
+
if self.flow_count == 0:
|
|
56
|
+
return "N/A"
|
|
57
|
+
return f"{(self.cache_hits_count / self.flow_count) * 100:.2f}%"
|
|
50
58
|
|
|
51
59
|
@classmethod
|
|
52
60
|
def empty(cls) -> HttpMetrics:
|
|
53
61
|
"""Create empty metrics when HTTP capture is unavailable."""
|
|
54
|
-
return cls(
|
|
62
|
+
return cls(
|
|
63
|
+
flow_count=0, duplicate_flow_count=0, unique_urls=[], cache_hits_count=0
|
|
64
|
+
)
|
|
55
65
|
|
|
56
66
|
|
|
57
67
|
@dataclass
|
|
@@ -285,17 +295,22 @@ def parse_http_dump(dump_file_path: Path) -> HttpMetrics:
|
|
|
285
295
|
unique_urls = list(set(all_urls))
|
|
286
296
|
duplicate_count = len(all_urls) - len(unique_urls)
|
|
287
297
|
|
|
298
|
+
# Cache hits are interpreted as duplicate requests to the same URL
|
|
299
|
+
# (requests that could potentially be served from cache)
|
|
300
|
+
cache_hits = duplicate_count
|
|
301
|
+
|
|
288
302
|
return HttpMetrics(
|
|
289
303
|
flow_count=len(flows),
|
|
290
304
|
duplicate_flow_count=duplicate_count,
|
|
291
305
|
unique_urls=sorted(unique_urls),
|
|
306
|
+
cache_hits_count=cache_hits,
|
|
292
307
|
)
|
|
293
308
|
|
|
294
309
|
|
|
295
310
|
def compute_http_metrics_comparison(
|
|
296
311
|
control_metrics: HttpMetrics,
|
|
297
312
|
target_metrics: HttpMetrics,
|
|
298
|
-
) -> dict[str, dict[str, int | str] | int]:
|
|
313
|
+
) -> dict[str, dict[str, int | str] | int | str]:
|
|
299
314
|
"""Compute HTTP metrics comparison between control and target.
|
|
300
315
|
|
|
301
316
|
This produces output in the same format as the legacy
|
|
@@ -312,10 +327,14 @@ def compute_http_metrics_comparison(
|
|
|
312
327
|
"control": {
|
|
313
328
|
"flow_count": control_metrics.flow_count,
|
|
314
329
|
"duplicate_flow_count": control_metrics.duplicate_flow_count,
|
|
330
|
+
"cache_hits_count": control_metrics.cache_hits_count,
|
|
331
|
+
"cache_hit_ratio": control_metrics.cache_hit_ratio,
|
|
315
332
|
},
|
|
316
333
|
"target": {
|
|
317
334
|
"flow_count": target_metrics.flow_count,
|
|
318
335
|
"duplicate_flow_count": target_metrics.duplicate_flow_count,
|
|
336
|
+
"cache_hits_count": target_metrics.cache_hits_count,
|
|
337
|
+
"cache_hit_ratio": target_metrics.cache_hit_ratio,
|
|
319
338
|
},
|
|
320
339
|
"difference": target_metrics.flow_count - control_metrics.flow_count,
|
|
321
340
|
}
|
|
@@ -256,4 +256,10 @@ class ExecutionResult:
|
|
|
256
256
|
file_path = airbyte_messages_dir / f"{type_name}.jsonl"
|
|
257
257
|
file_path.write_text("\n".join(messages))
|
|
258
258
|
|
|
259
|
+
# Save configured catalog (input) if available
|
|
260
|
+
if self.configured_catalog is not None:
|
|
261
|
+
catalog_path = output_dir / "configured_catalog.json"
|
|
262
|
+
catalog_path.write_text(self.configured_catalog.json(indent=2))
|
|
263
|
+
self.logger.info(f"Saved configured catalog to {catalog_path}")
|
|
264
|
+
|
|
259
265
|
self.logger.info(f"Artifacts saved to {output_dir}")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
+
"""Telemetry module for tracking usage analytics.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for tracking usage of various Airbyte operations
|
|
5
|
+
using Segment analytics. The tracking is optional and can be disabled via
|
|
6
|
+
environment variables.
|
|
7
|
+
|
|
8
|
+
Based on the legacy connector_live_tests/commons/segment_tracking.py implementation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from segment import analytics # type: ignore[import-untyped]
|
|
19
|
+
|
|
20
|
+
SEGMENT_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
analytics = None # type: ignore[assignment]
|
|
23
|
+
SEGMENT_AVAILABLE = False
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Environment variable to disable tracking
|
|
28
|
+
DISABLE_TRACKING_ENV_VAR = "AIRBYTE_DISABLE_TELEMETRY"
|
|
29
|
+
# Legacy env var for backward compatibility
|
|
30
|
+
LEGACY_DISABLE_TRACKING_ENV_VAR = "REGRESSION_TEST_DISABLE_TRACKING"
|
|
31
|
+
# Environment variable to enable debug mode
|
|
32
|
+
DEBUG_SEGMENT_ENV_VAR = "DEBUG_SEGMENT"
|
|
33
|
+
|
|
34
|
+
# Segment write key environment variable name
|
|
35
|
+
# The write key can be provided via environment variable or uses the default
|
|
36
|
+
# public key for the Airbyte analytics project. Segment write keys are designed
|
|
37
|
+
# to be embedded in client-side code for analytics tracking.
|
|
38
|
+
SEGMENT_WRITE_KEY_ENV_VAR = "SEGMENT_WRITE_KEY"
|
|
39
|
+
_DEFAULT_SEGMENT_WRITE_KEY = "hnWfMdEtXNKBjvmJ258F72wShsLmcsZ8"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_tracking_enabled() -> bool:
|
|
43
|
+
"""Check if tracking is enabled based on environment variables."""
|
|
44
|
+
if os.getenv(DISABLE_TRACKING_ENV_VAR) is not None:
|
|
45
|
+
return False
|
|
46
|
+
return os.getenv(LEGACY_DISABLE_TRACKING_ENV_VAR) is None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _on_error(error: Exception, items: Any) -> None:
|
|
50
|
+
"""Handle Segment tracking errors."""
|
|
51
|
+
logger.warning("An error occurred in Segment Tracking", exc_info=error)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _initialize_analytics() -> bool:
|
|
55
|
+
"""Initialize Segment analytics if available and enabled.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if analytics was initialized successfully, False otherwise.
|
|
59
|
+
"""
|
|
60
|
+
if not SEGMENT_AVAILABLE:
|
|
61
|
+
logger.debug("Segment analytics not available (package not installed)")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
if not _is_tracking_enabled():
|
|
65
|
+
logger.debug("Telemetry tracking is disabled via environment variable")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Use environment variable if set, otherwise use default public key
|
|
69
|
+
write_key = os.getenv(SEGMENT_WRITE_KEY_ENV_VAR, _DEFAULT_SEGMENT_WRITE_KEY)
|
|
70
|
+
analytics.write_key = write_key
|
|
71
|
+
analytics.send = True
|
|
72
|
+
analytics.debug = os.getenv(DEBUG_SEGMENT_ENV_VAR) is not None
|
|
73
|
+
analytics.on_error = _on_error
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def track_regression_test(
|
|
78
|
+
user_id: str | None,
|
|
79
|
+
connector_image: str,
|
|
80
|
+
command: str,
|
|
81
|
+
target_version: str,
|
|
82
|
+
control_version: str | None = None,
|
|
83
|
+
additional_properties: dict[str, Any] | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Track a regression test execution.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
user_id: The user ID to associate with the event. If None, uses "airbyte-ci".
|
|
89
|
+
connector_image: The connector image being tested.
|
|
90
|
+
command: The Airbyte command being run (spec, check, discover, read).
|
|
91
|
+
target_version: The target connector version being tested.
|
|
92
|
+
control_version: The control connector version (for comparison mode).
|
|
93
|
+
additional_properties: Additional properties to include in the event.
|
|
94
|
+
"""
|
|
95
|
+
if not _initialize_analytics():
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if not user_id:
|
|
99
|
+
user_id = "airbyte-ci"
|
|
100
|
+
|
|
101
|
+
analytics.identify(user_id)
|
|
102
|
+
|
|
103
|
+
properties: dict[str, Any] = {
|
|
104
|
+
"connector_image": connector_image,
|
|
105
|
+
"command": command,
|
|
106
|
+
"target_version": target_version,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if control_version:
|
|
110
|
+
properties["control_version"] = control_version
|
|
111
|
+
properties["test_mode"] = "comparison"
|
|
112
|
+
else:
|
|
113
|
+
properties["test_mode"] = "single_version"
|
|
114
|
+
|
|
115
|
+
if additional_properties:
|
|
116
|
+
properties.update(additional_properties)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
from importlib.metadata import version
|
|
120
|
+
|
|
121
|
+
properties["package_version"] = version("airbyte-ops-mcp")
|
|
122
|
+
except Exception:
|
|
123
|
+
properties["package_version"] = "unknown"
|
|
124
|
+
|
|
125
|
+
analytics.track(user_id, "regression_test_start", properties)
|
|
126
|
+
logger.debug(f"Tracked regression_test_start event for user {user_id}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def track_event(
|
|
130
|
+
user_id: str | None,
|
|
131
|
+
event_name: str,
|
|
132
|
+
properties: dict[str, Any] | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Track a generic event.
|
|
135
|
+
|
|
136
|
+
This is a general-purpose tracking function for events that don't fit
|
|
137
|
+
into the more specific tracking functions.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
user_id: The user ID to associate with the event. If None, uses "airbyte-ci".
|
|
141
|
+
event_name: The name of the event to track.
|
|
142
|
+
properties: Properties to include in the event.
|
|
143
|
+
"""
|
|
144
|
+
if not _initialize_analytics():
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if not user_id:
|
|
148
|
+
user_id = "airbyte-ci"
|
|
149
|
+
|
|
150
|
+
analytics.identify(user_id)
|
|
151
|
+
|
|
152
|
+
event_properties = properties or {}
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
from importlib.metadata import version
|
|
156
|
+
|
|
157
|
+
event_properties["package_version"] = version("airbyte-ops-mcp")
|
|
158
|
+
except Exception:
|
|
159
|
+
event_properties["package_version"] = "unknown"
|
|
160
|
+
|
|
161
|
+
analytics.track(user_id, event_name, event_properties)
|
|
162
|
+
logger.debug(f"Tracked {event_name} event for user {user_id}")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
regression_tests_artifacts
|