lmnr 0.6.16__py3-none-any.whl → 0.7.26__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/__init__.py +6 -15
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/{cli.py → cli/evals.py} +20 -102
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +9 -2
- lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
- lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
- lmnr/opentelemetry_lib/litellm/utils.py +82 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
- lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
- lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
- lmnr/opentelemetry_lib/tracing/processor.py +128 -30
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +12 -12
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +18 -44
- lmnr/sdk/browser/playwright_otel.py +104 -187
- lmnr/sdk/browser/pw_utils.py +249 -210
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +1 -1
- lmnr/sdk/client/asynchronous/async_client.py +47 -15
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +83 -17
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/sync_client.py +47 -15
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +103 -23
- lmnr/sdk/evaluations.py +122 -33
- lmnr/sdk/laminar.py +816 -333
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +124 -143
- lmnr/sdk/utils.py +115 -2
- lmnr/version.py +1 -1
- {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
- lmnr/sdk/client/synchronous/resources/agent.py +0 -323
- lmnr/sdk/datasets.py +0 -60
- lmnr-0.6.16.dist-info/LICENSE +0 -75
- lmnr-0.6.16.dist-info/RECORD +0 -61
- lmnr-0.6.16.dist-info/WHEEL +0 -4
- lmnr-0.6.16.dist-info/entry_points.txt +0 -3
lmnr/sdk/browser/pw_utils.py
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import logging
|
|
3
2
|
import os
|
|
4
3
|
import time
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
6
|
|
|
7
7
|
from opentelemetry import trace
|
|
8
8
|
|
|
9
|
+
from lmnr.opentelemetry_lib.tracing.context import get_current_context
|
|
10
|
+
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
|
9
11
|
from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
|
|
10
12
|
from lmnr.sdk.decorators import observe
|
|
11
13
|
from lmnr.sdk.browser.utils import retry_sync, retry_async
|
|
12
|
-
from lmnr.sdk.
|
|
14
|
+
from lmnr.sdk.browser.background_send_events import (
|
|
15
|
+
get_background_loop,
|
|
16
|
+
track_async_send,
|
|
17
|
+
)
|
|
13
18
|
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
|
14
|
-
|
|
19
|
+
from lmnr.sdk.log import get_default_logger
|
|
20
|
+
from lmnr.sdk.types import MaskInputOptions
|
|
15
21
|
|
|
16
22
|
try:
|
|
17
23
|
if is_package_installed("playwright"):
|
|
@@ -33,292 +39,325 @@ except ImportError as e:
|
|
|
33
39
|
"or `pip install patchright` to install one of the supported browsers."
|
|
34
40
|
) from e
|
|
35
41
|
|
|
36
|
-
logger =
|
|
42
|
+
logger = get_default_logger(__name__)
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
with open(os.path.join(current_dir, "rrweb", "rrweb.umd.min.cjs"), "r") as f:
|
|
40
|
-
RRWEB_CONTENT = f"() => {{ {f.read()} }}"
|
|
44
|
+
OLD_BUFFER_TIMEOUT = 60
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
async function compressEventData(data) {
|
|
50
|
-
const jsonString = JSON.stringify(data);
|
|
51
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
52
|
-
const compressedStream = blob.stream().pipeThrough(new CompressionStream('gzip'));
|
|
53
|
-
const compressedResponse = new Response(compressedStream);
|
|
54
|
-
const compressedData = await compressedResponse.arrayBuffer();
|
|
55
|
-
return Array.from(new Uint8Array(compressedData));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
window.lmnrGetAndClearEvents = () => {
|
|
59
|
-
const events = window.lmnrRrwebEventsBatch;
|
|
60
|
-
window.lmnrRrwebEventsBatch = new Set();
|
|
61
|
-
return Array.from(events);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
// Add heartbeat events
|
|
65
|
-
setInterval(async () => {
|
|
66
|
-
window.lmnrRrweb.record.addCustomEvent('heartbeat', {
|
|
67
|
-
title: document.title,
|
|
68
|
-
url: document.URL,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
}, 1000);
|
|
72
|
-
|
|
73
|
-
window.lmnrRrweb.record({
|
|
74
|
-
async emit(event) {
|
|
75
|
-
// Compress the data field
|
|
76
|
-
const compressedEvent = {
|
|
77
|
-
...event,
|
|
78
|
-
data: await compressEventData(event.data)
|
|
79
|
-
};
|
|
80
|
-
window.lmnrRrwebEventsBatch.add(compressedEvent);
|
|
81
|
-
},
|
|
82
|
-
recordCanvas: true,
|
|
83
|
-
collectFonts: true,
|
|
84
|
-
recordCrossOriginIframes: true
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
async def send_events_async(
|
|
91
|
-
page: Page, session_id: str, trace_id: str, client: AsyncLaminarClient
|
|
46
|
+
|
|
47
|
+
def create_send_events_handler(
|
|
48
|
+
chunk_buffers: dict,
|
|
49
|
+
session_id: str,
|
|
50
|
+
trace_id: str,
|
|
51
|
+
client: AsyncLaminarClient,
|
|
52
|
+
background_loop: asyncio.AbstractEventLoop,
|
|
92
53
|
):
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
# Check if function exists first
|
|
96
|
-
events = await page.evaluate(
|
|
97
|
-
"""
|
|
98
|
-
() => {
|
|
99
|
-
if (typeof window.lmnrGetAndClearEvents !== 'function') {
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
return window.lmnrGetAndClearEvents();
|
|
103
|
-
}
|
|
104
|
-
"""
|
|
105
|
-
)
|
|
54
|
+
"""
|
|
55
|
+
Create an async event handler for sending browser events.
|
|
106
56
|
|
|
107
|
-
|
|
108
|
-
|
|
57
|
+
This handler reassembles chunked event data and submits it to the background
|
|
58
|
+
loop for async HTTP sending. The handler itself processes chunks synchronously
|
|
59
|
+
but delegates the actual HTTP send to the background loop.
|
|
109
60
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
else:
|
|
117
|
-
logger.debug(f"Could not send events: {e}")
|
|
61
|
+
Args:
|
|
62
|
+
chunk_buffers: Dictionary to store incomplete chunk batches
|
|
63
|
+
session_id: Browser session ID
|
|
64
|
+
trace_id: OpenTelemetry trace ID
|
|
65
|
+
client: Async Laminar client for HTTP requests
|
|
66
|
+
background_loop: Background event loop for async sends
|
|
118
67
|
|
|
68
|
+
Returns:
|
|
69
|
+
An async function that handles incoming event chunks from the browser
|
|
70
|
+
"""
|
|
119
71
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if not events or len(events) == 0:
|
|
136
|
-
return
|
|
72
|
+
async def send_events_from_browser(chunk):
|
|
73
|
+
try:
|
|
74
|
+
# Handle chunked data
|
|
75
|
+
batch_id = chunk["batchId"]
|
|
76
|
+
chunk_index = chunk["chunkIndex"]
|
|
77
|
+
total_chunks = chunk["totalChunks"]
|
|
78
|
+
data = chunk["data"]
|
|
79
|
+
|
|
80
|
+
# Initialize buffer for this batch if needed
|
|
81
|
+
if batch_id not in chunk_buffers:
|
|
82
|
+
chunk_buffers[batch_id] = {
|
|
83
|
+
"chunks": {},
|
|
84
|
+
"total": total_chunks,
|
|
85
|
+
"timestamp": time.time(),
|
|
86
|
+
}
|
|
137
87
|
|
|
138
|
-
|
|
88
|
+
# Store chunk
|
|
89
|
+
chunk_buffers[batch_id]["chunks"][chunk_index] = data
|
|
90
|
+
|
|
91
|
+
# Check if we have all chunks
|
|
92
|
+
if len(chunk_buffers[batch_id]["chunks"]) == total_chunks:
|
|
93
|
+
# Reassemble the full message
|
|
94
|
+
full_data = ""
|
|
95
|
+
for i in range(total_chunks):
|
|
96
|
+
full_data += chunk_buffers[batch_id]["chunks"][i]
|
|
97
|
+
|
|
98
|
+
# Parse the JSON
|
|
99
|
+
events = orjson.loads(full_data)
|
|
100
|
+
|
|
101
|
+
# Send to server in background loop (independent of Playwright's loop)
|
|
102
|
+
if events and len(events) > 0:
|
|
103
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
104
|
+
client._browser_events.send(session_id, trace_id, events),
|
|
105
|
+
background_loop,
|
|
106
|
+
)
|
|
107
|
+
track_async_send(future)
|
|
108
|
+
|
|
109
|
+
# Clean up buffer
|
|
110
|
+
del chunk_buffers[batch_id]
|
|
111
|
+
|
|
112
|
+
# Clean up old incomplete buffers
|
|
113
|
+
current_time = time.time()
|
|
114
|
+
to_delete = []
|
|
115
|
+
for bid, buffer in chunk_buffers.items():
|
|
116
|
+
if current_time - buffer["timestamp"] > OLD_BUFFER_TIMEOUT:
|
|
117
|
+
to_delete.append(bid)
|
|
118
|
+
for bid in to_delete:
|
|
119
|
+
logger.debug(f"Cleaning up incomplete chunk buffer: {bid}")
|
|
120
|
+
del chunk_buffers[bid]
|
|
139
121
|
|
|
140
|
-
|
|
141
|
-
if str(e).startswith("Page.evaluate: Execution context was destroyed"):
|
|
142
|
-
logger.info("Execution context was destroyed, injecting rrweb again")
|
|
143
|
-
inject_rrweb_sync(page)
|
|
144
|
-
send_events_sync(page, session_id, trace_id, client)
|
|
145
|
-
else:
|
|
122
|
+
except Exception as e:
|
|
146
123
|
logger.debug(f"Could not send events: {e}")
|
|
147
124
|
|
|
125
|
+
return send_events_from_browser
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
129
|
+
with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
|
|
130
|
+
RRWEB_CONTENT = f"() => {{ {f.read()} }}"
|
|
148
131
|
|
|
149
|
-
|
|
132
|
+
with open(os.path.join(current_dir, "inject_script.js"), "r") as f:
|
|
133
|
+
INJECT_SCRIPT_CONTENT = f.read()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_mask_input_setting() -> MaskInputOptions:
|
|
137
|
+
"""Get the mask_input setting from session recording configuration."""
|
|
150
138
|
try:
|
|
151
|
-
|
|
139
|
+
config = TracerWrapper.get_session_recording_options()
|
|
140
|
+
return config.get(
|
|
141
|
+
"mask_input_options",
|
|
142
|
+
MaskInputOptions(
|
|
143
|
+
textarea=False,
|
|
144
|
+
text=False,
|
|
145
|
+
number=False,
|
|
146
|
+
select=False,
|
|
147
|
+
email=False,
|
|
148
|
+
tel=False,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
except (AttributeError, Exception):
|
|
152
|
+
# Fallback to default configuration if TracerWrapper is not initialized
|
|
153
|
+
return MaskInputOptions(
|
|
154
|
+
textarea=False,
|
|
155
|
+
text=False,
|
|
156
|
+
number=False,
|
|
157
|
+
select=False,
|
|
158
|
+
email=False,
|
|
159
|
+
tel=False,
|
|
160
|
+
)
|
|
152
161
|
|
|
153
|
-
|
|
162
|
+
|
|
163
|
+
def inject_session_recorder_sync(page: SyncPage):
|
|
164
|
+
try:
|
|
154
165
|
try:
|
|
155
166
|
is_loaded = page.evaluate(
|
|
156
167
|
"""() => typeof window.lmnrRrweb !== 'undefined'"""
|
|
157
168
|
)
|
|
158
169
|
except Exception as e:
|
|
159
|
-
logger.debug(f"Failed to check if
|
|
170
|
+
logger.debug(f"Failed to check if session recorder is loaded: {e}")
|
|
160
171
|
is_loaded = False
|
|
161
172
|
|
|
162
173
|
if not is_loaded:
|
|
163
174
|
|
|
164
|
-
def
|
|
175
|
+
def load_session_recorder():
|
|
165
176
|
try:
|
|
177
|
+
if page.is_closed():
|
|
178
|
+
return False
|
|
166
179
|
page.evaluate(RRWEB_CONTENT)
|
|
167
180
|
return True
|
|
168
181
|
except Exception as e:
|
|
169
|
-
logger.debug(f"Failed to load
|
|
182
|
+
logger.debug(f"Failed to load session recorder: {e}")
|
|
170
183
|
return False
|
|
171
184
|
|
|
172
185
|
if not retry_sync(
|
|
173
|
-
|
|
186
|
+
load_session_recorder,
|
|
187
|
+
delay=1,
|
|
188
|
+
error_message="Failed to load session recorder",
|
|
174
189
|
):
|
|
175
190
|
return
|
|
176
191
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
try:
|
|
193
|
+
if not page.is_closed():
|
|
194
|
+
page.evaluate(
|
|
195
|
+
f"({INJECT_SCRIPT_CONTENT})({orjson.dumps(get_mask_input_setting()).decode('utf-8')}, false)"
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.debug(f"Failed to inject session recorder: {e}")
|
|
181
199
|
|
|
182
200
|
except Exception as e:
|
|
183
|
-
logger.error(f"Error during
|
|
201
|
+
logger.error(f"Error during session recorder injection: {e}")
|
|
184
202
|
|
|
185
203
|
|
|
186
|
-
async def
|
|
204
|
+
async def inject_session_recorder_async(page: Page):
|
|
187
205
|
try:
|
|
188
|
-
await page.wait_for_load_state("domcontentloaded")
|
|
189
|
-
|
|
190
|
-
# Wrap the evaluate call in a try-catch
|
|
191
206
|
try:
|
|
192
207
|
is_loaded = await page.evaluate(
|
|
193
208
|
"""() => typeof window.lmnrRrweb !== 'undefined'"""
|
|
194
209
|
)
|
|
195
210
|
except Exception as e:
|
|
196
|
-
logger.debug(f"Failed to check if
|
|
211
|
+
logger.debug(f"Failed to check if session recorder is loaded: {e}")
|
|
197
212
|
is_loaded = False
|
|
198
213
|
|
|
199
214
|
if not is_loaded:
|
|
200
215
|
|
|
201
|
-
async def
|
|
216
|
+
async def load_session_recorder():
|
|
202
217
|
try:
|
|
218
|
+
if page.is_closed():
|
|
219
|
+
return False
|
|
203
220
|
await page.evaluate(RRWEB_CONTENT)
|
|
204
221
|
return True
|
|
205
222
|
except Exception as e:
|
|
206
|
-
logger.debug(f"Failed to load
|
|
223
|
+
logger.debug(f"Failed to load session recorder: {e}")
|
|
207
224
|
return False
|
|
208
225
|
|
|
209
226
|
if not await retry_async(
|
|
210
|
-
|
|
227
|
+
load_session_recorder,
|
|
228
|
+
delay=1,
|
|
229
|
+
error_message="Failed to load session recorder",
|
|
211
230
|
):
|
|
212
231
|
return
|
|
213
232
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
try:
|
|
234
|
+
if not page.is_closed():
|
|
235
|
+
await page.evaluate(
|
|
236
|
+
f"({INJECT_SCRIPT_CONTENT})({orjson.dumps(get_mask_input_setting()).decode('utf-8')}, false)"
|
|
237
|
+
)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.debug(f"Failed to inject session recorder placeholder: {e}")
|
|
218
240
|
|
|
219
241
|
except Exception as e:
|
|
220
|
-
logger.error(f"Error during
|
|
242
|
+
logger.error(f"Error during session recorder injection: {e}")
|
|
221
243
|
|
|
222
244
|
|
|
223
245
|
@observe(name="playwright.page", ignore_input=True, ignore_output=True)
|
|
224
|
-
def
|
|
225
|
-
page: SyncPage, session_id: str,
|
|
246
|
+
def start_recording_events_sync(
|
|
247
|
+
page: SyncPage, session_id: str, client: AsyncLaminarClient
|
|
226
248
|
):
|
|
227
|
-
trace.get_current_span().set_attribute("lmnr.internal.has_browser_session", True)
|
|
228
|
-
original_bring_to_front = page.bring_to_front
|
|
229
|
-
|
|
230
|
-
def bring_to_front():
|
|
231
|
-
original_bring_to_front()
|
|
232
|
-
page.evaluate(
|
|
233
|
-
"""() => {
|
|
234
|
-
if (window.lmnrRrweb) {
|
|
235
|
-
try {
|
|
236
|
-
window.lmnrRrweb.record.takeFullSnapshot();
|
|
237
|
-
} catch (e) {
|
|
238
|
-
console.error("Error taking full snapshot:", e);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}"""
|
|
242
|
-
)
|
|
243
249
|
|
|
244
|
-
|
|
250
|
+
ctx = get_current_context()
|
|
251
|
+
span = trace.get_current_span(ctx)
|
|
252
|
+
trace_id = format(span.get_span_context().trace_id, "032x")
|
|
253
|
+
span.set_attribute("lmnr.internal.has_browser_session", True)
|
|
254
|
+
|
|
255
|
+
# Get the background loop for async sends
|
|
256
|
+
background_loop = get_background_loop()
|
|
245
257
|
|
|
246
|
-
|
|
258
|
+
# Buffer for reassembling chunks
|
|
259
|
+
chunk_buffers = {}
|
|
260
|
+
|
|
261
|
+
# Create the async event handler (shared implementation)
|
|
262
|
+
send_events_from_browser = create_send_events_handler(
|
|
263
|
+
chunk_buffers, session_id, trace_id, client, background_loop
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def submit_event(chunk):
|
|
267
|
+
"""Sync wrapper that submits async handler to background loop."""
|
|
247
268
|
try:
|
|
248
|
-
|
|
269
|
+
# Submit async handler to background loop
|
|
270
|
+
asyncio.run_coroutine_threadsafe(
|
|
271
|
+
send_events_from_browser(chunk),
|
|
272
|
+
background_loop,
|
|
273
|
+
)
|
|
249
274
|
except Exception as e:
|
|
250
|
-
logger.
|
|
275
|
+
logger.debug(f"Error submitting event: {e}")
|
|
251
276
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
277
|
+
try:
|
|
278
|
+
page.expose_function("lmnrSendEvents", submit_event)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.debug(f"Could not expose function: {e}")
|
|
256
281
|
|
|
257
|
-
|
|
258
|
-
thread.start()
|
|
282
|
+
inject_session_recorder_sync(page)
|
|
259
283
|
|
|
260
|
-
def
|
|
284
|
+
def on_load(p):
|
|
261
285
|
try:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
except Exception:
|
|
265
|
-
|
|
286
|
+
if not p.is_closed():
|
|
287
|
+
inject_session_recorder_sync(p)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.debug(f"Error in on_load handler: {e}")
|
|
266
290
|
|
|
267
|
-
page.on("
|
|
268
|
-
page.on("close", on_close)
|
|
269
|
-
inject_rrweb_sync(page)
|
|
291
|
+
page.on("domcontentloaded", on_load)
|
|
270
292
|
|
|
271
293
|
|
|
272
294
|
@observe(name="playwright.page", ignore_input=True, ignore_output=True)
|
|
273
|
-
async def
|
|
274
|
-
page: Page, session_id: str,
|
|
295
|
+
async def start_recording_events_async(
|
|
296
|
+
page: Page, session_id: str, client: AsyncLaminarClient
|
|
275
297
|
):
|
|
276
|
-
|
|
298
|
+
ctx = get_current_context()
|
|
299
|
+
span = trace.get_current_span(ctx)
|
|
300
|
+
trace_id = format(span.get_span_context().trace_id, "032x")
|
|
301
|
+
span.set_attribute("lmnr.internal.has_browser_session", True)
|
|
277
302
|
|
|
278
|
-
async
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
303
|
+
# Get the background loop for async sends (independent of Playwright's loop)
|
|
304
|
+
background_loop = get_background_loop()
|
|
305
|
+
|
|
306
|
+
# Buffer for reassembling chunks
|
|
307
|
+
chunk_buffers = {}
|
|
308
|
+
|
|
309
|
+
# Create the async event handler (shared implementation)
|
|
310
|
+
send_events_from_browser = create_send_events_handler(
|
|
311
|
+
chunk_buffers, session_id, trace_id, client, background_loop
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
await page.expose_function("lmnrSendEvents", send_events_from_browser)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.debug(f"Could not expose function: {e}")
|
|
286
318
|
|
|
287
|
-
|
|
288
|
-
task = asyncio.create_task(collection_loop())
|
|
319
|
+
await inject_session_recorder_async(page)
|
|
289
320
|
|
|
290
|
-
async def on_load():
|
|
321
|
+
async def on_load(p):
|
|
291
322
|
try:
|
|
292
|
-
|
|
323
|
+
# Check if page is closed before attempting to inject
|
|
324
|
+
if not p.is_closed():
|
|
325
|
+
await inject_session_recorder_async(p)
|
|
293
326
|
except Exception as e:
|
|
294
|
-
logger.
|
|
327
|
+
logger.debug(f"Error in on_load handler: {e}")
|
|
295
328
|
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
task.cancel()
|
|
299
|
-
await send_events_async(page, session_id, trace_id, client)
|
|
300
|
-
except Exception:
|
|
301
|
-
pass
|
|
302
|
-
|
|
303
|
-
page.on("load", lambda: asyncio.create_task(on_load()))
|
|
304
|
-
page.on("close", lambda: asyncio.create_task(on_close()))
|
|
305
|
-
|
|
306
|
-
original_bring_to_front = page.bring_to_front
|
|
307
|
-
|
|
308
|
-
async def bring_to_front():
|
|
309
|
-
await original_bring_to_front()
|
|
310
|
-
|
|
311
|
-
await page.evaluate(
|
|
312
|
-
"""() => {
|
|
313
|
-
if (window.lmnrRrweb) {
|
|
314
|
-
try {
|
|
315
|
-
window.lmnrRrweb.record.takeFullSnapshot();
|
|
316
|
-
} catch (e) {
|
|
317
|
-
console.error("Error taking full snapshot:", e);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}"""
|
|
321
|
-
)
|
|
329
|
+
page.on("domcontentloaded", on_load)
|
|
322
330
|
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
|
|
332
|
+
def take_full_snapshot(page: Page):
|
|
333
|
+
return page.evaluate(
|
|
334
|
+
"""() => {
|
|
335
|
+
if (window.lmnrRrweb) {
|
|
336
|
+
try {
|
|
337
|
+
window.lmnrRrweb.record.takeFullSnapshot();
|
|
338
|
+
return true;
|
|
339
|
+
} catch (e) {
|
|
340
|
+
console.error("Error taking full snapshot:", e);
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}"""
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
async def take_full_snapshot_async(page: Page):
|
|
350
|
+
return await page.evaluate(
|
|
351
|
+
"""() => {
|
|
352
|
+
if (window.lmnrRrweb) {
|
|
353
|
+
try {
|
|
354
|
+
window.lmnrRrweb.record.takeFullSnapshot();
|
|
355
|
+
return true;
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.error("Error taking full snapshot:", e);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}"""
|
|
363
|
+
)
|