lmnr 0.4.53.dev0__py3-none-any.whl → 0.7.26__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 (133) hide show
  1. lmnr/__init__.py +32 -11
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/cli/evals.py +111 -0
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +70 -0
  7. lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
  8. lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
  9. lmnr/opentelemetry_lib/litellm/utils.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  56. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
  57. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  58. lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
  59. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
  60. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
  61. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  62. lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
  63. lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
  64. lmnr/opentelemetry_lib/tracing/processor.py +193 -0
  65. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  66. lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
  67. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  68. lmnr/opentelemetry_lib/utils/package_check.py +18 -0
  69. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  70. lmnr/sdk/browser/__init__.py +0 -0
  71. lmnr/sdk/browser/background_send_events.py +158 -0
  72. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  73. lmnr/sdk/browser/browser_use_otel.py +142 -0
  74. lmnr/sdk/browser/bubus_otel.py +71 -0
  75. lmnr/sdk/browser/cdp_utils.py +518 -0
  76. lmnr/sdk/browser/inject_script.js +514 -0
  77. lmnr/sdk/browser/patchright_otel.py +151 -0
  78. lmnr/sdk/browser/playwright_otel.py +322 -0
  79. lmnr/sdk/browser/pw_utils.py +363 -0
  80. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  81. lmnr/sdk/browser/utils.py +70 -0
  82. lmnr/sdk/client/asynchronous/async_client.py +180 -0
  83. lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
  84. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  85. lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
  86. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  87. lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
  88. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  89. lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
  90. lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
  91. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  92. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  93. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  94. lmnr/sdk/client/synchronous/resources/evals.py +263 -0
  95. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  96. lmnr/sdk/client/synchronous/resources/tags.py +83 -0
  97. lmnr/sdk/client/synchronous/sync_client.py +191 -0
  98. lmnr/sdk/datasets/__init__.py +94 -0
  99. lmnr/sdk/datasets/file_utils.py +91 -0
  100. lmnr/sdk/decorators.py +163 -26
  101. lmnr/sdk/eval_control.py +3 -2
  102. lmnr/sdk/evaluations.py +403 -191
  103. lmnr/sdk/laminar.py +1080 -549
  104. lmnr/sdk/log.py +7 -2
  105. lmnr/sdk/types.py +246 -134
  106. lmnr/sdk/utils.py +151 -7
  107. lmnr/version.py +46 -0
  108. {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
  109. lmnr-0.7.26.dist-info/RECORD +116 -0
  110. lmnr-0.7.26.dist-info/WHEEL +4 -0
  111. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  112. lmnr/cli.py +0 -101
  113. lmnr/openllmetry_sdk/.python-version +0 -1
  114. lmnr/openllmetry_sdk/__init__.py +0 -72
  115. lmnr/openllmetry_sdk/config/__init__.py +0 -9
  116. lmnr/openllmetry_sdk/decorators/base.py +0 -185
  117. lmnr/openllmetry_sdk/instruments.py +0 -38
  118. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  119. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  120. lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
  121. lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
  122. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  123. lmnr/openllmetry_sdk/utils/package_check.py +0 -7
  124. lmnr/openllmetry_sdk/version.py +0 -1
  125. lmnr/sdk/datasets.py +0 -55
  126. lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
  127. lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
  128. lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
  129. lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
  130. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  131. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  132. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  133. /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
@@ -0,0 +1,322 @@
1
+ import logging
2
+ import uuid
3
+
4
+ from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
5
+ from lmnr.sdk.browser.pw_utils import (
6
+ start_recording_events_async,
7
+ start_recording_events_sync,
8
+ take_full_snapshot,
9
+ take_full_snapshot_async,
10
+ )
11
+ from lmnr.sdk.browser.utils import with_tracer_and_client_wrapper
12
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
13
+ from lmnr.version import __version__
14
+
15
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
16
+ from opentelemetry.instrumentation.utils import unwrap
17
+ from opentelemetry.trace import (
18
+ get_tracer,
19
+ Tracer,
20
+ )
21
+ from typing import Collection
22
+ from wrapt import wrap_function_wrapper
23
+
24
+ try:
25
+ if is_package_installed("playwright"):
26
+ from playwright.async_api import Browser, BrowserContext
27
+ from playwright.sync_api import (
28
+ Browser as SyncBrowser,
29
+ BrowserContext as SyncBrowserContext,
30
+ )
31
+ elif is_package_installed("patchright"):
32
+ from patchright.async_api import Browser, BrowserContext
33
+ from patchright.sync_api import (
34
+ Browser as SyncBrowser,
35
+ BrowserContext as SyncBrowserContext,
36
+ )
37
+ else:
38
+ raise ImportError(
39
+ "Attempted to import lmnr.sdk.browser.playwright_otel, but neither "
40
+ "playwright nor patchright is installed. Use `pip install playwright` "
41
+ "or `pip install patchright` to install one of the supported browsers."
42
+ )
43
+ except ImportError as e:
44
+ raise ImportError(
45
+ f"Attempted to import {__file__}, but it is designed "
46
+ "to patch Playwright, which is not installed. Use `pip install playwright` "
47
+ "or `pip install patchright` to install Playwright or remove this import."
48
+ ) from e
49
+
50
+ # all available versions at https://pypi.org/project/playwright/#history
51
+ _instruments = ("playwright >= 1.9.0",)
52
+ logger = logging.getLogger(__name__)
53
+
54
+
55
+ @with_tracer_and_client_wrapper
56
+ def _wrap_new_browser_sync(
57
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
58
+ ):
59
+ browser: SyncBrowser = wrapped(*args, **kwargs)
60
+ session_id = str(uuid.uuid4().hex)
61
+
62
+ def create_page_handler(session_id, client):
63
+ def page_handler(page):
64
+ start_recording_events_sync(page, session_id, client)
65
+
66
+ return page_handler
67
+
68
+ for context in browser.contexts:
69
+ page_handler = create_page_handler(session_id, client)
70
+ context.on("page", page_handler)
71
+ for page in context.pages:
72
+ start_recording_events_sync(page, session_id, client)
73
+
74
+ return browser
75
+
76
+
77
+ @with_tracer_and_client_wrapper
78
+ async def _wrap_new_browser_async(
79
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
80
+ ):
81
+ browser: Browser = await wrapped(*args, **kwargs)
82
+ session_id = str(uuid.uuid4().hex)
83
+
84
+ def create_page_handler(session_id, client):
85
+ async def page_handler(page):
86
+ await start_recording_events_async(page, session_id, client)
87
+
88
+ return page_handler
89
+
90
+ for context in browser.contexts:
91
+ page_handler = create_page_handler(session_id, client)
92
+ context.on("page", page_handler)
93
+ for page in context.pages:
94
+ await start_recording_events_async(page, session_id, client)
95
+ return browser
96
+
97
+
98
+ @with_tracer_and_client_wrapper
99
+ def _wrap_new_context_sync(
100
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
101
+ ):
102
+ context: SyncBrowserContext = wrapped(*args, **kwargs)
103
+ session_id = str(uuid.uuid4().hex)
104
+
105
+ def create_page_handler(session_id, client):
106
+ def page_handler(page):
107
+ start_recording_events_sync(page, session_id, client)
108
+
109
+ return page_handler
110
+
111
+ page_handler = create_page_handler(session_id, client)
112
+ context.on("page", page_handler)
113
+ for page in context.pages:
114
+ start_recording_events_sync(page, session_id, client)
115
+
116
+ return context
117
+
118
+
119
+ @with_tracer_and_client_wrapper
120
+ async def _wrap_new_context_async(
121
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
122
+ ):
123
+ context: BrowserContext = await wrapped(*args, **kwargs)
124
+ session_id = str(uuid.uuid4().hex)
125
+
126
+ def create_page_handler(session_id, client):
127
+ async def page_handler(page):
128
+ await start_recording_events_async(page, session_id, client)
129
+
130
+ return page_handler
131
+
132
+ page_handler = create_page_handler(session_id, client)
133
+ context.on("page", page_handler)
134
+ for page in context.pages:
135
+ await start_recording_events_async(page, session_id, client)
136
+
137
+ return context
138
+
139
+
140
+ @with_tracer_and_client_wrapper
141
+ def _wrap_bring_to_front_sync(
142
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
143
+ ):
144
+ wrapped(*args, **kwargs)
145
+ take_full_snapshot(instance)
146
+
147
+
148
+ @with_tracer_and_client_wrapper
149
+ async def _wrap_bring_to_front_async(
150
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
151
+ ):
152
+ await wrapped(*args, **kwargs)
153
+ await take_full_snapshot_async(instance)
154
+
155
+
156
+ @with_tracer_and_client_wrapper
157
+ def _wrap_browser_new_page_sync(
158
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
159
+ ):
160
+ page = wrapped(*args, **kwargs)
161
+ session_id = str(uuid.uuid4().hex)
162
+ start_recording_events_sync(page, session_id, client)
163
+ return page
164
+
165
+
166
+ @with_tracer_and_client_wrapper
167
+ async def _wrap_browser_new_page_async(
168
+ tracer: Tracer, client: AsyncLaminarClient, to_wrap, wrapped, instance, args, kwargs
169
+ ):
170
+ page = await wrapped(*args, **kwargs)
171
+ session_id = str(uuid.uuid4().hex)
172
+ await start_recording_events_async(page, session_id, client)
173
+ return page
174
+
175
+
176
+ WRAPPED_METHODS = [
177
+ {
178
+ "package": "playwright.sync_api",
179
+ "object": "BrowserType",
180
+ "method": "launch",
181
+ "wrapper": _wrap_new_browser_sync,
182
+ },
183
+ {
184
+ "package": "playwright.sync_api",
185
+ "object": "BrowserType",
186
+ "method": "connect",
187
+ "wrapper": _wrap_new_browser_sync,
188
+ },
189
+ {
190
+ "package": "playwright.sync_api",
191
+ "object": "BrowserType",
192
+ "method": "connect_over_cdp",
193
+ "wrapper": _wrap_new_browser_sync,
194
+ },
195
+ {
196
+ "package": "playwright.sync_api",
197
+ "object": "Browser",
198
+ "method": "new_context",
199
+ "wrapper": _wrap_new_context_sync,
200
+ },
201
+ {
202
+ "package": "playwright.sync_api",
203
+ "object": "BrowserType",
204
+ "method": "launch_persistent_context",
205
+ "wrapper": _wrap_new_context_sync,
206
+ },
207
+ {
208
+ "package": "playwright.sync_api",
209
+ "object": "Page",
210
+ "method": "bring_to_front",
211
+ "wrapper": _wrap_bring_to_front_sync,
212
+ },
213
+ {
214
+ "package": "playwright.sync_api",
215
+ "object": "Browser",
216
+ "method": "new_page",
217
+ "wrapper": _wrap_browser_new_page_sync,
218
+ },
219
+ ]
220
+
221
+ WRAPPED_METHODS_ASYNC = [
222
+ {
223
+ "package": "playwright.async_api",
224
+ "object": "BrowserType",
225
+ "method": "launch",
226
+ "wrapper": _wrap_new_browser_async,
227
+ },
228
+ {
229
+ "package": "playwright.async_api",
230
+ "object": "BrowserType",
231
+ "method": "connect",
232
+ "wrapper": _wrap_new_browser_async,
233
+ },
234
+ {
235
+ "package": "playwright.async_api",
236
+ "object": "BrowserType",
237
+ "method": "connect_over_cdp",
238
+ "wrapper": _wrap_new_browser_async,
239
+ },
240
+ {
241
+ "package": "playwright.async_api",
242
+ "object": "Browser",
243
+ "method": "new_context",
244
+ "wrapper": _wrap_new_context_async,
245
+ },
246
+ {
247
+ "package": "playwright.async_api",
248
+ "object": "BrowserType",
249
+ "method": "launch_persistent_context",
250
+ "wrapper": _wrap_new_context_async,
251
+ },
252
+ {
253
+ "package": "playwright.async_api",
254
+ "object": "Page",
255
+ "method": "bring_to_front",
256
+ "wrapper": _wrap_bring_to_front_async,
257
+ },
258
+ {
259
+ "package": "playwright.async_api",
260
+ "object": "Browser",
261
+ "method": "new_page",
262
+ "wrapper": _wrap_browser_new_page_async,
263
+ },
264
+ ]
265
+
266
+
267
+ class PlaywrightInstrumentor(BaseInstrumentor):
268
+ def __init__(self, async_client: AsyncLaminarClient):
269
+ super().__init__()
270
+ self.async_client = async_client
271
+
272
+ def instrumentation_dependencies(self) -> Collection[str]:
273
+ return _instruments
274
+
275
+ def _instrument(self, **kwargs):
276
+ tracer_provider = kwargs.get("tracer_provider")
277
+ tracer = get_tracer(__name__, __version__, tracer_provider)
278
+
279
+ # Both sync and async methods use async_client because we are using
280
+ # a background asyncio loop for async sends
281
+ for wrapped_method in WRAPPED_METHODS:
282
+ wrap_package = wrapped_method.get("package")
283
+ wrap_object = wrapped_method.get("object")
284
+ wrap_method = wrapped_method.get("method")
285
+ try:
286
+ wrap_function_wrapper(
287
+ wrap_package,
288
+ f"{wrap_object}.{wrap_method}",
289
+ wrapped_method.get("wrapper")(
290
+ tracer,
291
+ self.async_client,
292
+ wrapped_method,
293
+ ),
294
+ )
295
+ except ModuleNotFoundError:
296
+ pass # that's ok, we don't want to fail if some module is missing
297
+
298
+ # Wrap async methods
299
+ for wrapped_method in WRAPPED_METHODS_ASYNC:
300
+ wrap_package = wrapped_method.get("package")
301
+ wrap_object = wrapped_method.get("object")
302
+ wrap_method = wrapped_method.get("method")
303
+ try:
304
+ wrap_function_wrapper(
305
+ wrap_package,
306
+ f"{wrap_object}.{wrap_method}",
307
+ wrapped_method.get("wrapper")(
308
+ tracer,
309
+ self.async_client,
310
+ wrapped_method,
311
+ ),
312
+ )
313
+ except ModuleNotFoundError:
314
+ pass # that's ok, we don't want to fail if some module is missing
315
+
316
+ def _uninstrument(self, **kwargs):
317
+ # Unwrap methods
318
+ for wrapped_method in WRAPPED_METHODS + WRAPPED_METHODS_ASYNC:
319
+ wrap_package = wrapped_method.get("package")
320
+ wrap_object = wrapped_method.get("object")
321
+ wrap_method = wrapped_method.get("method")
322
+ unwrap(wrap_package, f"{wrap_object}.{wrap_method}")
@@ -0,0 +1,363 @@
1
+ import asyncio
2
+ import os
3
+ import time
4
+
5
+ import orjson
6
+
7
+ from opentelemetry import trace
8
+
9
+ from lmnr.opentelemetry_lib.tracing.context import get_current_context
10
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
11
+ from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
12
+ from lmnr.sdk.decorators import observe
13
+ from lmnr.sdk.browser.utils import retry_sync, retry_async
14
+ from lmnr.sdk.browser.background_send_events import (
15
+ get_background_loop,
16
+ track_async_send,
17
+ )
18
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
19
+ from lmnr.sdk.log import get_default_logger
20
+ from lmnr.sdk.types import MaskInputOptions
21
+
22
+ try:
23
+ if is_package_installed("playwright"):
24
+ from playwright.async_api import Page
25
+ from playwright.sync_api import Page as SyncPage
26
+ elif is_package_installed("patchright"):
27
+ from patchright.async_api import Page
28
+ from patchright.sync_api import Page as SyncPage
29
+ else:
30
+ raise ImportError(
31
+ "Attempted to import lmnr.sdk.browser.pw_utils, but neither "
32
+ "playwright nor patchright is installed. Use `pip install playwright` "
33
+ "or `pip install patchright` to install one of the supported browsers."
34
+ )
35
+ except ImportError as e:
36
+ raise ImportError(
37
+ "Attempted to import lmnr.sdk.browser.pw_utils, but neither "
38
+ "playwright nor patchright is installed. Use `pip install playwright` "
39
+ "or `pip install patchright` to install one of the supported browsers."
40
+ ) from e
41
+
42
+ logger = get_default_logger(__name__)
43
+
44
+ OLD_BUFFER_TIMEOUT = 60
45
+
46
+
47
+ def create_send_events_handler(
48
+ chunk_buffers: dict,
49
+ session_id: str,
50
+ trace_id: str,
51
+ client: AsyncLaminarClient,
52
+ background_loop: asyncio.AbstractEventLoop,
53
+ ):
54
+ """
55
+ Create an async event handler for sending browser events.
56
+
57
+ This handler reassembles chunked event data and submits it to the background
58
+ loop for async HTTP sending. The handler itself processes chunks synchronously
59
+ but delegates the actual HTTP send to the background loop.
60
+
61
+ Args:
62
+ chunk_buffers: Dictionary to store incomplete chunk batches
63
+ session_id: Browser session ID
64
+ trace_id: OpenTelemetry trace ID
65
+ client: Async Laminar client for HTTP requests
66
+ background_loop: Background event loop for async sends
67
+
68
+ Returns:
69
+ An async function that handles incoming event chunks from the browser
70
+ """
71
+
72
+ async def send_events_from_browser(chunk):
73
+ try:
74
+ # Handle chunked data
75
+ batch_id = chunk["batchId"]
76
+ chunk_index = chunk["chunkIndex"]
77
+ total_chunks = chunk["totalChunks"]
78
+ data = chunk["data"]
79
+
80
+ # Initialize buffer for this batch if needed
81
+ if batch_id not in chunk_buffers:
82
+ chunk_buffers[batch_id] = {
83
+ "chunks": {},
84
+ "total": total_chunks,
85
+ "timestamp": time.time(),
86
+ }
87
+
88
+ # Store chunk
89
+ chunk_buffers[batch_id]["chunks"][chunk_index] = data
90
+
91
+ # Check if we have all chunks
92
+ if len(chunk_buffers[batch_id]["chunks"]) == total_chunks:
93
+ # Reassemble the full message
94
+ full_data = ""
95
+ for i in range(total_chunks):
96
+ full_data += chunk_buffers[batch_id]["chunks"][i]
97
+
98
+ # Parse the JSON
99
+ events = orjson.loads(full_data)
100
+
101
+ # Send to server in background loop (independent of Playwright's loop)
102
+ if events and len(events) > 0:
103
+ future = asyncio.run_coroutine_threadsafe(
104
+ client._browser_events.send(session_id, trace_id, events),
105
+ background_loop,
106
+ )
107
+ track_async_send(future)
108
+
109
+ # Clean up buffer
110
+ del chunk_buffers[batch_id]
111
+
112
+ # Clean up old incomplete buffers
113
+ current_time = time.time()
114
+ to_delete = []
115
+ for bid, buffer in chunk_buffers.items():
116
+ if current_time - buffer["timestamp"] > OLD_BUFFER_TIMEOUT:
117
+ to_delete.append(bid)
118
+ for bid in to_delete:
119
+ logger.debug(f"Cleaning up incomplete chunk buffer: {bid}")
120
+ del chunk_buffers[bid]
121
+
122
+ except Exception as e:
123
+ logger.debug(f"Could not send events: {e}")
124
+
125
+ return send_events_from_browser
126
+
127
+
128
+ current_dir = os.path.dirname(os.path.abspath(__file__))
129
+ with open(os.path.join(current_dir, "recorder", "record.umd.min.cjs"), "r") as f:
130
+ RRWEB_CONTENT = f"() => {{ {f.read()} }}"
131
+
132
+ with open(os.path.join(current_dir, "inject_script.js"), "r") as f:
133
+ INJECT_SCRIPT_CONTENT = f.read()
134
+
135
+
136
+ def get_mask_input_setting() -> MaskInputOptions:
137
+ """Get the mask_input setting from session recording configuration."""
138
+ try:
139
+ config = TracerWrapper.get_session_recording_options()
140
+ return config.get(
141
+ "mask_input_options",
142
+ MaskInputOptions(
143
+ textarea=False,
144
+ text=False,
145
+ number=False,
146
+ select=False,
147
+ email=False,
148
+ tel=False,
149
+ ),
150
+ )
151
+ except (AttributeError, Exception):
152
+ # Fallback to default configuration if TracerWrapper is not initialized
153
+ return MaskInputOptions(
154
+ textarea=False,
155
+ text=False,
156
+ number=False,
157
+ select=False,
158
+ email=False,
159
+ tel=False,
160
+ )
161
+
162
+
163
+ def inject_session_recorder_sync(page: SyncPage):
164
+ try:
165
+ try:
166
+ is_loaded = page.evaluate(
167
+ """() => typeof window.lmnrRrweb !== 'undefined'"""
168
+ )
169
+ except Exception as e:
170
+ logger.debug(f"Failed to check if session recorder is loaded: {e}")
171
+ is_loaded = False
172
+
173
+ if not is_loaded:
174
+
175
+ def load_session_recorder():
176
+ try:
177
+ if page.is_closed():
178
+ return False
179
+ page.evaluate(RRWEB_CONTENT)
180
+ return True
181
+ except Exception as e:
182
+ logger.debug(f"Failed to load session recorder: {e}")
183
+ return False
184
+
185
+ if not retry_sync(
186
+ load_session_recorder,
187
+ delay=1,
188
+ error_message="Failed to load session recorder",
189
+ ):
190
+ return
191
+
192
+ try:
193
+ if not page.is_closed():
194
+ page.evaluate(
195
+ f"({INJECT_SCRIPT_CONTENT})({orjson.dumps(get_mask_input_setting()).decode('utf-8')}, false)"
196
+ )
197
+ except Exception as e:
198
+ logger.debug(f"Failed to inject session recorder: {e}")
199
+
200
+ except Exception as e:
201
+ logger.error(f"Error during session recorder injection: {e}")
202
+
203
+
204
+ async def inject_session_recorder_async(page: Page):
205
+ try:
206
+ try:
207
+ is_loaded = await page.evaluate(
208
+ """() => typeof window.lmnrRrweb !== 'undefined'"""
209
+ )
210
+ except Exception as e:
211
+ logger.debug(f"Failed to check if session recorder is loaded: {e}")
212
+ is_loaded = False
213
+
214
+ if not is_loaded:
215
+
216
+ async def load_session_recorder():
217
+ try:
218
+ if page.is_closed():
219
+ return False
220
+ await page.evaluate(RRWEB_CONTENT)
221
+ return True
222
+ except Exception as e:
223
+ logger.debug(f"Failed to load session recorder: {e}")
224
+ return False
225
+
226
+ if not await retry_async(
227
+ load_session_recorder,
228
+ delay=1,
229
+ error_message="Failed to load session recorder",
230
+ ):
231
+ return
232
+
233
+ try:
234
+ if not page.is_closed():
235
+ await page.evaluate(
236
+ f"({INJECT_SCRIPT_CONTENT})({orjson.dumps(get_mask_input_setting()).decode('utf-8')}, false)"
237
+ )
238
+ except Exception as e:
239
+ logger.debug(f"Failed to inject session recorder placeholder: {e}")
240
+
241
+ except Exception as e:
242
+ logger.error(f"Error during session recorder injection: {e}")
243
+
244
+
245
+ @observe(name="playwright.page", ignore_input=True, ignore_output=True)
246
+ def start_recording_events_sync(
247
+ page: SyncPage, session_id: str, client: AsyncLaminarClient
248
+ ):
249
+
250
+ ctx = get_current_context()
251
+ span = trace.get_current_span(ctx)
252
+ trace_id = format(span.get_span_context().trace_id, "032x")
253
+ span.set_attribute("lmnr.internal.has_browser_session", True)
254
+
255
+ # Get the background loop for async sends
256
+ background_loop = get_background_loop()
257
+
258
+ # Buffer for reassembling chunks
259
+ chunk_buffers = {}
260
+
261
+ # Create the async event handler (shared implementation)
262
+ send_events_from_browser = create_send_events_handler(
263
+ chunk_buffers, session_id, trace_id, client, background_loop
264
+ )
265
+
266
+ def submit_event(chunk):
267
+ """Sync wrapper that submits async handler to background loop."""
268
+ try:
269
+ # Submit async handler to background loop
270
+ asyncio.run_coroutine_threadsafe(
271
+ send_events_from_browser(chunk),
272
+ background_loop,
273
+ )
274
+ except Exception as e:
275
+ logger.debug(f"Error submitting event: {e}")
276
+
277
+ try:
278
+ page.expose_function("lmnrSendEvents", submit_event)
279
+ except Exception as e:
280
+ logger.debug(f"Could not expose function: {e}")
281
+
282
+ inject_session_recorder_sync(page)
283
+
284
+ def on_load(p):
285
+ try:
286
+ if not p.is_closed():
287
+ inject_session_recorder_sync(p)
288
+ except Exception as e:
289
+ logger.debug(f"Error in on_load handler: {e}")
290
+
291
+ page.on("domcontentloaded", on_load)
292
+
293
+
294
+ @observe(name="playwright.page", ignore_input=True, ignore_output=True)
295
+ async def start_recording_events_async(
296
+ page: Page, session_id: str, client: AsyncLaminarClient
297
+ ):
298
+ ctx = get_current_context()
299
+ span = trace.get_current_span(ctx)
300
+ trace_id = format(span.get_span_context().trace_id, "032x")
301
+ span.set_attribute("lmnr.internal.has_browser_session", True)
302
+
303
+ # Get the background loop for async sends (independent of Playwright's loop)
304
+ background_loop = get_background_loop()
305
+
306
+ # Buffer for reassembling chunks
307
+ chunk_buffers = {}
308
+
309
+ # Create the async event handler (shared implementation)
310
+ send_events_from_browser = create_send_events_handler(
311
+ chunk_buffers, session_id, trace_id, client, background_loop
312
+ )
313
+
314
+ try:
315
+ await page.expose_function("lmnrSendEvents", send_events_from_browser)
316
+ except Exception as e:
317
+ logger.debug(f"Could not expose function: {e}")
318
+
319
+ await inject_session_recorder_async(page)
320
+
321
+ async def on_load(p):
322
+ try:
323
+ # Check if page is closed before attempting to inject
324
+ if not p.is_closed():
325
+ await inject_session_recorder_async(p)
326
+ except Exception as e:
327
+ logger.debug(f"Error in on_load handler: {e}")
328
+
329
+ page.on("domcontentloaded", on_load)
330
+
331
+
332
+ def take_full_snapshot(page: Page):
333
+ return page.evaluate(
334
+ """() => {
335
+ if (window.lmnrRrweb) {
336
+ try {
337
+ window.lmnrRrweb.record.takeFullSnapshot();
338
+ return true;
339
+ } catch (e) {
340
+ console.error("Error taking full snapshot:", e);
341
+ return false;
342
+ }
343
+ }
344
+ return false;
345
+ }"""
346
+ )
347
+
348
+
349
+ async def take_full_snapshot_async(page: Page):
350
+ return await page.evaluate(
351
+ """() => {
352
+ if (window.lmnrRrweb) {
353
+ try {
354
+ window.lmnrRrweb.record.takeFullSnapshot();
355
+ return true;
356
+ } catch (e) {
357
+ console.error("Error taking full snapshot:", e);
358
+ return false;
359
+ }
360
+ }
361
+ return false;
362
+ }"""
363
+ )