Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .riot/requirements/16cd2e7.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/16cd2e7.in
#
attrs==25.4.0
certifi==2025.11.12
charset-normalizer==3.4.4
coverage[toml]==7.12.0
greenlet==3.2.4
hypothesis==6.45.0
idna==3.11
iniconfig==2.3.0
mock==5.2.0
opentracing==2.4.0
packaging==25.0
playwright==1.56.0
pluggy==1.6.0
pyee==13.0.0
pygments==2.19.2
pytest==9.0.1
pytest-base-url==2.1.0
pytest-cov==7.0.0
pytest-mock==3.15.1
pytest-playwright==0.7.2
python-slugify==8.0.4
requests==2.32.5
sortedcontainers==2.4.0
text-unidecode==1.3
typing-extensions==4.15.0
urllib3==2.5.0
34 changes: 34 additions & 0 deletions .riot/requirements/1d937f5.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1d937f5.in
#
attrs==25.4.0
certifi==2025.11.12
charset-normalizer==3.4.4
coverage[toml]==7.12.0
exceptiongroup==1.3.1
greenlet==3.2.4
hypothesis==6.45.0
idna==3.11
iniconfig==2.3.0
mock==5.2.0
opentracing==2.4.0
packaging==25.0
playwright==1.56.0
pluggy==1.6.0
pyee==13.0.0
pygments==2.19.2
pytest==9.0.1
pytest-base-url==2.1.0
pytest-cov==7.0.0
pytest-mock==3.15.1
pytest-playwright==0.7.2
python-slugify==8.0.4
requests==2.32.5
sortedcontainers==2.4.0
text-unidecode==1.3
tomli==2.3.0
typing-extensions==4.15.0
urllib3==2.5.0
1 change: 1 addition & 0 deletions ddtrace/_monkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"unittest": True,
"coverage": False,
"selenium": True,
"playwright": True,
"valkey": True,
"openai_agents": True,
"ray": False,
Expand Down
1 change: 1 addition & 0 deletions ddtrace/contrib/integration_registry/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dbapi",
"dbapi_async",
"selenium",
"playwright",
}

DEPENDENCY_TO_INTEGRATION_MAPPING_SPECIAL_CASES = {
Expand Down
6 changes: 6 additions & 0 deletions ddtrace/contrib/integration_registry/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,12 @@ integrations:
dependency_names:
- selenium

- integration_name: playwright
is_external_package: true
is_tested: false
dependency_names:
- playwright

- integration_name: snowflake
is_external_package: true
is_tested: true
Expand Down
59 changes: 59 additions & 0 deletions ddtrace/contrib/internal/playwright/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Trace the Playwright browser automation library to trace browser requests and enable distributed tracing.

Enabling
~~~~~~~~

The Playwright integration is enabled by default in test contexts. Use
:func:`patch()<ddtrace.patch>` to enable the integration::

from ddtrace import patch
patch(playwright=True)

When using pytest, the `--ddtrace-patch-all` flag is required in order for this integration to
be enabled.

Global Configuration
~~~~~~~~~~~~~~~~~~~~

.. py:data:: ddtrace.config.playwright['distributed_tracing']

Include distributed tracing headers in browser requests sent from Playwright.
This option can also be set with the ``DD_PLAYWRIGHT_DISTRIBUTED_TRACING``
environment variable.

Default: ``True``

Instance Configuration
~~~~~~~~~~~~~~~~~~~~~~

The integration can be configured per instance::

from ddtrace import config

# Disable distributed tracing globally.
config.playwright['distributed_tracing'] = False

Headers tracing is supported for this integration.

How It Works
~~~~~~~~~~~~

The Playwright integration automatically injects Datadog distributed tracing headers
into all browser requests made through Playwright. This enables end-to-end tracing
from your application through to browser-initiated backend requests.

The integration uses a dual approach to ensure headers are injected:
1. **Context-level injection**: Headers added to BrowserContext.extra_http_headers (navigation)
2. **Route interception**: A catch-all route handler for JavaScript-initiated requests (fetch, XHR)

Headers injected include:
- ``x-datadog-trace-id``: The lower 64-bits of the 128-bit trace-id in decimal format
- ``x-datadog-parent-id``: The 64-bits span-id of the current span in decimal format
- ``x-datadog-sampling-priority``: Sampling decision (optional)
- ``x-datadog-origin``: Origin information (optional, not used for browser requests)
- ``x-datadog-tags``: Supplemental trace state information (optional)

This integration is particularly useful for E2E testing scenarios where you want to
trace requests from browser automation through to your backend services.
"""
212 changes: 212 additions & 0 deletions ddtrace/contrib/internal/playwright/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import os
from typing import Dict

from ddtrace import config
from ddtrace.constants import SPAN_KIND
from ddtrace.ext import SpanTypes
from ddtrace.internal.logger import get_logger
from ddtrace.internal.utils.formats import asbool
from ddtrace.propagation.http import HTTPPropagator
from ddtrace.trace import tracer


log = get_logger(__name__)

# Configure the playwright integration
config._add(
"playwright",
{
"distributed_tracing": asbool(os.getenv("DD_PLAYWRIGHT_DISTRIBUTED_TRACING", default=True)),
},
)


def get_version() -> str:
"""Get the Playwright version."""
try:
import playwright

return getattr(playwright, "__version__", "")
except (ImportError, AttributeError):
return ""


def _supported_versions() -> Dict[str, str]:
return {"playwright": "*"}


def _get_tracing_headers() -> Dict[str, str]:
"""
Get distributed tracing headers for the current span context.

Returns a dictionary of headers to inject into HTTP requests.
If no span is active, creates a temporary span.
"""
if not config.playwright.get("distributed_tracing", True):
return {}

headers = {}
try:
current_span = tracer.current_span()
if current_span:
HTTPPropagator.inject(current_span.context, headers)
else:
# No active span, create a temporary span for header injection
with tracer.trace("playwright.browser.request", span_type=SpanTypes.HTTP) as span:
span._set_tag_str(SPAN_KIND, "client")
span._set_tag_str("component", config.playwright.integration_name)
HTTPPropagator.inject(span.context, headers)
except Exception as e:
log.debug("Failed to get distributed tracing headers: %s", e)

return headers


def _patch_browser_context_new_context():
"""Patch Browser.new_context to inject distributed tracing headers."""
try:
from playwright.sync_api import Browser
except ImportError:
log.debug("Playwright not available for patching")
return

original_new_context = Browser.new_context

def _wrapped_new_context(*args, **kwargs):
# Get distributed tracing headers for current context
dd_headers = _get_tracing_headers()

# Add headers to extra_http_headers (for navigation requests)
if dd_headers:
extra_headers = kwargs.setdefault("extra_http_headers", {})
extra_headers.update(dd_headers)

# Create the browser context
context = original_new_context(*args, **kwargs)

# Store headers on context for route handler to reuse
if dd_headers:
context._dd_tracing_headers = dd_headers
_install_route_handler(context)

return context

Browser.new_context = _wrapped_new_context


def _install_route_handler(context) -> None:
"""
Install route handler to inject headers into JavaScript-initiated requests.

JavaScript fetch() and XHR requests don't inherit extra_http_headers,
so we intercept them via route handler and inject headers manually.
"""
if hasattr(context, "_dd_route_handler_installed"):
return

try:

def _inject_headers_handler(route, request):
"""Inject distributed tracing headers into the request."""
try:
# Get request headers and merge in our tracing headers
headers = dict(getattr(request, "headers", {}) or {})
headers.update(getattr(context, "_dd_tracing_headers", {}))

# Continue request with merged headers
route.continue_(headers=headers)

except Exception as e:
# Fallback: continue without modification if injection fails
log.debug("Failed to inject headers in route handler: %s", e)
try:
route.continue_()
except Exception:
pass

# Install catch-all route handler
context.route("**/*", _inject_headers_handler)
context._dd_route_handler_installed = True

except Exception as e:
log.debug("Failed to install route handler: %s", e)


def _patch_api_request_new_context():
"""Patch playwright.request.new_context for API requests."""
try:
import playwright

if not hasattr(playwright, "request"):
return

original_new_context = playwright.request.new_context

def _wrapped_api_new_context(*args, **kwargs):
# Get and inject distributed tracing headers
dd_headers = _get_tracing_headers()
if dd_headers:
extra_headers = kwargs.setdefault("extra_http_headers", {})
extra_headers.update(dd_headers)

return original_new_context(*args, **kwargs)

playwright.request.new_context = _wrapped_api_new_context

except Exception as e:
log.debug("Failed to patch API request context: %s", e)


def patch() -> None:
"""Apply the Playwright integration patch."""
try:
import playwright
except ImportError:
log.debug("Playwright not available, skipping patch")
return

if getattr(playwright, "_datadog_patch", False):
return

try:
# Patch Browser.new_context for browser contexts
_patch_browser_context_new_context()

# Patch API request context
_patch_api_request_new_context()

playwright._datadog_patch = True
log.debug("Playwright integration patched successfully")

except Exception as e:
log.debug("Failed to patch Playwright: %s", e)


def unpatch() -> None:
"""Remove the Playwright integration patch."""
try:
import playwright
except ImportError:
return

if not getattr(playwright, "_datadog_patch", False):
return

try:
from playwright.sync_api import Browser

# Restore original methods if they were patched
if hasattr(Browser, "_original_new_context"):
Browser.new_context = Browser._original_new_context
delattr(Browser, "_original_new_context")

# Restore API request context
if hasattr(playwright, "request") and hasattr(playwright.request, "_original_new_context"):
playwright.request.new_context = playwright.request._original_new_context
delattr(playwright.request, "_original_new_context")

playwright._datadog_patch = False
log.debug("Playwright integration unpatched successfully")

except Exception as e:
log.debug("Failed to unpatch Playwright: %s", e)
1 change: 1 addition & 0 deletions ddtrace/internal/settings/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"asyncpg",
"django",
"aiobotocore",
"playwright",
"pytest_bdd",
"starlette",
"valkey",
Expand Down
Loading
Loading