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