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.
- lmnr/__init__.py +32 -11
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/cli/evals.py +111 -0
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +70 -0
- lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
- lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
- lmnr/opentelemetry_lib/litellm/utils.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
- lmnr/opentelemetry_lib/tracing/processor.py +193 -0
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +18 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/__init__.py +0 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +142 -0
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +151 -0
- lmnr/sdk/browser/playwright_otel.py +322 -0
- lmnr/sdk/browser/pw_utils.py +363 -0
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +70 -0
- lmnr/sdk/client/asynchronous/async_client.py +180 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/asynchronous/resources/base.py +32 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/synchronous/resources/base.py +32 -0
- lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +263 -0
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/sync_client.py +191 -0
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +163 -26
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +403 -191
- lmnr/sdk/laminar.py +1080 -549
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +246 -134
- lmnr/sdk/utils.py +151 -7
- lmnr/version.py +46 -0
- {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/cli.py +0 -101
- lmnr/openllmetry_sdk/.python-version +0 -1
- lmnr/openllmetry_sdk/__init__.py +0 -72
- lmnr/openllmetry_sdk/config/__init__.py +0 -9
- lmnr/openllmetry_sdk/decorators/base.py +0 -185
- lmnr/openllmetry_sdk/instruments.py +0 -38
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
- lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
- lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
- lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
- lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
- lmnr/openllmetry_sdk/utils/package_check.py +0 -7
- lmnr/openllmetry_sdk/version.py +0 -1
- lmnr/sdk/datasets.py +0 -55
- lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
- lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
- lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
- lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- /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
|
+
)
|