lmnr 0.6.21__py3-none-any.whl → 0.7.1__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 (34) hide show
  1. lmnr/__init__.py +0 -4
  2. lmnr/opentelemetry_lib/decorators/__init__.py +81 -32
  3. lmnr/opentelemetry_lib/litellm/__init__.py +5 -2
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +6 -2
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +11 -2
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +3 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +6 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +141 -9
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +10 -2
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +6 -2
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +8 -2
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +4 -1
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +20 -4
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
  16. lmnr/opentelemetry_lib/tracing/__init__.py +89 -1
  17. lmnr/opentelemetry_lib/tracing/context.py +126 -0
  18. lmnr/opentelemetry_lib/tracing/processor.py +5 -6
  19. lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
  20. lmnr/sdk/browser/browser_use_otel.py +5 -5
  21. lmnr/sdk/browser/patchright_otel.py +14 -0
  22. lmnr/sdk/browser/playwright_otel.py +32 -6
  23. lmnr/sdk/browser/pw_utils.py +119 -112
  24. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  25. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  26. lmnr/sdk/laminar.py +156 -186
  27. lmnr/sdk/types.py +17 -11
  28. lmnr/version.py +1 -1
  29. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/METADATA +3 -2
  30. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/RECORD +32 -31
  31. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/WHEEL +1 -1
  32. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  33. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  34. {lmnr-0.6.21.dist-info → lmnr-0.7.1.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,8 @@
1
1
  from contextlib import contextmanager
2
+ from typing import Generator, Tuple
2
3
 
3
4
  from opentelemetry import trace
5
+ from opentelemetry.context import Context
4
6
  from lmnr.opentelemetry_lib.tracing import TracerWrapper
5
7
 
6
8
 
@@ -16,3 +18,30 @@ def get_tracer(flush_on_exit: bool = False):
16
18
  finally:
17
19
  if flush_on_exit:
18
20
  wrapper.flush()
21
+
22
+
23
+ @contextmanager
24
+ def get_tracer_with_context(
25
+ flush_on_exit: bool = False,
26
+ ) -> Generator[Tuple[trace.Tracer, Context], None, None]:
27
+ """Get tracer with isolated context. Returns (tracer, context) tuple."""
28
+ wrapper = TracerWrapper()
29
+ try:
30
+ tracer = wrapper.get_tracer()
31
+ context = wrapper.get_isolated_context()
32
+ yield tracer, context
33
+ finally:
34
+ if flush_on_exit:
35
+ wrapper.flush()
36
+
37
+
38
+ def copy_current_context() -> Context:
39
+ """Copy the current isolated context for use in threads/tasks."""
40
+ wrapper = TracerWrapper()
41
+ return wrapper.get_isolated_context()
42
+
43
+
44
+ def set_context_for_thread(context: Context) -> None:
45
+ """Set the isolated context for the current thread."""
46
+ wrapper = TracerWrapper()
47
+ wrapper.set_isolated_context(context)
@@ -28,8 +28,8 @@ WRAPPED_METHODS = [
28
28
  "object": "Agent",
29
29
  "method": "run",
30
30
  "span_name": "agent.run",
31
- "ignore_input": False,
32
- "ignore_output": False,
31
+ "ignore_input": True,
32
+ "ignore_output": True,
33
33
  "span_type": "DEFAULT",
34
34
  },
35
35
  {
@@ -47,15 +47,15 @@ WRAPPED_METHODS = [
47
47
  "method": "act",
48
48
  "span_name": "controller.act",
49
49
  "ignore_input": True,
50
- "ignore_output": False,
50
+ "ignore_output": True,
51
51
  "span_type": "DEFAULT",
52
52
  },
53
53
  {
54
54
  "package": "browser_use.controller.registry.service",
55
55
  "object": "Registry",
56
56
  "method": "execute_action",
57
- "ignore_input": False,
58
- "ignore_output": False,
57
+ "ignore_input": True,
58
+ "ignore_output": True,
59
59
  "span_type": "TOOL",
60
60
  },
61
61
  ]
@@ -1,4 +1,6 @@
1
1
  from lmnr.sdk.browser.playwright_otel import (
2
+ _wrap_bring_to_front_async,
3
+ _wrap_bring_to_front_sync,
2
4
  _wrap_new_browser_sync,
3
5
  _wrap_new_browser_async,
4
6
  _wrap_new_context_sync,
@@ -46,6 +48,12 @@ WRAPPED_METHODS = [
46
48
  "method": "launch_persistent_context",
47
49
  "wrapper": _wrap_new_context_sync,
48
50
  },
51
+ {
52
+ "package": "patchright.sync_api",
53
+ "object": "Page",
54
+ "method": "bring_to_front",
55
+ "wrapper": _wrap_bring_to_front_sync,
56
+ },
49
57
  ]
50
58
 
51
59
  WRAPPED_METHODS_ASYNC = [
@@ -79,6 +87,12 @@ WRAPPED_METHODS_ASYNC = [
79
87
  "method": "launch_persistent_context",
80
88
  "wrapper": _wrap_new_context_async,
81
89
  },
90
+ {
91
+ "package": "patchright.async_api",
92
+ "object": "Page",
93
+ "method": "bring_to_front",
94
+ "wrapper": _wrap_bring_to_front_async,
95
+ },
82
96
  ]
83
97
 
84
98
 
@@ -60,8 +60,15 @@ def _wrap_new_browser_sync(
60
60
  browser: SyncBrowser = wrapped(*args, **kwargs)
61
61
  session_id = str(uuid.uuid4().hex)
62
62
 
63
+ def create_page_handler(session_id, client):
64
+ def page_handler(page):
65
+ start_recording_events_sync(page, session_id, client)
66
+
67
+ return page_handler
68
+
63
69
  for context in browser.contexts:
64
- context.on("page", lambda p: start_recording_events_sync(p, session_id, client))
70
+ page_handler = create_page_handler(session_id, client)
71
+ context.on("page", page_handler)
65
72
  for page in context.pages:
66
73
  start_recording_events_sync(page, session_id, client)
67
74
 
@@ -75,10 +82,15 @@ async def _wrap_new_browser_async(
75
82
  browser: Browser = await wrapped(*args, **kwargs)
76
83
  session_id = str(uuid.uuid4().hex)
77
84
 
85
+ def create_page_handler(session_id, client):
86
+ async def page_handler(page):
87
+ await start_recording_events_async(page, session_id, client)
88
+
89
+ return page_handler
90
+
78
91
  for context in browser.contexts:
79
- context.on(
80
- "page", lambda p: start_recording_events_async(p, session_id, client)
81
- )
92
+ page_handler = create_page_handler(session_id, client)
93
+ context.on("page", page_handler)
82
94
  for page in context.pages:
83
95
  await start_recording_events_async(page, session_id, client)
84
96
  return browser
@@ -91,7 +103,14 @@ def _wrap_new_context_sync(
91
103
  context: SyncBrowserContext = wrapped(*args, **kwargs)
92
104
  session_id = str(uuid.uuid4().hex)
93
105
 
94
- context.on("page", lambda p: start_recording_events_sync(p, session_id, client))
106
+ def create_page_handler(session_id, client):
107
+ def page_handler(page):
108
+ start_recording_events_sync(page, session_id, client)
109
+
110
+ return page_handler
111
+
112
+ page_handler = create_page_handler(session_id, client)
113
+ context.on("page", page_handler)
95
114
  for page in context.pages:
96
115
  start_recording_events_sync(page, session_id, client)
97
116
 
@@ -105,7 +124,14 @@ async def _wrap_new_context_async(
105
124
  context: BrowserContext = await wrapped(*args, **kwargs)
106
125
  session_id = str(uuid.uuid4().hex)
107
126
 
108
- context.on("page", lambda p: start_recording_events_async(p, session_id, client))
127
+ def create_page_handler(session_id, client):
128
+ async def page_handler(page):
129
+ await start_recording_events_async(page, session_id, client)
130
+
131
+ return page_handler
132
+
133
+ page_handler = create_page_handler(session_id, client)
134
+ context.on("page", page_handler)
109
135
  for page in context.pages:
110
136
  await start_recording_events_async(page, session_id, client)
111
137
 
@@ -8,6 +8,7 @@ from lmnr.sdk.decorators import observe
8
8
  from lmnr.sdk.browser.utils import retry_sync, retry_async
9
9
  from lmnr.sdk.client.synchronous.sync_client import LaminarClient
10
10
  from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
11
+ from lmnr.opentelemetry_lib.tracing.context import get_current_context
11
12
 
12
13
  try:
13
14
  if is_package_installed("playwright"):
@@ -32,13 +33,15 @@ except ImportError as e:
32
33
  logger = logging.getLogger(__name__)
33
34
 
34
35
  current_dir = os.path.dirname(os.path.abspath(__file__))
35
- with open(os.path.join(current_dir, "rrweb", "rrweb.umd.min.cjs"), "r") as f:
36
+ with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
36
37
  RRWEB_CONTENT = f"() => {{ {f.read()} }}"
37
38
 
38
39
  INJECT_PLACEHOLDER = """
39
40
  () => {
40
41
  const BATCH_TIMEOUT = 2000; // Send events after 2 seconds
41
-
42
+ const MAX_WORKER_PROMISES = 50; // Max concurrent worker promises
43
+ const HEARTBEAT_INTERVAL = 1000;
44
+
42
45
  window.lmnrRrwebEventsBatch = [];
43
46
 
44
47
  // Create a Web Worker for heavy JSON processing with chunked processing
@@ -97,6 +100,29 @@ INJECT_PLACEHOLDER = """
97
100
  let workerPromises = new Map();
98
101
  let workerId = 0;
99
102
 
103
+ // Cleanup function for worker
104
+ const cleanupWorker = () => {
105
+ if (compressionWorker) {
106
+ compressionWorker.terminate();
107
+ compressionWorker = null;
108
+ }
109
+ workerPromises.clear();
110
+ workerId = 0;
111
+ };
112
+
113
+ // Clean up stale promises to prevent memory leaks
114
+ const cleanupStalePromises = () => {
115
+ if (workerPromises.size > MAX_WORKER_PROMISES) {
116
+ const toDelete = [];
117
+ for (const [id, promise] of workerPromises) {
118
+ if (toDelete.length >= workerPromises.size - MAX_WORKER_PROMISES) break;
119
+ toDelete.push(id);
120
+ promise.reject(new Error('Promise cleaned up due to memory pressure'));
121
+ }
122
+ toDelete.forEach(id => workerPromises.delete(id));
123
+ }
124
+ };
125
+
100
126
  // Non-blocking JSON.stringify using chunked processing
101
127
  function stringifyNonBlocking(obj, chunkSize = 10000) {
102
128
  return new Promise((resolve, reject) => {
@@ -196,6 +222,9 @@ INJECT_PLACEHOLDER = """
196
222
  // Alternative: Use transferable objects for maximum efficiency
197
223
  async function compressLargeObjectTransferable(data) {
198
224
  try {
225
+ // Clean up stale promises first
226
+ cleanupStalePromises();
227
+
199
228
  // Stringify on main thread but non-blocking
200
229
  const jsonString = await stringifyNonBlocking(data);
201
230
 
@@ -219,11 +248,24 @@ INJECT_PLACEHOLDER = """
219
248
  }
220
249
  }
221
250
  };
251
+
252
+ compressionWorker.onerror = (error) => {
253
+ console.error('Compression worker error:', error);
254
+ cleanupWorker();
255
+ };
222
256
  }
223
257
 
224
258
  const id = ++workerId;
225
259
  workerPromises.set(id, { resolve, reject });
226
260
 
261
+ // Set timeout to prevent hanging promises
262
+ setTimeout(() => {
263
+ if (workerPromises.has(id)) {
264
+ workerPromises.delete(id);
265
+ reject(new Error('Compression timeout'));
266
+ }
267
+ }, 10000);
268
+
227
269
  // Transfer the ArrayBuffer (no copying!)
228
270
  compressionWorker.postMessage({
229
271
  buffer,
@@ -262,15 +304,32 @@ INJECT_PLACEHOLDER = """
262
304
  }
263
305
  }
264
306
  };
307
+
308
+ compressionWorker.onerror = (error) => {
309
+ console.error('Compression worker error:', error);
310
+ cleanupWorker();
311
+ };
265
312
  }
266
313
 
267
314
  const id = ++workerId;
268
315
  workerPromises.set(id, { resolve, reject });
316
+
317
+ // Set timeout to prevent hanging promises
318
+ setTimeout(() => {
319
+ if (workerPromises.has(id)) {
320
+ workerPromises.delete(id);
321
+ reject(new Error('Compression timeout'));
322
+ }
323
+ }, 10000);
324
+
269
325
  compressionWorker.postMessage({ jsonString, id });
270
326
  });
271
327
  }
272
328
  }
273
329
 
330
+
331
+ setInterval(cleanupWorker, 5000);
332
+
274
333
  function isLargeEvent(type) {
275
334
  const LARGE_EVENT_TYPES = [
276
335
  2, // FullSnapshot
@@ -299,14 +358,15 @@ INJECT_PLACEHOLDER = """
299
358
 
300
359
  setInterval(sendBatchIfReady, BATCH_TIMEOUT);
301
360
 
302
- // Add heartbeat events
303
- setInterval(async () => {
304
- window.lmnrRrweb.record.addCustomEvent('heartbeat', {
305
- title: document.title,
306
- url: document.URL,
307
- })
308
- }, 1000);
309
-
361
+ async function bufferToBase64(buffer) {
362
+ const base64url = await new Promise(r => {
363
+ const reader = new FileReader()
364
+ reader.onload = () => r(reader.result)
365
+ reader.readAsDataURL(new Blob([buffer]))
366
+ });
367
+ return base64url.slice(base64url.indexOf(',') + 1);
368
+ }
369
+
310
370
  window.lmnrRrweb.record({
311
371
  async emit(event) {
312
372
  try {
@@ -315,9 +375,10 @@ INJECT_PLACEHOLDER = """
315
375
  await compressLargeObject(event.data, true) :
316
376
  await compressSmallObject(event.data);
317
377
 
378
+ const base64Data = await bufferToBase64(compressedResult);
318
379
  const eventToSend = {
319
380
  ...event,
320
- data: compressedResult,
381
+ data: base64Data,
321
382
  };
322
383
  window.lmnrRrwebEventsBatch.push(eventToSend);
323
384
  } catch (error) {
@@ -328,63 +389,22 @@ INJECT_PLACEHOLDER = """
328
389
  collectFonts: true,
329
390
  recordCrossOriginIframes: true
330
391
  });
331
- }
332
- """
333
-
334
392
 
335
- async def send_events_async(
336
- page: Page, session_id: str, trace_id: str, client: AsyncLaminarClient
337
- ):
338
- """Fetch events from the page and send them to the server"""
339
- try:
340
- # Check if function exists first
341
- events = await page.evaluate(
342
- """
343
- () => {
344
- if (typeof window.lmnrGetAndClearEvents !== 'function') {
345
- return [];
346
- }
347
- return window.lmnrGetAndClearEvents();
348
- }
349
- """
350
- )
351
-
352
- if not events or len(events) == 0:
353
- return
354
-
355
- await client._browser_events.send(session_id, trace_id, events)
356
- except Exception as e:
357
- if "Page.evaluate: Target page, context or browser has been closed" not in str(
358
- e
359
- ):
360
- logger.debug(f"Could not send events: {e}")
361
-
362
-
363
- def send_events_sync(
364
- page: SyncPage, session_id: str, trace_id: str, client: LaminarClient
365
- ):
366
- """Synchronous version of send_events"""
367
- try:
368
- events = page.evaluate(
369
- """
370
- () => {
371
- if (typeof window.lmnrGetAndClearEvents !== 'function') {
372
- return [];
373
- }
374
- return window.lmnrGetAndClearEvents();
375
- }
376
- """
377
- )
378
- if not events or len(events) == 0:
379
- return
393
+ function heartbeat() {
394
+ // Add heartbeat events
395
+ setInterval(() => {
396
+ window.lmnrRrweb.record.addCustomEvent('heartbeat', {
397
+ title: document.title,
398
+ url: document.URL,
399
+ })
400
+ }, HEARTBEAT_INTERVAL
401
+ );
402
+ }
380
403
 
381
- client._browser_events.send(session_id, trace_id, events)
404
+ heartbeat();
382
405
 
383
- except Exception as e:
384
- if "Page.evaluate: Target page, context or browser has been closed" not in str(
385
- e
386
- ):
387
- logger.debug(f"Could not send events: {e}")
406
+ }
407
+ """
388
408
 
389
409
 
390
410
  def inject_session_recorder_sync(page: SyncPage):
@@ -414,10 +434,10 @@ def inject_session_recorder_sync(page: SyncPage):
414
434
  ):
415
435
  return
416
436
 
417
- try:
418
- page.evaluate(INJECT_PLACEHOLDER)
419
- except Exception as e:
420
- logger.debug(f"Failed to inject session recorder: {e}")
437
+ try:
438
+ page.evaluate(INJECT_PLACEHOLDER)
439
+ except Exception as e:
440
+ logger.debug(f"Failed to inject session recorder: {e}")
421
441
 
422
442
  except Exception as e:
423
443
  logger.error(f"Error during session recorder injection: {e}")
@@ -450,10 +470,10 @@ async def inject_session_recorder_async(page: Page):
450
470
  ):
451
471
  return
452
472
 
453
- try:
454
- await page.evaluate(INJECT_PLACEHOLDER)
455
- except Exception as e:
456
- logger.debug(f"Failed to inject session recorder placeholder: {e}")
473
+ try:
474
+ await page.evaluate(INJECT_PLACEHOLDER)
475
+ except Exception as e:
476
+ logger.debug(f"Failed to inject session recorder placeholder: {e}")
457
477
 
458
478
  except Exception as e:
459
479
  logger.error(f"Error during session recorder injection: {e}")
@@ -461,7 +481,9 @@ async def inject_session_recorder_async(page: Page):
461
481
 
462
482
  @observe(name="playwright.page", ignore_input=True, ignore_output=True)
463
483
  def start_recording_events_sync(page: SyncPage, session_id: str, client: LaminarClient):
464
- span = trace.get_current_span()
484
+
485
+ ctx = get_current_context()
486
+ span = trace.get_current_span(ctx)
465
487
  trace_id = format(span.get_span_context().trace_id, "032x")
466
488
  span.set_attribute("lmnr.internal.has_browser_session", True)
467
489
 
@@ -471,24 +493,6 @@ def start_recording_events_sync(page: SyncPage, session_id: str, client: Laminar
471
493
  except Exception:
472
494
  pass
473
495
 
474
- def on_load():
475
- try:
476
- inject_session_recorder_sync(page)
477
- except Exception as e:
478
- logger.error(f"Error in on_load handler: {e}")
479
-
480
- def on_close():
481
- try:
482
- send_events_sync(page, session_id, trace_id, client)
483
- except Exception:
484
- pass
485
-
486
- page.on("load", on_load)
487
- page.on("close", on_close)
488
-
489
- inject_session_recorder_sync(page)
490
-
491
- # Expose function to browser so it can call us when events are ready
492
496
  def send_events_from_browser(events):
493
497
  try:
494
498
  if events and len(events) > 0:
@@ -501,12 +505,23 @@ def start_recording_events_sync(page: SyncPage, session_id: str, client: Laminar
501
505
  except Exception as e:
502
506
  logger.debug(f"Could not expose function: {e}")
503
507
 
508
+ inject_session_recorder_sync(page)
509
+
510
+ def on_load(p):
511
+ try:
512
+ inject_session_recorder_sync(p)
513
+ except Exception as e:
514
+ logger.error(f"Error in on_load handler: {e}")
515
+
516
+ page.on("domcontentloaded", on_load)
517
+
504
518
 
505
519
  @observe(name="playwright.page", ignore_input=True, ignore_output=True)
506
520
  async def start_recording_events_async(
507
521
  page: Page, session_id: str, client: AsyncLaminarClient
508
522
  ):
509
- span = trace.get_current_span()
523
+ ctx = get_current_context()
524
+ span = trace.get_current_span(ctx)
510
525
  trace_id = format(span.get_span_context().trace_id, "032x")
511
526
  span.set_attribute("lmnr.internal.has_browser_session", True)
512
527
 
@@ -517,25 +532,7 @@ async def start_recording_events_async(
517
532
  return
518
533
  except Exception:
519
534
  pass
520
-
521
- async def on_load(p):
522
- try:
523
- await inject_session_recorder_async(p)
524
- except Exception as e:
525
- logger.error(f"Error in on_load handler: {e}")
526
-
527
- async def on_close(p):
528
- try:
529
- # Send any remaining events before closing
530
- await send_events_async(p, session_id, trace_id, client)
531
- except Exception:
532
- pass
533
-
534
- page.on("load", on_load)
535
- page.on("close", on_close)
536
-
537
- await inject_session_recorder_async(page)
538
-
535
+
539
536
  async def send_events_from_browser(events):
540
537
  try:
541
538
  if events and len(events) > 0:
@@ -548,6 +545,16 @@ async def start_recording_events_async(
548
545
  except Exception as e:
549
546
  logger.debug(f"Could not expose function: {e}")
550
547
 
548
+ await inject_session_recorder_async(page)
549
+
550
+ async def on_load(p):
551
+ try:
552
+ await inject_session_recorder_async(p)
553
+ except Exception as e:
554
+ logger.error(f"Error in on_load handler: {e}")
555
+
556
+ page.on("domcontentloaded", on_load)
557
+
551
558
 
552
559
  def take_full_snapshot(page: Page):
553
560
  return page.evaluate(