lmnr 0.4.58__py3-none-any.whl → 0.4.60__py3-none-any.whl

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