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.
Files changed (28) hide show
  1. lmnr/__init__.py +0 -4
  2. lmnr/opentelemetry_lib/decorators/__init__.py +38 -28
  3. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +6 -2
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +4 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +3 -0
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +3 -0
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +3 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +3 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +7 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +190 -0
  12. lmnr/opentelemetry_lib/tracing/__init__.py +89 -1
  13. lmnr/opentelemetry_lib/tracing/context.py +109 -0
  14. lmnr/opentelemetry_lib/tracing/processor.py +5 -6
  15. lmnr/opentelemetry_lib/tracing/tracer.py +29 -0
  16. lmnr/sdk/browser/browser_use_otel.py +5 -5
  17. lmnr/sdk/browser/patchright_otel.py +14 -0
  18. lmnr/sdk/browser/playwright_otel.py +32 -6
  19. lmnr/sdk/browser/pw_utils.py +78 -6
  20. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  21. lmnr/sdk/laminar.py +109 -164
  22. lmnr/sdk/types.py +0 -6
  23. lmnr/version.py +1 -1
  24. {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/METADATA +3 -2
  25. {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/RECORD +27 -26
  26. {lmnr-0.6.21.dist-info → lmnr-0.7.0.dist-info}/WHEEL +1 -1
  27. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  28. {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.instrumentation.threading import ThreadingInstrumentor
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
- association_properties = get_value("association_properties")
80
- if association_properties is not None:
81
- _set_association_properties_attributes(span, association_properties)
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": False,
32
- "ignore_output": False,
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": False,
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": False,
58
- "ignore_output": False,
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
- context.on("page", lambda p: start_recording_events_sync(p, session_id, client))
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
- context.on(
80
- "page", lambda p: start_recording_events_async(p, session_id, client)
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
- context.on("page", lambda p: start_recording_events_sync(p, session_id, client))
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
- context.on("page", lambda p: start_recording_events_async(p, session_id, client))
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
 
@@ -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(async () => {
362
+ setInterval(() => {
304
363
  window.lmnrRrweb.record.addCustomEvent('heartbeat', {
305
364
  title: document.title,
306
365
  url: document.URL,
307
366
  })
308
- }, 1000);
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: compressedResult,
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
- span = trace.get_current_span()
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
- span = trace.get_current_span()
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
 
@@ -25,6 +25,7 @@ class AsyncBrowserEvents(BaseAsyncResource):
25
25
  "source": f"python@{PYTHON_VERSION}",
26
26
  "sdkVersion": __version__,
27
27
  }
28
+
28
29
  compressed_payload = gzip.compress(json.dumps(payload).encode("utf-8"))
29
30
  response = await self._client.post(
30
31
  url,