Class: Datadog::Contrib::Rack::RumInjection

Inherits:
Object
  • Object
show all
Includes:
Environment::Helpers
Defined in:
lib/ddtrace/contrib/rack/rum_injection.rb

Overview

RumInjection ensures that the Rack Response has rum information injected into it's html. The middleware modifies the response body of non-cached html so that it can be retrieved by the rum browser-sdk in the application frontend.

Constant Summary collapse

RUM_INJECTION_FLAG =
'datadog.rum_injection_flag'.freeze
INLINE =
'inline'.freeze
IDENTITY =
'identity'.freeze
HTML_CONTENT =
'text/html'.freeze
XHTML_CONTENT =
'application/xhtml+xml'.freeze
NO_CACHE =
'no-cache'.freeze
NO_STORE =
'no-store'.freeze
PRIVATE =
'private'.freeze
MAX_AGE =
'max-age'.freeze
SMAX_AGE =
's-maxage'.freeze
MAX_AGE_ZERO =
'max-age=0'.freeze
SMAX_AGE_ZERO =
's-maxage=0'.freeze
CONTENT_TYPE_STREAMING =
'text/event-stream'.freeze
CACHE_CONTROL_HEADER =
'Cache-Control'.freeze
CONTENT_DISPOSITION_HEADER =
'Content-Disposition'.freeze
CONTENT_ENCODING_HEADER =
'Content-Encoding'.freeze
CONTENT_LENGTH_HEADER =
'Content-Length'.freeze
CONTENT_TYPE_HEADER =
'Content-Type'.freeze
EXPIRES_HEADER =
'Expires'.freeze
SURROGATE_CACHE_CONTROL_HEADER =
'Surrogate-Control'.freeze
TRANSFER_ENCODING_HEADER =
'Transfer-Encoding'.freeze
ACTION_CONTROLLER_INSTANCE =
'action_controller.instance'.freeze
TRANSFER_ENCODING_CHUNKED =
'chunked'.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Environment::Helpers

#env_to_bool, #env_to_float, #env_to_int, #env_to_list

Constructor Details

#initialize(app) ⇒ RumInjection

Returns a new instance of RumInjection.


41
42
43
# File 'lib/ddtrace/contrib/rack/rum_injection.rb', line 41

def initialize(app)
  @app = app
end

Class Method Details

.inject_rum_data(supplied_env = nil) ⇒ Object


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/ddtrace/contrib/rack/rum_injection.rb', line 94

def self.inject_rum_data(supplied_env = nil)
  tracer = Datadog.configuration[:rack][:tracer]
  return '' unless tracer.enabled

  span = tracer.active_span

  # only return trace id if sampled
  trace_id = span && span.sampled ? span.trace_id : nil
  return '' unless trace_id

  begin
    # the user can ensure there is not a double patching from auto-injection
    # by passing in the rack env so that a flag can be set. This flag is checked
    # later by auto injection. the goal is to support main frameworks and
    # document what we support OOTB. Most frameworks that are rack compatible also expose
    # a helper in the framework controller/template the user can use to access the rack env
    # request.env (rails), env (rails/sinatra/grape?). Should provide example snippets.
    supplied_env[RUM_INJECTION_FLAG] = true if supplied_env
  rescue StandardError => error
    Datadog.logger.debug("rack request Environment unavailable: #{error.message}")
  end

  unix_time = DateTime.now.strftime('%Q').to_i
  tag_string = %(\n<meta name="dd-trace-id" content="#{trace_id}" />\
  <meta name="dd-trace-time" content="#{unix_time}" />)

  tag_string.respond_to?(:html_safe) ? tag_string.html_safe : tag_string
rescue StandardError => err
  # maybe shouldnt log in case datadog is disabled or not required in?
  Datadog.logger.warn("datadog inject_rum_data failed: #{err.message}")
  ''
end

Instance Method Details

#call(env) ⇒ Object


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/ddtrace/contrib/rack/rum_injection.rb', line 45

def call(env)
  result = @app.call(env)

  begin
    return result unless configuration[:rum_injection_enabled] == true

    status, headers, body = result

    # need significant safety here to ensure result is parsable + injectable
    # shouldn't be gzipped/compressed yet since we've injected our middleware in the stack after rack deflater
    # or any other compression middleware for that matter
    # also ensure its non-cacheable, is html, is not streaming, and is not an attachment
    return result unless headers && should_inject?(headers, env)

    trace_id = current_trace_id

    # do not inject if no trace or trace is not sampled
    return result unless trace_id

    # update content length and return new response if we don't fail on rum injection
    if body.respond_to?(:each)
      # we need to insert the trace_id and expiry meta tags
      unix_time = DateTime.now.strftime('%Q').to_i

      html_comment = html_comment_template(trace_id, unix_time)

      rum_body = RumBody.new(body, html_comment)

      update_content_length(headers, html_comment)
      # ensure idempotency on injection in case middleware is inserted or called twice
      env[RUM_INJECTION_FLAG] = true

      updated_response = ::Rack::Response.new(rum_body, status, headers)
      Datadog.logger.debug { "Rum injection successful: #{html_comment}" }
      return updated_response.finish
    else
      Datadog.logger.debug('Rum injection unsuccessful')
      return result
    end

    # catchall if an earlier conditional is not met
    result
  rescue StandardError => e
    Datadog.logger.warn("error checking rum injectability #{e.class}: #{e.message} #{e.backtrace.join("\n")}")
    # we should ensure we don't interfere if original app response if our injection code has an exception
    result
  end
end