lmnr 0.4.55__tar.gz → 0.4.56__tar.gz
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-0.4.55 → lmnr-0.4.56}/PKG-INFO +1 -1
- {lmnr-0.4.55 → lmnr-0.4.56}/pyproject.toml +1 -1
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/__init__.py +4 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/instruments.py +1 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/tracing/tracing.py +10 -0
- lmnr-0.4.56/src/lmnr/sdk/browser/__init__.py +9 -0
- lmnr-0.4.56/src/lmnr/sdk/browser/playwright_patch.py +192 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/laminar.py +2 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/LICENSE +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/README.md +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/__init__.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/cli.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/.flake8 +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/.python-version +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/config/__init__.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/decorators/__init__.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/decorators/base.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/tracing/__init__.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/tracing/attributes.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/tracing/context_manager.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/utils/__init__.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/utils/json_encoder.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/utils/package_check.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/openllmetry_sdk/version.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/__init__.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/datasets.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/decorators.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/eval_control.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/evaluations.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/log.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/types.py +0 -0
- {lmnr-0.4.55 → lmnr-0.4.56}/src/lmnr/sdk/utils.py +0 -0
@@ -32,6 +32,8 @@ class Traceloop:
|
|
32
32
|
should_enrich_metrics: bool = False,
|
33
33
|
resource_attributes: dict = {},
|
34
34
|
instruments: Optional[Set[Instruments]] = None,
|
35
|
+
base_http_url: Optional[str] = None,
|
36
|
+
project_api_key: Optional[str] = None,
|
35
37
|
) -> None:
|
36
38
|
if not is_tracing_enabled():
|
37
39
|
return
|
@@ -69,4 +71,6 @@ class Traceloop:
|
|
69
71
|
exporter=exporter,
|
70
72
|
should_enrich_metrics=should_enrich_metrics,
|
71
73
|
instruments=instruments,
|
74
|
+
base_http_url=base_http_url,
|
75
|
+
project_api_key=project_api_key,
|
72
76
|
)
|
@@ -6,6 +6,7 @@ import logging
|
|
6
6
|
from contextvars import Context
|
7
7
|
from lmnr.sdk.log import VerboseColorfulFormatter
|
8
8
|
from lmnr.openllmetry_sdk.instruments import Instruments
|
9
|
+
from lmnr.sdk.browser import init_browser_tracing
|
9
10
|
from lmnr.openllmetry_sdk.tracing.attributes import (
|
10
11
|
ASSOCIATION_PROPERTIES,
|
11
12
|
SPAN_INSTRUMENTATION_SOURCE,
|
@@ -80,6 +81,8 @@ class TracerWrapper(object):
|
|
80
81
|
exporter: Optional[SpanExporter] = None,
|
81
82
|
should_enrich_metrics: bool = False,
|
82
83
|
instruments: Optional[Set[Instruments]] = None,
|
84
|
+
base_http_url: Optional[str] = None,
|
85
|
+
project_api_key: Optional[str] = None,
|
83
86
|
) -> "TracerWrapper":
|
84
87
|
cls._initialize_logger(cls)
|
85
88
|
if not hasattr(cls, "instance"):
|
@@ -122,6 +125,8 @@ class TracerWrapper(object):
|
|
122
125
|
instrument_set = init_instrumentations(
|
123
126
|
should_enrich_metrics,
|
124
127
|
instruments,
|
128
|
+
base_http_url=base_http_url,
|
129
|
+
project_api_key=project_api_key,
|
125
130
|
)
|
126
131
|
|
127
132
|
if not instrument_set:
|
@@ -286,6 +291,8 @@ def init_instrumentations(
|
|
286
291
|
should_enrich_metrics: bool,
|
287
292
|
instruments: Optional[Set[Instruments]] = None,
|
288
293
|
block_instruments: Optional[Set[Instruments]] = None,
|
294
|
+
base_http_url: Optional[str] = None,
|
295
|
+
project_api_key: Optional[str] = None,
|
289
296
|
):
|
290
297
|
block_instruments = block_instruments or set()
|
291
298
|
# These libraries are not instrumented by default,
|
@@ -397,6 +404,9 @@ def init_instrumentations(
|
|
397
404
|
elif instrument == Instruments.WEAVIATE:
|
398
405
|
if init_weaviate_instrumentor():
|
399
406
|
instrument_set = True
|
407
|
+
elif instrument == Instruments.PLAYWRIGHT:
|
408
|
+
if init_browser_tracing(base_http_url, project_api_key):
|
409
|
+
instrument_set = True
|
400
410
|
else:
|
401
411
|
module_logger.warning(
|
402
412
|
f"Warning: {instrument} instrumentation does not exist."
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from lmnr.openllmetry_sdk.utils.package_check import is_package_installed
|
2
|
+
|
3
|
+
|
4
|
+
def init_browser_tracing(http_url: str, project_api_key: str):
|
5
|
+
if is_package_installed("playwright"):
|
6
|
+
from .playwright_patch import init_playwright_tracing
|
7
|
+
|
8
|
+
init_playwright_tracing(http_url, project_api_key)
|
9
|
+
# Other browsers can be added here
|
@@ -0,0 +1,192 @@
|
|
1
|
+
import opentelemetry
|
2
|
+
import uuid
|
3
|
+
|
4
|
+
try:
|
5
|
+
from playwright.async_api import BrowserContext, Page
|
6
|
+
from playwright.sync_api import (
|
7
|
+
BrowserContext as SyncBrowserContext,
|
8
|
+
Page as SyncPage,
|
9
|
+
)
|
10
|
+
except ImportError as e:
|
11
|
+
raise ImportError(
|
12
|
+
f"Attempated to import {__file__}, but it is designed "
|
13
|
+
"to patch Playwright, which is not installed. Use `pip install playwright` "
|
14
|
+
"to install Playwright or remove this import."
|
15
|
+
) from e
|
16
|
+
|
17
|
+
_original_new_page = None
|
18
|
+
_original_goto = None
|
19
|
+
_original_new_page_async = None
|
20
|
+
_original_goto_async = None
|
21
|
+
|
22
|
+
INJECT_PLACEHOLDER = """
|
23
|
+
([baseUrl, projectApiKey]) => {
|
24
|
+
const serverUrl = `${baseUrl}/v1/browser-sessions/events`;
|
25
|
+
const BATCH_SIZE = 16;
|
26
|
+
const FLUSH_INTERVAL = 1000;
|
27
|
+
const HEARTBEAT_INTERVAL = 1000; // 1 second heartbeat
|
28
|
+
|
29
|
+
window.rrwebEventsBatch = [];
|
30
|
+
|
31
|
+
window.sendBatch = async () => {
|
32
|
+
if (window.rrwebEventsBatch.length === 0) return;
|
33
|
+
|
34
|
+
const eventsPayload = {
|
35
|
+
sessionId: window.rrwebSessionId,
|
36
|
+
traceId: window.traceId,
|
37
|
+
events: window.rrwebEventsBatch
|
38
|
+
};
|
39
|
+
|
40
|
+
try {
|
41
|
+
await fetch(serverUrl, {
|
42
|
+
method: 'POST',
|
43
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${projectApiKey}` },
|
44
|
+
body: JSON.stringify(eventsPayload),
|
45
|
+
});
|
46
|
+
window.rrwebEventsBatch = [];
|
47
|
+
} catch (error) {
|
48
|
+
console.error('Failed to send events:', error);
|
49
|
+
}
|
50
|
+
};
|
51
|
+
|
52
|
+
setInterval(() => window.sendBatch(), FLUSH_INTERVAL);
|
53
|
+
|
54
|
+
// Add heartbeat event
|
55
|
+
setInterval(() => {
|
56
|
+
window.rrwebEventsBatch.push({
|
57
|
+
type: 6, // Custom event type
|
58
|
+
data: { source: 'heartbeat' },
|
59
|
+
timestamp: Date.now()
|
60
|
+
});
|
61
|
+
}, HEARTBEAT_INTERVAL);
|
62
|
+
|
63
|
+
window.rrweb.record({
|
64
|
+
emit(event) {
|
65
|
+
window.rrwebEventsBatch.push(event);
|
66
|
+
|
67
|
+
if (window.rrwebEventsBatch.length >= BATCH_SIZE) {
|
68
|
+
window.sendBatch();
|
69
|
+
}
|
70
|
+
}
|
71
|
+
});
|
72
|
+
|
73
|
+
// Simplified beforeunload handler
|
74
|
+
window.addEventListener('beforeunload', () => {
|
75
|
+
window.sendBatch();
|
76
|
+
});
|
77
|
+
}
|
78
|
+
"""
|
79
|
+
|
80
|
+
|
81
|
+
def init_playwright_tracing(http_url: str, project_api_key: str):
|
82
|
+
def inject_rrweb(page: SyncPage):
|
83
|
+
# Get current trace ID from active span
|
84
|
+
current_span = opentelemetry.trace.get_current_span()
|
85
|
+
current_span.set_attribute("lmnr.internal.has_browser_session", True)
|
86
|
+
trace_id = format(current_span.get_span_context().trace_id, "032x")
|
87
|
+
session_id = str(uuid.uuid4().hex)
|
88
|
+
|
89
|
+
# Generate UUID session ID and set trace ID
|
90
|
+
page.evaluate(
|
91
|
+
"""([traceId, sessionId]) => {
|
92
|
+
window.rrwebSessionId = sessionId;
|
93
|
+
window.traceId = traceId;
|
94
|
+
}""",
|
95
|
+
[trace_id, session_id],
|
96
|
+
)
|
97
|
+
|
98
|
+
# Load rrweb and set up recording
|
99
|
+
page.add_script_tag(
|
100
|
+
url="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"
|
101
|
+
)
|
102
|
+
|
103
|
+
# Update the recording setup to include trace ID
|
104
|
+
page.evaluate(
|
105
|
+
INJECT_PLACEHOLDER,
|
106
|
+
[http_url, project_api_key],
|
107
|
+
)
|
108
|
+
|
109
|
+
async def inject_rrweb_async(page: Page):
|
110
|
+
# Wait for the page to be in a ready state first
|
111
|
+
await page.wait_for_load_state("domcontentloaded")
|
112
|
+
|
113
|
+
# Get current trace ID from active span
|
114
|
+
current_span = opentelemetry.trace.get_current_span()
|
115
|
+
current_span.set_attribute("lmnr.internal.has_browser_session", True)
|
116
|
+
trace_id = format(current_span.get_span_context().trace_id, "032x")
|
117
|
+
session_id = str(uuid.uuid4().hex)
|
118
|
+
|
119
|
+
# Wait for any existing script load to complete
|
120
|
+
await page.wait_for_load_state("networkidle")
|
121
|
+
|
122
|
+
# Generate UUID session ID and set trace ID
|
123
|
+
await page.evaluate(
|
124
|
+
"""([traceId, sessionId]) => {
|
125
|
+
window.rrwebSessionId = sessionId;
|
126
|
+
window.traceId = traceId;
|
127
|
+
}""",
|
128
|
+
[trace_id, session_id],
|
129
|
+
)
|
130
|
+
|
131
|
+
# Load rrweb and set up recording
|
132
|
+
await page.add_script_tag(
|
133
|
+
url="https://cdn.jsdelivr.net/npm/rrweb@latest/dist/rrweb.min.js"
|
134
|
+
)
|
135
|
+
|
136
|
+
await page.wait_for_function("""(() => window.rrweb || 'rrweb' in window)""")
|
137
|
+
|
138
|
+
# Update the recording setup to include trace ID
|
139
|
+
await page.evaluate(
|
140
|
+
INJECT_PLACEHOLDER,
|
141
|
+
[http_url, project_api_key],
|
142
|
+
)
|
143
|
+
|
144
|
+
async def patched_new_page_async(self: BrowserContext, *args, **kwargs):
|
145
|
+
# Call the original new_page (returns a Page object)
|
146
|
+
page = await _original_new_page_async(self, *args, **kwargs)
|
147
|
+
# Inject rrweb automatically after the page is created
|
148
|
+
await inject_rrweb_async(page)
|
149
|
+
return page
|
150
|
+
|
151
|
+
async def patched_goto_async(self: Page, *args, **kwargs):
|
152
|
+
# Call the original goto
|
153
|
+
result = await _original_goto_async(self, *args, **kwargs)
|
154
|
+
# Inject rrweb after navigation
|
155
|
+
await inject_rrweb_async(self)
|
156
|
+
return result
|
157
|
+
|
158
|
+
def patched_new_page(self: SyncBrowserContext, *args, **kwargs):
|
159
|
+
# Call the original new_page (returns a Page object)
|
160
|
+
page = _original_new_page(self, *args, **kwargs)
|
161
|
+
# Inject rrweb automatically after the page is created
|
162
|
+
inject_rrweb(page)
|
163
|
+
return page
|
164
|
+
|
165
|
+
def patched_goto(self: SyncPage, *args, **kwargs):
|
166
|
+
# Call the original goto
|
167
|
+
result = _original_goto(self, *args, **kwargs)
|
168
|
+
# Inject rrweb after navigation
|
169
|
+
inject_rrweb(self)
|
170
|
+
return result
|
171
|
+
|
172
|
+
def patch_browser():
|
173
|
+
"""
|
174
|
+
Overrides BrowserContext.new_page with a patched async function
|
175
|
+
that injects rrweb into every new page.
|
176
|
+
"""
|
177
|
+
global _original_new_page, _original_goto, _original_new_page_async, _original_goto_async
|
178
|
+
if _original_new_page_async is None or _original_goto_async is None:
|
179
|
+
_original_new_page_async = BrowserContext.new_page
|
180
|
+
BrowserContext.new_page = patched_new_page_async
|
181
|
+
|
182
|
+
_original_goto_async = Page.goto
|
183
|
+
Page.goto = patched_goto_async
|
184
|
+
|
185
|
+
if _original_new_page is None or _original_goto is None:
|
186
|
+
_original_new_page = SyncBrowserContext.new_page
|
187
|
+
SyncBrowserContext.new_page = patched_new_page
|
188
|
+
|
189
|
+
_original_goto = SyncPage.goto
|
190
|
+
SyncPage.goto = patched_goto
|
191
|
+
|
192
|
+
patch_browser()
|
@@ -142,6 +142,8 @@ class Laminar:
|
|
142
142
|
cls._initialize_logger()
|
143
143
|
|
144
144
|
Traceloop.init(
|
145
|
+
base_http_url=cls.__base_http_url,
|
146
|
+
project_api_key=cls.__project_api_key,
|
145
147
|
exporter=OTLPSpanExporter(
|
146
148
|
endpoint=cls.__base_grpc_url,
|
147
149
|
headers={"authorization": f"Bearer {cls.__project_api_key}"},
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|