lmnr 0.6.21__py3-none-any.whl → 0.7.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 +0 -4
- lmnr/opentelemetry_lib/decorators/__init__.py +38 -28
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +6 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +4 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +3 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +7 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +89 -1
- lmnr/opentelemetry_lib/tracing/context.py +109 -0
- lmnr/opentelemetry_lib/tracing/processor.py +5 -6
- lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
- lmnr/sdk/browser/browser_use_otel.py +5 -5
- lmnr/sdk/browser/patchright_otel.py +14 -0
- lmnr/sdk/browser/playwright_otel.py +32 -6
- lmnr/sdk/browser/pw_utils.py +78 -6
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/laminar.py +109 -164
- lmnr/sdk/types.py +0 -6
- lmnr/version.py +1 -1
- {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/METADATA +3 -2
- {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/RECORD +27 -26
- {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/WHEEL +1 -1
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/entry_points.txt +0 -0
@@ -10,9 +10,22 @@ from lmnr.opentelemetry_lib.tracing.instruments import (
|
|
10
10
|
Instruments,
|
11
11
|
init_instrumentations,
|
12
12
|
)
|
13
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
14
|
+
attach_context,
|
15
|
+
detach_context,
|
16
|
+
get_current_context,
|
17
|
+
get_token_stack,
|
18
|
+
_isolated_token_stack,
|
19
|
+
_isolated_token_stack_storage,
|
20
|
+
set_token_stack,
|
21
|
+
)
|
13
22
|
|
14
23
|
from opentelemetry import trace
|
15
|
-
from opentelemetry.
|
24
|
+
from opentelemetry.context import Context
|
25
|
+
|
26
|
+
# instead of importing from opentelemetry.instrumentation.threading,
|
27
|
+
# we import from our modified copy to use Laminar's isolated context.
|
28
|
+
from ..opentelemetry.instrumentation.threading import ThreadingInstrumentor
|
16
29
|
from opentelemetry.sdk.resources import Resource
|
17
30
|
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
|
18
31
|
from opentelemetry.sdk.trace.export import SpanExporter
|
@@ -32,6 +45,7 @@ class TracerWrapper(object):
|
|
32
45
|
_async_client: AsyncLaminarClient
|
33
46
|
_resource: Resource
|
34
47
|
_span_processor: SpanProcessor
|
48
|
+
_original_thread_init = None
|
35
49
|
|
36
50
|
def __new__(
|
37
51
|
cls,
|
@@ -91,6 +105,9 @@ class TracerWrapper(object):
|
|
91
105
|
|
92
106
|
obj._tracer_provider.add_span_processor(obj._span_processor)
|
93
107
|
|
108
|
+
# Setup threading context inheritance
|
109
|
+
obj._setup_threading_inheritance()
|
110
|
+
|
94
111
|
# This is not a real instrumentation and does not generate telemetry
|
95
112
|
# data, but it is required to ensure that OpenTelemetry context
|
96
113
|
# propagation is enabled.
|
@@ -113,6 +130,43 @@ class TracerWrapper(object):
|
|
113
130
|
|
114
131
|
return cls.instance
|
115
132
|
|
133
|
+
def _setup_threading_inheritance(self):
|
134
|
+
"""Setup threading inheritance for isolated context."""
|
135
|
+
if TracerWrapper._original_thread_init is None:
|
136
|
+
# Monkey patch Thread.__init__ to capture context inheritance
|
137
|
+
TracerWrapper._original_thread_init = threading.Thread.__init__
|
138
|
+
|
139
|
+
def patched_thread_init(thread_self, *args, **kwargs):
|
140
|
+
# Capture current isolated context and token stack for inheritance
|
141
|
+
current_context = get_current_context()
|
142
|
+
current_token_stack = get_token_stack().copy()
|
143
|
+
|
144
|
+
# Get the original target function
|
145
|
+
original_target = kwargs.get("target")
|
146
|
+
if not original_target and args:
|
147
|
+
original_target = args[0]
|
148
|
+
|
149
|
+
# Only inherit if we have a target function
|
150
|
+
if original_target:
|
151
|
+
# Create a wrapper function that sets up context
|
152
|
+
def thread_wrapper(*target_args, **target_kwargs):
|
153
|
+
# Set inherited context and token stack in the new thread
|
154
|
+
attach_context(current_context)
|
155
|
+
set_token_stack(current_token_stack)
|
156
|
+
# Run original target
|
157
|
+
return original_target(*target_args, **target_kwargs)
|
158
|
+
|
159
|
+
# Replace the target with our wrapper
|
160
|
+
if "target" in kwargs:
|
161
|
+
kwargs["target"] = thread_wrapper
|
162
|
+
elif args:
|
163
|
+
args = (thread_wrapper,) + args[1:]
|
164
|
+
|
165
|
+
# Call original init
|
166
|
+
TracerWrapper._original_thread_init(thread_self, *args, **kwargs)
|
167
|
+
|
168
|
+
threading.Thread.__init__ = patched_thread_init
|
169
|
+
|
116
170
|
def exit_handler(self):
|
117
171
|
if isinstance(self._span_processor, LaminarSpanProcessor):
|
118
172
|
self._span_processor.clear()
|
@@ -124,6 +178,31 @@ class TracerWrapper(object):
|
|
124
178
|
console_log_handler.setFormatter(VerboseColorfulFormatter())
|
125
179
|
self._logger.addHandler(console_log_handler)
|
126
180
|
|
181
|
+
def get_isolated_context(self) -> Context:
|
182
|
+
"""Get the current isolated context."""
|
183
|
+
return get_current_context()
|
184
|
+
|
185
|
+
def push_span_context(self, span: trace.Span) -> Context:
|
186
|
+
"""Push a new context with the given span onto the stack."""
|
187
|
+
current_ctx = get_current_context()
|
188
|
+
new_context = trace.set_span_in_context(span, current_ctx)
|
189
|
+
token = attach_context(new_context)
|
190
|
+
|
191
|
+
# Store the token for later detachment - tokens are much lighter than contexts
|
192
|
+
current_stack = get_token_stack().copy()
|
193
|
+
current_stack.append(token)
|
194
|
+
set_token_stack(current_stack)
|
195
|
+
|
196
|
+
return new_context
|
197
|
+
|
198
|
+
def pop_span_context(self) -> None:
|
199
|
+
"""Pop the current span context from the stack."""
|
200
|
+
current_stack = get_token_stack().copy()
|
201
|
+
if current_stack:
|
202
|
+
token = current_stack.pop()
|
203
|
+
set_token_stack(current_stack)
|
204
|
+
detach_context(token)
|
205
|
+
|
127
206
|
@staticmethod
|
128
207
|
def set_static_params(
|
129
208
|
resource_attributes: dict,
|
@@ -144,6 +223,15 @@ class TracerWrapper(object):
|
|
144
223
|
# Any state cleanup. Now used in between tests
|
145
224
|
if isinstance(cls.instance._span_processor, LaminarSpanProcessor):
|
146
225
|
cls.instance._span_processor.clear()
|
226
|
+
# Clear the isolated context state for clean test state
|
227
|
+
try:
|
228
|
+
_isolated_token_stack.set([])
|
229
|
+
except LookupError:
|
230
|
+
pass
|
231
|
+
if hasattr(_isolated_token_stack_storage, "token_stack"):
|
232
|
+
_isolated_token_stack_storage.token_stack = []
|
233
|
+
# Reset the isolated context to a fresh state
|
234
|
+
attach_context(Context())
|
147
235
|
|
148
236
|
def shutdown(self):
|
149
237
|
if self._tracer_provider is None:
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import threading
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from contextvars import ContextVar
|
5
|
+
from opentelemetry.context import Context, Token
|
6
|
+
|
7
|
+
|
8
|
+
class _IsolatedRuntimeContext(ABC):
|
9
|
+
"""The isolated RuntimeContext interface, identical to OpenTelemetry's _RuntimeContext
|
10
|
+
but isolated from the global context.
|
11
|
+
"""
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def attach(self, context: Context) -> Token[Context]:
|
15
|
+
"""Sets the current `Context` object. Returns a
|
16
|
+
token that can be used to reset to the previous `Context`.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
context: The Context to set.
|
20
|
+
"""
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def get_current(self) -> Context:
|
24
|
+
"""Returns the current `Context` object."""
|
25
|
+
|
26
|
+
@abstractmethod
|
27
|
+
def detach(self, token: Token[Context]) -> None:
|
28
|
+
"""Resets Context to a previous value
|
29
|
+
|
30
|
+
Args:
|
31
|
+
token: A reference to a previous Context.
|
32
|
+
"""
|
33
|
+
|
34
|
+
|
35
|
+
class IsolatedContextVarsRuntimeContext(_IsolatedRuntimeContext):
|
36
|
+
"""An isolated implementation of the RuntimeContext interface which wraps ContextVar
|
37
|
+
but uses its own ContextVar instead of the global one.
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self) -> None:
|
41
|
+
self._current_context = ContextVar(
|
42
|
+
"isolated_current_context", default=Context()
|
43
|
+
)
|
44
|
+
|
45
|
+
def attach(self, context: Context) -> Token[Context]:
|
46
|
+
"""Sets the current `Context` object. Returns a
|
47
|
+
token that can be used to reset to the previous `Context`.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
context: The Context to set.
|
51
|
+
"""
|
52
|
+
return self._current_context.set(context)
|
53
|
+
|
54
|
+
def get_current(self) -> Context:
|
55
|
+
"""Returns the current `Context` object."""
|
56
|
+
return self._current_context.get()
|
57
|
+
|
58
|
+
def detach(self, token: Token[Context]) -> None:
|
59
|
+
"""Resets Context to a previous value
|
60
|
+
|
61
|
+
Args:
|
62
|
+
token: A reference to a previous Context.
|
63
|
+
"""
|
64
|
+
self._current_context.reset(token)
|
65
|
+
|
66
|
+
|
67
|
+
# Create the isolated runtime context
|
68
|
+
_ISOLATED_RUNTIME_CONTEXT = IsolatedContextVarsRuntimeContext()
|
69
|
+
|
70
|
+
# Token stack for push/pop API compatibility - much lighter than copying contexts
|
71
|
+
_isolated_token_stack: ContextVar[list[Token[Context]]] = ContextVar(
|
72
|
+
"isolated_token_stack", default=[]
|
73
|
+
)
|
74
|
+
|
75
|
+
# Thread-local storage for threading support
|
76
|
+
_isolated_token_stack_storage = threading.local()
|
77
|
+
|
78
|
+
|
79
|
+
def get_token_stack() -> list[Token[Context]]:
|
80
|
+
"""Get the token stack, supporting both asyncio and threading."""
|
81
|
+
try:
|
82
|
+
return _isolated_token_stack.get()
|
83
|
+
except LookupError:
|
84
|
+
if not hasattr(_isolated_token_stack_storage, "token_stack"):
|
85
|
+
_isolated_token_stack_storage.token_stack = []
|
86
|
+
return _isolated_token_stack_storage.token_stack
|
87
|
+
|
88
|
+
|
89
|
+
def set_token_stack(stack: list[Token[Context]]) -> None:
|
90
|
+
"""Set the token stack, supporting both asyncio and threading."""
|
91
|
+
try:
|
92
|
+
_isolated_token_stack.set(stack)
|
93
|
+
except LookupError:
|
94
|
+
_isolated_token_stack_storage.token_stack = stack
|
95
|
+
|
96
|
+
|
97
|
+
def get_current_context() -> Context:
|
98
|
+
"""Get the current isolated context."""
|
99
|
+
return _ISOLATED_RUNTIME_CONTEXT.get_current()
|
100
|
+
|
101
|
+
|
102
|
+
def attach_context(context: Context) -> Token[Context]:
|
103
|
+
"""Attach a context to the isolated runtime context."""
|
104
|
+
return _ISOLATED_RUNTIME_CONTEXT.attach(context)
|
105
|
+
|
106
|
+
|
107
|
+
def detach_context(token: Token[Context]) -> None:
|
108
|
+
"""Detach a context from the isolated runtime context."""
|
109
|
+
_ISOLATED_RUNTIME_CONTEXT.detach(token)
|
@@ -17,9 +17,6 @@ from lmnr.opentelemetry_lib.tracing.attributes import (
|
|
17
17
|
SPAN_SDK_VERSION,
|
18
18
|
)
|
19
19
|
from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
|
20
|
-
from lmnr.opentelemetry_lib.tracing.context_properties import (
|
21
|
-
_set_association_properties_attributes,
|
22
|
-
)
|
23
20
|
from lmnr.version import PYTHON_VERSION, __version__
|
24
21
|
|
25
22
|
|
@@ -76,9 +73,11 @@ class LaminarSpanProcessor(SpanProcessor):
|
|
76
73
|
span.set_attribute(SPAN_SDK_VERSION, __version__)
|
77
74
|
span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
|
78
75
|
|
79
|
-
|
80
|
-
|
81
|
-
|
76
|
+
if span.name == "LangGraph.workflow":
|
77
|
+
graph_context = get_value("lmnr.langgraph.graph") or {}
|
78
|
+
for key, value in graph_context.items():
|
79
|
+
span.set_attribute(f"lmnr.association.properties.{key}", value)
|
80
|
+
|
82
81
|
self.instance.on_start(span, parent_context)
|
83
82
|
|
84
83
|
def on_end(self, span: Span):
|
@@ -1,6 +1,8 @@
|
|
1
1
|
from contextlib import contextmanager
|
2
|
+
from typing import Generator, Tuple
|
2
3
|
|
3
4
|
from opentelemetry import trace
|
5
|
+
from opentelemetry.context import Context
|
4
6
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
5
7
|
|
6
8
|
|
@@ -16,3 +18,30 @@ def get_tracer(flush_on_exit: bool = False):
|
|
16
18
|
finally:
|
17
19
|
if flush_on_exit:
|
18
20
|
wrapper.flush()
|
21
|
+
|
22
|
+
|
23
|
+
@contextmanager
|
24
|
+
def get_tracer_with_context(
|
25
|
+
flush_on_exit: bool = False,
|
26
|
+
) -> Generator[Tuple[trace.Tracer, Context], None, None]:
|
27
|
+
"""Get tracer with isolated context. Returns (tracer, context) tuple."""
|
28
|
+
wrapper = TracerWrapper()
|
29
|
+
try:
|
30
|
+
tracer = wrapper.get_tracer()
|
31
|
+
context = wrapper.get_isolated_context()
|
32
|
+
yield tracer, context
|
33
|
+
finally:
|
34
|
+
if flush_on_exit:
|
35
|
+
wrapper.flush()
|
36
|
+
|
37
|
+
|
38
|
+
def copy_current_context() -> Context:
|
39
|
+
"""Copy the current isolated context for use in threads/tasks."""
|
40
|
+
wrapper = TracerWrapper()
|
41
|
+
return wrapper.get_isolated_context()
|
42
|
+
|
43
|
+
|
44
|
+
def set_context_for_thread(context: Context) -> None:
|
45
|
+
"""Set the isolated context for the current thread."""
|
46
|
+
wrapper = TracerWrapper()
|
47
|
+
wrapper.set_isolated_context(context)
|
@@ -28,8 +28,8 @@ WRAPPED_METHODS = [
|
|
28
28
|
"object": "Agent",
|
29
29
|
"method": "run",
|
30
30
|
"span_name": "agent.run",
|
31
|
-
"ignore_input":
|
32
|
-
"ignore_output":
|
31
|
+
"ignore_input": True,
|
32
|
+
"ignore_output": True,
|
33
33
|
"span_type": "DEFAULT",
|
34
34
|
},
|
35
35
|
{
|
@@ -47,15 +47,15 @@ WRAPPED_METHODS = [
|
|
47
47
|
"method": "act",
|
48
48
|
"span_name": "controller.act",
|
49
49
|
"ignore_input": True,
|
50
|
-
"ignore_output":
|
50
|
+
"ignore_output": True,
|
51
51
|
"span_type": "DEFAULT",
|
52
52
|
},
|
53
53
|
{
|
54
54
|
"package": "browser_use.controller.registry.service",
|
55
55
|
"object": "Registry",
|
56
56
|
"method": "execute_action",
|
57
|
-
"ignore_input":
|
58
|
-
"ignore_output":
|
57
|
+
"ignore_input": True,
|
58
|
+
"ignore_output": True,
|
59
59
|
"span_type": "TOOL",
|
60
60
|
},
|
61
61
|
]
|
@@ -1,4 +1,6 @@
|
|
1
1
|
from lmnr.sdk.browser.playwright_otel import (
|
2
|
+
_wrap_bring_to_front_async,
|
3
|
+
_wrap_bring_to_front_sync,
|
2
4
|
_wrap_new_browser_sync,
|
3
5
|
_wrap_new_browser_async,
|
4
6
|
_wrap_new_context_sync,
|
@@ -46,6 +48,12 @@ WRAPPED_METHODS = [
|
|
46
48
|
"method": "launch_persistent_context",
|
47
49
|
"wrapper": _wrap_new_context_sync,
|
48
50
|
},
|
51
|
+
{
|
52
|
+
"package": "patchright.sync_api",
|
53
|
+
"object": "Page",
|
54
|
+
"method": "bring_to_front",
|
55
|
+
"wrapper": _wrap_bring_to_front_sync,
|
56
|
+
},
|
49
57
|
]
|
50
58
|
|
51
59
|
WRAPPED_METHODS_ASYNC = [
|
@@ -79,6 +87,12 @@ WRAPPED_METHODS_ASYNC = [
|
|
79
87
|
"method": "launch_persistent_context",
|
80
88
|
"wrapper": _wrap_new_context_async,
|
81
89
|
},
|
90
|
+
{
|
91
|
+
"package": "patchright.async_api",
|
92
|
+
"object": "Page",
|
93
|
+
"method": "bring_to_front",
|
94
|
+
"wrapper": _wrap_bring_to_front_async,
|
95
|
+
},
|
82
96
|
]
|
83
97
|
|
84
98
|
|
@@ -60,8 +60,15 @@ def _wrap_new_browser_sync(
|
|
60
60
|
browser: SyncBrowser = wrapped(*args, **kwargs)
|
61
61
|
session_id = str(uuid.uuid4().hex)
|
62
62
|
|
63
|
+
def create_page_handler(session_id, client):
|
64
|
+
def page_handler(page):
|
65
|
+
start_recording_events_sync(page, session_id, client)
|
66
|
+
|
67
|
+
return page_handler
|
68
|
+
|
63
69
|
for context in browser.contexts:
|
64
|
-
|
70
|
+
page_handler = create_page_handler(session_id, client)
|
71
|
+
context.on("page", page_handler)
|
65
72
|
for page in context.pages:
|
66
73
|
start_recording_events_sync(page, session_id, client)
|
67
74
|
|
@@ -75,10 +82,15 @@ async def _wrap_new_browser_async(
|
|
75
82
|
browser: Browser = await wrapped(*args, **kwargs)
|
76
83
|
session_id = str(uuid.uuid4().hex)
|
77
84
|
|
85
|
+
def create_page_handler(session_id, client):
|
86
|
+
async def page_handler(page):
|
87
|
+
await start_recording_events_async(page, session_id, client)
|
88
|
+
|
89
|
+
return page_handler
|
90
|
+
|
78
91
|
for context in browser.contexts:
|
79
|
-
|
80
|
-
|
81
|
-
)
|
92
|
+
page_handler = create_page_handler(session_id, client)
|
93
|
+
context.on("page", page_handler)
|
82
94
|
for page in context.pages:
|
83
95
|
await start_recording_events_async(page, session_id, client)
|
84
96
|
return browser
|
@@ -91,7 +103,14 @@ def _wrap_new_context_sync(
|
|
91
103
|
context: SyncBrowserContext = wrapped(*args, **kwargs)
|
92
104
|
session_id = str(uuid.uuid4().hex)
|
93
105
|
|
94
|
-
|
106
|
+
def create_page_handler(session_id, client):
|
107
|
+
def page_handler(page):
|
108
|
+
start_recording_events_sync(page, session_id, client)
|
109
|
+
|
110
|
+
return page_handler
|
111
|
+
|
112
|
+
page_handler = create_page_handler(session_id, client)
|
113
|
+
context.on("page", page_handler)
|
95
114
|
for page in context.pages:
|
96
115
|
start_recording_events_sync(page, session_id, client)
|
97
116
|
|
@@ -105,7 +124,14 @@ async def _wrap_new_context_async(
|
|
105
124
|
context: BrowserContext = await wrapped(*args, **kwargs)
|
106
125
|
session_id = str(uuid.uuid4().hex)
|
107
126
|
|
108
|
-
|
127
|
+
def create_page_handler(session_id, client):
|
128
|
+
async def page_handler(page):
|
129
|
+
await start_recording_events_async(page, session_id, client)
|
130
|
+
|
131
|
+
return page_handler
|
132
|
+
|
133
|
+
page_handler = create_page_handler(session_id, client)
|
134
|
+
context.on("page", page_handler)
|
109
135
|
for page in context.pages:
|
110
136
|
await start_recording_events_async(page, session_id, client)
|
111
137
|
|
lmnr/sdk/browser/pw_utils.py
CHANGED
@@ -8,6 +8,7 @@ from lmnr.sdk.decorators import observe
|
|
8
8
|
from lmnr.sdk.browser.utils import retry_sync, retry_async
|
9
9
|
from lmnr.sdk.client.synchronous.sync_client import LaminarClient
|
10
10
|
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
11
|
+
from lmnr.opentelemetry_lib.tracing.context import get_current_context
|
11
12
|
|
12
13
|
try:
|
13
14
|
if is_package_installed("playwright"):
|
@@ -38,7 +39,9 @@ with open(os.path.join(current_dir, "rrweb", "rrweb.umd.min.cjs"), "r") as f:
|
|
38
39
|
INJECT_PLACEHOLDER = """
|
39
40
|
() => {
|
40
41
|
const BATCH_TIMEOUT = 2000; // Send events after 2 seconds
|
41
|
-
|
42
|
+
const MAX_WORKER_PROMISES = 50; // Max concurrent worker promises
|
43
|
+
const HEARTBEAT_INTERVAL = 1000;
|
44
|
+
|
42
45
|
window.lmnrRrwebEventsBatch = [];
|
43
46
|
|
44
47
|
// Create a Web Worker for heavy JSON processing with chunked processing
|
@@ -97,6 +100,29 @@ INJECT_PLACEHOLDER = """
|
|
97
100
|
let workerPromises = new Map();
|
98
101
|
let workerId = 0;
|
99
102
|
|
103
|
+
// Cleanup function for worker
|
104
|
+
const cleanupWorker = () => {
|
105
|
+
if (compressionWorker) {
|
106
|
+
compressionWorker.terminate();
|
107
|
+
compressionWorker = null;
|
108
|
+
}
|
109
|
+
workerPromises.clear();
|
110
|
+
workerId = 0;
|
111
|
+
};
|
112
|
+
|
113
|
+
// Clean up stale promises to prevent memory leaks
|
114
|
+
const cleanupStalePromises = () => {
|
115
|
+
if (workerPromises.size > MAX_WORKER_PROMISES) {
|
116
|
+
const toDelete = [];
|
117
|
+
for (const [id, promise] of workerPromises) {
|
118
|
+
if (toDelete.length >= workerPromises.size - MAX_WORKER_PROMISES) break;
|
119
|
+
toDelete.push(id);
|
120
|
+
promise.reject(new Error('Promise cleaned up due to memory pressure'));
|
121
|
+
}
|
122
|
+
toDelete.forEach(id => workerPromises.delete(id));
|
123
|
+
}
|
124
|
+
};
|
125
|
+
|
100
126
|
// Non-blocking JSON.stringify using chunked processing
|
101
127
|
function stringifyNonBlocking(obj, chunkSize = 10000) {
|
102
128
|
return new Promise((resolve, reject) => {
|
@@ -196,6 +222,9 @@ INJECT_PLACEHOLDER = """
|
|
196
222
|
// Alternative: Use transferable objects for maximum efficiency
|
197
223
|
async function compressLargeObjectTransferable(data) {
|
198
224
|
try {
|
225
|
+
// Clean up stale promises first
|
226
|
+
cleanupStalePromises();
|
227
|
+
|
199
228
|
// Stringify on main thread but non-blocking
|
200
229
|
const jsonString = await stringifyNonBlocking(data);
|
201
230
|
|
@@ -219,11 +248,24 @@ INJECT_PLACEHOLDER = """
|
|
219
248
|
}
|
220
249
|
}
|
221
250
|
};
|
251
|
+
|
252
|
+
compressionWorker.onerror = (error) => {
|
253
|
+
console.error('Compression worker error:', error);
|
254
|
+
cleanupWorker();
|
255
|
+
};
|
222
256
|
}
|
223
257
|
|
224
258
|
const id = ++workerId;
|
225
259
|
workerPromises.set(id, { resolve, reject });
|
226
260
|
|
261
|
+
// Set timeout to prevent hanging promises
|
262
|
+
setTimeout(() => {
|
263
|
+
if (workerPromises.has(id)) {
|
264
|
+
workerPromises.delete(id);
|
265
|
+
reject(new Error('Compression timeout'));
|
266
|
+
}
|
267
|
+
}, 10000);
|
268
|
+
|
227
269
|
// Transfer the ArrayBuffer (no copying!)
|
228
270
|
compressionWorker.postMessage({
|
229
271
|
buffer,
|
@@ -262,15 +304,32 @@ INJECT_PLACEHOLDER = """
|
|
262
304
|
}
|
263
305
|
}
|
264
306
|
};
|
307
|
+
|
308
|
+
compressionWorker.onerror = (error) => {
|
309
|
+
console.error('Compression worker error:', error);
|
310
|
+
cleanupWorker();
|
311
|
+
};
|
265
312
|
}
|
266
313
|
|
267
314
|
const id = ++workerId;
|
268
315
|
workerPromises.set(id, { resolve, reject });
|
316
|
+
|
317
|
+
// Set timeout to prevent hanging promises
|
318
|
+
setTimeout(() => {
|
319
|
+
if (workerPromises.has(id)) {
|
320
|
+
workerPromises.delete(id);
|
321
|
+
reject(new Error('Compression timeout'));
|
322
|
+
}
|
323
|
+
}, 10000);
|
324
|
+
|
269
325
|
compressionWorker.postMessage({ jsonString, id });
|
270
326
|
});
|
271
327
|
}
|
272
328
|
}
|
273
329
|
|
330
|
+
|
331
|
+
setInterval(cleanupWorker, 5000);
|
332
|
+
|
274
333
|
function isLargeEvent(type) {
|
275
334
|
const LARGE_EVENT_TYPES = [
|
276
335
|
2, // FullSnapshot
|
@@ -300,13 +359,22 @@ INJECT_PLACEHOLDER = """
|
|
300
359
|
setInterval(sendBatchIfReady, BATCH_TIMEOUT);
|
301
360
|
|
302
361
|
// Add heartbeat events
|
303
|
-
setInterval(
|
362
|
+
setInterval(() => {
|
304
363
|
window.lmnrRrweb.record.addCustomEvent('heartbeat', {
|
305
364
|
title: document.title,
|
306
365
|
url: document.URL,
|
307
366
|
})
|
308
|
-
},
|
367
|
+
}, HEARTBEAT_INTERVAL);
|
309
368
|
|
369
|
+
async function bufferToBase64(buffer) {
|
370
|
+
const base64url = await new Promise(r => {
|
371
|
+
const reader = new FileReader()
|
372
|
+
reader.onload = () => r(reader.result)
|
373
|
+
reader.readAsDataURL(new Blob([buffer]))
|
374
|
+
});
|
375
|
+
return base64url.slice(base64url.indexOf(',') + 1);
|
376
|
+
}
|
377
|
+
|
310
378
|
window.lmnrRrweb.record({
|
311
379
|
async emit(event) {
|
312
380
|
try {
|
@@ -315,9 +383,10 @@ INJECT_PLACEHOLDER = """
|
|
315
383
|
await compressLargeObject(event.data, true) :
|
316
384
|
await compressSmallObject(event.data);
|
317
385
|
|
386
|
+
const base64Data = await bufferToBase64(compressedResult);
|
318
387
|
const eventToSend = {
|
319
388
|
...event,
|
320
|
-
data:
|
389
|
+
data: base64Data,
|
321
390
|
};
|
322
391
|
window.lmnrRrwebEventsBatch.push(eventToSend);
|
323
392
|
} catch (error) {
|
@@ -461,7 +530,9 @@ async def inject_session_recorder_async(page: Page):
|
|
461
530
|
|
462
531
|
@observe(name="playwright.page", ignore_input=True, ignore_output=True)
|
463
532
|
def start_recording_events_sync(page: SyncPage, session_id: str, client: LaminarClient):
|
464
|
-
|
533
|
+
|
534
|
+
ctx = get_current_context()
|
535
|
+
span = trace.get_current_span(ctx)
|
465
536
|
trace_id = format(span.get_span_context().trace_id, "032x")
|
466
537
|
span.set_attribute("lmnr.internal.has_browser_session", True)
|
467
538
|
|
@@ -506,7 +577,8 @@ def start_recording_events_sync(page: SyncPage, session_id: str, client: Laminar
|
|
506
577
|
async def start_recording_events_async(
|
507
578
|
page: Page, session_id: str, client: AsyncLaminarClient
|
508
579
|
):
|
509
|
-
|
580
|
+
ctx = get_current_context()
|
581
|
+
span = trace.get_current_span(ctx)
|
510
582
|
trace_id = format(span.get_span_context().trace_id, "032x")
|
511
583
|
span.set_attribute("lmnr.internal.has_browser_session", True)
|
512
584
|
|