fix: flatten React Router structure — no nested Routes, each page is a top-level route

This commit is contained in:
enzo
2026-07-22 19:21:29 +08:00
parent 07ff8c2a12
commit 1de5fcf698
3720 changed files with 1254827 additions and 49 deletions
@@ -0,0 +1,7 @@
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
__all__ = [
"SentryPropagator",
"SentrySpanProcessor",
]
@@ -0,0 +1,9 @@
from sentry_sdk.integrations import DidNotEnable
try:
from opentelemetry.context import create_key
except ImportError:
raise DidNotEnable("opentelemetry not installed")
SENTRY_TRACE_KEY = create_key("sentry-trace")
SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
@@ -0,0 +1,81 @@
"""
IMPORTANT: The contents of this file are part of a proof of concept and as such
are experimental and not suitable for production use. They may be changed or
removed at any time without prior notice.
"""
import warnings
from typing import TYPE_CHECKING
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import logger
if TYPE_CHECKING:
from typing import Any, Dict, Optional
try:
from opentelemetry import trace
from opentelemetry.propagate import set_global_textmap
from opentelemetry.sdk.trace import TracerProvider
except ImportError:
raise DidNotEnable("opentelemetry not installed")
try:
from opentelemetry.instrumentation.django import ( # type: ignore[import-not-found]
DjangoInstrumentor,
)
except ImportError:
DjangoInstrumentor = None
CONFIGURABLE_INSTRUMENTATIONS = {
DjangoInstrumentor: {"is_sql_commentor_enabled": True},
}
class OpenTelemetryIntegration(Integration):
identifier = "opentelemetry"
@staticmethod
def setup_once() -> None:
pass
def setup_once_with_options(
self, options: "Optional[Dict[str, Any]]" = None
) -> None:
if has_span_streaming_enabled(options):
logger.warning(
"[OTel] OpenTelemetryIntegration is not compatible with span streaming "
"(trace_lifecycle='stream') and will be disabled."
)
return
warnings.warn(
"OpenTelemetryIntegration is deprecated. "
"Please use OTLPIntegration instead: "
"https://docs.sentry.io/platforms/python/integrations/otlp/",
DeprecationWarning,
stacklevel=2,
)
_setup_sentry_tracing()
# _setup_instrumentors()
logger.debug("[OTel] Finished setting up OpenTelemetry integration")
def _setup_sentry_tracing() -> None:
provider = TracerProvider()
provider.add_span_processor(SentrySpanProcessor())
trace.set_tracer_provider(provider)
set_global_textmap(SentryPropagator())
def _setup_instrumentors() -> None:
for instrumentor, kwargs in CONFIGURABLE_INSTRUMENTATIONS.items():
if instrumentor is None:
continue
instrumentor().instrument(**kwargs)
@@ -0,0 +1,128 @@
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.opentelemetry.consts import (
SENTRY_BAGGAGE_KEY,
SENTRY_TRACE_KEY,
)
from sentry_sdk.integrations.opentelemetry.span_processor import (
SentrySpanProcessor,
)
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
)
from sentry_sdk.tracing_utils import Baggage, extract_sentrytrace_data
try:
from opentelemetry import trace
from opentelemetry.context import (
Context,
get_current,
set_value,
)
from opentelemetry.propagators.textmap import (
CarrierT,
Getter,
Setter,
TextMapPropagator,
default_getter,
default_setter,
)
from opentelemetry.trace import (
NonRecordingSpan,
SpanContext,
TraceFlags,
)
except ImportError:
raise DidNotEnable("opentelemetry not installed")
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Optional, Set
class SentryPropagator(TextMapPropagator):
"""
Propagates tracing headers for Sentry's tracing system in a way OTel understands.
"""
def extract(
self,
carrier: "CarrierT",
context: "Optional[Context]" = None,
getter: "Getter[CarrierT]" = default_getter,
) -> "Context":
if context is None:
context = get_current()
sentry_trace = getter.get(carrier, SENTRY_TRACE_HEADER_NAME)
if not sentry_trace:
return context
sentrytrace = extract_sentrytrace_data(sentry_trace[0])
if not sentrytrace:
return context
context = set_value(SENTRY_TRACE_KEY, sentrytrace, context)
trace_id, span_id = sentrytrace["trace_id"], sentrytrace["parent_span_id"]
span_context = SpanContext(
trace_id=int(trace_id, 16), # type: ignore
span_id=int(span_id, 16), # type: ignore
# we simulate a sampled trace on the otel side and leave the sampling to sentry
trace_flags=TraceFlags(TraceFlags.SAMPLED),
is_remote=True,
)
baggage_header = getter.get(carrier, BAGGAGE_HEADER_NAME)
if baggage_header:
baggage = Baggage.from_incoming_header(baggage_header[0])
else:
# If there's an incoming sentry-trace but no incoming baggage header,
# for instance in traces coming from older SDKs,
# baggage will be empty and frozen and won't be populated as head SDK.
baggage = Baggage(sentry_items={})
baggage.freeze()
context = set_value(SENTRY_BAGGAGE_KEY, baggage, context)
span = NonRecordingSpan(span_context)
modified_context = trace.set_span_in_context(span, context)
return modified_context
def inject(
self,
carrier: "CarrierT",
context: "Optional[Context]" = None,
setter: "Setter[CarrierT]" = default_setter,
) -> None:
if context is None:
context = get_current()
current_span = trace.get_current_span(context)
current_span_context = current_span.get_span_context()
if not current_span_context.is_valid:
return
span_id = trace.format_span_id(current_span_context.span_id)
span_map = SentrySpanProcessor.otel_span_map
sentry_span = span_map.get(span_id, None)
if not sentry_span:
return
setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_span.to_traceparent())
if sentry_span.containing_transaction:
baggage = sentry_span.containing_transaction.get_baggage()
if baggage:
baggage_data = baggage.serialize()
if baggage_data:
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_data)
@property
def fields(self) -> "Set[str]":
return {SENTRY_TRACE_HEADER_NAME, BAGGAGE_HEADER_NAME}
@@ -0,0 +1,407 @@
from datetime import datetime, timezone
from time import time
from typing import TYPE_CHECKING, cast
from urllib3.util import parse_url as urlparse
from sentry_sdk import get_client, start_transaction
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.opentelemetry.consts import (
SENTRY_BAGGAGE_KEY,
SENTRY_TRACE_KEY,
)
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.tracing import Span as SentrySpan
from sentry_sdk.tracing import Transaction
try:
from opentelemetry.context import get_value
from opentelemetry.sdk.trace import ReadableSpan as OTelSpan
from opentelemetry.sdk.trace import SpanProcessor
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import (
SpanKind,
format_span_id,
format_trace_id,
get_current_span,
)
from opentelemetry.trace.span import (
INVALID_SPAN_ID,
INVALID_TRACE_ID,
)
except ImportError:
raise DidNotEnable("opentelemetry not installed")
if TYPE_CHECKING:
from typing import Any, Optional, Union
from opentelemetry import context as context_api
from opentelemetry.trace import SpanContext
from sentry_sdk._types import Event, Hint
OPEN_TELEMETRY_CONTEXT = "otel"
SPAN_MAX_TIME_OPEN_MINUTES = 10
SPAN_ORIGIN = "auto.otel"
def link_trace_context_to_error_event(
event: "Event", otel_span_map: "dict[str, Union[Transaction, SentrySpan]]"
) -> "Event":
client = get_client()
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
return event
if hasattr(event, "type") and event["type"] == "transaction":
return event
otel_span = get_current_span()
if not otel_span:
return event
ctx = otel_span.get_span_context()
if ctx.trace_id == INVALID_TRACE_ID or ctx.span_id == INVALID_SPAN_ID:
return event
sentry_span = otel_span_map.get(format_span_id(ctx.span_id), None)
if not sentry_span:
return event
contexts = event.setdefault("contexts", {})
contexts.setdefault("trace", {}).update(sentry_span.get_trace_context())
return event
class SentrySpanProcessor(SpanProcessor):
"""
Converts OTel spans into Sentry spans so they can be sent to the Sentry backend.
"""
# The mapping from otel span ids to sentry spans
otel_span_map: "dict[str, Union[Transaction, SentrySpan]]" = {}
# The currently open spans. Elements will be discarded after SPAN_MAX_TIME_OPEN_MINUTES
open_spans: "dict[int, set[str]]" = {}
initialized: "bool" = False
def __new__(cls) -> "SentrySpanProcessor":
if not hasattr(cls, "instance"):
cls.instance = super().__new__(cls)
# "instance" class attribute is guaranteed to be set above (mypy believes instance is an instance-only attribute).
return cls.instance # type: ignore[misc]
def __init__(self) -> None:
if self.initialized:
return
@add_global_event_processor
def global_event_processor(event: "Event", hint: "Hint") -> "Event":
return link_trace_context_to_error_event(event, self.otel_span_map)
self.initialized = True
def _prune_old_spans(self: "SentrySpanProcessor") -> None:
"""
Prune spans that have been open for too long.
"""
current_time_minutes = int(time() / 60)
for span_start_minutes in list(
self.open_spans.keys()
): # making a list because we change the dict
# prune empty open spans buckets
if self.open_spans[span_start_minutes] == set():
self.open_spans.pop(span_start_minutes)
# prune old buckets
elif current_time_minutes - span_start_minutes > SPAN_MAX_TIME_OPEN_MINUTES:
for span_id in self.open_spans.pop(span_start_minutes):
self.otel_span_map.pop(span_id, None)
def on_start(
self,
otel_span: "OTelSpan",
parent_context: "Optional[context_api.Context]" = None,
) -> None:
client = get_client()
if not client.parsed_dsn:
return
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
return
span_context = otel_span.get_span_context()
if span_context is None or not span_context.is_valid:
return
if self._is_sentry_span(otel_span):
return
trace_data = self._get_trace_data(
span_context, otel_span.parent, parent_context
)
parent_span_id = trace_data["parent_span_id"]
sentry_parent_span = (
self.otel_span_map.get(parent_span_id) if parent_span_id else None
)
start_timestamp = None
if otel_span.start_time is not None:
start_timestamp = datetime.fromtimestamp(
otel_span.start_time / 1e9, timezone.utc
) # OTel spans have nanosecond precision
sentry_span = None
if sentry_parent_span:
sentry_span = sentry_parent_span.start_child(
span_id=trace_data["span_id"],
name=otel_span.name,
start_timestamp=start_timestamp,
instrumenter=INSTRUMENTER.OTEL,
origin=SPAN_ORIGIN,
)
else:
sentry_span = start_transaction(
name=otel_span.name,
span_id=trace_data["span_id"],
parent_span_id=parent_span_id,
trace_id=trace_data["trace_id"],
baggage=trace_data["baggage"],
start_timestamp=start_timestamp,
instrumenter=INSTRUMENTER.OTEL,
origin=SPAN_ORIGIN,
)
self.otel_span_map[trace_data["span_id"]] = sentry_span
if otel_span.start_time is not None:
span_start_in_minutes = int(
otel_span.start_time / 1e9 / 60
) # OTel spans have nanosecond precision
self.open_spans.setdefault(span_start_in_minutes, set()).add(
trace_data["span_id"]
)
self._prune_old_spans()
def on_end(self, otel_span: "OTelSpan") -> None:
client = get_client()
if client.options["instrumenter"] != INSTRUMENTER.OTEL:
return
span_context = otel_span.get_span_context()
if span_context is None or not span_context.is_valid:
return
span_id = format_span_id(span_context.span_id)
sentry_span = self.otel_span_map.pop(span_id, None)
if not sentry_span:
return
sentry_span.op = otel_span.name
self._update_span_with_otel_status(sentry_span, otel_span)
if isinstance(sentry_span, Transaction):
sentry_span.name = otel_span.name
sentry_span.set_context(
OPEN_TELEMETRY_CONTEXT, self._get_otel_context(otel_span)
)
self._update_transaction_with_otel_data(sentry_span, otel_span)
else:
self._update_span_with_otel_data(sentry_span, otel_span)
end_timestamp = None
if otel_span.end_time is not None:
end_timestamp = datetime.fromtimestamp(
otel_span.end_time / 1e9, timezone.utc
) # OTel spans have nanosecond precision
sentry_span.finish(end_timestamp=end_timestamp)
if otel_span.start_time is not None:
span_start_in_minutes = int(
otel_span.start_time / 1e9 / 60
) # OTel spans have nanosecond precision
self.open_spans.setdefault(span_start_in_minutes, set()).discard(span_id)
self._prune_old_spans()
def _is_sentry_span(self, otel_span: "OTelSpan") -> bool:
"""
Break infinite loop:
HTTP requests to Sentry are caught by OTel and send again to Sentry.
"""
otel_span_url = None
if otel_span.attributes is not None:
otel_span_url = otel_span.attributes.get(SpanAttributes.HTTP_URL)
otel_span_url = cast("Optional[str]", otel_span_url)
parsed_dsn = get_client().parsed_dsn
dsn_url = parsed_dsn.netloc if parsed_dsn else None
if otel_span_url and dsn_url and dsn_url in otel_span_url:
return True
return False
def _get_otel_context(self, otel_span: "OTelSpan") -> "dict[str, Any]":
"""
Returns the OTel context for Sentry.
See: https://develop.sentry.dev/sdk/performance/opentelemetry/#step-5-add-opentelemetry-context
"""
ctx = {}
if otel_span.attributes:
ctx["attributes"] = dict(otel_span.attributes)
if otel_span.resource.attributes:
ctx["resource"] = dict(otel_span.resource.attributes)
return ctx
def _get_trace_data(
self,
span_context: "SpanContext",
parent_span_context: "Optional[SpanContext]",
parent_context: "Optional[context_api.Context]",
) -> "dict[str, Any]":
"""
Extracts tracing information from one OTel span's context and its parent OTel context.
"""
trace_data: "dict[str, Any]" = {}
span_id = format_span_id(span_context.span_id)
trace_data["span_id"] = span_id
trace_id = format_trace_id(span_context.trace_id)
trace_data["trace_id"] = trace_id
parent_span_id = (
format_span_id(parent_span_context.span_id) if parent_span_context else None
)
trace_data["parent_span_id"] = parent_span_id
sentry_trace_data = get_value(SENTRY_TRACE_KEY, parent_context)
sentry_trace_data = cast("dict[str, Union[str, bool, None]]", sentry_trace_data)
trace_data["parent_sampled"] = (
sentry_trace_data["parent_sampled"] if sentry_trace_data else None
)
baggage = get_value(SENTRY_BAGGAGE_KEY, parent_context)
trace_data["baggage"] = baggage
return trace_data
def _update_span_with_otel_status(
self, sentry_span: "SentrySpan", otel_span: "OTelSpan"
) -> None:
"""
Set the Sentry span status from the OTel span
"""
if otel_span.status.is_unset:
return
if otel_span.status.is_ok:
sentry_span.set_status(SPANSTATUS.OK)
return
sentry_span.set_status(SPANSTATUS.INTERNAL_ERROR)
def _update_span_with_otel_data(
self, sentry_span: "SentrySpan", otel_span: "OTelSpan"
) -> None:
"""
Convert OTel span data and update the Sentry span with it.
This should eventually happen on the server when ingesting the spans.
"""
sentry_span.set_data("otel.kind", otel_span.kind)
op = otel_span.name
description = otel_span.name
if otel_span.attributes is not None:
for key, val in otel_span.attributes.items():
sentry_span.set_data(key, val)
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
http_method = cast("Optional[str]", http_method)
db_query = otel_span.attributes.get(SpanAttributes.DB_SYSTEM)
if http_method:
op = "http"
if otel_span.kind == SpanKind.SERVER:
op += ".server"
elif otel_span.kind == SpanKind.CLIENT:
op += ".client"
description = http_method
peer_name = otel_span.attributes.get(SpanAttributes.NET_PEER_NAME, None)
if peer_name:
description += " {}".format(peer_name)
target = otel_span.attributes.get(SpanAttributes.HTTP_TARGET, None)
if target:
description += " {}".format(target)
if not peer_name and not target:
url = otel_span.attributes.get(SpanAttributes.HTTP_URL, None)
url = cast("Optional[str]", url)
if url:
parsed_url = urlparse(url)
url = "{}://{}{}".format(
parsed_url.scheme, parsed_url.netloc, parsed_url.path
)
description += " {}".format(url)
status_code = otel_span.attributes.get(
SpanAttributes.HTTP_STATUS_CODE, None
)
status_code = cast("Optional[int]", status_code)
if status_code:
sentry_span.set_http_status(status_code)
elif db_query:
op = "db"
statement = otel_span.attributes.get(SpanAttributes.DB_STATEMENT, None)
statement = cast("Optional[str]", statement)
if statement:
description = statement
sentry_span.op = op
sentry_span.description = description
def _update_transaction_with_otel_data(
self, sentry_span: "SentrySpan", otel_span: "OTelSpan"
) -> None:
if otel_span.attributes is None:
return
http_method = otel_span.attributes.get(SpanAttributes.HTTP_METHOD)
if http_method:
status_code = otel_span.attributes.get(SpanAttributes.HTTP_STATUS_CODE)
status_code = cast("Optional[int]", status_code)
if status_code:
sentry_span.set_http_status(status_code)
op = "http"
if otel_span.kind == SpanKind.SERVER:
op += ".server"
elif otel_span.kind == SpanKind.CLIENT:
op += ".client"
sentry_span.op = op