lmnr 0.5.2__py3-none-any.whl → 0.6.0__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 +7 -2
- lmnr/cli.py +10 -8
- lmnr/opentelemetry_lib/__init__.py +55 -0
- lmnr/{openllmetry_sdk/decorators/base.py → opentelemetry_lib/decorators/__init__.py} +24 -15
- lmnr/{openllmetry_sdk → opentelemetry_lib}/opentelemetry/instrumentation/google_genai/utils.py +1 -1
- lmnr/opentelemetry_lib/tracing/__init__.py +139 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +398 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +14 -7
- lmnr/opentelemetry_lib/tracing/context_properties.py +53 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +60 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +121 -0
- lmnr/opentelemetry_lib/tracing/processor.py +96 -0
- lmnr/{openllmetry_sdk/tracing/context_manager.py → opentelemetry_lib/tracing/tracer.py} +6 -1
- lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/package_check.py +3 -1
- 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 +116 -74
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +98 -0
- lmnr/sdk/client/asynchronous/resources/agent.py +22 -1
- lmnr/sdk/client/synchronous/resources/agent.py +23 -1
- lmnr/sdk/decorators.py +5 -3
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +10 -16
- lmnr/sdk/laminar.py +16 -34
- lmnr/sdk/types.py +2 -0
- lmnr/sdk/utils.py +2 -3
- lmnr/version.py +1 -1
- {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/METADATA +65 -63
- lmnr-0.6.0.dist-info/RECORD +54 -0
- {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/WHEEL +1 -1
- lmnr/openllmetry_sdk/__init__.py +0 -75
- lmnr/openllmetry_sdk/config/__init__.py +0 -12
- lmnr/openllmetry_sdk/decorators/__init__.py +0 -0
- lmnr/openllmetry_sdk/instruments.py +0 -41
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
- lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
- lmnr/openllmetry_sdk/tracing/tracing.py +0 -998
- lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
- 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}/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}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- {lmnr-0.5.2.dist-info → lmnr-0.6.0.dist-info}/LICENSE +0 -0
- {lmnr-0.5.2.dist-info → lmnr-0.6.0.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,28 @@ 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
|
-
|
103
|
+
events = await page.evaluate(
|
98
104
|
"""
|
99
|
-
|
105
|
+
() => {
|
106
|
+
if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
|
107
|
+
return [];
|
108
|
+
}
|
109
|
+
return window.lmnrGetAndClearEvents();
|
110
|
+
}
|
100
111
|
"""
|
101
112
|
)
|
102
|
-
if not has_function:
|
103
|
-
return
|
104
113
|
|
105
|
-
events = await page.evaluate("window.lmnrGetAndClearEvents()")
|
106
114
|
if not events or len(events) == 0:
|
107
115
|
return
|
108
116
|
|
109
117
|
await client._browser_events.send(session_id, trace_id, events)
|
110
|
-
|
111
118
|
except Exception as e:
|
112
|
-
|
119
|
+
if str(e).startswith("Page.evaluate: Execution context was destroyed"):
|
120
|
+
logger.info("Execution context was destroyed, injecting rrweb again")
|
121
|
+
await inject_rrweb_async(page)
|
122
|
+
await send_events_async(page, session_id, trace_id, client)
|
123
|
+
else:
|
124
|
+
logger.debug(f"Could not send events: {e}")
|
113
125
|
|
114
126
|
|
115
127
|
def send_events_sync(
|
@@ -117,23 +129,28 @@ def send_events_sync(
|
|
117
129
|
):
|
118
130
|
"""Synchronous version of send_events"""
|
119
131
|
try:
|
120
|
-
|
121
|
-
has_function = page.evaluate(
|
132
|
+
events = page.evaluate(
|
122
133
|
"""
|
123
|
-
|
134
|
+
() => {
|
135
|
+
if (!window.lmnrPageIsFocused || typeof window.lmnrGetAndClearEvents !== 'function') {
|
136
|
+
return [];
|
137
|
+
}
|
138
|
+
return window.lmnrGetAndClearEvents();
|
139
|
+
}
|
124
140
|
"""
|
125
141
|
)
|
126
|
-
if not has_function:
|
127
|
-
return
|
128
|
-
|
129
|
-
events = page.evaluate("window.lmnrGetAndClearEvents()")
|
130
142
|
if not events or len(events) == 0:
|
131
143
|
return
|
132
144
|
|
133
145
|
client._browser_events.send(session_id, trace_id, events)
|
134
146
|
|
135
147
|
except Exception as e:
|
136
|
-
|
148
|
+
if str(e).startswith("Page.evaluate: Execution context was destroyed"):
|
149
|
+
logger.info("Execution context was destroyed, injecting rrweb again")
|
150
|
+
inject_rrweb_sync(page)
|
151
|
+
send_events_sync(page, session_id, trace_id, client)
|
152
|
+
else:
|
153
|
+
logger.debug(f"Could not send events: {e}")
|
137
154
|
|
138
155
|
|
139
156
|
def inject_rrweb_sync(page: SyncPage):
|
@@ -154,10 +171,6 @@ def inject_rrweb_sync(page: SyncPage):
|
|
154
171
|
def load_rrweb():
|
155
172
|
try:
|
156
173
|
page.evaluate(RRWEB_CONTENT)
|
157
|
-
page.wait_for_function(
|
158
|
-
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
159
|
-
timeout=5000,
|
160
|
-
)
|
161
174
|
return True
|
162
175
|
except Exception as e:
|
163
176
|
logger.debug(f"Failed to load rrweb: {e}")
|
@@ -195,10 +208,6 @@ async def inject_rrweb_async(page: Page):
|
|
195
208
|
async def load_rrweb():
|
196
209
|
try:
|
197
210
|
await page.evaluate(RRWEB_CONTENT)
|
198
|
-
await page.wait_for_function(
|
199
|
-
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
200
|
-
timeout=5000,
|
201
|
-
)
|
202
211
|
return True
|
203
212
|
except Exception as e:
|
204
213
|
logger.debug(f"Failed to load rrweb: {e}")
|
@@ -223,11 +232,23 @@ def handle_navigation_sync(
|
|
223
232
|
page: SyncPage, session_id: str, trace_id: str, client: LaminarClient
|
224
233
|
):
|
225
234
|
trace.get_current_span().set_attribute("lmnr.internal.has_browser_session", True)
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
235
|
+
original_bring_to_front = page.bring_to_front
|
236
|
+
|
237
|
+
def bring_to_front():
|
238
|
+
original_bring_to_front()
|
239
|
+
page.evaluate(
|
240
|
+
"""() => {
|
241
|
+
if (window.lmnrRrweb) {
|
242
|
+
try {
|
243
|
+
window.lmnrRrweb.record.takeFullSnapshot();
|
244
|
+
} catch (e) {
|
245
|
+
console.error("Error taking full snapshot:", e);
|
246
|
+
}
|
247
|
+
}
|
248
|
+
}"""
|
249
|
+
)
|
250
|
+
|
251
|
+
page.bring_to_front = bring_to_front
|
231
252
|
|
232
253
|
def on_load():
|
233
254
|
try:
|
@@ -235,49 +256,37 @@ def handle_navigation_sync(
|
|
235
256
|
except Exception as e:
|
236
257
|
logger.error(f"Error in on_load handler: {e}")
|
237
258
|
|
238
|
-
page.on("load", on_load)
|
239
|
-
inject_rrweb_sync(page)
|
240
|
-
|
241
259
|
def collection_loop():
|
242
260
|
while not page.is_closed(): # Stop when page closes
|
243
261
|
send_events_sync(page, session_id, trace_id, client)
|
244
262
|
time.sleep(2)
|
245
263
|
|
246
|
-
# Clean up when page closes
|
247
|
-
if page_id in instrumented_pages:
|
248
|
-
instrumented_pages.remove(page_id)
|
249
|
-
|
250
264
|
thread = threading.Thread(target=collection_loop, daemon=True)
|
251
265
|
thread.start()
|
252
266
|
|
267
|
+
def on_close():
|
268
|
+
try:
|
269
|
+
send_events_sync(page, session_id, trace_id, client)
|
270
|
+
thread.join()
|
271
|
+
except Exception:
|
272
|
+
pass
|
273
|
+
|
274
|
+
page.on("load", on_load)
|
275
|
+
page.on("close", on_close)
|
276
|
+
inject_rrweb_sync(page)
|
277
|
+
|
253
278
|
|
254
279
|
@observe(name="playwright.page", ignore_input=True, ignore_output=True)
|
255
280
|
async def handle_navigation_async(
|
256
281
|
page: Page, session_id: str, trace_id: str, client: AsyncLaminarClient
|
257
282
|
):
|
258
283
|
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
284
|
|
274
285
|
async def collection_loop():
|
275
286
|
try:
|
276
287
|
while not page.is_closed(): # Stop when page closes
|
277
288
|
await send_events_async(page, session_id, trace_id, client)
|
278
289
|
await asyncio.sleep(2)
|
279
|
-
# Clean up when page closes
|
280
|
-
async_instrumented_pages.remove(page_id)
|
281
290
|
logger.info("Event collection stopped")
|
282
291
|
except Exception as e:
|
283
292
|
logger.error(f"Event collection stopped: {e}")
|
@@ -285,5 +294,38 @@ async def handle_navigation_async(
|
|
285
294
|
# Create and store task
|
286
295
|
task = asyncio.create_task(collection_loop())
|
287
296
|
|
288
|
-
|
289
|
-
|
297
|
+
async def on_load():
|
298
|
+
try:
|
299
|
+
await inject_rrweb_async(page)
|
300
|
+
except Exception as e:
|
301
|
+
logger.error(f"Error in on_load handler: {e}")
|
302
|
+
|
303
|
+
async def on_close():
|
304
|
+
try:
|
305
|
+
task.cancel()
|
306
|
+
await send_events_async(page, session_id, trace_id, client)
|
307
|
+
except Exception:
|
308
|
+
pass
|
309
|
+
|
310
|
+
page.on("load", lambda: asyncio.create_task(on_load()))
|
311
|
+
page.on("close", lambda: asyncio.create_task(on_close()))
|
312
|
+
|
313
|
+
original_bring_to_front = page.bring_to_front
|
314
|
+
|
315
|
+
async def bring_to_front():
|
316
|
+
await original_bring_to_front()
|
317
|
+
|
318
|
+
await page.evaluate(
|
319
|
+
"""() => {
|
320
|
+
if (window.lmnrRrweb) {
|
321
|
+
try {
|
322
|
+
window.lmnrRrweb.record.takeFullSnapshot();
|
323
|
+
} catch (e) {
|
324
|
+
console.error("Error taking full snapshot:", e);
|
325
|
+
}
|
326
|
+
}
|
327
|
+
}"""
|
328
|
+
)
|
329
|
+
|
330
|
+
page.bring_to_front = bring_to_front
|
331
|
+
await inject_rrweb_async(page)
|