lmnr 0.5.2__py3-none-any.whl → 0.5.3__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 (41) hide show
  1. lmnr/__init__.py +2 -2
  2. lmnr/cli.py +10 -8
  3. lmnr/{openllmetry_sdk → opentelemetry_lib}/__init__.py +3 -3
  4. lmnr/{openllmetry_sdk → opentelemetry_lib}/decorators/base.py +5 -5
  5. lmnr/{openllmetry_sdk → opentelemetry_lib}/instruments.py +1 -0
  6. lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/utils.py +1 -1
  7. lmnr/opentelemetry_lib/tracing/__init__.py +1 -0
  8. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/context_manager.py +1 -1
  9. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/tracing.py +23 -5
  10. lmnr/sdk/browser/browser_use_otel.py +20 -3
  11. lmnr/sdk/browser/patchright_otel.py +177 -0
  12. lmnr/sdk/browser/playwright_otel.py +16 -7
  13. lmnr/sdk/browser/pw_utils.py +118 -80
  14. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +98 -0
  15. lmnr/sdk/client/asynchronous/resources/agent.py +19 -0
  16. lmnr/sdk/client/synchronous/resources/agent.py +20 -0
  17. lmnr/sdk/decorators.py +3 -3
  18. lmnr/sdk/eval_control.py +3 -2
  19. lmnr/sdk/evaluations.py +8 -14
  20. lmnr/sdk/laminar.py +8 -8
  21. lmnr/sdk/types.py +2 -0
  22. lmnr/version.py +1 -1
  23. {lmnr-0.5.2.dist-info → lmnr-0.5.3.dist-info}/METADATA +2 -2
  24. lmnr-0.5.3.dist-info/RECORD +55 -0
  25. {lmnr-0.5.2.dist-info → lmnr-0.5.3.dist-info}/WHEEL +1 -1
  26. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  27. lmnr/sdk/browser/rrweb/rrweb.min.js +0 -18
  28. lmnr-0.5.2.dist-info/RECORD +0 -54
  29. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  30. /lmnr/{openllmetry_sdk → opentelemetry_lib}/config/__init__.py +0 -0
  31. /lmnr/{openllmetry_sdk → opentelemetry_lib}/decorators/__init__.py +0 -0
  32. /lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/__init__.py +0 -0
  33. /lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/config.py +0 -0
  34. /lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +0 -0
  35. /lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/content_allow_list.py +0 -0
  36. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  37. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/in_memory_span_exporter.py +0 -0
  38. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  39. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/package_check.py +0 -0
  40. {lmnr-0.5.2.dist-info → lmnr-0.5.3.dist-info}/LICENSE +0 -0
  41. {lmnr-0.5.2.dist-info → lmnr-0.5.3.dist-info}/entry_points.txt +0 -0
@@ -23,21 +23,29 @@ except ImportError as e:
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
- # Track pages we've already instrumented to avoid double-instrumentation
27
- instrumented_pages = set()
28
- async_instrumented_pages = set()
29
-
30
-
31
26
  current_dir = os.path.dirname(os.path.abspath(__file__))
32
- with open(os.path.join(current_dir, "rrweb", "rrweb.min.js"), "r") as f:
27
+ with open(os.path.join(current_dir, "rrweb", "rrweb.umd.min.cjs"), "r") as f:
33
28
  RRWEB_CONTENT = f"() => {{ {f.read()} }}"
34
29
 
35
30
  INJECT_PLACEHOLDER = """
36
31
  () => {
37
32
  const BATCH_SIZE = 1000; // Maximum events to store in memory
38
-
33
+
39
34
  window.lmnrRrwebEventsBatch = new Set();
40
35
 
36
+ // Track page focus state
37
+ window.lmnrPageIsFocused = true;
38
+
39
+ window.addEventListener('blur', () => {
40
+ window.lmnrPageIsFocused = false;
41
+ console.log('Page lost focus');
42
+ });
43
+
44
+ window.addEventListener('focus', () => {
45
+ window.lmnrPageIsFocused = true;
46
+ console.log('Page gained focus');
47
+ });
48
+
41
49
  // Utility function to compress individual event data
42
50
  async function compressEventData(data) {
43
51
  const jsonString = JSON.stringify(data);
@@ -47,7 +55,7 @@ INJECT_PLACEHOLDER = """
47
55
  const compressedData = await compressedResponse.arrayBuffer();
48
56
  return Array.from(new Uint8Array(compressedData));
49
57
  }
50
-
58
+
51
59
  window.lmnrGetAndClearEvents = () => {
52
60
  const events = window.lmnrRrwebEventsBatch;
53
61
  window.lmnrRrwebEventsBatch = new Set();
@@ -56,26 +64,24 @@ INJECT_PLACEHOLDER = """
56
64
 
57
65
  // Add heartbeat events
58
66
  setInterval(async () => {
59
- const heartbeat = {
60
- type: 6,
61
- data: await compressEventData({ source: 'heartbeat' }),
62
- timestamp: Date.now()
63
- };
64
-
65
- window.lmnrRrwebEventsBatch.add(heartbeat);
66
-
67
- // Prevent memory issues by limiting batch size
68
- if (window.lmnrRrwebEventsBatch.size > BATCH_SIZE) {
69
- window.lmnrRrwebEventsBatch = new Set(Array.from(window.lmnrRrwebEventsBatch).slice(-BATCH_SIZE));
67
+ if (!window.lmnrPageIsFocused) {
68
+ return;
70
69
  }
70
+
71
+ window.lmnrRrweb.record.addCustomEvent('heartbeat', {
72
+ title: document.title,
73
+ url: document.URL,
74
+ })
75
+
71
76
  }, 1000);
72
77
 
73
78
  window.lmnrRrweb.record({
74
79
  async emit(event) {
75
- // Ignore events from all tabs except the current one
76
- if (document.visibilityState === 'hidden' || document.hidden) {
80
+ // Ignore events when page is not focused
81
+ if (!window.lmnrPageIsFocused) {
77
82
  return;
78
83
  }
84
+
79
85
  // Compress the data field
80
86
  const compressedEvent = {
81
87
  ...event,
@@ -94,22 +100,26 @@ async def send_events_async(
94
100
  """Fetch events from the page and send them to the server"""
95
101
  try:
96
102
  # Check if function exists first
97
- has_function = await page.evaluate(
98
- """
99
- () => typeof window.lmnrGetAndClearEvents === 'function'
100
- """
101
- )
102
- if not has_function:
103
- return
103
+ events = await page.evaluate("""
104
+ () => {
105
+ if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
106
+ return [];
107
+ }
108
+ return window.lmnrGetAndClearEvents();
109
+ }
110
+ """)
104
111
 
105
- events = await page.evaluate("window.lmnrGetAndClearEvents()")
106
112
  if not events or len(events) == 0:
107
113
  return
108
114
 
109
115
  await client._browser_events.send(session_id, trace_id, events)
110
-
111
116
  except Exception as e:
112
- logger.error(f"Error sending events: {e}")
117
+ if str(e).startswith("Page.evaluate: Execution context was destroyed"):
118
+ logger.info("Execution context was destroyed, injecting rrweb again")
119
+ await inject_rrweb_async(page)
120
+ await send_events_async(page, session_id, trace_id, client)
121
+ else:
122
+ logger.debug(f"Could not send events: {e}")
113
123
 
114
124
 
115
125
  def send_events_sync(
@@ -117,23 +127,26 @@ def send_events_sync(
117
127
  ):
118
128
  """Synchronous version of send_events"""
119
129
  try:
120
- # Check if function exists first
121
- has_function = page.evaluate(
122
- """
123
- () => typeof window.lmnrGetAndClearEvents === 'function'
124
- """
125
- )
126
- if not has_function:
127
- return
128
-
129
- events = page.evaluate("window.lmnrGetAndClearEvents()")
130
+ events = page.evaluate("""
131
+ () => {
132
+ if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
133
+ return [];
134
+ }
135
+ return window.lmnrGetAndClearEvents();
136
+ }
137
+ """)
130
138
  if not events or len(events) == 0:
131
139
  return
132
140
 
133
141
  client._browser_events.send(session_id, trace_id, events)
134
142
 
135
143
  except Exception as e:
136
- logger.error(f"Error sending events: {e}")
144
+ if str(e).startswith("Page.evaluate: Execution context was destroyed"):
145
+ logger.info("Execution context was destroyed, injecting rrweb again")
146
+ inject_rrweb_sync(page)
147
+ send_events_sync(page, session_id, trace_id, client)
148
+ else:
149
+ logger.debug(f"Could not send events: {e}")
137
150
 
138
151
 
139
152
  def inject_rrweb_sync(page: SyncPage):
@@ -154,10 +167,6 @@ def inject_rrweb_sync(page: SyncPage):
154
167
  def load_rrweb():
155
168
  try:
156
169
  page.evaluate(RRWEB_CONTENT)
157
- page.wait_for_function(
158
- """(() => typeof window.lmnrRrweb !== 'undefined')""",
159
- timeout=5000,
160
- )
161
170
  return True
162
171
  except Exception as e:
163
172
  logger.debug(f"Failed to load rrweb: {e}")
@@ -195,10 +204,6 @@ async def inject_rrweb_async(page: Page):
195
204
  async def load_rrweb():
196
205
  try:
197
206
  await page.evaluate(RRWEB_CONTENT)
198
- await page.wait_for_function(
199
- """(() => typeof window.lmnrRrweb !== 'undefined')""",
200
- timeout=5000,
201
- )
202
207
  return True
203
208
  except Exception as e:
204
209
  logger.debug(f"Failed to load rrweb: {e}")
@@ -223,11 +228,23 @@ def handle_navigation_sync(
223
228
  page: SyncPage, session_id: str, trace_id: str, client: LaminarClient
224
229
  ):
225
230
  trace.get_current_span().set_attribute("lmnr.internal.has_browser_session", True)
226
- # Check if we've already instrumented this page
227
- page_id = id(page)
228
- if page_id in instrumented_pages:
229
- return
230
- instrumented_pages.add(page_id)
231
+ original_bring_to_front = page.bring_to_front
232
+
233
+ def bring_to_front():
234
+ original_bring_to_front()
235
+ page.evaluate(
236
+ """() => {
237
+ if (window.lmnrRrweb) {
238
+ try {
239
+ window.lmnrRrweb.record.takeFullSnapshot();
240
+ } catch (e) {
241
+ console.error("Error taking full snapshot:", e);
242
+ }
243
+ }
244
+ }"""
245
+ )
246
+
247
+ page.bring_to_front = bring_to_front
231
248
 
232
249
  def on_load():
233
250
  try:
@@ -235,49 +252,37 @@ def handle_navigation_sync(
235
252
  except Exception as e:
236
253
  logger.error(f"Error in on_load handler: {e}")
237
254
 
238
- page.on("load", on_load)
239
- inject_rrweb_sync(page)
240
-
241
255
  def collection_loop():
242
256
  while not page.is_closed(): # Stop when page closes
243
257
  send_events_sync(page, session_id, trace_id, client)
244
258
  time.sleep(2)
245
259
 
246
- # Clean up when page closes
247
- if page_id in instrumented_pages:
248
- instrumented_pages.remove(page_id)
249
-
250
260
  thread = threading.Thread(target=collection_loop, daemon=True)
251
261
  thread.start()
252
262
 
263
+ def on_close():
264
+ try:
265
+ send_events_sync(page, session_id, trace_id, client)
266
+ thread.join()
267
+ except Exception:
268
+ pass
269
+
270
+ page.on("load", on_load)
271
+ page.on("close", on_close)
272
+ inject_rrweb_sync(page)
273
+
253
274
 
254
275
  @observe(name="playwright.page", ignore_input=True, ignore_output=True)
255
276
  async def handle_navigation_async(
256
277
  page: Page, session_id: str, trace_id: str, client: AsyncLaminarClient
257
278
  ):
258
279
  trace.get_current_span().set_attribute("lmnr.internal.has_browser_session", True)
259
- # Check if we've already instrumented this page
260
- page_id = id(page)
261
- if page_id in async_instrumented_pages:
262
- return
263
- async_instrumented_pages.add(page_id)
264
-
265
- async def on_load():
266
- try:
267
- await inject_rrweb_async(page)
268
- except Exception as e:
269
- logger.error(f"Error in on_load handler: {e}")
270
-
271
- page.on("load", lambda: asyncio.create_task(on_load()))
272
- await inject_rrweb_async(page)
273
280
 
274
281
  async def collection_loop():
275
282
  try:
276
283
  while not page.is_closed(): # Stop when page closes
277
284
  await send_events_async(page, session_id, trace_id, client)
278
285
  await asyncio.sleep(2)
279
- # Clean up when page closes
280
- async_instrumented_pages.remove(page_id)
281
286
  logger.info("Event collection stopped")
282
287
  except Exception as e:
283
288
  logger.error(f"Event collection stopped: {e}")
@@ -285,5 +290,38 @@ async def handle_navigation_async(
285
290
  # Create and store task
286
291
  task = asyncio.create_task(collection_loop())
287
292
 
288
- # Clean up task when page closes
289
- page.on("close", lambda: task.cancel())
293
+ async def on_load():
294
+ try:
295
+ await inject_rrweb_async(page)
296
+ except Exception as e:
297
+ logger.error(f"Error in on_load handler: {e}")
298
+
299
+ async def on_close():
300
+ try:
301
+ task.cancel()
302
+ await send_events_async(page, session_id, trace_id, client)
303
+ except Exception:
304
+ pass
305
+
306
+ page.on("load", lambda: asyncio.create_task(on_load()))
307
+ page.on("close", lambda: asyncio.create_task(on_close()))
308
+
309
+ original_bring_to_front = page.bring_to_front
310
+
311
+ async def bring_to_front():
312
+ await original_bring_to_front()
313
+
314
+ await page.evaluate(
315
+ """() => {
316
+ if (window.lmnrRrweb) {
317
+ try {
318
+ window.lmnrRrweb.record.takeFullSnapshot();
319
+ } catch (e) {
320
+ console.error("Error taking full snapshot:", e);
321
+ }
322
+ }
323
+ }"""
324
+ )
325
+
326
+ page.bring_to_front = bring_to_front
327
+ await inject_rrweb_async(page)