lmnr 0.6.16__py3-none-any.whl → 0.7.26__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 (113) hide show
  1. lmnr/__init__.py +6 -15
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/{cli.py → cli/evals.py} +20 -102
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +9 -2
  7. lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
  8. lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
  9. lmnr/opentelemetry_lib/litellm/utils.py +82 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  56. lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
  57. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
  58. lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
  59. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  60. lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
  61. lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
  62. lmnr/opentelemetry_lib/tracing/processor.py +128 -30
  63. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  64. lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
  65. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  66. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  67. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  68. lmnr/sdk/browser/background_send_events.py +158 -0
  69. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  70. lmnr/sdk/browser/browser_use_otel.py +12 -12
  71. lmnr/sdk/browser/bubus_otel.py +71 -0
  72. lmnr/sdk/browser/cdp_utils.py +518 -0
  73. lmnr/sdk/browser/inject_script.js +514 -0
  74. lmnr/sdk/browser/patchright_otel.py +18 -44
  75. lmnr/sdk/browser/playwright_otel.py +104 -187
  76. lmnr/sdk/browser/pw_utils.py +249 -210
  77. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  78. lmnr/sdk/browser/utils.py +1 -1
  79. lmnr/sdk/client/asynchronous/async_client.py +47 -15
  80. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
  81. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  82. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  83. lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
  84. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  85. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  86. lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
  87. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  88. lmnr/sdk/client/synchronous/resources/evals.py +83 -17
  89. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  90. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  91. lmnr/sdk/client/synchronous/sync_client.py +47 -15
  92. lmnr/sdk/datasets/__init__.py +94 -0
  93. lmnr/sdk/datasets/file_utils.py +91 -0
  94. lmnr/sdk/decorators.py +103 -23
  95. lmnr/sdk/evaluations.py +122 -33
  96. lmnr/sdk/laminar.py +816 -333
  97. lmnr/sdk/log.py +7 -2
  98. lmnr/sdk/types.py +124 -143
  99. lmnr/sdk/utils.py +115 -2
  100. lmnr/version.py +1 -1
  101. {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
  102. lmnr-0.7.26.dist-info/RECORD +116 -0
  103. lmnr-0.7.26.dist-info/WHEEL +4 -0
  104. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  105. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  106. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  107. lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
  108. lmnr/sdk/client/synchronous/resources/agent.py +0 -323
  109. lmnr/sdk/datasets.py +0 -60
  110. lmnr-0.6.16.dist-info/LICENSE +0 -75
  111. lmnr-0.6.16.dist-info/RECORD +0 -61
  112. lmnr-0.6.16.dist-info/WHEEL +0 -4
  113. lmnr-0.6.16.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,518 @@
1
+ import asyncio
2
+ import orjson
3
+ import os
4
+ import threading
5
+ import time
6
+
7
+ from weakref import WeakKeyDictionary
8
+
9
+ from opentelemetry import trace
10
+
11
+ from lmnr.sdk.decorators import observe
12
+ from lmnr.sdk.browser.utils import retry_async
13
+ from lmnr.sdk.browser.background_send_events import (
14
+ get_background_loop,
15
+ track_async_send,
16
+ )
17
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
18
+ from lmnr.opentelemetry_lib.tracing.context import get_current_context
19
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
20
+ from lmnr.sdk.log import get_default_logger
21
+ from lmnr.sdk.types import MaskInputOptions
22
+
23
+ logger = get_default_logger(__name__)
24
+
25
+ OLD_BUFFER_TIMEOUT = 60
26
+ CDP_OPERATION_TIMEOUT_SECONDS = 10
27
+
28
+ # CDP ContextId is int
29
+ frame_to_isolated_context_id: dict[str, int] = {}
30
+
31
+ # Store locks per event loop to avoid pytest-asyncio issues
32
+ _locks: WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Lock] = WeakKeyDictionary()
33
+ _locks_lock = threading.Lock()
34
+ _fallback_lock = asyncio.Lock()
35
+
36
+
37
+ def get_lock() -> asyncio.Lock:
38
+ """Get or create a lock for the current event loop.
39
+
40
+ This ensures each event loop gets its own lock instance, avoiding
41
+ cross-event-loop binding issues that occur in pytest-asyncio when
42
+ tests run in different event loops.
43
+ """
44
+ try:
45
+ loop = asyncio.get_running_loop()
46
+ except RuntimeError:
47
+ # No event loop running
48
+ logger.warning("No event loop running, using fallback lock")
49
+ return _fallback_lock
50
+
51
+ with _locks_lock:
52
+ if loop not in _locks:
53
+ _locks[loop] = asyncio.Lock()
54
+ return _locks[loop]
55
+
56
+
57
+ current_dir = os.path.dirname(os.path.abspath(__file__))
58
+ with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
59
+ RRWEB_CONTENT = f"() => {{ {f.read()} }}"
60
+
61
+ with open(os.path.join(current_dir, "inject_script.js"), "r") as f:
62
+ INJECT_SCRIPT_CONTENT = f.read()
63
+
64
+
65
+ async def should_skip_page(cdp_session):
66
+ """Checks if the page url is an error page or an empty page.
67
+ This function returns True in case of any error in our code, because
68
+ it is safer to not record events than to try to inject the recorder
69
+ into something that is already broken.
70
+ """
71
+ cdp_client = cdp_session.cdp_client
72
+
73
+ try:
74
+ # Get the current page URL
75
+ result = await asyncio.wait_for(
76
+ cdp_client.send.Runtime.evaluate(
77
+ {
78
+ "expression": "window.location.href",
79
+ "returnByValue": True,
80
+ },
81
+ session_id=cdp_session.session_id,
82
+ ),
83
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
84
+ )
85
+
86
+ url = result.get("result", {}).get("value", "")
87
+
88
+ # Comprehensive list of browser error URLs
89
+ error_url_patterns = [
90
+ "about:blank",
91
+ # Chrome error pages
92
+ "chrome-error://",
93
+ "chrome://network-error/",
94
+ "chrome://network-errors/",
95
+ # Chrome crash and debugging pages
96
+ "chrome://crash/",
97
+ "chrome://crashdump/",
98
+ "chrome://kill/",
99
+ "chrome://hang/",
100
+ "chrome://shorthang/",
101
+ "chrome://gpuclean/",
102
+ "chrome://gpucrash/",
103
+ "chrome://gpuhang/",
104
+ "chrome://memory-exhaust/",
105
+ "chrome://memory-pressure-critical/",
106
+ "chrome://memory-pressure-moderate/",
107
+ "chrome://inducebrowsercrashforrealz/",
108
+ "chrome://inducebrowserdcheckforrealz/",
109
+ "chrome://inducebrowserheapcorruption/",
110
+ "chrome://heapcorruptioncrash/",
111
+ "chrome://badcastcrash/",
112
+ "chrome://ppapiflashcrash/",
113
+ "chrome://ppapiflashhang/",
114
+ "chrome://quit/",
115
+ "chrome://restart/",
116
+ # Firefox error pages
117
+ "about:neterror",
118
+ "about:certerror",
119
+ "about:blocked",
120
+ # Firefox crash and debugging pages
121
+ "about:crashcontent",
122
+ "about:crashparent",
123
+ "about:crashes",
124
+ "about:tabcrashed",
125
+ # Edge error pages (similar to Chrome)
126
+ "edge-error://",
127
+ "edge://crash/",
128
+ "edge://kill/",
129
+ "edge://hang/",
130
+ # Safari/WebKit error indicators (data URLs with error content)
131
+ "webkit-error://",
132
+ ]
133
+
134
+ # Check if current URL matches any error pattern
135
+ if any(url.startswith(pattern) for pattern in error_url_patterns):
136
+ logger.debug(f"Detected browser error page from URL: {url}")
137
+ return True
138
+
139
+ # Additional check for data URLs that might contain error pages
140
+ if url.startswith("data:") and any(
141
+ error_term in url.lower()
142
+ for error_term in ["error", "crash", "failed", "unavailable", "not found"]
143
+ ):
144
+ logger.debug(f"Detected error page from data URL: {url[:100]}...")
145
+ return True
146
+
147
+ return False
148
+
149
+ except asyncio.TimeoutError:
150
+ logger.debug("Timeout error when checking if error page")
151
+ return True
152
+ except Exception as e:
153
+ logger.debug(f"Error during checking if error page: {e}")
154
+ return True
155
+
156
+
157
+ def get_mask_input_setting() -> MaskInputOptions:
158
+ """Get the mask_input setting from session recording configuration."""
159
+ try:
160
+ config = TracerWrapper.get_session_recording_options()
161
+ return config.get(
162
+ "mask_input_options",
163
+ MaskInputOptions(
164
+ textarea=False,
165
+ text=False,
166
+ number=False,
167
+ select=False,
168
+ email=False,
169
+ tel=False,
170
+ ),
171
+ )
172
+ except (AttributeError, Exception):
173
+ # Fallback to default configuration if TracerWrapper is not initialized
174
+ return MaskInputOptions(
175
+ textarea=False,
176
+ text=False,
177
+ number=False,
178
+ select=False,
179
+ email=False,
180
+ tel=False,
181
+ )
182
+
183
+
184
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
185
+ async def get_isolated_context_id(cdp_session) -> int | None:
186
+ async with get_lock():
187
+ tree = {}
188
+ try:
189
+ tree = await asyncio.wait_for(
190
+ cdp_session.cdp_client.send.Page.getFrameTree(
191
+ session_id=cdp_session.session_id
192
+ ),
193
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
194
+ )
195
+ except asyncio.TimeoutError:
196
+ logger.debug("Timeout error when getting frame tree")
197
+ return None
198
+ except Exception as e:
199
+ logger.debug(f"Failed to get frame tree: {e}")
200
+ return None
201
+ frame = tree.get("frameTree", {}).get("frame", {})
202
+ frame_id = frame.get("id")
203
+ loader_id = frame.get("loaderId")
204
+
205
+ if frame_id is None or loader_id is None:
206
+ logger.debug("Failed to get frame id or loader id")
207
+ return None
208
+ key = f"{frame_id}_{loader_id}"
209
+
210
+ if key in frame_to_isolated_context_id:
211
+ return frame_to_isolated_context_id[key]
212
+
213
+ try:
214
+ result = await asyncio.wait_for(
215
+ cdp_session.cdp_client.send.Page.createIsolatedWorld(
216
+ {
217
+ "frameId": frame_id,
218
+ "worldName": "laminar-isolated-context",
219
+ },
220
+ session_id=cdp_session.session_id,
221
+ ),
222
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
223
+ )
224
+ except asyncio.TimeoutError:
225
+ logger.debug("Timeout error when getting isolated context id")
226
+ return None
227
+ except Exception as e:
228
+ logger.debug(f"Failed to get isolated context id: {e}")
229
+ return None
230
+ isolated_context_id = result["executionContextId"]
231
+ frame_to_isolated_context_id[key] = isolated_context_id
232
+ return isolated_context_id
233
+
234
+
235
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
236
+ async def inject_session_recorder(cdp_session) -> int | None:
237
+ """Injects the session recorder base as well as the recorder itself.
238
+ Returns the isolated context id if successful.
239
+ """
240
+ isolated_context_id = None
241
+ cdp_client = cdp_session.cdp_client
242
+ try:
243
+ should_skip = True
244
+ try:
245
+ should_skip = await should_skip_page(cdp_session)
246
+ except Exception as e:
247
+ logger.debug(f"Failed to check if error page: {e}")
248
+
249
+ if should_skip:
250
+ logger.debug("Empty page detected, skipping session recorder injection")
251
+ return
252
+
253
+ isolated_context_id = await get_isolated_context_id(cdp_session)
254
+ try:
255
+ is_loaded = await is_recorder_present(cdp_session, isolated_context_id)
256
+ except Exception as e:
257
+ logger.debug(f"Failed to check if session recorder is loaded: {e}")
258
+ is_loaded = False
259
+
260
+ if is_loaded:
261
+ return
262
+
263
+ if isolated_context_id is None:
264
+ logger.debug("Failed to get isolated context id")
265
+ return
266
+
267
+ async def load_session_recorder():
268
+ try:
269
+ await asyncio.wait_for(
270
+ cdp_client.send.Runtime.evaluate(
271
+ {
272
+ "expression": f"({RRWEB_CONTENT})()",
273
+ "contextId": isolated_context_id,
274
+ },
275
+ session_id=cdp_session.session_id,
276
+ ),
277
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
278
+ )
279
+ return True
280
+ except asyncio.TimeoutError:
281
+ logger.debug("Timeout error when loading session recorder base")
282
+ return False
283
+ except Exception as e:
284
+ logger.debug(f"Failed to load session recorder base: {e}")
285
+ return False
286
+
287
+ if not await retry_async(
288
+ load_session_recorder,
289
+ retries=3,
290
+ delay=1,
291
+ error_message="Failed to load session recorder",
292
+ ):
293
+ return
294
+
295
+ try:
296
+ await asyncio.wait_for(
297
+ cdp_client.send.Runtime.evaluate(
298
+ {
299
+ "expression": f"({INJECT_SCRIPT_CONTENT})({orjson.dumps(get_mask_input_setting()).decode('utf-8')}, true)",
300
+ "contextId": isolated_context_id,
301
+ },
302
+ session_id=cdp_session.session_id,
303
+ ),
304
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
305
+ )
306
+ return isolated_context_id
307
+ except asyncio.TimeoutError:
308
+ logger.debug("Timeout error when injecting session recorder")
309
+ except Exception as e:
310
+ logger.debug(f"Failed to inject recorder: {e}")
311
+
312
+ except Exception as e:
313
+ logger.debug(f"Error during session recorder injection: {e}")
314
+
315
+
316
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
317
+ @observe(name="cdp_use.session", ignore_input=True, ignore_output=True)
318
+ async def start_recording_events(
319
+ cdp_session,
320
+ lmnr_session_id: str,
321
+ client: AsyncLaminarClient,
322
+ ):
323
+ cdp_client = cdp_session.cdp_client
324
+
325
+ ctx = get_current_context()
326
+ span = trace.get_current_span(ctx)
327
+ trace_id = format(span.get_span_context().trace_id, "032x")
328
+ span.set_attribute("lmnr.internal.has_browser_session", True)
329
+
330
+ isolated_context_id = await inject_session_recorder(cdp_session)
331
+ if isolated_context_id is None:
332
+ logger.debug("Failed to inject session recorder, not registering bindings")
333
+ return
334
+
335
+ # Get the background loop for async sends (independent of CDP's loop)
336
+ background_loop = get_background_loop()
337
+
338
+ # Buffer for reassembling chunks
339
+ chunk_buffers = {}
340
+
341
+ async def send_events_from_browser(chunk: dict):
342
+ try:
343
+ # Handle chunked data
344
+ batch_id = chunk["batchId"]
345
+ chunk_index = chunk["chunkIndex"]
346
+ total_chunks = chunk["totalChunks"]
347
+ data = chunk["data"]
348
+
349
+ # Initialize buffer for this batch if needed
350
+ if batch_id not in chunk_buffers:
351
+ chunk_buffers[batch_id] = {
352
+ "chunks": {},
353
+ "total": total_chunks,
354
+ "timestamp": time.time(),
355
+ }
356
+
357
+ # Store chunk
358
+ chunk_buffers[batch_id]["chunks"][chunk_index] = data
359
+
360
+ # Check if we have all chunks
361
+ if len(chunk_buffers[batch_id]["chunks"]) == total_chunks:
362
+ # Reassemble the full message
363
+ full_data = ""
364
+ for i in range(total_chunks):
365
+ full_data += chunk_buffers[batch_id]["chunks"][i]
366
+
367
+ # Parse the JSON
368
+ events = orjson.loads(full_data)
369
+
370
+ # Send to server in background loop (independent of CDP's loop)
371
+ if events and len(events) > 0:
372
+ future = asyncio.run_coroutine_threadsafe(
373
+ client._browser_events.send(lmnr_session_id, trace_id, events),
374
+ background_loop,
375
+ )
376
+ track_async_send(future)
377
+
378
+ # Clean up buffer
379
+ del chunk_buffers[batch_id]
380
+
381
+ # Clean up old incomplete buffers
382
+ current_time = time.time()
383
+ to_delete = []
384
+ for bid, buffer in chunk_buffers.items():
385
+ if current_time - buffer["timestamp"] > OLD_BUFFER_TIMEOUT:
386
+ to_delete.append(bid)
387
+ for bid in to_delete:
388
+ logger.debug(f"Cleaning up incomplete chunk buffer: {bid}")
389
+ del chunk_buffers[bid]
390
+
391
+ except Exception as e:
392
+ logger.debug(f"Could not send events: {e}")
393
+
394
+ # cdp_use.cdp.runtime.events.BindingCalledEvent
395
+ async def send_events_callback(event, cdp_session_id: str | None = None):
396
+ if event["name"] != "lmnrSendEvents":
397
+ return
398
+ if event["executionContextId"] != isolated_context_id:
399
+ return
400
+ asyncio.create_task(send_events_from_browser(orjson.loads(event["payload"])))
401
+
402
+ await cdp_client.send.Runtime.addBinding(
403
+ {
404
+ "name": "lmnrSendEvents",
405
+ "executionContextId": isolated_context_id,
406
+ },
407
+ session_id=cdp_session.session_id,
408
+ )
409
+ cdp_client.register.Runtime.bindingCalled(send_events_callback)
410
+
411
+ await enable_target_discovery(cdp_session)
412
+ register_on_target_created(cdp_session, lmnr_session_id, client)
413
+
414
+
415
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
416
+ async def enable_target_discovery(cdp_session):
417
+ cdp_client = cdp_session.cdp_client
418
+ await cdp_client.send.Target.setDiscoverTargets(
419
+ {
420
+ "discover": True,
421
+ },
422
+ session_id=cdp_session.session_id,
423
+ )
424
+
425
+
426
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
427
+ def register_on_target_created(
428
+ cdp_session, lmnr_session_id: str, client: AsyncLaminarClient
429
+ ):
430
+ # cdp_use.cdp.target.events.TargetCreatedEvent
431
+ def on_target_created(event, cdp_session_id: str | None = None):
432
+ target_info = event["targetInfo"]
433
+ if target_info["type"] == "page":
434
+ asyncio.create_task(inject_session_recorder(cdp_session=cdp_session))
435
+
436
+ try:
437
+ cdp_session.cdp_client.register.Target.targetCreated(on_target_created)
438
+ except Exception as e:
439
+ logger.debug(f"Failed to register on target created: {e}")
440
+
441
+
442
+ # browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
443
+ async def is_recorder_present(
444
+ cdp_session, isolated_context_id: int | None = None
445
+ ) -> bool:
446
+ # This function returns True on any error, because it is safer to not record
447
+ # events than to try to inject the recorder into a broken context.
448
+ cdp_client = cdp_session.cdp_client
449
+ if isolated_context_id is None:
450
+ isolated_context_id = await get_isolated_context_id(cdp_session)
451
+ if isolated_context_id is None:
452
+ logger.debug("Failed to get isolated context id")
453
+ return True
454
+
455
+ try:
456
+ result = await asyncio.wait_for(
457
+ cdp_client.send.Runtime.evaluate(
458
+ {
459
+ "expression": "typeof window.lmnrRrweb !== 'undefined'",
460
+ "contextId": isolated_context_id,
461
+ },
462
+ session_id=cdp_session.session_id,
463
+ ),
464
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
465
+ )
466
+ if result and "result" in result and "value" in result["result"]:
467
+ return result["result"]["value"]
468
+ return False
469
+ except asyncio.TimeoutError:
470
+ logger.debug("Timeout error when checking if session recorder is present")
471
+ return True
472
+ except Exception:
473
+ logger.debug("Exception when checking if session recorder is present")
474
+ return True
475
+
476
+
477
+ async def take_full_snapshot(cdp_session):
478
+ cdp_client = cdp_session.cdp_client
479
+ isolated_context_id = await get_isolated_context_id(cdp_session)
480
+ if isolated_context_id is None:
481
+ logger.debug("Failed to get isolated context id")
482
+ return False
483
+
484
+ if await should_skip_page(cdp_session):
485
+ logger.debug("Skipping full snapshot")
486
+ return False
487
+
488
+ try:
489
+ result = await asyncio.wait_for(
490
+ cdp_client.send.Runtime.evaluate(
491
+ {
492
+ "expression": """(() => {
493
+ if (window.lmnrRrweb) {
494
+ try {
495
+ window.lmnrRrweb.record.takeFullSnapshot();
496
+ return true;
497
+ } catch (e) {
498
+ console.error("Error taking full snapshot:", e);
499
+ return false;
500
+ }
501
+ }
502
+ return false;
503
+ })()""",
504
+ "contextId": isolated_context_id,
505
+ },
506
+ session_id=cdp_session.session_id,
507
+ ),
508
+ timeout=CDP_OPERATION_TIMEOUT_SECONDS,
509
+ )
510
+ except asyncio.TimeoutError:
511
+ logger.debug("Timeout error when taking full snapshot")
512
+ return False
513
+ except Exception as e:
514
+ logger.debug(f"Error when taking full snapshot: {e}")
515
+ return False
516
+ if result and "result" in result and "value" in result["result"]:
517
+ return result["result"]["value"]
518
+ return False