lmnr 0.4.58__py3-none-any.whl → 0.4.60__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/openllmetry_sdk/config/__init__.py +3 -0
- lmnr/openllmetry_sdk/decorators/base.py +45 -27
- lmnr/openllmetry_sdk/tracing/attributes.py +3 -0
- lmnr/openllmetry_sdk/tracing/tracing.py +25 -5
- lmnr/py.typed +0 -0
- lmnr/sdk/browser/playwright_patch.py +169 -68
- lmnr/sdk/browser/rrweb/rrweb.min.js +18 -0
- lmnr/sdk/decorators.py +16 -3
- lmnr/sdk/evaluations.py +8 -0
- lmnr/sdk/laminar.py +49 -13
- lmnr/version.py +5 -0
- {lmnr-0.4.58.dist-info → lmnr-0.4.60.dist-info}/METADATA +1 -1
- {lmnr-0.4.58.dist-info → lmnr-0.4.60.dist-info}/RECORD +16 -14
- lmnr/openllmetry_sdk/version.py +0 -1
- {lmnr-0.4.58.dist-info → lmnr-0.4.60.dist-info}/LICENSE +0 -0
- {lmnr-0.4.58.dist-info → lmnr-0.4.60.dist-info}/WHEEL +0 -0
- {lmnr-0.4.58.dist-info → lmnr-0.4.60.dist-info}/entry_points.txt +0 -0
@@ -4,7 +4,7 @@ import logging
|
|
4
4
|
import os
|
5
5
|
import pydantic
|
6
6
|
import types
|
7
|
-
from typing import Any, Optional
|
7
|
+
from typing import Any, Literal, Optional, Union
|
8
8
|
|
9
9
|
from opentelemetry import trace
|
10
10
|
from opentelemetry import context as context_api
|
@@ -12,9 +12,10 @@ from opentelemetry.trace import Span
|
|
12
12
|
|
13
13
|
from lmnr.sdk.utils import get_input_from_func_args, is_method
|
14
14
|
from lmnr.openllmetry_sdk.tracing import get_tracer
|
15
|
-
from lmnr.openllmetry_sdk.tracing.attributes import SPAN_INPUT, SPAN_OUTPUT
|
15
|
+
from lmnr.openllmetry_sdk.tracing.attributes import SPAN_INPUT, SPAN_OUTPUT, SPAN_TYPE
|
16
16
|
from lmnr.openllmetry_sdk.tracing.tracing import TracerWrapper
|
17
17
|
from lmnr.openllmetry_sdk.utils.json_encoder import JSONEncoder
|
18
|
+
from lmnr.openllmetry_sdk.config import MAX_MANUAL_SPAN_PAYLOAD_SIZE
|
18
19
|
|
19
20
|
|
20
21
|
class CustomJSONEncoder(JSONEncoder):
|
@@ -38,6 +39,9 @@ def json_dumps(data: dict) -> str:
|
|
38
39
|
|
39
40
|
def entity_method(
|
40
41
|
name: Optional[str] = None,
|
42
|
+
ignore_input: bool = False,
|
43
|
+
ignore_output: bool = False,
|
44
|
+
span_type: Union[Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]] = "DEFAULT",
|
41
45
|
):
|
42
46
|
def decorate(fn):
|
43
47
|
@wraps(fn)
|
@@ -48,21 +52,22 @@ def entity_method(
|
|
48
52
|
span_name = name or fn.__name__
|
49
53
|
|
50
54
|
with get_tracer() as tracer:
|
51
|
-
span = tracer.start_span(span_name)
|
55
|
+
span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
|
52
56
|
|
53
57
|
ctx = trace.set_span_in_context(span, context_api.get_current())
|
54
58
|
ctx_token = context_api.attach(ctx)
|
55
59
|
|
56
60
|
try:
|
57
|
-
if _should_send_prompts():
|
58
|
-
|
59
|
-
|
60
|
-
json_dumps(
|
61
|
-
get_input_from_func_args(
|
62
|
-
fn, is_method(fn), args, kwargs
|
63
|
-
)
|
64
|
-
),
|
61
|
+
if _should_send_prompts() and not ignore_input:
|
62
|
+
inp = json_dumps(
|
63
|
+
get_input_from_func_args(fn, is_method(fn), args, kwargs)
|
65
64
|
)
|
65
|
+
if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
|
66
|
+
span.set_attribute(
|
67
|
+
SPAN_INPUT, "Laminar: input too large to record"
|
68
|
+
)
|
69
|
+
else:
|
70
|
+
span.set_attribute(SPAN_INPUT, inp)
|
66
71
|
except TypeError:
|
67
72
|
pass
|
68
73
|
|
@@ -78,11 +83,14 @@ def entity_method(
|
|
78
83
|
return _handle_generator(span, res)
|
79
84
|
|
80
85
|
try:
|
81
|
-
if _should_send_prompts():
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
+
if _should_send_prompts() and not ignore_output:
|
87
|
+
output = json_dumps(res)
|
88
|
+
if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
|
89
|
+
span.set_attribute(
|
90
|
+
SPAN_OUTPUT, "Laminar: output too large to record"
|
91
|
+
)
|
92
|
+
else:
|
93
|
+
span.set_attribute(SPAN_OUTPUT, output)
|
86
94
|
except TypeError:
|
87
95
|
pass
|
88
96
|
|
@@ -99,6 +107,9 @@ def entity_method(
|
|
99
107
|
# Async Decorators
|
100
108
|
def aentity_method(
|
101
109
|
name: Optional[str] = None,
|
110
|
+
ignore_input: bool = False,
|
111
|
+
ignore_output: bool = False,
|
112
|
+
span_type: Union[Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]] = "DEFAULT",
|
102
113
|
):
|
103
114
|
def decorate(fn):
|
104
115
|
@wraps(fn)
|
@@ -109,21 +120,22 @@ def aentity_method(
|
|
109
120
|
span_name = name or fn.__name__
|
110
121
|
|
111
122
|
with get_tracer() as tracer:
|
112
|
-
span = tracer.start_span(span_name)
|
123
|
+
span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
|
113
124
|
|
114
125
|
ctx = trace.set_span_in_context(span, context_api.get_current())
|
115
126
|
ctx_token = context_api.attach(ctx)
|
116
127
|
|
117
128
|
try:
|
118
|
-
if _should_send_prompts():
|
119
|
-
|
120
|
-
|
121
|
-
json_dumps(
|
122
|
-
get_input_from_func_args(
|
123
|
-
fn, is_method(fn), args, kwargs
|
124
|
-
)
|
125
|
-
),
|
129
|
+
if _should_send_prompts() and not ignore_input:
|
130
|
+
inp = json_dumps(
|
131
|
+
get_input_from_func_args(fn, is_method(fn), args, kwargs)
|
126
132
|
)
|
133
|
+
if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
|
134
|
+
span.set_attribute(
|
135
|
+
SPAN_INPUT, "Laminar: input too large to record"
|
136
|
+
)
|
137
|
+
else:
|
138
|
+
span.set_attribute(SPAN_INPUT, inp)
|
127
139
|
except TypeError:
|
128
140
|
pass
|
129
141
|
|
@@ -139,8 +151,14 @@ def aentity_method(
|
|
139
151
|
return await _ahandle_generator(span, ctx_token, res)
|
140
152
|
|
141
153
|
try:
|
142
|
-
if _should_send_prompts():
|
143
|
-
|
154
|
+
if _should_send_prompts() and not ignore_output:
|
155
|
+
output = json_dumps(res)
|
156
|
+
if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
|
157
|
+
span.set_attribute(
|
158
|
+
SPAN_OUTPUT, "Laminar: output too large to record"
|
159
|
+
)
|
160
|
+
else:
|
161
|
+
span.set_attribute(SPAN_OUTPUT, output)
|
144
162
|
except TypeError:
|
145
163
|
pass
|
146
164
|
|
@@ -5,7 +5,10 @@ SPAN_INPUT = "lmnr.span.input"
|
|
5
5
|
SPAN_OUTPUT = "lmnr.span.output"
|
6
6
|
SPAN_TYPE = "lmnr.span.type"
|
7
7
|
SPAN_PATH = "lmnr.span.path"
|
8
|
+
SPAN_IDS_PATH = "lmnr.span.ids_path"
|
8
9
|
SPAN_INSTRUMENTATION_SOURCE = "lmnr.span.instrumentation_source"
|
10
|
+
SPAN_SDK_VERSION = "lmnr.span.sdk_version"
|
11
|
+
SPAN_LANGUAGE_VERSION = "lmnr.span.language_version"
|
9
12
|
OVERRIDE_PARENT_SPAN = "lmnr.internal.override_parent_span"
|
10
13
|
|
11
14
|
ASSOCIATION_PROPERTIES = "lmnr.association.properties"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import atexit
|
2
2
|
import copy
|
3
3
|
import logging
|
4
|
-
|
4
|
+
import uuid
|
5
5
|
|
6
6
|
from contextvars import Context
|
7
7
|
from lmnr.sdk.log import VerboseColorfulFormatter
|
@@ -9,7 +9,10 @@ from lmnr.openllmetry_sdk.instruments import Instruments
|
|
9
9
|
from lmnr.sdk.browser import init_browser_tracing
|
10
10
|
from lmnr.openllmetry_sdk.tracing.attributes import (
|
11
11
|
ASSOCIATION_PROPERTIES,
|
12
|
+
SPAN_IDS_PATH,
|
12
13
|
SPAN_INSTRUMENTATION_SOURCE,
|
14
|
+
SPAN_SDK_VERSION,
|
15
|
+
SPAN_LANGUAGE_VERSION,
|
13
16
|
SPAN_PATH,
|
14
17
|
TRACING_LEVEL,
|
15
18
|
)
|
@@ -23,6 +26,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
23
26
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
24
27
|
OTLPSpanExporter as GRPCExporter,
|
25
28
|
)
|
29
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import Compression
|
26
30
|
from opentelemetry.instrumentation.threading import ThreadingInstrumentor
|
27
31
|
from opentelemetry.context import get_value, attach, get_current, set_value, Context
|
28
32
|
from opentelemetry.propagate import set_global_textmap
|
@@ -38,6 +42,8 @@ from opentelemetry.trace import get_tracer_provider, ProxyTracerProvider
|
|
38
42
|
|
39
43
|
from typing import Dict, Optional, Set
|
40
44
|
|
45
|
+
from lmnr.version import SDK_VERSION, PYTHON_VERSION
|
46
|
+
|
41
47
|
module_logger = logging.getLogger(__name__)
|
42
48
|
console_log_handler = logging.StreamHandler()
|
43
49
|
console_log_handler.setFormatter(VerboseColorfulFormatter())
|
@@ -72,6 +78,7 @@ class TracerWrapper(object):
|
|
72
78
|
__tracer_provider: TracerProvider = None
|
73
79
|
__logger: logging.Logger = None
|
74
80
|
__span_id_to_path: dict[int, list[str]] = {}
|
81
|
+
__span_id_lists: dict[int, list[str]] = {}
|
75
82
|
|
76
83
|
def __new__(
|
77
84
|
cls,
|
@@ -132,9 +139,9 @@ class TracerWrapper(object):
|
|
132
139
|
)
|
133
140
|
|
134
141
|
if not instrument_set:
|
135
|
-
cls.__logger.
|
136
|
-
"No
|
137
|
-
"
|
142
|
+
cls.__logger.info(
|
143
|
+
"No instruments set through Laminar. "
|
144
|
+
"Only enabling basic OpenTelemetry tracing."
|
138
145
|
)
|
139
146
|
|
140
147
|
obj.__content_allow_list = ContentAllowList()
|
@@ -162,12 +169,22 @@ class TracerWrapper(object):
|
|
162
169
|
parent_span_path = span_path_in_context or (
|
163
170
|
self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
|
164
171
|
)
|
172
|
+
parent_span_ids_path = (
|
173
|
+
self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
|
174
|
+
)
|
165
175
|
span_path = parent_span_path + [span.name] if parent_span_path else [span.name]
|
176
|
+
span_ids_path = parent_span_ids_path + [
|
177
|
+
str(uuid.UUID(int=span.get_span_context().span_id))
|
178
|
+
]
|
166
179
|
span.set_attribute(SPAN_PATH, span_path)
|
180
|
+
span.set_attribute(SPAN_IDS_PATH, span_ids_path)
|
167
181
|
set_value("span_path", span_path, get_current())
|
168
182
|
self.__span_id_to_path[span.get_span_context().span_id] = span_path
|
183
|
+
self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
|
169
184
|
|
170
185
|
span.set_attribute(SPAN_INSTRUMENTATION_SOURCE, "python")
|
186
|
+
span.set_attribute(SPAN_SDK_VERSION, SDK_VERSION)
|
187
|
+
span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
|
171
188
|
|
172
189
|
association_properties = get_value("association_properties")
|
173
190
|
if association_properties is not None:
|
@@ -203,6 +220,7 @@ class TracerWrapper(object):
|
|
203
220
|
def clear(cls):
|
204
221
|
# Any state cleanup. Now used in between tests
|
205
222
|
cls.__span_id_to_path = {}
|
223
|
+
cls.__span_id_lists = {}
|
206
224
|
|
207
225
|
def flush(self):
|
208
226
|
self.__spans_processor.force_flush()
|
@@ -268,7 +286,9 @@ def init_spans_exporter(api_endpoint: str, headers: Dict[str, str]) -> SpanExpor
|
|
268
286
|
if "http" in api_endpoint.lower() or "https" in api_endpoint.lower():
|
269
287
|
return HTTPExporter(endpoint=f"{api_endpoint}/v1/traces", headers=headers)
|
270
288
|
else:
|
271
|
-
return GRPCExporter(
|
289
|
+
return GRPCExporter(
|
290
|
+
endpoint=f"{api_endpoint}", headers=headers, compression=Compression.Gzip
|
291
|
+
)
|
272
292
|
|
273
293
|
|
274
294
|
def init_tracer_provider(resource: Resource) -> TracerProvider:
|
lmnr/py.typed
ADDED
File without changes
|
@@ -1,6 +1,11 @@
|
|
1
1
|
import opentelemetry
|
2
2
|
import uuid
|
3
3
|
import asyncio
|
4
|
+
import logging
|
5
|
+
import time
|
6
|
+
import os
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
4
9
|
|
5
10
|
try:
|
6
11
|
from playwright.async_api import BrowserContext, Page
|
@@ -18,21 +23,25 @@ except ImportError as e:
|
|
18
23
|
_original_new_page = None
|
19
24
|
_original_new_page_async = None
|
20
25
|
|
26
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
27
|
+
with open(os.path.join(current_dir, "rrweb", "rrweb.min.js"), "r") as f:
|
28
|
+
RRWEB_CONTENT = f"() => {{ {f.read()} }}"
|
29
|
+
|
21
30
|
INJECT_PLACEHOLDER = """
|
22
31
|
([baseUrl, projectApiKey]) => {
|
23
32
|
const serverUrl = `${baseUrl}/v1/browser-sessions/events`;
|
24
33
|
const FLUSH_INTERVAL = 1000;
|
25
34
|
const HEARTBEAT_INTERVAL = 1000;
|
26
35
|
|
27
|
-
window.
|
36
|
+
window.lmnrRrwebEventsBatch = [];
|
28
37
|
|
29
|
-
window.
|
30
|
-
if (window.
|
38
|
+
window.lmnrSendRrwebEventsBatch = async () => {
|
39
|
+
if (window.lmnrRrwebEventsBatch.length === 0) return;
|
31
40
|
|
32
41
|
const eventsPayload = {
|
33
|
-
sessionId: window.
|
34
|
-
traceId: window.
|
35
|
-
events: window.
|
42
|
+
sessionId: window.lmnrRrwebSessionId,
|
43
|
+
traceId: window.lmnrTraceId,
|
44
|
+
events: window.lmnrRrwebEventsBatch
|
36
45
|
};
|
37
46
|
|
38
47
|
try {
|
@@ -53,69 +62,141 @@ INJECT_PLACEHOLDER = """
|
|
53
62
|
headers: {
|
54
63
|
'Content-Type': 'application/json',
|
55
64
|
'Content-Encoding': 'gzip',
|
56
|
-
'Authorization': `Bearer ${projectApiKey}
|
65
|
+
'Authorization': `Bearer ${projectApiKey}`,
|
66
|
+
'Accept': 'application/json'
|
57
67
|
},
|
58
68
|
body: blob,
|
59
|
-
credentials: 'omit',
|
60
69
|
mode: 'cors',
|
61
|
-
|
70
|
+
credentials: 'omit'
|
62
71
|
});
|
63
72
|
|
64
73
|
if (!response.ok) {
|
65
|
-
|
74
|
+
console.error(`HTTP error! status: ${response.status}`);
|
75
|
+
if (response.status === 0) {
|
76
|
+
console.error('Possible CORS issue - check network tab for details');
|
77
|
+
}
|
66
78
|
}
|
67
|
-
|
68
|
-
window.
|
79
|
+
|
80
|
+
window.lmnrRrwebEventsBatch = [];
|
69
81
|
} catch (error) {
|
70
82
|
console.error('Failed to send events:', error);
|
71
83
|
}
|
72
84
|
};
|
73
85
|
|
74
|
-
setInterval(() => window.
|
86
|
+
setInterval(() => window.lmnrSendRrwebEventsBatch(), FLUSH_INTERVAL);
|
75
87
|
|
76
88
|
setInterval(() => {
|
77
|
-
window.
|
89
|
+
window.lmnrRrwebEventsBatch.push({
|
78
90
|
type: 6,
|
79
91
|
data: { source: 'heartbeat' },
|
80
92
|
timestamp: Date.now()
|
81
93
|
});
|
82
94
|
}, HEARTBEAT_INTERVAL);
|
83
95
|
|
84
|
-
window.
|
96
|
+
window.lmnrRrweb.record({
|
85
97
|
emit(event) {
|
86
|
-
window.
|
98
|
+
window.lmnrRrwebEventsBatch.push(event);
|
87
99
|
}
|
88
100
|
});
|
89
101
|
|
90
102
|
window.addEventListener('beforeunload', () => {
|
91
|
-
window.
|
103
|
+
window.lmnrSendRrwebEventsBatch();
|
92
104
|
});
|
93
105
|
}
|
94
106
|
"""
|
95
107
|
|
96
108
|
|
109
|
+
def retry_sync(func, retries=5, delay=0.5, error_message="Operation failed"):
|
110
|
+
"""Utility function for retry logic in synchronous operations"""
|
111
|
+
for attempt in range(retries):
|
112
|
+
try:
|
113
|
+
result = func()
|
114
|
+
if result: # If function returns truthy value, consider it successful
|
115
|
+
return result
|
116
|
+
if attempt == retries - 1: # Last attempt
|
117
|
+
logger.error(f"{error_message} after all retries")
|
118
|
+
return None
|
119
|
+
except Exception as e:
|
120
|
+
if attempt == retries - 1: # Last attempt
|
121
|
+
logger.error(f"{error_message}: {e}")
|
122
|
+
return None
|
123
|
+
time.sleep(delay)
|
124
|
+
return None
|
125
|
+
|
126
|
+
|
127
|
+
async def retry_async(func, retries=5, delay=0.5, error_message="Operation failed"):
|
128
|
+
"""Utility function for retry logic in asynchronous operations"""
|
129
|
+
for attempt in range(retries):
|
130
|
+
try:
|
131
|
+
result = await func()
|
132
|
+
if result: # If function returns truthy value, consider it successful
|
133
|
+
return result
|
134
|
+
if attempt == retries - 1: # Last attempt
|
135
|
+
logger.error(f"{error_message} after all retries")
|
136
|
+
return None
|
137
|
+
except Exception as e:
|
138
|
+
if attempt == retries - 1: # Last attempt
|
139
|
+
logger.error(f"{error_message}: {e}")
|
140
|
+
return None
|
141
|
+
await asyncio.sleep(delay)
|
142
|
+
return None
|
143
|
+
|
144
|
+
|
97
145
|
def init_playwright_tracing(http_url: str, project_api_key: str):
|
98
146
|
|
99
147
|
def inject_rrweb(page: SyncPage):
|
148
|
+
# Wait for the page to be in a ready state first
|
149
|
+
page.wait_for_load_state("domcontentloaded")
|
150
|
+
|
151
|
+
# First check if rrweb is already loaded
|
152
|
+
is_loaded = page.evaluate(
|
153
|
+
"""
|
154
|
+
() => typeof window.lmnrRrweb !== 'undefined'
|
155
|
+
"""
|
156
|
+
)
|
157
|
+
|
158
|
+
if not is_loaded:
|
159
|
+
|
160
|
+
def load_rrweb():
|
161
|
+
page.evaluate(RRWEB_CONTENT)
|
162
|
+
# Verify script loaded successfully
|
163
|
+
page.wait_for_function(
|
164
|
+
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
165
|
+
timeout=5000,
|
166
|
+
)
|
167
|
+
return True
|
168
|
+
|
169
|
+
if not retry_sync(
|
170
|
+
load_rrweb, delay=1, error_message="Failed to load rrweb"
|
171
|
+
):
|
172
|
+
return
|
173
|
+
|
100
174
|
# Get current trace ID from active span
|
101
175
|
current_span = opentelemetry.trace.get_current_span()
|
102
|
-
current_span.
|
176
|
+
if current_span.is_recording():
|
177
|
+
current_span.set_attribute("lmnr.internal.has_browser_session", True)
|
178
|
+
|
103
179
|
trace_id = format(current_span.get_span_context().trace_id, "032x")
|
104
180
|
session_id = str(uuid.uuid4().hex)
|
105
181
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
182
|
+
def set_window_vars():
|
183
|
+
page.evaluate(
|
184
|
+
"""([traceId, sessionId]) => {
|
185
|
+
window.lmnrRrwebSessionId = sessionId;
|
186
|
+
window.lmnrTraceId = traceId;
|
187
|
+
}""",
|
188
|
+
[trace_id, session_id],
|
189
|
+
)
|
190
|
+
return page.evaluate(
|
191
|
+
"""
|
192
|
+
() => window.lmnrRrwebSessionId && window.lmnrTraceId
|
193
|
+
"""
|
194
|
+
)
|
114
195
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
196
|
+
if not retry_sync(
|
197
|
+
set_window_vars, error_message="Failed to set window variables"
|
198
|
+
):
|
199
|
+
return
|
119
200
|
|
120
201
|
# Update the recording setup to include trace ID
|
121
202
|
page.evaluate(
|
@@ -124,41 +205,64 @@ def init_playwright_tracing(http_url: str, project_api_key: str):
|
|
124
205
|
)
|
125
206
|
|
126
207
|
async def inject_rrweb_async(page: Page):
|
127
|
-
|
128
|
-
|
129
|
-
|
208
|
+
# Wait for the page to be in a ready state first
|
209
|
+
await page.wait_for_load_state("domcontentloaded")
|
210
|
+
|
211
|
+
# First check if rrweb is already loaded
|
212
|
+
is_loaded = await page.evaluate(
|
213
|
+
"""
|
214
|
+
() => typeof window.lmnrRrweb !== 'undefined'
|
215
|
+
"""
|
216
|
+
)
|
217
|
+
|
218
|
+
if not is_loaded:
|
130
219
|
|
131
|
-
|
132
|
-
|
220
|
+
async def load_rrweb():
|
221
|
+
await page.evaluate(RRWEB_CONTENT)
|
222
|
+
# Verify script loaded successfully
|
223
|
+
await page.wait_for_function(
|
224
|
+
"""(() => typeof window.lmnrRrweb !== 'undefined')""",
|
225
|
+
timeout=5000,
|
226
|
+
)
|
227
|
+
return True
|
228
|
+
|
229
|
+
if not await retry_async(
|
230
|
+
load_rrweb, delay=1, error_message="Failed to load rrweb"
|
231
|
+
):
|
232
|
+
return
|
233
|
+
|
234
|
+
# Get current trace ID from active span
|
235
|
+
current_span = opentelemetry.trace.get_current_span()
|
236
|
+
if current_span.is_recording():
|
133
237
|
current_span.set_attribute("lmnr.internal.has_browser_session", True)
|
134
|
-
trace_id = format(current_span.get_span_context().trace_id, "032x")
|
135
|
-
session_id = str(uuid.uuid4().hex)
|
136
238
|
|
137
|
-
|
239
|
+
trace_id = format(current_span.get_span_context().trace_id, "032x")
|
240
|
+
session_id = str(uuid.uuid4().hex)
|
241
|
+
|
242
|
+
async def set_window_vars():
|
138
243
|
await page.evaluate(
|
139
244
|
"""([traceId, sessionId]) => {
|
140
|
-
window.
|
141
|
-
window.
|
245
|
+
window.lmnrRrwebSessionId = sessionId;
|
246
|
+
window.lmnrTraceId = traceId;
|
142
247
|
}""",
|
143
248
|
[trace_id, session_id],
|
144
249
|
)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
250
|
+
return await page.evaluate(
|
251
|
+
"""
|
252
|
+
() => window.lmnrRrwebSessionId && window.lmnrTraceId
|
253
|
+
"""
|
149
254
|
)
|
150
255
|
|
151
|
-
|
152
|
-
|
153
|
-
|
256
|
+
if not await retry_async(
|
257
|
+
set_window_vars, error_message="Failed to set window variables"
|
258
|
+
):
|
259
|
+
return
|
154
260
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
except Exception as e:
|
161
|
-
print(f"Error injecting rrweb: {e}")
|
261
|
+
# Update the recording setup to include trace ID
|
262
|
+
await page.evaluate(
|
263
|
+
INJECT_PLACEHOLDER,
|
264
|
+
[http_url, project_api_key],
|
265
|
+
)
|
162
266
|
|
163
267
|
def handle_navigation(page: SyncPage):
|
164
268
|
def on_load():
|
@@ -187,18 +291,16 @@ def init_playwright_tracing(http_url: str, project_api_key: str):
|
|
187
291
|
csp = headers[header_name]
|
188
292
|
parts = csp.split(";")
|
189
293
|
for i, part in enumerate(parts):
|
190
|
-
if "
|
191
|
-
parts[i] = f"{part.strip()}
|
192
|
-
elif "connect-src" in part:
|
193
|
-
parts[i] = f"{part.strip()} " + http_url
|
194
|
-
if not any("connect-src" in part for part in parts):
|
195
|
-
parts.append(" connect-src 'self' " + http_url)
|
294
|
+
if "connect-src" in part:
|
295
|
+
parts[i] = f"{part.strip()} {http_url}"
|
196
296
|
headers[header_name] = ";".join(parts)
|
197
297
|
|
198
298
|
await route.fulfill(response=response, headers=headers)
|
199
|
-
except Exception:
|
299
|
+
except Exception as e:
|
300
|
+
logger.debug(f"Error handling route: {e}")
|
200
301
|
await route.continue_()
|
201
302
|
|
303
|
+
# Intercept all navigation requests to modify CSP headers
|
202
304
|
await self.route("**/*", handle_route)
|
203
305
|
page = await _original_new_page_async(self, *args, **kwargs)
|
204
306
|
await handle_navigation_async(page)
|
@@ -217,19 +319,18 @@ def init_playwright_tracing(http_url: str, project_api_key: str):
|
|
217
319
|
csp = headers[header_name]
|
218
320
|
parts = csp.split(";")
|
219
321
|
for i, part in enumerate(parts):
|
220
|
-
if "
|
221
|
-
parts[i] = f"{part.strip()}
|
222
|
-
elif "connect-src" in part:
|
223
|
-
parts[i] = f"{part.strip()} " + http_url
|
322
|
+
if "connect-src" in part:
|
323
|
+
parts[i] = f"{part.strip()} {http_url}"
|
224
324
|
if not any("connect-src" in part for part in parts):
|
225
|
-
parts.append(" connect-src 'self' "
|
325
|
+
parts.append(f" connect-src 'self' {http_url}")
|
226
326
|
headers[header_name] = ";".join(parts)
|
227
327
|
|
228
328
|
route.fulfill(response=response, headers=headers)
|
229
|
-
except Exception:
|
230
|
-
|
329
|
+
except Exception as e:
|
330
|
+
logger.debug(f"Error handling route: {e}")
|
231
331
|
route.continue_()
|
232
332
|
|
333
|
+
# Intercept all navigation requests to modify CSP headers
|
233
334
|
self.route("**/*", handle_route)
|
234
335
|
page = _original_new_page(self, *args, **kwargs)
|
235
336
|
handle_navigation(page)
|