vibesurf 0.1.31__py3-none-any.whl → 0.1.33__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 (35) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +1 -1
  3. vibe_surf/agents/prompts/vibe_surf_prompt.py +6 -0
  4. vibe_surf/agents/report_writer_agent.py +50 -0
  5. vibe_surf/agents/vibe_surf_agent.py +56 -1
  6. vibe_surf/backend/api/composio.py +952 -0
  7. vibe_surf/backend/database/migrations/v005_add_composio_integration.sql +33 -0
  8. vibe_surf/backend/database/migrations/v006_add_credentials_table.sql +26 -0
  9. vibe_surf/backend/database/models.py +53 -1
  10. vibe_surf/backend/database/queries.py +312 -2
  11. vibe_surf/backend/main.py +28 -0
  12. vibe_surf/backend/shared_state.py +123 -9
  13. vibe_surf/chrome_extension/scripts/api-client.js +32 -0
  14. vibe_surf/chrome_extension/scripts/settings-manager.js +954 -1
  15. vibe_surf/chrome_extension/sidepanel.html +190 -0
  16. vibe_surf/chrome_extension/styles/settings-integrations.css +927 -0
  17. vibe_surf/chrome_extension/styles/settings-modal.css +7 -3
  18. vibe_surf/chrome_extension/styles/settings-responsive.css +37 -5
  19. vibe_surf/cli.py +98 -3
  20. vibe_surf/telemetry/__init__.py +60 -0
  21. vibe_surf/telemetry/service.py +112 -0
  22. vibe_surf/telemetry/views.py +156 -0
  23. vibe_surf/tools/browser_use_tools.py +90 -90
  24. vibe_surf/tools/composio_client.py +456 -0
  25. vibe_surf/tools/mcp_client.py +21 -2
  26. vibe_surf/tools/vibesurf_tools.py +290 -87
  27. vibe_surf/tools/views.py +16 -0
  28. vibe_surf/tools/website_api/youtube/client.py +35 -13
  29. vibe_surf/utils.py +13 -0
  30. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/METADATA +11 -9
  31. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/RECORD +35 -26
  32. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/WHEEL +0 -0
  33. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/entry_points.txt +0 -0
  34. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/licenses/LICENSE +0 -0
  35. {vibesurf-0.1.31.dist-info → vibesurf-0.1.33.dist-info}/top_level.txt +0 -0
@@ -58,12 +58,12 @@
58
58
  display: flex;
59
59
  background: var(--bg-secondary);
60
60
  border-bottom: 1px solid var(--border-color);
61
- overflow-x: auto;
61
+ overflow: visible;
62
62
  }
63
63
 
64
64
  .settings-tab {
65
65
  flex: 1;
66
- padding: 16px 24px;
66
+ padding: 16px 8px;
67
67
  border: none;
68
68
  background: transparent;
69
69
  color: var(--text-secondary);
@@ -73,7 +73,11 @@
73
73
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
74
74
  position: relative;
75
75
  white-space: nowrap;
76
- min-width: 150px;
76
+ min-width: 0;
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ text-align: center;
77
81
  }
78
82
 
79
83
  .settings-tab::before {
@@ -17,9 +17,9 @@
17
17
  }
18
18
 
19
19
  .settings-tab {
20
- padding: 14px 20px;
20
+ padding: 14px 6px;
21
21
  font-size: 14px;
22
- min-width: 120px;
22
+ min-width: 0;
23
23
  }
24
24
 
25
25
  .settings-tab-content {
@@ -80,9 +80,41 @@
80
80
  }
81
81
 
82
82
  .settings-tab {
83
- padding: 12px 16px;
84
- font-size: 13px;
85
- min-width: 100px;
83
+ padding: 12px 4px;
84
+ font-size: 12px;
85
+ min-width: 0;
86
+ writing-mode: horizontal-tb;
87
+ }
88
+
89
+ /* Make tabs even more compact for very narrow screens */
90
+ @media (max-width: 400px) {
91
+ .settings-tab {
92
+ padding: 10px 2px;
93
+ font-size: 11px;
94
+ line-height: 1.2;
95
+ }
96
+
97
+ .settings-tab svg {
98
+ display: none; /* Hide icons on very small screens */
99
+ }
100
+ }
101
+
102
+ /* Ultra narrow screens - consider vertical text */
103
+ @media (max-width: 350px) {
104
+ .settings-tabs {
105
+ flex-wrap: wrap;
106
+ height: auto;
107
+ }
108
+
109
+ .settings-tab {
110
+ writing-mode: vertical-rl;
111
+ text-orientation: mixed;
112
+ padding: 8px 4px;
113
+ min-height: 60px;
114
+ width: calc(20% - 2px);
115
+ font-size: 10px;
116
+ line-height: 1.1;
117
+ }
86
118
  }
87
119
 
88
120
  .settings-tab-content {
vibe_surf/cli.py CHANGED
@@ -10,6 +10,7 @@ import glob
10
10
  import json
11
11
  import socket
12
12
  import platform
13
+ import time
13
14
  import importlib.util
14
15
  from pathlib import Path
15
16
  from typing import Optional
@@ -39,6 +40,9 @@ console = Console()
39
40
 
40
41
  # Add logger import for the workspace directory logging
41
42
  from vibe_surf.logger import get_logger
43
+ from vibe_surf.telemetry.service import ProductTelemetry
44
+ from vibe_surf.telemetry.views import CLITelemetryEvent
45
+
42
46
  logger = get_logger(__name__)
43
47
 
44
48
 
@@ -118,6 +122,35 @@ def find_edge_browser() -> Optional[str]:
118
122
  return _find_browser_from_patterns(patterns)
119
123
 
120
124
 
125
+ def find_brave_browser() -> Optional[str]:
126
+ """Find Brave browser executable."""
127
+ system = platform.system()
128
+ patterns = []
129
+
130
+ if system == 'Darwin': # macOS
131
+ patterns = [
132
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
133
+ ]
134
+ elif system == 'Linux':
135
+ patterns = [
136
+ '/usr/bin/brave-browser',
137
+ '/usr/bin/brave',
138
+ '/usr/local/bin/brave',
139
+ '/snap/bin/brave',
140
+ '/usr/bin/brave-browser-stable',
141
+ '/usr/bin/brave-browser-beta',
142
+ '/usr/bin/brave-browser-dev',
143
+ ]
144
+ elif system == 'Windows':
145
+ patterns = [
146
+ r'C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe',
147
+ r'C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe',
148
+ r'%LOCALAPPDATA%\BraveSoftware\Brave-Browser\Application\brave.exe',
149
+ ]
150
+
151
+ return _find_browser_from_patterns(patterns)
152
+
153
+
121
154
  def _find_browser_from_patterns(patterns: list[str]) -> Optional[str]:
122
155
  """Helper function to find browser from patterns."""
123
156
  system = platform.system()
@@ -182,7 +215,7 @@ def find_available_port(start_port: int) -> int:
182
215
  def select_browser() -> Optional[str]:
183
216
  """Interactive browser selection."""
184
217
  console.print("\n[bold cyan]🌐 Browser Selection[/bold cyan]")
185
- console.print("VibeSurf supports Chrome and Edge browsers.\n")
218
+ console.print("VibeSurf supports Chrome, Edge, and Brave browsers.\n")
186
219
 
187
220
  options = []
188
221
  browsers = {}
@@ -197,11 +230,19 @@ def select_browser() -> Optional[str]:
197
230
  # Check for Edge
198
231
  edge_path = find_edge_browser()
199
232
  if edge_path:
200
- option_num = "2" if "1" not in options else "1" if not chrome_path else "2"
233
+ option_num = str(len(options) + 1)
201
234
  options.append(option_num)
202
235
  browsers[option_num] = ("Edge", edge_path)
203
236
  console.print(f"[green]{option_num}.[/green] Microsoft Edge ([dim]{edge_path}[/dim])")
204
237
 
238
+ # Check for Brave
239
+ brave_path = find_brave_browser()
240
+ if brave_path:
241
+ option_num = str(len(options) + 1)
242
+ options.append(option_num)
243
+ browsers[option_num] = ("Brave", brave_path)
244
+ console.print(f"[green]{option_num}.[/green] Brave Browser ([dim]{brave_path}[/dim])")
245
+
205
246
  # Custom browser option
206
247
  custom_option = str(len(options) + 1)
207
248
  options.append(custom_option)
@@ -212,7 +253,7 @@ def select_browser() -> Optional[str]:
212
253
  options.append(quit_option)
213
254
  console.print(f"[red]{quit_option}.[/red] Quit")
214
255
 
215
- if not chrome_path and not edge_path:
256
+ if not chrome_path and not edge_path and not brave_path:
216
257
  console.print("\n[yellow]⚠️ No supported browsers found automatically.[/yellow]")
217
258
 
218
259
  while True:
@@ -377,6 +418,10 @@ def get_browser_execution_path() -> Optional[str]:
377
418
  def main():
378
419
  """Main CLI entry point."""
379
420
  try:
421
+ # Initialize telemetry
422
+ telemetry = ProductTelemetry()
423
+ start_time = time.time()
424
+
380
425
  # Display logo
381
426
  console.print(Panel(VIBESURF_LOGO, title="[bold cyan]VibeSurf CLI[/bold cyan]", border_style="cyan"))
382
427
  console.print("[dim]A powerful browser automation tool for vibe surfing 🏄‍♂️[/dim]")
@@ -384,6 +429,14 @@ def main():
384
429
  console.print(f"[dim]Version: {vibe_surf.__version__}[/dim]\n")
385
430
  console.print(f"[dim]Author: WarmShao and Community Contributors [/dim]\n")
386
431
 
432
+ # Capture telemetry start event
433
+ start_event = CLITelemetryEvent(
434
+ version=vibe_surf.__version__,
435
+ action='start',
436
+ mode='interactive'
437
+ )
438
+ telemetry.capture(start_event)
439
+
387
440
  # Check for existing browser path from configuration
388
441
  browser_path = get_browser_execution_path()
389
442
 
@@ -405,10 +458,52 @@ def main():
405
458
  # Start backend
406
459
  start_backend(port)
407
460
 
461
+ # Capture telemetry completion event
462
+ end_time = time.time()
463
+ duration = end_time - start_time
464
+ completion_event = CLITelemetryEvent(
465
+ version=vibe_surf.__version__,
466
+ action='startup_completed',
467
+ mode='interactive',
468
+ duration_seconds=duration,
469
+ browser_path=browser_path
470
+ )
471
+ telemetry.capture(completion_event)
472
+ telemetry.flush()
473
+
408
474
  except KeyboardInterrupt:
409
475
  console.print("\n[yellow]👋 Goodbye![/yellow]")
476
+ # Capture telemetry interruption event
477
+ try:
478
+ end_time = time.time()
479
+ duration = end_time - start_time
480
+ interrupt_event = CLITelemetryEvent(
481
+ version=vibe_surf.__version__,
482
+ action='interrupted',
483
+ mode='interactive',
484
+ duration_seconds=duration
485
+ )
486
+ telemetry.capture(interrupt_event)
487
+ telemetry.flush()
488
+ except:
489
+ pass
410
490
  except Exception as e:
411
491
  console.print(f"\n[red]❌ Unexpected error: {e}[/red]")
492
+ # Capture telemetry error event
493
+ try:
494
+ end_time = time.time()
495
+ duration = end_time - start_time
496
+ error_event = CLITelemetryEvent(
497
+ version=vibe_surf.__version__,
498
+ action='error',
499
+ mode='interactive',
500
+ duration_seconds=duration,
501
+ error_message=str(e)[:200]
502
+ )
503
+ telemetry.capture(error_event)
504
+ telemetry.flush()
505
+ except:
506
+ pass
412
507
  sys.exit(1)
413
508
 
414
509
 
@@ -0,0 +1,60 @@
1
+ """
2
+ Telemetry for VibeSurf.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ # Type stubs for lazy imports
8
+ if TYPE_CHECKING:
9
+ from vibe_surf.telemetry.service import ProductTelemetry
10
+ from vibe_surf.telemetry.views import (
11
+ BaseTelemetryEvent,
12
+ CLITelemetryEvent,
13
+ MCPClientTelemetryEvent,
14
+ MCPServerTelemetryEvent,
15
+ VibeSurfAgentTelemetryEvent,
16
+ ReportWriterTelemetryEvent,
17
+ BackendTelemetryEvent,
18
+ )
19
+
20
+ # Lazy imports mapping
21
+ _LAZY_IMPORTS = {
22
+ 'ProductTelemetry': ('vibe_surf.telemetry.service', 'ProductTelemetry'),
23
+ 'BaseTelemetryEvent': ('vibe_surf.telemetry.views', 'BaseTelemetryEvent'),
24
+ 'CLITelemetryEvent': ('vibe_surf.telemetry.views', 'CLITelemetryEvent'),
25
+ 'MCPClientTelemetryEvent': ('vibe_surf.telemetry.views', 'MCPClientTelemetryEvent'),
26
+ 'MCPServerTelemetryEvent': ('vibe_surf.telemetry.views', 'MCPServerTelemetryEvent'),
27
+ 'VibeSurfAgentTelemetryEvent': ('vibe_surf.telemetry.views', 'VibeSurfAgentTelemetryEvent'),
28
+ 'ReportWriterTelemetryEvent': ('vibe_surf.telemetry.views', 'ReportWriterTelemetryEvent'),
29
+ 'BackendTelemetryEvent': ('vibe_surf.telemetry.views', 'BackendTelemetryEvent'),
30
+ }
31
+
32
+
33
+ def __getattr__(name: str):
34
+ """Lazy import mechanism for telemetry components."""
35
+ if name in _LAZY_IMPORTS:
36
+ module_path, attr_name = _LAZY_IMPORTS[name]
37
+ try:
38
+ from importlib import import_module
39
+
40
+ module = import_module(module_path)
41
+ attr = getattr(module, attr_name)
42
+ # Cache the imported attribute in the module's globals
43
+ globals()[name] = attr
44
+ return attr
45
+ except ImportError as e:
46
+ raise ImportError(f'Failed to import {name} from {module_path}: {e}') from e
47
+
48
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
49
+
50
+
51
+ __all__ = [
52
+ 'BaseTelemetryEvent',
53
+ 'ProductTelemetry',
54
+ 'CLITelemetryEvent',
55
+ 'MCPClientTelemetryEvent',
56
+ 'MCPServerTelemetryEvent',
57
+ 'VibeSurfAgentTelemetryEvent',
58
+ 'ReportWriterTelemetryEvent',
59
+ 'BackendTelemetryEvent',
60
+ ]
@@ -0,0 +1,112 @@
1
+ import logging
2
+ import os
3
+ import pdb
4
+
5
+ from dotenv import load_dotenv
6
+ from posthog import Posthog
7
+ from uuid_extensions import uuid7str
8
+
9
+ from vibe_surf.telemetry.views import BaseTelemetryEvent
10
+ from vibe_surf.utils import singleton
11
+ from vibe_surf.logger import get_logger
12
+
13
+ load_dotenv()
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ POSTHOG_EVENT_SETTINGS = {
18
+ 'process_person_profile': True,
19
+ }
20
+
21
+
22
+ @singleton
23
+ class ProductTelemetry:
24
+ """
25
+ Service for capturing anonymized telemetry data.
26
+
27
+ If the environment variable `ANONYMIZED_TELEMETRY=False`, anonymized telemetry will be disabled.
28
+ """
29
+ WORKSPACE_DIR = os.getenv('VIBESURF_WORKSPACE', './vibesurf_workspace')
30
+ USER_ID_PATH = os.path.join(WORKSPACE_DIR, 'telemetry', 'userid')
31
+ PROJECT_API_KEY = 'phc_lCYnQqFlfNHAlh1TJGqaTvD8EFPCKR7ONsEHbbWuPVr'
32
+ HOST = 'https://us.i.posthog.com'
33
+ UNKNOWN_USER_ID = 'UNKNOWN'
34
+
35
+ _curr_user_id = None
36
+
37
+ def __init__(self) -> None:
38
+ telemetry_enabled = os.getenv('VIBESURF_ANONYMIZED_TELEMETRY', 'true').lower() in ("true", "1", "yes", "on")
39
+ self.debug_logging = os.getenv("VIBESURF_DEBUG", "false").lower() in ("true", "1", "yes", "on")
40
+
41
+ telemetry_disabled = not telemetry_enabled
42
+
43
+ if telemetry_disabled:
44
+ self._posthog_client = None
45
+ else:
46
+ self._posthog_client = Posthog(
47
+ project_api_key=self.PROJECT_API_KEY,
48
+ host=self.HOST,
49
+ disable_geoip=False,
50
+ enable_exception_autocapture=True,
51
+ )
52
+
53
+ # Silence posthog's logging
54
+ if not self.debug_logging:
55
+ posthog_logger = logging.getLogger('posthog')
56
+ posthog_logger.disabled = True
57
+
58
+ if self._posthog_client is None:
59
+ logger.debug('Telemetry disabled')
60
+
61
+ def capture(self, event: BaseTelemetryEvent) -> None:
62
+ if self._posthog_client is None:
63
+ return
64
+
65
+ self._direct_capture(event)
66
+
67
+ def _direct_capture(self, event: BaseTelemetryEvent) -> None:
68
+ """
69
+ Should not be thread blocking because posthog magically handles it
70
+ """
71
+ if self._posthog_client is None:
72
+ return
73
+
74
+ try:
75
+ self._posthog_client.capture(
76
+ distinct_id=self.user_id,
77
+ event=event.name,
78
+ properties={**event.properties, **POSTHOG_EVENT_SETTINGS},
79
+ )
80
+ except Exception as e:
81
+ logger.error(f'Failed to send telemetry event {event.name}: {e}')
82
+
83
+ def flush(self) -> None:
84
+ if self._posthog_client:
85
+ try:
86
+ self._posthog_client.flush()
87
+ logger.debug('PostHog client telemetry queue flushed.')
88
+ except Exception as e:
89
+ logger.error(f'Failed to flush PostHog client: {e}')
90
+ else:
91
+ logger.debug('PostHog client not available, skipping flush.')
92
+
93
+ @property
94
+ def user_id(self) -> str:
95
+ if self._curr_user_id:
96
+ return self._curr_user_id
97
+
98
+ # File access may fail due to permissions or other reasons. We don't want to
99
+ # crash so we catch all exceptions.
100
+ try:
101
+ if not os.path.exists(self.USER_ID_PATH):
102
+ os.makedirs(os.path.dirname(self.USER_ID_PATH), exist_ok=True)
103
+ with open(self.USER_ID_PATH, 'w') as f:
104
+ new_user_id = uuid7str()
105
+ f.write(new_user_id)
106
+ self._curr_user_id = new_user_id
107
+ else:
108
+ with open(self.USER_ID_PATH) as f:
109
+ self._curr_user_id = f.read()
110
+ except Exception:
111
+ self._curr_user_id = 'UNKNOWN_USER_ID'
112
+ return self._curr_user_id
@@ -0,0 +1,156 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Sequence
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Any
5
+
6
+ from browser_use.config import is_running_in_docker
7
+
8
+
9
+ @dataclass
10
+ class BaseTelemetryEvent(ABC):
11
+ @property
12
+ @abstractmethod
13
+ def name(self) -> str:
14
+ pass
15
+
16
+ @property
17
+ def properties(self) -> dict[str, Any]:
18
+ props = {k: v for k, v in asdict(self).items() if k != 'name'}
19
+ # Add Docker context if running in Docker
20
+ props['is_docker'] = is_running_in_docker()
21
+ return props
22
+
23
+
24
+ @dataclass
25
+ class AgentTelemetryEvent(BaseTelemetryEvent):
26
+ # start details
27
+ task: str
28
+ model: str
29
+ model_provider: str
30
+ max_steps: int
31
+ max_actions_per_step: int
32
+ use_vision: bool
33
+ version: str
34
+ source: str
35
+ cdp_url: str | None
36
+ # step details
37
+ action_errors: Sequence[str | None]
38
+ action_history: Sequence[list[dict] | None]
39
+ urls_visited: Sequence[str | None]
40
+ # end details
41
+ steps: int
42
+ total_input_tokens: int
43
+ total_duration_seconds: float
44
+ success: bool | None
45
+ final_result_response: str | None
46
+ error_message: str | None
47
+
48
+ name: str = 'agent_event'
49
+
50
+
51
+ @dataclass
52
+ class MCPClientTelemetryEvent(BaseTelemetryEvent):
53
+ """Telemetry event for MCP client usage"""
54
+
55
+ server_name: str
56
+ command: str
57
+ tools_discovered: int
58
+ version: str
59
+ action: str # 'connect', 'disconnect', 'tool_call'
60
+ tool_name: str | None = None
61
+ duration_seconds: float | None = None
62
+ error_message: str | None = None
63
+
64
+ name: str = 'mcp_client_event'
65
+
66
+
67
+ @dataclass
68
+ class MCPServerTelemetryEvent(BaseTelemetryEvent):
69
+ """Telemetry event for MCP server usage"""
70
+
71
+ version: str
72
+ action: str # 'start', 'stop', 'tool_call'
73
+ tool_name: str | None = None
74
+ duration_seconds: float | None = None
75
+ error_message: str | None = None
76
+ parent_process_cmdline: str | None = None
77
+
78
+ name: str = 'mcp_server_event'
79
+
80
+
81
+ @dataclass
82
+ class ComposioTelemetryEvent(BaseTelemetryEvent):
83
+ """Telemetry event for Composio client usage"""
84
+
85
+ toolkit_slugs: list[str]
86
+ tools_registered: int
87
+ version: str
88
+ action: str # 'register', 'unregister', 'tool_call'
89
+ toolkit_slug: str | None = None
90
+ tool_name: str | None = None
91
+ duration_seconds: float | None = None
92
+ error_message: str | None = None
93
+
94
+ name: str = 'composio_client_event'
95
+
96
+
97
+ @dataclass
98
+ class CLITelemetryEvent(BaseTelemetryEvent):
99
+ """Telemetry event for CLI usage"""
100
+
101
+ version: str
102
+ action: str # 'start', 'message_sent', 'task_completed', 'error'
103
+ mode: str # 'interactive', 'oneshot', 'mcp_server'
104
+ model: str | None = None
105
+ model_provider: str | None = None
106
+ browser_path: str | None = None
107
+ duration_seconds: float | None = None
108
+ error_message: str | None = None
109
+
110
+ name: str = 'cli_event'
111
+
112
+
113
+ @dataclass
114
+ class VibeSurfAgentTelemetryEvent(BaseTelemetryEvent):
115
+ """Telemetry event for VibeSurf Agent usage"""
116
+
117
+ version: str
118
+ action: str # 'start', 'task_completed', 'error'
119
+ task_description: str | None = None
120
+ model: str | None = None
121
+ model_provider: str | None = None
122
+ duration_seconds: float | None = None
123
+ success: bool | None = None
124
+ error_message: str | None = None
125
+ session_id: str | None = None
126
+
127
+ name: str = 'vibesurf_agent_event'
128
+
129
+
130
+ @dataclass
131
+ class ReportWriterTelemetryEvent(BaseTelemetryEvent):
132
+ """Telemetry event for Report Writer Agent usage"""
133
+
134
+ version: str
135
+ action: str # 'start', 'report_completed', 'error'
136
+ model: str | None = None
137
+ model_provider: str | None = None
138
+ duration_seconds: float | None = None
139
+ success: bool | None = None
140
+ error_message: str | None = None
141
+ report_type: str | None = None
142
+
143
+ name: str = 'report_writer_event'
144
+
145
+
146
+ @dataclass
147
+ class BackendTelemetryEvent(BaseTelemetryEvent):
148
+ """Telemetry event for Backend API usage"""
149
+
150
+ version: str
151
+ action: str # 'startup', 'shutdown', 'api_call'
152
+ api_endpoint: str | None = None
153
+ duration_seconds: float | None = None
154
+ error_message: str | None = None
155
+
156
+ name: str = 'backend_event'