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