lmnr 0.5.2__py3-none-any.whl → 0.6.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 (48) hide show
  1. lmnr/__init__.py +7 -2
  2. lmnr/cli.py +10 -8
  3. lmnr/opentelemetry_lib/__init__.py +55 -0
  4. lmnr/{openllmetry_sdk/decorators/base.py → opentelemetry_lib/decorators/__init__.py} +24 -15
  5. lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/utils.py +1 -1
  6. lmnr/opentelemetry_lib/tracing/__init__.py +139 -0
  7. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +398 -0
  8. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +14 -7
  9. lmnr/opentelemetry_lib/tracing/context_properties.py +53 -0
  10. lmnr/opentelemetry_lib/tracing/exporter.py +60 -0
  11. lmnr/opentelemetry_lib/tracing/instruments.py +121 -0
  12. lmnr/opentelemetry_lib/tracing/processor.py +96 -0
  13. lmnr/{openllmetry_sdk/tracing/context_manager.py → opentelemetry_lib/tracing/tracer.py} +6 -1
  14. lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/package_check.py +3 -1
  15. lmnr/sdk/browser/browser_use_otel.py +20 -3
  16. lmnr/sdk/browser/patchright_otel.py +177 -0
  17. lmnr/sdk/browser/playwright_otel.py +16 -7
  18. lmnr/sdk/browser/pw_utils.py +116 -74
  19. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +98 -0
  20. lmnr/sdk/client/asynchronous/resources/agent.py +22 -1
  21. lmnr/sdk/client/synchronous/resources/agent.py +23 -1
  22. lmnr/sdk/decorators.py +5 -3
  23. lmnr/sdk/eval_control.py +3 -2
  24. lmnr/sdk/evaluations.py +10 -16
  25. lmnr/sdk/laminar.py +16 -34
  26. lmnr/sdk/types.py +2 -0
  27. lmnr/sdk/utils.py +2 -3
  28. lmnr/version.py +1 -1
  29. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/METADATA +65 -63
  30. lmnr-0.6.0.dist-info/RECORD +54 -0
  31. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/WHEEL +1 -1
  32. lmnr/openllmetry_sdk/__init__.py +0 -75
  33. lmnr/openllmetry_sdk/config/__init__.py +0 -12
  34. lmnr/openllmetry_sdk/decorators/__init__.py +0 -0
  35. lmnr/openllmetry_sdk/instruments.py +0 -41
  36. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  37. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  38. lmnr/openllmetry_sdk/tracing/tracing.py +0 -998
  39. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  40. lmnr/sdk/browser/rrweb/rrweb.min.js +0 -18
  41. lmnr-0.5.2.dist-info/RECORD +0 -54
  42. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  43. /lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/__init__.py +0 -0
  44. /lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/config.py +0 -0
  45. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  46. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  47. {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/LICENSE +0 -0
  48. {lmnr-0.5.2.dist-info → lmnr-0.6.0.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,28 @@ 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(
103
+ events = await page.evaluate(
98
104
  """
99
- () => typeof window.lmnrGetAndClearEvents === 'function'
105
+ () => {
106
+ if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
107
+ return [];
108
+ }
109
+ return window.lmnrGetAndClearEvents();
110
+ }
100
111
  """
101
112
  )
102
- if not has_function:
103
- return
104
113
 
105
- events = await page.evaluate("window.lmnrGetAndClearEvents()")
106
114
  if not events or len(events) == 0:
107
115
  return
108
116
 
109
117
  await client._browser_events.send(session_id, trace_id, events)
110
-
111
118
  except Exception as e:
112
- logger.error(f"Error sending events: {e}")
119
+ if str(e).startswith("Page.evaluate: Execution context was destroyed"):
120
+ logger.info("Execution context was destroyed, injecting rrweb again")
121
+ await inject_rrweb_async(page)
122
+ await send_events_async(page, session_id, trace_id, client)
123
+ else:
124
+ logger.debug(f"Could not send events: {e}")
113
125
 
114
126
 
115
127
  def send_events_sync(
@@ -117,23 +129,28 @@ def send_events_sync(
117
129
  ):
118
130
  """Synchronous version of send_events"""
119
131
  try:
120
- # Check if function exists first
121
- has_function = page.evaluate(
132
+ events = page.evaluate(
122
133
  """
123
- () => typeof window.lmnrGetAndClearEvents === 'function'
134
+ () => {
135
+ if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
136
+ return [];
137
+ }
138
+ return window.lmnrGetAndClearEvents();
139
+ }
124
140
  """
125
141
  )
126
- if not has_function:
127
- return
128
-
129
- events = page.evaluate("window.lmnrGetAndClearEvents()")
130
142
  if not events or len(events) == 0:
131
143
  return
132
144
 
133
145
  client._browser_events.send(session_id, trace_id, events)
134
146
 
135
147
  except Exception as e:
136
- logger.error(f"Error sending events: {e}")
148
+ if str(e).startswith("Page.evaluate: Execution context was destroyed"):
149
+ logger.info("Execution context was destroyed, injecting rrweb again")
150
+ inject_rrweb_sync(page)
151
+ send_events_sync(page, session_id, trace_id, client)
152
+ else:
153
+ logger.debug(f"Could not send events: {e}")
137
154
 
138
155
 
139
156
  def inject_rrweb_sync(page: SyncPage):
@@ -154,10 +171,6 @@ def inject_rrweb_sync(page: SyncPage):
154
171
  def load_rrweb():
155
172
  try:
156
173
  page.evaluate(RRWEB_CONTENT)
157
- page.wait_for_function(
158
- """(() => typeof window.lmnrRrweb !== 'undefined')""",
159
- timeout=5000,
160
- )
161
174
  return True
162
175
  except Exception as e:
163
176
  logger.debug(f"Failed to load rrweb: {e}")
@@ -195,10 +208,6 @@ async def inject_rrweb_async(page: Page):
195
208
  async def load_rrweb():
196
209
  try:
197
210
  await page.evaluate(RRWEB_CONTENT)
198
- await page.wait_for_function(
199
- """(() => typeof window.lmnrRrweb !== 'undefined')""",
200
- timeout=5000,
201
- )
202
211
  return True
203
212
  except Exception as e:
204
213
  logger.debug(f"Failed to load rrweb: {e}")
@@ -223,11 +232,23 @@ def handle_navigation_sync(
223
232
  page: SyncPage, session_id: str, trace_id: str, client: LaminarClient
224
233
  ):
225
234
  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)
235
+ original_bring_to_front = page.bring_to_front
236
+
237
+ def bring_to_front():
238
+ original_bring_to_front()
239
+ page.evaluate(
240
+ """() => {
241
+ if (window.lmnrRrweb) {
242
+ try {
243
+ window.lmnrRrweb.record.takeFullSnapshot();
244
+ } catch (e) {
245
+ console.error("Error taking full snapshot:", e);
246
+ }
247
+ }
248
+ }"""
249
+ )
250
+
251
+ page.bring_to_front = bring_to_front
231
252
 
232
253
  def on_load():
233
254
  try:
@@ -235,49 +256,37 @@ def handle_navigation_sync(
235
256
  except Exception as e:
236
257
  logger.error(f"Error in on_load handler: {e}")
237
258
 
238
- page.on("load", on_load)
239
- inject_rrweb_sync(page)
240
-
241
259
  def collection_loop():
242
260
  while not page.is_closed(): # Stop when page closes
243
261
  send_events_sync(page, session_id, trace_id, client)
244
262
  time.sleep(2)
245
263
 
246
- # Clean up when page closes
247
- if page_id in instrumented_pages:
248
- instrumented_pages.remove(page_id)
249
-
250
264
  thread = threading.Thread(target=collection_loop, daemon=True)
251
265
  thread.start()
252
266
 
267
+ def on_close():
268
+ try:
269
+ send_events_sync(page, session_id, trace_id, client)
270
+ thread.join()
271
+ except Exception:
272
+ pass
273
+
274
+ page.on("load", on_load)
275
+ page.on("close", on_close)
276
+ inject_rrweb_sync(page)
277
+
253
278
 
254
279
  @observe(name="playwright.page", ignore_input=True, ignore_output=True)
255
280
  async def handle_navigation_async(
256
281
  page: Page, session_id: str, trace_id: str, client: AsyncLaminarClient
257
282
  ):
258
283
  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
284
 
274
285
  async def collection_loop():
275
286
  try:
276
287
  while not page.is_closed(): # Stop when page closes
277
288
  await send_events_async(page, session_id, trace_id, client)
278
289
  await asyncio.sleep(2)
279
- # Clean up when page closes
280
- async_instrumented_pages.remove(page_id)
281
290
  logger.info("Event collection stopped")
282
291
  except Exception as e:
283
292
  logger.error(f"Event collection stopped: {e}")
@@ -285,5 +294,38 @@ async def handle_navigation_async(
285
294
  # Create and store task
286
295
  task = asyncio.create_task(collection_loop())
287
296
 
288
- # Clean up task when page closes
289
- page.on("close", lambda: task.cancel())
297
+ async def on_load():
298
+ try:
299
+ await inject_rrweb_async(page)
300
+ except Exception as e:
301
+ logger.error(f"Error in on_load handler: {e}")
302
+
303
+ async def on_close():
304
+ try:
305
+ task.cancel()
306
+ await send_events_async(page, session_id, trace_id, client)
307
+ except Exception:
308
+ pass
309
+
310
+ page.on("load", lambda: asyncio.create_task(on_load()))
311
+ page.on("close", lambda: asyncio.create_task(on_close()))
312
+
313
+ original_bring_to_front = page.bring_to_front
314
+
315
+ async def bring_to_front():
316
+ await original_bring_to_front()
317
+
318
+ await page.evaluate(
319
+ """() => {
320
+ if (window.lmnrRrweb) {
321
+ try {
322
+ window.lmnrRrweb.record.takeFullSnapshot();
323
+ } catch (e) {
324
+ console.error("Error taking full snapshot:", e);
325
+ }
326
+ }
327
+ }"""
328
+ )
329
+
330
+ page.bring_to_front = bring_to_front
331
+ await inject_rrweb_async(page)