lmnr 0.7.9__py3-none-any.whl → 0.7.11__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.
- lmnr/opentelemetry_lib/tracing/__init__.py +13 -2
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +36 -3
- lmnr/opentelemetry_lib/tracing/instruments.py +4 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +12 -7
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +318 -87
- lmnr/sdk/evaluations.py +22 -2
- lmnr/sdk/laminar.py +3 -3
- lmnr/version.py +1 -1
- {lmnr-0.7.9.dist-info → lmnr-0.7.11.dist-info}/METADATA +1 -1
- {lmnr-0.7.9.dist-info → lmnr-0.7.11.dist-info}/RECORD +13 -12
- {lmnr-0.7.9.dist-info → lmnr-0.7.11.dist-info}/WHEEL +0 -0
- {lmnr-0.7.9.dist-info → lmnr-0.7.11.dist-info}/entry_points.txt +0 -0
@@ -219,8 +219,19 @@ class TracerWrapper(object):
|
|
219
219
|
|
220
220
|
@classmethod
|
221
221
|
def verify_initialized(cls) -> bool:
|
222
|
-
|
223
|
-
|
222
|
+
# This is not using lock, but it is fine to return False from here
|
223
|
+
# even if initialization is going on.
|
224
|
+
|
225
|
+
# If we try to acquire the lock here, it may deadlock if an automatic
|
226
|
+
# instrumentation is importing a file that (at the top level) has a
|
227
|
+
# function annotated with Laminar's `observe` decorator.
|
228
|
+
# The decorator is evaluated at import time, inside `init_instrumentations`,
|
229
|
+
# which is called by the `TracerWrapper` constructor while holding the lock.
|
230
|
+
# Without the lock here, we will simply return False, which will cause
|
231
|
+
# the decorator to return the original function. This is fine, at runtime,
|
232
|
+
# the next import statement will re-evaluate the decorator, and Laminar will
|
233
|
+
# have been initialized by that time.
|
234
|
+
return hasattr(cls, "instance") and hasattr(cls.instance, "_span_processor")
|
224
235
|
|
225
236
|
@classmethod
|
226
237
|
def clear(cls):
|
@@ -51,9 +51,19 @@ class BedrockInstrumentorInitializer(InstrumentorInitializer):
|
|
51
51
|
|
52
52
|
|
53
53
|
class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
|
54
|
-
|
55
|
-
|
56
|
-
|
54
|
+
"""Instruments for different versions of browser-use:
|
55
|
+
|
56
|
+
- browser-use < 0.5: BrowserUseLegacyInstrumentor to track agent_step and
|
57
|
+
other structure spans. Session instrumentation is controlled by
|
58
|
+
Instruments.PLAYWRIGHT (or Instruments.PATCHRIGHT for several versions
|
59
|
+
in 0.4.* that used patchright)
|
60
|
+
- browser-use ~= 0.5: Structure spans live in browser_use package itself.
|
61
|
+
Session instrumentation is controlled by Instruments.PLAYWRIGHT
|
62
|
+
- browser-use >= 0.6.0rc1: BubusInstrumentor to keep spans structure.
|
63
|
+
Session instrumentation is controlled by Instruments.BROWSER_USE_SESSION
|
64
|
+
"""
|
65
|
+
|
66
|
+
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
57
67
|
if not is_package_installed("browser-use"):
|
58
68
|
return None
|
59
69
|
|
@@ -65,6 +75,19 @@ class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
|
|
65
75
|
|
66
76
|
return BrowserUseLegacyInstrumentor()
|
67
77
|
|
78
|
+
return None
|
79
|
+
|
80
|
+
|
81
|
+
class BrowserUseSessionInstrumentorInitializer(InstrumentorInitializer):
|
82
|
+
def init_instrumentor(
|
83
|
+
self, client, async_client, *args, **kwargs
|
84
|
+
) -> BaseInstrumentor | None:
|
85
|
+
if not is_package_installed("browser-use"):
|
86
|
+
return None
|
87
|
+
|
88
|
+
version = get_package_version("browser-use")
|
89
|
+
from packaging.version import parse
|
90
|
+
|
68
91
|
if version and parse(version) >= parse("0.6.0rc1"):
|
69
92
|
from lmnr.sdk.browser.browser_use_cdp_otel import BrowserUseInstrumentor
|
70
93
|
|
@@ -73,6 +96,16 @@ class BrowserUseInstrumentorInitializer(InstrumentorInitializer):
|
|
73
96
|
return None
|
74
97
|
|
75
98
|
|
99
|
+
class BubusInstrumentorInitializer(InstrumentorInitializer):
|
100
|
+
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
101
|
+
if not is_package_installed("bubus"):
|
102
|
+
return None
|
103
|
+
|
104
|
+
from lmnr.sdk.browser.bubus_otel import BubusInstrumentor
|
105
|
+
|
106
|
+
return BubusInstrumentor()
|
107
|
+
|
108
|
+
|
76
109
|
class ChromaInstrumentorInitializer(InstrumentorInitializer):
|
77
110
|
def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None:
|
78
111
|
if not is_package_installed("chromadb"):
|
@@ -17,6 +17,8 @@ class Instruments(Enum):
|
|
17
17
|
ANTHROPIC = "anthropic"
|
18
18
|
BEDROCK = "bedrock"
|
19
19
|
BROWSER_USE = "browser_use"
|
20
|
+
BROWSER_USE_SESSION = "browser_use_session"
|
21
|
+
BUBUS = "bubus"
|
20
22
|
CHROMA = "chroma"
|
21
23
|
COHERE = "cohere"
|
22
24
|
CREWAI = "crewai"
|
@@ -60,6 +62,8 @@ INSTRUMENTATION_INITIALIZERS: dict[
|
|
60
62
|
Instruments.ANTHROPIC: initializers.AnthropicInstrumentorInitializer(),
|
61
63
|
Instruments.BEDROCK: initializers.BedrockInstrumentorInitializer(),
|
62
64
|
Instruments.BROWSER_USE: initializers.BrowserUseInstrumentorInitializer(),
|
65
|
+
Instruments.BROWSER_USE_SESSION: initializers.BrowserUseSessionInstrumentorInitializer(),
|
66
|
+
Instruments.BUBUS: initializers.BubusInstrumentorInitializer(),
|
63
67
|
Instruments.CHROMA: initializers.ChromaInstrumentorInitializer(),
|
64
68
|
Instruments.COHERE: initializers.CohereInstrumentorInitializer(),
|
65
69
|
Instruments.CREWAI: initializers.CrewAIInstrumentorInitializer(),
|
@@ -1,3 +1,6 @@
|
|
1
|
+
import asyncio
|
2
|
+
import uuid
|
3
|
+
|
1
4
|
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
2
5
|
from lmnr.sdk.browser.utils import with_tracer_and_client_wrapper
|
3
6
|
from lmnr.version import __version__
|
@@ -12,7 +15,6 @@ from opentelemetry.instrumentation.utils import unwrap
|
|
12
15
|
from opentelemetry.trace import get_tracer, Tracer
|
13
16
|
from typing import Collection
|
14
17
|
from wrapt import wrap_function_wrapper
|
15
|
-
import uuid
|
16
18
|
|
17
19
|
# Stable versions, e.g. 0.6.0, satisfy this condition too
|
18
20
|
_instruments = ("browser-use >= 0.6.0rc1",)
|
@@ -33,12 +35,7 @@ WRAPPED_METHODS = [
|
|
33
35
|
]
|
34
36
|
|
35
37
|
|
36
|
-
|
37
|
-
async def _wrap(
|
38
|
-
tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
|
39
|
-
):
|
40
|
-
result = await wrapped(*args, **kwargs)
|
41
|
-
|
38
|
+
async def process_wrapped_result(result, instance, client, to_wrap):
|
42
39
|
if to_wrap.get("action") == "inject_session_recorder":
|
43
40
|
is_registered = await is_recorder_present(result)
|
44
41
|
if not is_registered:
|
@@ -50,6 +47,14 @@ async def _wrap(
|
|
50
47
|
cdp_session = await instance.get_or_create_cdp_session(target_id)
|
51
48
|
await take_full_snapshot(cdp_session)
|
52
49
|
|
50
|
+
|
51
|
+
@with_tracer_and_client_wrapper
|
52
|
+
async def _wrap(
|
53
|
+
tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
|
54
|
+
):
|
55
|
+
result = await wrapped(*args, **kwargs)
|
56
|
+
asyncio.create_task(process_wrapped_result(result, instance, client, to_wrap))
|
57
|
+
|
53
58
|
return result
|
54
59
|
|
55
60
|
|
@@ -0,0 +1,71 @@
|
|
1
|
+
from typing import Collection
|
2
|
+
|
3
|
+
from lmnr import Laminar
|
4
|
+
from lmnr.opentelemetry_lib.tracing.context import get_current_context
|
5
|
+
from lmnr.sdk.log import get_default_logger
|
6
|
+
|
7
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
8
|
+
from opentelemetry.instrumentation.utils import unwrap
|
9
|
+
from opentelemetry.trace import NonRecordingSpan, get_current_span
|
10
|
+
from wrapt import wrap_function_wrapper
|
11
|
+
|
12
|
+
|
13
|
+
_instruments = ("bubus >= 1.3.0",)
|
14
|
+
event_id_to_span_context = {}
|
15
|
+
logger = get_default_logger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
def wrap_dispatch(wrapped, instance, args, kwargs):
|
19
|
+
event = args[0] if args and len(args) > 0 else kwargs.get("event", None)
|
20
|
+
if event and hasattr(event, "event_id"):
|
21
|
+
event_id = event.event_id
|
22
|
+
if event_id:
|
23
|
+
span = get_current_span(get_current_context())
|
24
|
+
event_id_to_span_context[event_id] = span.get_span_context()
|
25
|
+
return wrapped(*args, **kwargs)
|
26
|
+
|
27
|
+
|
28
|
+
async def wrap_process_event(wrapped, instance, args, kwargs):
|
29
|
+
event = args[0] if args and len(args) > 0 else kwargs.get("event", None)
|
30
|
+
span_context = None
|
31
|
+
if event and hasattr(event, "event_id"):
|
32
|
+
event_id = event.event_id
|
33
|
+
if event_id:
|
34
|
+
span_context = event_id_to_span_context.get(event_id)
|
35
|
+
if not span_context:
|
36
|
+
return await wrapped(*args, **kwargs)
|
37
|
+
if not Laminar.is_initialized():
|
38
|
+
return await wrapped(*args, **kwargs)
|
39
|
+
with Laminar.use_span(NonRecordingSpan(span_context)):
|
40
|
+
return await wrapped(*args, **kwargs)
|
41
|
+
|
42
|
+
|
43
|
+
class BubusInstrumentor(BaseInstrumentor):
|
44
|
+
def __init__(self):
|
45
|
+
super().__init__()
|
46
|
+
|
47
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
48
|
+
return _instruments
|
49
|
+
|
50
|
+
def _instrument(self, **kwargs):
|
51
|
+
try:
|
52
|
+
wrap_function_wrapper("bubus.service", "EventBus.dispatch", wrap_dispatch)
|
53
|
+
except (ModuleNotFoundError, ImportError):
|
54
|
+
pass
|
55
|
+
try:
|
56
|
+
wrap_function_wrapper(
|
57
|
+
"bubus.service", "EventBus.process_event", wrap_process_event
|
58
|
+
)
|
59
|
+
except (ModuleNotFoundError, ImportError):
|
60
|
+
pass
|
61
|
+
|
62
|
+
def _uninstrument(self, **kwargs):
|
63
|
+
try:
|
64
|
+
unwrap("bubus.service", "EventBus.dispatch")
|
65
|
+
except (ModuleNotFoundError, ImportError):
|
66
|
+
pass
|
67
|
+
try:
|
68
|
+
unwrap("bubus.service", "EventBus.process_event")
|
69
|
+
except (ModuleNotFoundError, ImportError):
|
70
|
+
pass
|
71
|
+
event_id_to_span_context.clear()
|
lmnr/sdk/browser/cdp_utils.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
import
|
1
|
+
import asyncio
|
2
2
|
import logging
|
3
|
+
import orjson
|
3
4
|
import os
|
4
5
|
import time
|
5
|
-
import asyncio
|
6
6
|
|
7
7
|
from opentelemetry import trace
|
8
8
|
|
@@ -16,6 +16,11 @@ from lmnr.sdk.types import MaskInputOptions
|
|
16
16
|
logger = logging.getLogger(__name__)
|
17
17
|
|
18
18
|
OLD_BUFFER_TIMEOUT = 60
|
19
|
+
CDP_OPERATION_TIMEOUT_SECONDS = 10
|
20
|
+
|
21
|
+
# CDP ContextId is int
|
22
|
+
frame_to_isolated_context_id: dict[str, int] = {}
|
23
|
+
lock = asyncio.Lock()
|
19
24
|
|
20
25
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
21
26
|
with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
|
@@ -448,8 +453,6 @@ INJECT_PLACEHOLDER = """
|
|
448
453
|
}
|
449
454
|
}
|
450
455
|
|
451
|
-
setInterval(sendBatchIfReady, BATCH_TIMEOUT);
|
452
|
-
|
453
456
|
async function bufferToBase64(buffer) {
|
454
457
|
const base64url = await new Promise(r => {
|
455
458
|
const reader = new FileReader()
|
@@ -458,55 +461,152 @@ INJECT_PLACEHOLDER = """
|
|
458
461
|
});
|
459
462
|
return base64url.slice(base64url.indexOf(',') + 1);
|
460
463
|
}
|
461
|
-
|
462
|
-
window.
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
464
|
+
|
465
|
+
if (!window.lmnrStartedRecordingEvents) {
|
466
|
+
setInterval(sendBatchIfReady, BATCH_TIMEOUT);
|
467
|
+
|
468
|
+
window.lmnrRrweb.record({
|
469
|
+
async emit(event) {
|
470
|
+
try {
|
471
|
+
const isLarge = isLargeEvent(event.type);
|
472
|
+
const compressedResult = isLarge ?
|
473
|
+
await compressLargeObject(event.data) :
|
474
|
+
await compressSmallObject(event.data);
|
475
|
+
|
476
|
+
const base64Data = await bufferToBase64(compressedResult);
|
477
|
+
const eventToSend = {
|
478
|
+
...event,
|
479
|
+
data: base64Data,
|
480
|
+
};
|
481
|
+
window.lmnrRrwebEventsBatch.push(eventToSend);
|
482
|
+
} catch (error) {
|
483
|
+
console.warn('Failed to push event to batch', error);
|
484
|
+
}
|
485
|
+
},
|
486
|
+
recordCanvas: true,
|
487
|
+
collectFonts: true,
|
488
|
+
recordCrossOriginIframes: true,
|
489
|
+
maskInputOptions: {
|
490
|
+
password: true,
|
491
|
+
textarea: maskInputOptions.textarea || false,
|
492
|
+
text: maskInputOptions.text || false,
|
493
|
+
number: maskInputOptions.number || false,
|
494
|
+
select: maskInputOptions.select || false,
|
495
|
+
email: maskInputOptions.email || false,
|
496
|
+
tel: maskInputOptions.tel || false,
|
478
497
|
}
|
479
|
-
}
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
tel: maskInputOptions.tel || false,
|
498
|
+
});
|
499
|
+
|
500
|
+
function heartbeat() {
|
501
|
+
// Add heartbeat events
|
502
|
+
setInterval(() => {
|
503
|
+
window.lmnrRrweb.record.addCustomEvent('heartbeat', {
|
504
|
+
title: document.title,
|
505
|
+
url: document.URL,
|
506
|
+
})
|
507
|
+
}, HEARTBEAT_INTERVAL
|
508
|
+
);
|
491
509
|
}
|
492
|
-
});
|
493
|
-
|
494
|
-
function heartbeat() {
|
495
|
-
// Add heartbeat events
|
496
|
-
setInterval(() => {
|
497
|
-
window.lmnrRrweb.record.addCustomEvent('heartbeat', {
|
498
|
-
title: document.title,
|
499
|
-
url: document.URL,
|
500
|
-
})
|
501
|
-
}, HEARTBEAT_INTERVAL
|
502
|
-
);
|
503
|
-
}
|
504
510
|
|
505
|
-
|
511
|
+
heartbeat();
|
512
|
+
window.lmnrStartedRecordingEvents = true;
|
513
|
+
}
|
506
514
|
}
|
507
515
|
"""
|
508
516
|
|
509
517
|
|
518
|
+
async def should_skip_page(cdp_session):
|
519
|
+
"""Checks if the page url is an error page or an empty page.
|
520
|
+
This function returns True in case of any error in our code, because
|
521
|
+
it is safer to not record events than to try to inject the recorder
|
522
|
+
into something that is already broken.
|
523
|
+
"""
|
524
|
+
cdp_client = cdp_session.cdp_client
|
525
|
+
|
526
|
+
try:
|
527
|
+
# Get the current page URL
|
528
|
+
result = await asyncio.wait_for(
|
529
|
+
cdp_client.send.Runtime.evaluate(
|
530
|
+
{
|
531
|
+
"expression": "window.location.href",
|
532
|
+
"returnByValue": True,
|
533
|
+
},
|
534
|
+
session_id=cdp_session.session_id,
|
535
|
+
),
|
536
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
537
|
+
)
|
538
|
+
|
539
|
+
url = result.get("result", {}).get("value", "")
|
540
|
+
|
541
|
+
# Comprehensive list of browser error URLs
|
542
|
+
error_url_patterns = [
|
543
|
+
"about:blank",
|
544
|
+
# Chrome error pages
|
545
|
+
"chrome-error://",
|
546
|
+
"chrome://network-error/",
|
547
|
+
"chrome://network-errors/",
|
548
|
+
# Chrome crash and debugging pages
|
549
|
+
"chrome://crash/",
|
550
|
+
"chrome://crashdump/",
|
551
|
+
"chrome://kill/",
|
552
|
+
"chrome://hang/",
|
553
|
+
"chrome://shorthang/",
|
554
|
+
"chrome://gpuclean/",
|
555
|
+
"chrome://gpucrash/",
|
556
|
+
"chrome://gpuhang/",
|
557
|
+
"chrome://memory-exhaust/",
|
558
|
+
"chrome://memory-pressure-critical/",
|
559
|
+
"chrome://memory-pressure-moderate/",
|
560
|
+
"chrome://inducebrowsercrashforrealz/",
|
561
|
+
"chrome://inducebrowserdcheckforrealz/",
|
562
|
+
"chrome://inducebrowserheapcorruption/",
|
563
|
+
"chrome://heapcorruptioncrash/",
|
564
|
+
"chrome://badcastcrash/",
|
565
|
+
"chrome://ppapiflashcrash/",
|
566
|
+
"chrome://ppapiflashhang/",
|
567
|
+
"chrome://quit/",
|
568
|
+
"chrome://restart/",
|
569
|
+
# Firefox error pages
|
570
|
+
"about:neterror",
|
571
|
+
"about:certerror",
|
572
|
+
"about:blocked",
|
573
|
+
# Firefox crash and debugging pages
|
574
|
+
"about:crashcontent",
|
575
|
+
"about:crashparent",
|
576
|
+
"about:crashes",
|
577
|
+
"about:tabcrashed",
|
578
|
+
# Edge error pages (similar to Chrome)
|
579
|
+
"edge-error://",
|
580
|
+
"edge://crash/",
|
581
|
+
"edge://kill/",
|
582
|
+
"edge://hang/",
|
583
|
+
# Safari/WebKit error indicators (data URLs with error content)
|
584
|
+
"webkit-error://",
|
585
|
+
]
|
586
|
+
|
587
|
+
# Check if current URL matches any error pattern
|
588
|
+
if any(url.startswith(pattern) for pattern in error_url_patterns):
|
589
|
+
logger.debug(f"Detected browser error page from URL: {url}")
|
590
|
+
return True
|
591
|
+
|
592
|
+
# Additional check for data URLs that might contain error pages
|
593
|
+
if url.startswith("data:") and any(
|
594
|
+
error_term in url.lower()
|
595
|
+
for error_term in ["error", "crash", "failed", "unavailable", "not found"]
|
596
|
+
):
|
597
|
+
logger.debug(f"Detected error page from data URL: {url[:100]}...")
|
598
|
+
return True
|
599
|
+
|
600
|
+
return False
|
601
|
+
|
602
|
+
except asyncio.TimeoutError:
|
603
|
+
logger.debug("Timeout error when checking if error page")
|
604
|
+
return True
|
605
|
+
except Exception as e:
|
606
|
+
logger.debug(f"Error during checking if error page: {e}")
|
607
|
+
return True
|
608
|
+
|
609
|
+
|
510
610
|
def get_mask_input_setting() -> MaskInputOptions:
|
511
611
|
"""Get the mask_input setting from session recording configuration."""
|
512
612
|
try:
|
@@ -534,12 +634,78 @@ def get_mask_input_setting() -> MaskInputOptions:
|
|
534
634
|
)
|
535
635
|
|
536
636
|
|
537
|
-
# browser_use.browser.session.CDPSession (browser-use >=
|
538
|
-
async def
|
637
|
+
# browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
|
638
|
+
async def get_isolated_context_id(cdp_session) -> int | None:
|
639
|
+
async with lock:
|
640
|
+
tree = {}
|
641
|
+
try:
|
642
|
+
tree = await asyncio.wait_for(
|
643
|
+
cdp_session.cdp_client.send.Page.getFrameTree(
|
644
|
+
session_id=cdp_session.session_id
|
645
|
+
),
|
646
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
647
|
+
)
|
648
|
+
except asyncio.TimeoutError:
|
649
|
+
logger.debug("Timeout error when getting frame tree")
|
650
|
+
return None
|
651
|
+
except Exception as e:
|
652
|
+
logger.debug(f"Failed to get frame tree: {e}")
|
653
|
+
return None
|
654
|
+
frame = tree.get("frameTree", {}).get("frame", {})
|
655
|
+
frame_id = frame.get("id")
|
656
|
+
loader_id = frame.get("loaderId")
|
657
|
+
|
658
|
+
if frame_id is None or loader_id is None:
|
659
|
+
logger.debug("Failed to get frame id or loader id")
|
660
|
+
return None
|
661
|
+
key = f"{frame_id}_{loader_id}"
|
662
|
+
|
663
|
+
if key in frame_to_isolated_context_id:
|
664
|
+
return frame_to_isolated_context_id[key]
|
665
|
+
|
666
|
+
try:
|
667
|
+
result = await asyncio.wait_for(
|
668
|
+
cdp_session.cdp_client.send.Page.createIsolatedWorld(
|
669
|
+
{
|
670
|
+
"frameId": frame_id,
|
671
|
+
"worldName": "laminar-isolated-context",
|
672
|
+
},
|
673
|
+
session_id=cdp_session.session_id,
|
674
|
+
),
|
675
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
676
|
+
)
|
677
|
+
except asyncio.TimeoutError:
|
678
|
+
logger.debug("Timeout error when getting isolated context id")
|
679
|
+
return None
|
680
|
+
except Exception as e:
|
681
|
+
logger.debug(f"Failed to get isolated context id: {e}")
|
682
|
+
return None
|
683
|
+
isolated_context_id = result["executionContextId"]
|
684
|
+
frame_to_isolated_context_id[key] = isolated_context_id
|
685
|
+
return isolated_context_id
|
686
|
+
|
687
|
+
|
688
|
+
# browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
|
689
|
+
async def inject_session_recorder(cdp_session) -> int | None:
|
690
|
+
"""Injects the session recorder base as well as the recorder itself.
|
691
|
+
Returns the isolated context id if successful.
|
692
|
+
"""
|
693
|
+
isolated_context_id = None
|
539
694
|
cdp_client = cdp_session.cdp_client
|
540
695
|
try:
|
696
|
+
should_skip = True
|
541
697
|
try:
|
542
|
-
|
698
|
+
should_skip = await should_skip_page(cdp_session)
|
699
|
+
except Exception as e:
|
700
|
+
logger.debug(f"Failed to check if error page: {e}")
|
701
|
+
|
702
|
+
if should_skip:
|
703
|
+
logger.debug("Empty page detected, skipping session recorder injection")
|
704
|
+
return
|
705
|
+
|
706
|
+
isolated_context_id = await get_isolated_context_id(cdp_session)
|
707
|
+
try:
|
708
|
+
is_loaded = await is_recorder_present(cdp_session, isolated_context_id)
|
543
709
|
except Exception as e:
|
544
710
|
logger.debug(f"Failed to check if session recorder is loaded: {e}")
|
545
711
|
is_loaded = False
|
@@ -547,42 +713,60 @@ async def inject_session_recorder(cdp_session):
|
|
547
713
|
if is_loaded:
|
548
714
|
return
|
549
715
|
|
716
|
+
if isolated_context_id is None:
|
717
|
+
logger.debug("Failed to get isolated context id")
|
718
|
+
return
|
719
|
+
|
550
720
|
async def load_session_recorder():
|
551
721
|
try:
|
552
|
-
await
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
722
|
+
await asyncio.wait_for(
|
723
|
+
cdp_client.send.Runtime.evaluate(
|
724
|
+
{
|
725
|
+
"expression": f"({RRWEB_CONTENT})()",
|
726
|
+
"contextId": isolated_context_id,
|
727
|
+
},
|
728
|
+
session_id=cdp_session.session_id,
|
729
|
+
),
|
730
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
558
731
|
)
|
559
732
|
return True
|
733
|
+
except asyncio.TimeoutError:
|
734
|
+
logger.debug("Timeout error when loading session recorder base")
|
735
|
+
return False
|
560
736
|
except Exception as e:
|
561
|
-
logger.
|
737
|
+
logger.debug(f"Failed to load session recorder base: {e}")
|
562
738
|
return False
|
563
739
|
|
564
740
|
if not await retry_async(
|
565
741
|
load_session_recorder,
|
742
|
+
retries=3,
|
566
743
|
delay=1,
|
567
744
|
error_message="Failed to load session recorder",
|
568
745
|
):
|
569
746
|
return
|
570
747
|
|
571
748
|
try:
|
572
|
-
await
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
749
|
+
await asyncio.wait_for(
|
750
|
+
cdp_client.send.Runtime.evaluate(
|
751
|
+
{
|
752
|
+
"expression": f"({INJECT_PLACEHOLDER})({orjson.dumps(get_mask_input_setting()).decode('utf-8')})",
|
753
|
+
"contextId": isolated_context_id,
|
754
|
+
},
|
755
|
+
session_id=cdp_session.session_id,
|
756
|
+
),
|
757
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
577
758
|
)
|
759
|
+
return isolated_context_id
|
760
|
+
except asyncio.TimeoutError:
|
761
|
+
logger.debug("Timeout error when injecting session recorder")
|
578
762
|
except Exception as e:
|
579
|
-
logger.debug(f"Failed to inject
|
763
|
+
logger.debug(f"Failed to inject recorder: {e}")
|
580
764
|
|
581
765
|
except Exception as e:
|
582
|
-
logger.
|
766
|
+
logger.debug(f"Error during session recorder injection: {e}")
|
583
767
|
|
584
768
|
|
585
|
-
# browser_use.browser.session.CDPSession (browser-use >=
|
769
|
+
# browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
|
586
770
|
@observe(name="cdp_use.session", ignore_input=True, ignore_output=True)
|
587
771
|
async def start_recording_events(
|
588
772
|
cdp_session,
|
@@ -596,7 +780,10 @@ async def start_recording_events(
|
|
596
780
|
trace_id = format(span.get_span_context().trace_id, "032x")
|
597
781
|
span.set_attribute("lmnr.internal.has_browser_session", True)
|
598
782
|
|
599
|
-
await inject_session_recorder(cdp_session)
|
783
|
+
isolated_context_id = await inject_session_recorder(cdp_session)
|
784
|
+
if isolated_context_id is None:
|
785
|
+
logger.debug("Failed to inject session recorder, not registering bindings")
|
786
|
+
return
|
600
787
|
|
601
788
|
# Buffer for reassembling chunks
|
602
789
|
chunk_buffers = {}
|
@@ -654,11 +841,14 @@ async def start_recording_events(
|
|
654
841
|
async def send_events_callback(event, cdp_session_id: str | None = None):
|
655
842
|
if event["name"] != "lmnrSendEvents":
|
656
843
|
return
|
844
|
+
if event["executionContextId"] != isolated_context_id:
|
845
|
+
return
|
657
846
|
await send_events_from_browser(orjson.loads(event["payload"]))
|
658
847
|
|
659
848
|
await cdp_client.send.Runtime.addBinding(
|
660
849
|
{
|
661
850
|
"name": "lmnrSendEvents",
|
851
|
+
"executionContextId": isolated_context_id,
|
662
852
|
},
|
663
853
|
session_id=cdp_session.session_id,
|
664
854
|
)
|
@@ -668,7 +858,7 @@ async def start_recording_events(
|
|
668
858
|
register_on_target_created(cdp_session, lmnr_session_id, client)
|
669
859
|
|
670
860
|
|
671
|
-
# browser_use.browser.session.CDPSession (browser-use >=
|
861
|
+
# browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
|
672
862
|
async def enable_target_discovery(cdp_session):
|
673
863
|
cdp_client = cdp_session.cdp_client
|
674
864
|
await cdp_client.send.Target.setDiscoverTargets(
|
@@ -679,7 +869,7 @@ async def enable_target_discovery(cdp_session):
|
|
679
869
|
)
|
680
870
|
|
681
871
|
|
682
|
-
# browser_use.browser.session.CDPSession (browser-use >=
|
872
|
+
# browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
|
683
873
|
def register_on_target_created(
|
684
874
|
cdp_session, lmnr_session_id: str, client: AsyncLaminarClient
|
685
875
|
):
|
@@ -689,31 +879,63 @@ def register_on_target_created(
|
|
689
879
|
if target_info["type"] == "page":
|
690
880
|
asyncio.create_task(inject_session_recorder(cdp_session=cdp_session))
|
691
881
|
|
692
|
-
|
882
|
+
try:
|
883
|
+
cdp_session.cdp_client.register.Target.targetCreated(on_target_created)
|
884
|
+
except Exception as e:
|
885
|
+
logger.debug(f"Failed to register on target created: {e}")
|
693
886
|
|
694
887
|
|
695
|
-
# browser_use.browser.session.CDPSession (browser-use >=
|
696
|
-
async def is_recorder_present(
|
888
|
+
# browser_use.browser.session.CDPSession (browser-use >= 0.6.0)
|
889
|
+
async def is_recorder_present(
|
890
|
+
cdp_session, isolated_context_id: int | None = None
|
891
|
+
) -> bool:
|
892
|
+
# This function returns True on any error, because it is safer to not record
|
893
|
+
# events than to try to inject the recorder into a broken context.
|
697
894
|
cdp_client = cdp_session.cdp_client
|
895
|
+
if isolated_context_id is None:
|
896
|
+
isolated_context_id = await get_isolated_context_id(cdp_session)
|
897
|
+
if isolated_context_id is None:
|
898
|
+
logger.debug("Failed to get isolated context id")
|
899
|
+
return True
|
698
900
|
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
901
|
+
try:
|
902
|
+
result = await asyncio.wait_for(
|
903
|
+
cdp_client.send.Runtime.evaluate(
|
904
|
+
{
|
905
|
+
"expression": "typeof window.lmnrRrweb !== 'undefined'",
|
906
|
+
"contextId": isolated_context_id,
|
907
|
+
},
|
908
|
+
session_id=cdp_session.session_id,
|
909
|
+
),
|
910
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
911
|
+
)
|
912
|
+
if result and "result" in result and "value" in result["result"]:
|
913
|
+
return result["result"]["value"]
|
914
|
+
return False
|
915
|
+
except asyncio.TimeoutError:
|
916
|
+
logger.debug("Timeout error when checking if session recorder is present")
|
917
|
+
return True
|
918
|
+
except Exception:
|
919
|
+
logger.debug("Exception when checking if session recorder is present")
|
920
|
+
return True
|
710
921
|
|
711
922
|
|
712
923
|
async def take_full_snapshot(cdp_session):
|
713
924
|
cdp_client = cdp_session.cdp_client
|
714
|
-
|
715
|
-
|
716
|
-
|
925
|
+
isolated_context_id = await get_isolated_context_id(cdp_session)
|
926
|
+
if isolated_context_id is None:
|
927
|
+
logger.debug("Failed to get isolated context id")
|
928
|
+
return False
|
929
|
+
|
930
|
+
if await should_skip_page(cdp_session):
|
931
|
+
logger.debug("Skipping full snapshot")
|
932
|
+
return False
|
933
|
+
|
934
|
+
try:
|
935
|
+
result = await asyncio.wait_for(
|
936
|
+
cdp_client.send.Runtime.evaluate(
|
937
|
+
{
|
938
|
+
"expression": """(() => {
|
717
939
|
if (window.lmnrRrweb) {
|
718
940
|
try {
|
719
941
|
window.lmnrRrweb.record.takeFullSnapshot();
|
@@ -725,9 +947,18 @@ async def take_full_snapshot(cdp_session):
|
|
725
947
|
}
|
726
948
|
return false;
|
727
949
|
})()""",
|
728
|
-
|
729
|
-
|
730
|
-
|
950
|
+
"contextId": isolated_context_id,
|
951
|
+
},
|
952
|
+
session_id=cdp_session.session_id,
|
953
|
+
),
|
954
|
+
timeout=CDP_OPERATION_TIMEOUT_SECONDS,
|
955
|
+
)
|
956
|
+
except asyncio.TimeoutError:
|
957
|
+
logger.debug("Timeout error when taking full snapshot")
|
958
|
+
return False
|
959
|
+
except Exception as e:
|
960
|
+
logger.debug(f"Error when taking full snapshot: {e}")
|
961
|
+
return False
|
731
962
|
if result and "result" in result and "value" in result["result"]:
|
732
963
|
return result["result"]["value"]
|
733
964
|
return False
|
lmnr/sdk/evaluations.py
CHANGED
@@ -112,7 +112,12 @@ class Evaluation:
|
|
112
112
|
base_http_url: str | None = None,
|
113
113
|
http_port: int | None = None,
|
114
114
|
grpc_port: int | None = None,
|
115
|
-
instruments:
|
115
|
+
instruments: (
|
116
|
+
set[Instruments] | list[Instruments] | tuple[Instruments] | None
|
117
|
+
) = None,
|
118
|
+
disabled_instruments: (
|
119
|
+
set[Instruments] | list[Instruments] | tuple[Instruments] | None
|
120
|
+
) = None,
|
116
121
|
max_export_batch_size: int | None = MAX_EXPORT_BATCH_SIZE,
|
117
122
|
trace_export_timeout_seconds: int | None = None,
|
118
123
|
):
|
@@ -172,6 +177,10 @@ class Evaluation:
|
|
172
177
|
used.
|
173
178
|
See https://docs.lmnr.ai/tracing/automatic-instrumentation
|
174
179
|
Defaults to None.
|
180
|
+
disabled_instruments (set[Instruments] | None, optional): Set of modules\
|
181
|
+
to disable auto-instrumentations. If None, only modules passed\
|
182
|
+
as `instruments` will be disabled.
|
183
|
+
Defaults to None.
|
175
184
|
"""
|
176
185
|
|
177
186
|
if not evaluators:
|
@@ -234,6 +243,7 @@ class Evaluation:
|
|
234
243
|
http_port=http_port,
|
235
244
|
grpc_port=grpc_port,
|
236
245
|
instruments=instruments,
|
246
|
+
disabled_instruments=disabled_instruments,
|
237
247
|
max_export_batch_size=max_export_batch_size,
|
238
248
|
export_timeout_seconds=trace_export_timeout_seconds,
|
239
249
|
)
|
@@ -432,7 +442,12 @@ def evaluate(
|
|
432
442
|
base_http_url: str | None = None,
|
433
443
|
http_port: int | None = None,
|
434
444
|
grpc_port: int | None = None,
|
435
|
-
instruments:
|
445
|
+
instruments: (
|
446
|
+
set[Instruments] | list[Instruments] | tuple[Instruments] | None
|
447
|
+
) = None,
|
448
|
+
disabled_instruments: (
|
449
|
+
set[Instruments] | list[Instruments] | tuple[Instruments] | None
|
450
|
+
) = None,
|
436
451
|
max_export_batch_size: int | None = MAX_EXPORT_BATCH_SIZE,
|
437
452
|
trace_export_timeout_seconds: int | None = None,
|
438
453
|
) -> Awaitable[None] | None:
|
@@ -493,6 +508,10 @@ def evaluate(
|
|
493
508
|
auto-instrument. If None, all available instruments\
|
494
509
|
will be used.
|
495
510
|
Defaults to None.
|
511
|
+
disabled_instruments (set[Instruments] | None, optional): Set of modules\
|
512
|
+
to disable auto-instrumentations. If None, no\
|
513
|
+
If None, only modules passed as `instruments` will be disabled.
|
514
|
+
Defaults to None.
|
496
515
|
trace_export_timeout_seconds (int | None, optional): The timeout for\
|
497
516
|
trace export on OpenTelemetry exporter. Defaults to None.
|
498
517
|
"""
|
@@ -510,6 +529,7 @@ def evaluate(
|
|
510
529
|
http_port=http_port,
|
511
530
|
grpc_port=grpc_port,
|
512
531
|
instruments=instruments,
|
532
|
+
disabled_instruments=disabled_instruments,
|
513
533
|
max_export_batch_size=max_export_batch_size,
|
514
534
|
trace_export_timeout_seconds=trace_export_timeout_seconds,
|
515
535
|
)
|
lmnr/sdk/laminar.py
CHANGED
@@ -421,14 +421,14 @@ class Laminar:
|
|
421
421
|
|
422
422
|
Usage example:
|
423
423
|
```python
|
424
|
-
from src.lmnr import Laminar
|
424
|
+
from src.lmnr import Laminar
|
425
425
|
def foo(span):
|
426
|
-
with use_span(span):
|
426
|
+
with Laminar.use_span(span):
|
427
427
|
with Laminar.start_as_current_span("foo_inner"):
|
428
428
|
some_function()
|
429
429
|
|
430
430
|
def bar():
|
431
|
-
with use_span(span):
|
431
|
+
with Laminar.use_span(span):
|
432
432
|
openai_client.chat.completions.create()
|
433
433
|
|
434
434
|
span = Laminar.start_span("outer")
|
lmnr/version.py
CHANGED
@@ -46,12 +46,12 @@ lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py,sh
|
|
46
46
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py,sha256=1f86cdf738e2f68586b0a4569bb1e40edddd85c529f511ef49945ddb7b61fab5,2648
|
47
47
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py,sha256=764e4fe979fb08d7821419a3cc5c3ae89a6664b626ef928259f8f175c939eaea,6334
|
48
48
|
lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py,sha256=90aa8558467d7e469fe1a6c75372c113da403557715f03b522b2fab94b287c40,6320
|
49
|
-
lmnr/opentelemetry_lib/tracing/__init__.py,sha256=
|
50
|
-
lmnr/opentelemetry_lib/tracing/_instrument_initializers.py,sha256=
|
49
|
+
lmnr/opentelemetry_lib/tracing/__init__.py,sha256=69dd8575908cedfebd29cd48c288cbf2cca1f689c469266bcdf79c7351bed99d,10979
|
50
|
+
lmnr/opentelemetry_lib/tracing/_instrument_initializers.py,sha256=cb2ce23406bdcbcad14489428d8a3438a948299bbb68a179cc3cbd44645154c0,16471
|
51
51
|
lmnr/opentelemetry_lib/tracing/attributes.py,sha256=a879e337ff4e8569a4454544d303ccbc3b04bd42e1cdb765eb563aeaa08f731d,1653
|
52
52
|
lmnr/opentelemetry_lib/tracing/context.py,sha256=83f842be0fc29a96647cbf005c39ea761b0fb5913c4102f965411f47906a6135,4103
|
53
53
|
lmnr/opentelemetry_lib/tracing/exporter.py,sha256=6af8e61fd873e8f5db315d9b9f1edbf46b860ba7e50140f0bdcc6864c6d35a03,2082
|
54
|
-
lmnr/opentelemetry_lib/tracing/instruments.py,sha256=
|
54
|
+
lmnr/opentelemetry_lib/tracing/instruments.py,sha256=8482c9df1310a47117667469eeeddb60707da484ee7e80b69f506ef1594e380a,5848
|
55
55
|
lmnr/opentelemetry_lib/tracing/processor.py,sha256=cbc70f138e70c878ef57b02a2c46ef48dd7f694a522623a82dff1623b73d1e1c,3353
|
56
56
|
lmnr/opentelemetry_lib/tracing/tracer.py,sha256=33769a9a97385f5697eb0e0a6b1813a57ed956c7a8379d7ac2523e700e7dd528,1362
|
57
57
|
lmnr/opentelemetry_lib/utils/__init__.py,sha256=a4d85fd06def4dde5c728734de2d4c5c36eb89c49a8aa09b8b50cb5a149e90af,604
|
@@ -61,9 +61,10 @@ lmnr/opentelemetry_lib/utils/wrappers.py,sha256=f7b1134809f2c408976a71e661fee5cb
|
|
61
61
|
lmnr/py.typed,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
62
62
|
lmnr/sdk/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
63
63
|
lmnr/sdk/browser/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
|
64
|
-
lmnr/sdk/browser/browser_use_cdp_otel.py,sha256=
|
64
|
+
lmnr/sdk/browser/browser_use_cdp_otel.py,sha256=4ee0ce6b60b8b20c8a862d5ebce9f2a99dbf0867d549cc44455294ab1281137d,3379
|
65
65
|
lmnr/sdk/browser/browser_use_otel.py,sha256=e5549878c07bad451efef9f460ce52202284cff50075bce700ca61749102c5eb,5065
|
66
|
-
lmnr/sdk/browser/
|
66
|
+
lmnr/sdk/browser/bubus_otel.py,sha256=92991ba5f6b5e2a6e131ab57df7c383345b57982acfdb1437540a1609ad1f876,2465
|
67
|
+
lmnr/sdk/browser/cdp_utils.py,sha256=e49b2a526f4c6dfc0646e26d33c0f4beebbc9dad5d5d2b0389737b339c6cc0b4,35067
|
67
68
|
lmnr/sdk/browser/patchright_otel.py,sha256=9d22ab1f28f1eddbcfd0032a14fe306bfe00bfc7f11128cb99836c4dd15fb7c8,4800
|
68
69
|
lmnr/sdk/browser/playwright_otel.py,sha256=859d220d856c8fe7104863efca0c6a3ed5464d778675e07d7f79c48f73d5e838,10416
|
69
70
|
lmnr/sdk/browser/pw_utils.py,sha256=a75769eb977d8e56c38a0eefad09b87550b872f8d4df186b36a8c4d4af2bffaf,29021
|
@@ -88,13 +89,13 @@ lmnr/sdk/client/synchronous/sync_client.py,sha256=0bebe88e3aed689505e9ed3d32036f
|
|
88
89
|
lmnr/sdk/datasets.py,sha256=3fd851c5f97bf88eaa84b1451a053eaff23b4497cbb45eac2f9ea0e5f2886c00,1708
|
89
90
|
lmnr/sdk/decorators.py,sha256=c709b76a814e019c919fd811591850787a2f266b7b6f46123f66ddd92e1092d5,6920
|
90
91
|
lmnr/sdk/eval_control.py,sha256=291394ac385c653ae9b5167e871bebeb4fe8fc6b7ff2ed38e636f87015dcba86,184
|
91
|
-
lmnr/sdk/evaluations.py,sha256=
|
92
|
-
lmnr/sdk/laminar.py,sha256=
|
92
|
+
lmnr/sdk/evaluations.py,sha256=7e55cbca77fa32cb64cb77aed8076a1994258a5b652c7f1d45231928e4aefe26,23885
|
93
|
+
lmnr/sdk/laminar.py,sha256=199bab54d49f918f110d7b98569551d8ab7ea4e93828af7381ba56b1181e855b,37539
|
93
94
|
lmnr/sdk/log.py,sha256=9edfd83263f0d4845b1b2d1beeae2b4ed3f8628de941f371a893d72b79c348d4,2213
|
94
95
|
lmnr/sdk/types.py,sha256=f8a8368e225c4d2f82df54d92f029065afb60c3eff494c77c6e574963ed524ff,13454
|
95
96
|
lmnr/sdk/utils.py,sha256=0c5a81c305dcd3922f4b31c4f42cf83719c03888725838395adae167de92db76,5019
|
96
|
-
lmnr/version.py,sha256=
|
97
|
-
lmnr-0.7.
|
98
|
-
lmnr-0.7.
|
99
|
-
lmnr-0.7.
|
100
|
-
lmnr-0.7.
|
97
|
+
lmnr/version.py,sha256=52bb47a5d5e8f391c347d7742269dd0b231aa0de29e1ddbac7958d9b94cb47db,1322
|
98
|
+
lmnr-0.7.11.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
|
99
|
+
lmnr-0.7.11.dist-info/entry_points.txt,sha256=abdf3411b7dd2d7329a241f2da6669bab4e314a747a586ecdb9f888f3035003c,39
|
100
|
+
lmnr-0.7.11.dist-info/METADATA,sha256=1beeb3d2258e4416445fea5d6e5d4c788fd5ccabfedb7e814352f78863001cad,14197
|
101
|
+
lmnr-0.7.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|