optexity-browser-use 0.9.5__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.
- browser_use/__init__.py +157 -0
- browser_use/actor/__init__.py +11 -0
- browser_use/actor/element.py +1175 -0
- browser_use/actor/mouse.py +134 -0
- browser_use/actor/page.py +561 -0
- browser_use/actor/playground/flights.py +41 -0
- browser_use/actor/playground/mixed_automation.py +54 -0
- browser_use/actor/playground/playground.py +236 -0
- browser_use/actor/utils.py +176 -0
- browser_use/agent/cloud_events.py +282 -0
- browser_use/agent/gif.py +424 -0
- browser_use/agent/judge.py +170 -0
- browser_use/agent/message_manager/service.py +473 -0
- browser_use/agent/message_manager/utils.py +52 -0
- browser_use/agent/message_manager/views.py +98 -0
- browser_use/agent/prompts.py +413 -0
- browser_use/agent/service.py +2316 -0
- browser_use/agent/system_prompt.md +185 -0
- browser_use/agent/system_prompt_flash.md +10 -0
- browser_use/agent/system_prompt_no_thinking.md +183 -0
- browser_use/agent/views.py +743 -0
- browser_use/browser/__init__.py +41 -0
- browser_use/browser/cloud/cloud.py +203 -0
- browser_use/browser/cloud/views.py +89 -0
- browser_use/browser/events.py +578 -0
- browser_use/browser/profile.py +1158 -0
- browser_use/browser/python_highlights.py +548 -0
- browser_use/browser/session.py +3225 -0
- browser_use/browser/session_manager.py +399 -0
- browser_use/browser/video_recorder.py +162 -0
- browser_use/browser/views.py +200 -0
- browser_use/browser/watchdog_base.py +260 -0
- browser_use/browser/watchdogs/__init__.py +0 -0
- browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
- browser_use/browser/watchdogs/crash_watchdog.py +335 -0
- browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
- browser_use/browser/watchdogs/dom_watchdog.py +817 -0
- browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
- browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
- browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
- browser_use/browser/watchdogs/popups_watchdog.py +143 -0
- browser_use/browser/watchdogs/recording_watchdog.py +126 -0
- browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
- browser_use/browser/watchdogs/security_watchdog.py +280 -0
- browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
- browser_use/cli.py +2359 -0
- browser_use/code_use/__init__.py +16 -0
- browser_use/code_use/formatting.py +192 -0
- browser_use/code_use/namespace.py +665 -0
- browser_use/code_use/notebook_export.py +276 -0
- browser_use/code_use/service.py +1340 -0
- browser_use/code_use/system_prompt.md +574 -0
- browser_use/code_use/utils.py +150 -0
- browser_use/code_use/views.py +171 -0
- browser_use/config.py +505 -0
- browser_use/controller/__init__.py +3 -0
- browser_use/dom/enhanced_snapshot.py +161 -0
- browser_use/dom/markdown_extractor.py +169 -0
- browser_use/dom/playground/extraction.py +312 -0
- browser_use/dom/playground/multi_act.py +32 -0
- browser_use/dom/serializer/clickable_elements.py +200 -0
- browser_use/dom/serializer/code_use_serializer.py +287 -0
- browser_use/dom/serializer/eval_serializer.py +478 -0
- browser_use/dom/serializer/html_serializer.py +212 -0
- browser_use/dom/serializer/paint_order.py +197 -0
- browser_use/dom/serializer/serializer.py +1170 -0
- browser_use/dom/service.py +825 -0
- browser_use/dom/utils.py +129 -0
- browser_use/dom/views.py +906 -0
- browser_use/exceptions.py +5 -0
- browser_use/filesystem/__init__.py +0 -0
- browser_use/filesystem/file_system.py +619 -0
- browser_use/init_cmd.py +376 -0
- browser_use/integrations/gmail/__init__.py +24 -0
- browser_use/integrations/gmail/actions.py +115 -0
- browser_use/integrations/gmail/service.py +225 -0
- browser_use/llm/__init__.py +155 -0
- browser_use/llm/anthropic/chat.py +242 -0
- browser_use/llm/anthropic/serializer.py +312 -0
- browser_use/llm/aws/__init__.py +36 -0
- browser_use/llm/aws/chat_anthropic.py +242 -0
- browser_use/llm/aws/chat_bedrock.py +289 -0
- browser_use/llm/aws/serializer.py +257 -0
- browser_use/llm/azure/chat.py +91 -0
- browser_use/llm/base.py +57 -0
- browser_use/llm/browser_use/__init__.py +3 -0
- browser_use/llm/browser_use/chat.py +201 -0
- browser_use/llm/cerebras/chat.py +193 -0
- browser_use/llm/cerebras/serializer.py +109 -0
- browser_use/llm/deepseek/chat.py +212 -0
- browser_use/llm/deepseek/serializer.py +109 -0
- browser_use/llm/exceptions.py +29 -0
- browser_use/llm/google/__init__.py +3 -0
- browser_use/llm/google/chat.py +542 -0
- browser_use/llm/google/serializer.py +120 -0
- browser_use/llm/groq/chat.py +229 -0
- browser_use/llm/groq/parser.py +158 -0
- browser_use/llm/groq/serializer.py +159 -0
- browser_use/llm/messages.py +238 -0
- browser_use/llm/models.py +271 -0
- browser_use/llm/oci_raw/__init__.py +10 -0
- browser_use/llm/oci_raw/chat.py +443 -0
- browser_use/llm/oci_raw/serializer.py +229 -0
- browser_use/llm/ollama/chat.py +97 -0
- browser_use/llm/ollama/serializer.py +143 -0
- browser_use/llm/openai/chat.py +264 -0
- browser_use/llm/openai/like.py +15 -0
- browser_use/llm/openai/serializer.py +165 -0
- browser_use/llm/openrouter/chat.py +211 -0
- browser_use/llm/openrouter/serializer.py +26 -0
- browser_use/llm/schema.py +176 -0
- browser_use/llm/views.py +48 -0
- browser_use/logging_config.py +330 -0
- browser_use/mcp/__init__.py +18 -0
- browser_use/mcp/__main__.py +12 -0
- browser_use/mcp/client.py +544 -0
- browser_use/mcp/controller.py +264 -0
- browser_use/mcp/server.py +1114 -0
- browser_use/observability.py +204 -0
- browser_use/py.typed +0 -0
- browser_use/sandbox/__init__.py +41 -0
- browser_use/sandbox/sandbox.py +637 -0
- browser_use/sandbox/views.py +132 -0
- browser_use/screenshots/__init__.py +1 -0
- browser_use/screenshots/service.py +52 -0
- browser_use/sync/__init__.py +6 -0
- browser_use/sync/auth.py +357 -0
- browser_use/sync/service.py +161 -0
- browser_use/telemetry/__init__.py +51 -0
- browser_use/telemetry/service.py +112 -0
- browser_use/telemetry/views.py +101 -0
- browser_use/tokens/__init__.py +0 -0
- browser_use/tokens/custom_pricing.py +24 -0
- browser_use/tokens/mappings.py +4 -0
- browser_use/tokens/service.py +580 -0
- browser_use/tokens/views.py +108 -0
- browser_use/tools/registry/service.py +572 -0
- browser_use/tools/registry/views.py +174 -0
- browser_use/tools/service.py +1675 -0
- browser_use/tools/utils.py +82 -0
- browser_use/tools/views.py +100 -0
- browser_use/utils.py +670 -0
- optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
- optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
- optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
- optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
- optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloud sync service for sending events to the Browser Use cloud.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from bubus import BaseEvent
|
|
9
|
+
|
|
10
|
+
from browser_use.config import CONFIG
|
|
11
|
+
from browser_use.sync.auth import TEMP_USER_ID, DeviceAuthClient
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CloudSync:
|
|
17
|
+
"""Service for syncing events to the Browser Use cloud"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_url: str | None = None, allow_session_events_for_auth: bool = False):
|
|
20
|
+
# Backend API URL for all API requests - can be passed directly or defaults to env var
|
|
21
|
+
self.base_url = base_url or CONFIG.BROWSER_USE_CLOUD_API_URL
|
|
22
|
+
self.auth_client = DeviceAuthClient(base_url=self.base_url)
|
|
23
|
+
self.session_id: str | None = None
|
|
24
|
+
self.allow_session_events_for_auth = allow_session_events_for_auth
|
|
25
|
+
self.auth_flow_active = False # Flag to indicate auth flow is running
|
|
26
|
+
# Check if cloud sync is actually enabled - if not, we should remain silent
|
|
27
|
+
self.enabled = CONFIG.BROWSER_USE_CLOUD_SYNC
|
|
28
|
+
|
|
29
|
+
async def handle_event(self, event: BaseEvent) -> None:
|
|
30
|
+
"""Handle an event by sending it to the cloud"""
|
|
31
|
+
try:
|
|
32
|
+
# If cloud sync is disabled, don't handle any events
|
|
33
|
+
if not self.enabled:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Extract session ID from CreateAgentSessionEvent
|
|
37
|
+
if event.event_type == 'CreateAgentSessionEvent' and hasattr(event, 'id'):
|
|
38
|
+
self.session_id = str(event.id) # type: ignore
|
|
39
|
+
|
|
40
|
+
# Send events based on authentication status and context
|
|
41
|
+
if self.auth_client.is_authenticated:
|
|
42
|
+
# User is authenticated - send all events
|
|
43
|
+
await self._send_event(event)
|
|
44
|
+
elif self.allow_session_events_for_auth:
|
|
45
|
+
# Special case: allow ALL events during auth flow
|
|
46
|
+
await self._send_event(event)
|
|
47
|
+
# Mark auth flow as active when we see a session event
|
|
48
|
+
if event.event_type == 'CreateAgentSessionEvent':
|
|
49
|
+
self.auth_flow_active = True
|
|
50
|
+
else:
|
|
51
|
+
# User is not authenticated and no auth in progress - don't send anything
|
|
52
|
+
logger.debug(f'Skipping event {event.event_type} - user not authenticated')
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f'Failed to handle {event.event_type} event: {type(e).__name__}: {e}', exc_info=True)
|
|
56
|
+
|
|
57
|
+
async def _send_event(self, event: BaseEvent) -> None:
|
|
58
|
+
"""Send event to cloud API"""
|
|
59
|
+
try:
|
|
60
|
+
headers = {}
|
|
61
|
+
|
|
62
|
+
# Override user_id only if it's not already set to a specific value
|
|
63
|
+
# This allows CLI and other code to explicitly set temp user_id when needed
|
|
64
|
+
if self.auth_client and self.auth_client.is_authenticated:
|
|
65
|
+
# Only override if we're fully authenticated and event doesn't have temp user_id
|
|
66
|
+
current_user_id = getattr(event, 'user_id', None)
|
|
67
|
+
if current_user_id != TEMP_USER_ID:
|
|
68
|
+
setattr(event, 'user_id', str(self.auth_client.user_id))
|
|
69
|
+
else:
|
|
70
|
+
# Set temp user_id if not already set
|
|
71
|
+
if not hasattr(event, 'user_id') or not getattr(event, 'user_id', None):
|
|
72
|
+
setattr(event, 'user_id', TEMP_USER_ID)
|
|
73
|
+
|
|
74
|
+
# Add auth headers if available
|
|
75
|
+
if self.auth_client:
|
|
76
|
+
headers.update(self.auth_client.get_headers())
|
|
77
|
+
|
|
78
|
+
# Send event (batch format with direct BaseEvent serialization)
|
|
79
|
+
async with httpx.AsyncClient() as client:
|
|
80
|
+
# Serialize event and add device_id to all events
|
|
81
|
+
event_data = event.model_dump(mode='json')
|
|
82
|
+
if self.auth_client and self.auth_client.device_id:
|
|
83
|
+
event_data['device_id'] = self.auth_client.device_id
|
|
84
|
+
|
|
85
|
+
response = await client.post(
|
|
86
|
+
f'{self.base_url.rstrip("/")}/api/v1/events',
|
|
87
|
+
json={'events': [event_data]},
|
|
88
|
+
headers=headers,
|
|
89
|
+
timeout=10.0,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if response.status_code >= 400:
|
|
93
|
+
# Log error but don't raise - we want to fail silently
|
|
94
|
+
logger.debug(
|
|
95
|
+
f'Failed to send sync event: POST {response.request.url} {response.status_code} - {response.text}'
|
|
96
|
+
)
|
|
97
|
+
except httpx.TimeoutException:
|
|
98
|
+
logger.debug(f'Event send timed out after 10 seconds: {event}')
|
|
99
|
+
except httpx.ConnectError as e:
|
|
100
|
+
# logger.warning(f'⚠️ Failed to connect to cloud service at {self.base_url}: {e}')
|
|
101
|
+
pass
|
|
102
|
+
except httpx.HTTPError as e:
|
|
103
|
+
logger.debug(f'HTTP error sending event {event}: {type(e).__name__}: {e}')
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug(f'Unexpected error sending event {event}: {type(e).__name__}: {e}')
|
|
106
|
+
|
|
107
|
+
# async def _update_wal_user_ids(self, session_id: str) -> None:
|
|
108
|
+
# """Update user IDs in WAL file after authentication"""
|
|
109
|
+
# try:
|
|
110
|
+
# assert self.auth_client, 'Cloud sync must be authenticated to update WAL user ID'
|
|
111
|
+
|
|
112
|
+
# wal_path = CONFIG.BROWSER_USE_CONFIG_DIR / 'events' / f'{session_id}.jsonl'
|
|
113
|
+
# if not await anyio.Path(wal_path).exists():
|
|
114
|
+
# raise FileNotFoundError(
|
|
115
|
+
# f'CloudSync failed to update saved event user_ids after auth: Agent EventBus WAL file not found: {wal_path}'
|
|
116
|
+
# )
|
|
117
|
+
|
|
118
|
+
# # Read all events
|
|
119
|
+
# events = []
|
|
120
|
+
# content = await anyio.Path(wal_path).read_text()
|
|
121
|
+
# for line in content.splitlines():
|
|
122
|
+
# if line.strip():
|
|
123
|
+
# events.append(json.loads(line))
|
|
124
|
+
|
|
125
|
+
# # Update user_id and device_id
|
|
126
|
+
# user_id = self.auth_client.user_id
|
|
127
|
+
# device_id = self.auth_client.device_id
|
|
128
|
+
# for event in events:
|
|
129
|
+
# if 'user_id' in event:
|
|
130
|
+
# event['user_id'] = user_id
|
|
131
|
+
# # Add device_id to all events
|
|
132
|
+
# event['device_id'] = device_id
|
|
133
|
+
|
|
134
|
+
# # Write back
|
|
135
|
+
# updated_content = '\n'.join(json.dumps(event) for event in events) + '\n'
|
|
136
|
+
# await anyio.Path(wal_path).write_text(updated_content)
|
|
137
|
+
|
|
138
|
+
# except Exception as e:
|
|
139
|
+
# logger.warning(f'Failed to update WAL user IDs: {e}')
|
|
140
|
+
|
|
141
|
+
def set_auth_flow_active(self) -> None:
|
|
142
|
+
"""Mark auth flow as active to allow all events"""
|
|
143
|
+
self.auth_flow_active = True
|
|
144
|
+
|
|
145
|
+
async def authenticate(self, show_instructions: bool = True) -> bool:
|
|
146
|
+
"""Authenticate with the cloud service"""
|
|
147
|
+
# If cloud sync is disabled, don't authenticate
|
|
148
|
+
if not self.enabled:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
# Check if already authenticated first
|
|
152
|
+
if self.auth_client.is_authenticated:
|
|
153
|
+
import logging
|
|
154
|
+
|
|
155
|
+
logger = logging.getLogger(__name__)
|
|
156
|
+
if show_instructions:
|
|
157
|
+
logger.info('✅ Already authenticated! Skipping OAuth flow.')
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
# Not authenticated - run OAuth flow
|
|
161
|
+
return await self.auth_client.authenticate(agent_session_id=self.session_id, show_instructions=show_instructions)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Telemetry for Browser Use.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
# Type stubs for lazy imports
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from browser_use.telemetry.service import ProductTelemetry
|
|
10
|
+
from browser_use.telemetry.views import (
|
|
11
|
+
BaseTelemetryEvent,
|
|
12
|
+
CLITelemetryEvent,
|
|
13
|
+
MCPClientTelemetryEvent,
|
|
14
|
+
MCPServerTelemetryEvent,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Lazy imports mapping
|
|
18
|
+
_LAZY_IMPORTS = {
|
|
19
|
+
'ProductTelemetry': ('browser_use.telemetry.service', 'ProductTelemetry'),
|
|
20
|
+
'BaseTelemetryEvent': ('browser_use.telemetry.views', 'BaseTelemetryEvent'),
|
|
21
|
+
'CLITelemetryEvent': ('browser_use.telemetry.views', 'CLITelemetryEvent'),
|
|
22
|
+
'MCPClientTelemetryEvent': ('browser_use.telemetry.views', 'MCPClientTelemetryEvent'),
|
|
23
|
+
'MCPServerTelemetryEvent': ('browser_use.telemetry.views', 'MCPServerTelemetryEvent'),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __getattr__(name: str):
|
|
28
|
+
"""Lazy import mechanism for telemetry components."""
|
|
29
|
+
if name in _LAZY_IMPORTS:
|
|
30
|
+
module_path, attr_name = _LAZY_IMPORTS[name]
|
|
31
|
+
try:
|
|
32
|
+
from importlib import import_module
|
|
33
|
+
|
|
34
|
+
module = import_module(module_path)
|
|
35
|
+
attr = getattr(module, attr_name)
|
|
36
|
+
# Cache the imported attribute in the module's globals
|
|
37
|
+
globals()[name] = attr
|
|
38
|
+
return attr
|
|
39
|
+
except ImportError as e:
|
|
40
|
+
raise ImportError(f'Failed to import {name} from {module_path}: {e}') from e
|
|
41
|
+
|
|
42
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
'BaseTelemetryEvent',
|
|
47
|
+
'ProductTelemetry',
|
|
48
|
+
'CLITelemetryEvent',
|
|
49
|
+
'MCPClientTelemetryEvent',
|
|
50
|
+
'MCPServerTelemetryEvent',
|
|
51
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from posthog import Posthog
|
|
6
|
+
from uuid_extensions import uuid7str
|
|
7
|
+
|
|
8
|
+
from browser_use.telemetry.views import BaseTelemetryEvent
|
|
9
|
+
from browser_use.utils import singleton
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
from browser_use.config import CONFIG
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
POSTHOG_EVENT_SETTINGS = {
|
|
19
|
+
'process_person_profile': True,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@singleton
|
|
24
|
+
class ProductTelemetry:
|
|
25
|
+
"""
|
|
26
|
+
Service for capturing anonymized telemetry data.
|
|
27
|
+
|
|
28
|
+
If the environment variable `ANONYMIZED_TELEMETRY=False`, anonymized telemetry will be disabled.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
USER_ID_PATH = str(CONFIG.BROWSER_USE_CONFIG_DIR / 'device_id')
|
|
32
|
+
PROJECT_API_KEY = 'phc_F8JMNjW1i2KbGUTaW1unnDdLSPCoyc52SGRU0JecaUh'
|
|
33
|
+
HOST = 'https://eu.i.posthog.com'
|
|
34
|
+
UNKNOWN_USER_ID = 'UNKNOWN'
|
|
35
|
+
|
|
36
|
+
_curr_user_id = None
|
|
37
|
+
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
telemetry_disabled = not CONFIG.ANONYMIZED_TELEMETRY
|
|
40
|
+
self.debug_logging = CONFIG.BROWSER_USE_LOGGING_LEVEL == 'debug'
|
|
41
|
+
|
|
42
|
+
if telemetry_disabled:
|
|
43
|
+
self._posthog_client = None
|
|
44
|
+
else:
|
|
45
|
+
logger.info('Using anonymized telemetry, see https://docs.browser-use.com/development/telemetry.')
|
|
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,101 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from typing import Any, Literal
|
|
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 | Literal['auto']
|
|
33
|
+
version: str
|
|
34
|
+
source: str
|
|
35
|
+
cdp_url: str | None
|
|
36
|
+
agent_type: str | None # 'code' for CodeAgent, None for regular Agent
|
|
37
|
+
# step details
|
|
38
|
+
action_errors: Sequence[str | None]
|
|
39
|
+
action_history: Sequence[list[dict] | None]
|
|
40
|
+
urls_visited: Sequence[str | None]
|
|
41
|
+
# end details
|
|
42
|
+
steps: int
|
|
43
|
+
total_input_tokens: int
|
|
44
|
+
total_output_tokens: int
|
|
45
|
+
prompt_cached_tokens: int
|
|
46
|
+
total_tokens: int
|
|
47
|
+
total_duration_seconds: float
|
|
48
|
+
success: bool | None
|
|
49
|
+
final_result_response: str | None
|
|
50
|
+
error_message: str | None
|
|
51
|
+
# judge details
|
|
52
|
+
judge_verdict: bool | None = None
|
|
53
|
+
judge_reasoning: str | None = None
|
|
54
|
+
judge_failure_reason: str | None = None
|
|
55
|
+
|
|
56
|
+
name: str = 'agent_event'
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class MCPClientTelemetryEvent(BaseTelemetryEvent):
|
|
61
|
+
"""Telemetry event for MCP client usage"""
|
|
62
|
+
|
|
63
|
+
server_name: str
|
|
64
|
+
command: str
|
|
65
|
+
tools_discovered: int
|
|
66
|
+
version: str
|
|
67
|
+
action: str # 'connect', 'disconnect', 'tool_call'
|
|
68
|
+
tool_name: str | None = None
|
|
69
|
+
duration_seconds: float | None = None
|
|
70
|
+
error_message: str | None = None
|
|
71
|
+
|
|
72
|
+
name: str = 'mcp_client_event'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class MCPServerTelemetryEvent(BaseTelemetryEvent):
|
|
77
|
+
"""Telemetry event for MCP server usage"""
|
|
78
|
+
|
|
79
|
+
version: str
|
|
80
|
+
action: str # 'start', 'stop', 'tool_call'
|
|
81
|
+
tool_name: str | None = None
|
|
82
|
+
duration_seconds: float | None = None
|
|
83
|
+
error_message: str | None = None
|
|
84
|
+
parent_process_cmdline: str | None = None
|
|
85
|
+
|
|
86
|
+
name: str = 'mcp_server_event'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class CLITelemetryEvent(BaseTelemetryEvent):
|
|
91
|
+
"""Telemetry event for CLI usage"""
|
|
92
|
+
|
|
93
|
+
version: str
|
|
94
|
+
action: str # 'start', 'message_sent', 'task_completed', 'error'
|
|
95
|
+
mode: str # 'interactive', 'oneshot', 'mcp_server'
|
|
96
|
+
model: str | None = None
|
|
97
|
+
model_provider: str | None = None
|
|
98
|
+
duration_seconds: float | None = None
|
|
99
|
+
error_message: str | None = None
|
|
100
|
+
|
|
101
|
+
name: str = 'cli_event'
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom model pricing for models not available in LiteLLM's pricing data.
|
|
3
|
+
|
|
4
|
+
Prices are per token (not per 1M tokens).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
# Custom model pricing data
|
|
10
|
+
# Format matches LiteLLM's model_prices_and_context_window.json structure
|
|
11
|
+
CUSTOM_MODEL_PRICING: dict[str, dict[str, Any]] = {
|
|
12
|
+
'bu-1-0': {
|
|
13
|
+
'input_cost_per_token': 0.2 / 1_000_000, # $0.50 per 1M tokens
|
|
14
|
+
'output_cost_per_token': 2.00 / 1_000_000, # $3.00 per 1M tokens
|
|
15
|
+
'cache_read_input_token_cost': 0.02 / 1_000_000, # $0.10 per 1M tokens
|
|
16
|
+
'cache_creation_input_token_cost': None, # Not specified
|
|
17
|
+
'max_tokens': None, # Not specified
|
|
18
|
+
'max_input_tokens': None, # Not specified
|
|
19
|
+
'max_output_tokens': None, # Not specified
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
CUSTOM_MODEL_PRICING['bu-latest'] = CUSTOM_MODEL_PRICING['bu-1-0']
|
|
23
|
+
|
|
24
|
+
CUSTOM_MODEL_PRICING['smart'] = CUSTOM_MODEL_PRICING['bu-1-0']
|