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.
Files changed (53) hide show
  1. {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/METADATA +1 -1
  2. {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/RECORD +13 -52
  3. airbyte_ops_mcp/cli/cloud.py +42 -3
  4. airbyte_ops_mcp/cloud_admin/api_client.py +473 -0
  5. airbyte_ops_mcp/cloud_admin/models.py +56 -0
  6. airbyte_ops_mcp/mcp/cloud_connector_versions.py +460 -0
  7. airbyte_ops_mcp/mcp/prerelease.py +6 -46
  8. airbyte_ops_mcp/regression_tests/ci_output.py +151 -71
  9. airbyte_ops_mcp/regression_tests/http_metrics.py +21 -2
  10. airbyte_ops_mcp/regression_tests/models.py +6 -0
  11. airbyte_ops_mcp/telemetry.py +162 -0
  12. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/.gitignore +0 -1
  13. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/README.md +0 -420
  14. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/__init__.py +0 -2
  15. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/__init__.py +0 -1
  16. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/__init__.py +0 -8
  17. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/base_backend.py +0 -16
  18. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/duckdb_backend.py +0 -87
  19. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/file_backend.py +0 -165
  20. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connection_objects_retrieval.py +0 -377
  21. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connector_runner.py +0 -247
  22. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/errors.py +0 -7
  23. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/evaluation_modes.py +0 -25
  24. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/hacks.py +0 -23
  25. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/json_schema_helper.py +0 -384
  26. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/mitm_addons.py +0 -37
  27. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/models.py +0 -595
  28. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/proxy.py +0 -207
  29. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/secret_access.py +0 -47
  30. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/segment_tracking.py +0 -45
  31. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/utils.py +0 -214
  32. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/conftest.py.disabled +0 -751
  33. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/consts.py +0 -4
  34. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/poetry.lock +0 -4480
  35. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/pytest.ini +0 -9
  36. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/__init__.py +0 -1
  37. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_check.py +0 -61
  38. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_discover.py +0 -117
  39. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_read.py +0 -627
  40. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_spec.py +0 -43
  41. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/report.py +0 -542
  42. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/stash_keys.py +0 -38
  43. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/__init__.py +0 -0
  44. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/private_details.html.j2 +0 -305
  45. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/report.html.j2 +0 -515
  46. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/utils.py +0 -187
  47. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/__init__.py +0 -0
  48. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_check.py +0 -61
  49. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_discover.py +0 -217
  50. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_read.py +0 -177
  51. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_spec.py +0 -631
  52. {airbyte_internal_ops-0.4.1.dist-info → airbyte_internal_ops-0.5.0.dist-info}/WHEEL +0 -0
  53. {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 generate_regression_report(
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 regression test report modeled on legacy HTML reports.
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 structure mirrors the
186
- legacy connector_live_tests HTML report.
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
- run_id = os.getenv("GITHUB_RUN_ID", "")
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
- "# Regression Test Report",
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 | Success |",
268
- "|---------|-----------|---------|",
269
- f"| Control ({control_version}) | {control_result['exit_code']} | {control_result['success']} |",
270
- f"| Target ({target_version}) | {target_result['exit_code']} | {target_result['success']} |",
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
- "## Command Execution Metrics",
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
- "### Message Types",
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
- "### Record Count per Stream",
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
- "### HTTP Metrics",
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
- "### Control",
399
+ "### Summary",
400
+ "",
401
+ f"**Result:** {'PASS' if result['success'] else 'FAIL'}",
348
402
  "",
349
- f"- **Image:** `{control_image}`",
350
- f"- **Exit Code:** {control_result['exit_code']}",
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
- "### Target",
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:** `{target_image}`",
358
- f"- **Exit Code:** {target_result['exit_code']}",
359
- f"- **Success:** {target_result['success']}",
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 the **Regression Test Report** check or in artifact {artifact_link}.
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(flow_count=0, duplicate_flow_count=0, unique_urls=[])
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