templates.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import functools
  2. from django.template import TemplateSyntaxError
  3. from django.utils.safestring import mark_safe
  4. from django import VERSION as DJANGO_VERSION
  5. import sentry_sdk
  6. from sentry_sdk.consts import OP
  7. from sentry_sdk.utils import ensure_integration_enabled
  8. from typing import TYPE_CHECKING
  9. if TYPE_CHECKING:
  10. from typing import Any
  11. from typing import Dict
  12. from typing import Optional
  13. from typing import Iterator
  14. from typing import Tuple
  15. try:
  16. # support Django 1.9
  17. from django.template.base import Origin
  18. except ImportError:
  19. # backward compatibility
  20. from django.template.loader import LoaderOrigin as Origin
  21. def get_template_frame_from_exception(exc_value):
  22. # type: (Optional[BaseException]) -> Optional[Dict[str, Any]]
  23. # As of Django 1.9 or so the new template debug thing showed up.
  24. if hasattr(exc_value, "template_debug"):
  25. return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore
  26. # As of r16833 (Django) all exceptions may contain a
  27. # ``django_template_source`` attribute (rather than the legacy
  28. # ``TemplateSyntaxError.source`` check)
  29. if hasattr(exc_value, "django_template_source"):
  30. return _get_template_frame_from_source(
  31. exc_value.django_template_source # type: ignore
  32. )
  33. if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
  34. source = exc_value.source
  35. if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
  36. return _get_template_frame_from_source(source) # type: ignore
  37. return None
  38. def _get_template_name_description(template_name):
  39. # type: (str) -> str
  40. if isinstance(template_name, (list, tuple)):
  41. if template_name:
  42. return "[{}, ...]".format(template_name[0])
  43. else:
  44. return template_name
  45. def patch_templates():
  46. # type: () -> None
  47. from django.template.response import SimpleTemplateResponse
  48. from sentry_sdk.integrations.django import DjangoIntegration
  49. real_rendered_content = SimpleTemplateResponse.rendered_content
  50. @property # type: ignore
  51. @ensure_integration_enabled(DjangoIntegration, real_rendered_content.fget)
  52. def rendered_content(self):
  53. # type: (SimpleTemplateResponse) -> str
  54. with sentry_sdk.start_span(
  55. op=OP.TEMPLATE_RENDER,
  56. name=_get_template_name_description(self.template_name),
  57. origin=DjangoIntegration.origin,
  58. ) as span:
  59. span.set_data("context", self.context_data)
  60. return real_rendered_content.fget(self)
  61. SimpleTemplateResponse.rendered_content = rendered_content
  62. if DJANGO_VERSION < (1, 7):
  63. return
  64. import django.shortcuts
  65. real_render = django.shortcuts.render
  66. @functools.wraps(real_render)
  67. @ensure_integration_enabled(DjangoIntegration, real_render)
  68. def render(request, template_name, context=None, *args, **kwargs):
  69. # type: (django.http.HttpRequest, str, Optional[Dict[str, Any]], *Any, **Any) -> django.http.HttpResponse
  70. # Inject trace meta tags into template context
  71. context = context or {}
  72. if "sentry_trace_meta" not in context:
  73. context["sentry_trace_meta"] = mark_safe(
  74. sentry_sdk.get_current_scope().trace_propagation_meta()
  75. )
  76. with sentry_sdk.start_span(
  77. op=OP.TEMPLATE_RENDER,
  78. name=_get_template_name_description(template_name),
  79. origin=DjangoIntegration.origin,
  80. ) as span:
  81. span.set_data("context", context)
  82. return real_render(request, template_name, context, *args, **kwargs)
  83. django.shortcuts.render = render
  84. def _get_template_frame_from_debug(debug):
  85. # type: (Dict[str, Any]) -> Dict[str, Any]
  86. if debug is None:
  87. return None
  88. lineno = debug["line"]
  89. filename = debug["name"]
  90. if filename is None:
  91. filename = "<django template>"
  92. pre_context = []
  93. post_context = []
  94. context_line = None
  95. for i, line in debug["source_lines"]:
  96. if i < lineno:
  97. pre_context.append(line)
  98. elif i > lineno:
  99. post_context.append(line)
  100. else:
  101. context_line = line
  102. return {
  103. "filename": filename,
  104. "lineno": lineno,
  105. "pre_context": pre_context[-5:],
  106. "post_context": post_context[:5],
  107. "context_line": context_line,
  108. "in_app": True,
  109. }
  110. def _linebreak_iter(template_source):
  111. # type: (str) -> Iterator[int]
  112. yield 0
  113. p = template_source.find("\n")
  114. while p >= 0:
  115. yield p + 1
  116. p = template_source.find("\n", p + 1)
  117. def _get_template_frame_from_source(source):
  118. # type: (Tuple[Origin, Tuple[int, int]]) -> Optional[Dict[str, Any]]
  119. if not source:
  120. return None
  121. origin, (start, end) = source
  122. filename = getattr(origin, "loadname", None)
  123. if filename is None:
  124. filename = "<django template>"
  125. template_source = origin.reload()
  126. lineno = None
  127. upto = 0
  128. pre_context = []
  129. post_context = []
  130. context_line = None
  131. for num, next in enumerate(_linebreak_iter(template_source)):
  132. line = template_source[upto:next]
  133. if start >= upto and end <= next:
  134. lineno = num
  135. context_line = line
  136. elif lineno is None:
  137. pre_context.append(line)
  138. else:
  139. post_context.append(line)
  140. upto = next
  141. if context_line is None or lineno is None:
  142. return None
  143. return {
  144. "filename": filename,
  145. "lineno": lineno,
  146. "pre_context": pre_context[-5:],
  147. "post_context": post_context[:5],
  148. "context_line": context_line,
  149. }