Class: Skadi::ControllerDelegate

Inherits:
Object
  • Object
show all
Defined in:
lib/skadi/controller_delegate.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(controller, bot_protection: true) ⇒ ControllerDelegate

Returns a new instance of ControllerDelegate.

Parameters:

  • controller (ActionController::Base)


26
27
28
29
30
31
32
33
34
# File 'lib/skadi/controller_delegate.rb', line 26

def initialize(controller, bot_protection: true)
  @controller = controller

  @events = []
  @demographics = []

  @do_not_track = bot_protection && Skadi.configuration.do_not_track_bots? && user_agent.bot?
  @new_visit = false
end

Instance Attribute Details

#controllerActionController::Base (readonly)

The controller that instantiated us

Returns:

  • (ActionController::Base)


5
6
7
# File 'lib/skadi/controller_delegate.rb', line 5

def controller
  @controller
end

#demographicsObject (readonly)

Returns the value of attribute demographics.



7
8
9
# File 'lib/skadi/controller_delegate.rb', line 7

def demographics
  @demographics
end

#eventsObject (readonly)

Returns the value of attribute events.



7
8
9
# File 'lib/skadi/controller_delegate.rb', line 7

def events
  @events
end

#viewObject (readonly)

Returns the value of attribute view.



7
8
9
# File 'lib/skadi/controller_delegate.rb', line 7

def view
  @view
end

#visitObject (readonly)

Returns the value of attribute visit.



7
8
9
# File 'lib/skadi/controller_delegate.rb', line 7

def visit
  @visit
end

Instance Method Details

#_attach(view: nil, visit: nil) ⇒ Object

Internal. Manually set the view and visit for the current request.



53
54
55
56
# File 'lib/skadi/controller_delegate.rb', line 53

def _attach(view: nil, visit: nil)
  @visit = visit
  @view = view
end

#_persistObject

Internal. Saves the visit, view and any events or demographics after the controller action.



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
# File 'lib/skadi/controller_delegate.rb', line 59

def _persist
  return if do_not_track?

  @visit&.save
  if @view
    @view.visit = @visit
    @view.save
  end

  if @events.any?
    Skadi::Event.redact_and_insert(@events, visit: @visit, view: @view)
  end

  if @demographics.any?
    Demographic.create_or_increment_all(@demographics)
  end

  cookie_manager.renew!
rescue => e
  # Analytics must not interfere with the host app's request on failure
  Rails.logger.error("Skadi: failed to persist analytics for #{controller.controller_name}##{controller.action_name} (visit: #{@visit.try(:id).inspect}, view: #{@view.try(:id).inspect}, events: #{@events.count}, demographics: #{@demographics.count}): #{e.class}, #{e.message}; Line: #{e.backtrace&.first}")

  # Ensure errors are visible in test and development
  raise if Rails.env.local?
end

#_prepareObject

Internal. Performs the before-action tasks: build the visit and view, and process user agent info.



38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/skadi/controller_delegate.rb', line 38

def _prepare
  return if do_not_track?

  build_visit
  build_view
  queue_user_agent_demographics
rescue => e
  # Analytics must not interfere with the host app's request on failure
  Rails.logger.error("Skadi: failed to prepare analytics for #{controller.controller_name}##{controller.action_name} (visit: #{@visit.try(:id).inspect}, view: #{@view.try(:id).inspect}, events: #{@events.count}, demographics: #{@demographics.count}): #{e.class}, #{e.message}; Line: #{e.backtrace&.first}")

  # Ensure errors are visible in test and development
  raise if Rails.env.local?
end

#anonymity_set_consent!(consent) ⇒ Object

Set consent for tracking by anonymity set

Parameters:

  • consent (TrueClass, FalseClass)


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/skadi/controller_delegate.rb', line 136

def anonymity_set_consent!(consent)
  anonymity_set = AnonymitySet.calculate(request.remote_ip, request.user_agent)

  if consent
    cookie_manager.use_anonymity_sets = true

    # If a visit is attached to the request, we update it with the anonymity set token
    if @visit
      @visit.tracking_token ||= anonymity_set
    else
      # Build the visit without the request, because the current request is likely not the original first request
      @visit = Visit.build_from(anonymity_set)
      @view.visit = @visit if @view
    end
  else
    cookie_manager.use_anonymity_sets = false

    return if @visit.nil?

    # Check to see if the currrent visit is using an anonymity set
    if @visit&.tracking_token && !@visit.cookies_enabled
      # If so, delete it from the db so existing data is anonymised instantly
      # Note: this needs to be a DB update because there may be other visits outside the visit limit
      Skadi::Visit.where(tracking_token: anonymity_set).update_all(tracking_token: nil)

      # Update the local instance of the visit if it uses anonymity sets so it doesn't get re-set when saved
      @visit.tracking_token = nil if @visit.tracking_token == anonymity_set
    end
  end
end

#anonymity_set_consent?Boolean

Returns:

  • (Boolean)


167
168
169
170
171
172
173
# File 'lib/skadi/controller_delegate.rb', line 167

def anonymity_set_consent?
  cookie_value = cookie_manager.use_anonymity_sets
  return cookie_value unless cookie_value.nil?

  # There is no explicit consent or opt-out, so we use the configured default value
  return Skadi.configuration.use_anonymity_sets
end

Set consent for tracking by cookie

Parameters:

  • consent (TrueClass, FalseClass)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/skadi/controller_delegate.rb', line 177

def cookie_consent!(consent)
  if consent
    return unless cookie_manager.tracking_token.nil?

    # Re-use an existing cookie-based token
    tracking_token = @visit&.tracking_token if @visit&.cookies_enabled
    tracking_token ||= ::SecureRandom.uuid_v7

    cookie_manager.tracking_token = tracking_token

    # Update the existing visit with the tracking token if we've generated a new one
    if @visit
      @visit.tracking_token = tracking_token
      @visit.cookies_enabled = true
    else
      @visit = Visit.build_from(tracking_token, cookies_enabled: true)
      @view.visit = @visit if @view
    end
  else
    cookie_manager.tracking_token = nil

    return if @visit.nil?

    # No need to anonymise existing sessions here because there is no way to link to the user once the tracking token is deleted.
    @visit.cookies_enabled = false

    # If the user has opted in for anonymity sets
    if cookie_manager.use_anonymity_sets == true || (Skadi.configuration.use_anonymity_sets && cookie_manager.use_anonymity_sets != false)
      @visit.tracking_token = AnonymitySet.calculate(request.remote_ip, request.user_agent)
    end
  end
end

Returns:

  • (Boolean)


210
# File 'lib/skadi/controller_delegate.rb', line 210

def cookie_consent? = cookie_manager.tracking_token.present?

#demographic(name, value, action_specific: false, uri: nil) ⇒ Object

Create or increment a demographic with a given name or value. If the action_specific parameter is set to true, the demographic is linked specifically to the current action. Demographics are not linked to any other individual data. E.g:

skadi.demographic("browser", "Chrome")
skadi.demographic("branch", "A", action_specific: true)

If the name/value/action combination doesn't exist for the current date, a new row is added to the database with count set to 1. If it does, the existing record's count is incremented.

Parameters:

  • name (String)
  • value (String)
  • action_specific (TrueClass, FalseClass) (defaults to: false)


109
110
111
112
113
114
115
116
# File 'lib/skadi/controller_delegate.rb', line 109

def demographic(name, value, action_specific: false, uri: nil)
  raise ArgumentError.new "Skadi::ControllerDelegate.demographic expects String as first parameter, got #{name.is_a?(String) ? "empty string" : name.class.name}" unless name.is_a?(String) && name.present?
  raise ArgumentError.new "Skadi::ControllerDelegate.demographic expects String as second parameter, got #{value.is_a?(String) ? "empty string" : value.class.name}" unless value.is_a?(String) && value.present?

  demographic = { name:, value:, uri: action_specific ? (uri || request.route_uri_pattern) : nil }

  @demographics << demographic
end

#do_not_track!Object

Disable tracking for the current request



92
93
94
# File 'lib/skadi/controller_delegate.rb', line 92

def do_not_track!
  @do_not_track = true
end

#do_not_track?Boolean

Whether Skadi tracking has been disabled for the current request

Returns:

  • (Boolean)


87
88
89
# File 'lib/skadi/controller_delegate.rb', line 87

def do_not_track?
  @do_not_track
end

#event(name, properties = {}, sensitive: false) ⇒ Object

Create an event with the given name and properties. By default, events are linked to the current visit and view, but if the sensitive parameter is set to true, the event is not linked to the visit and view, and the time of the event is set to the start of the current day.

Parameters:

  • name (String)
  • properties (Hash) (defaults to: {})
  • sensitive (TrueClass, FalseClass) (defaults to: false)


125
126
127
128
129
130
131
132
# File 'lib/skadi/controller_delegate.rb', line 125

def event(name, properties = {}, sensitive: false)
  raise ArgumentError.new "Skadi::ControllerDelegate.event expects String as first parameter, got #{name.is_a?(String) ? "empty string" : name.class.name}" unless name.is_a?(String) && name.present?
  raise ArgumentError.new "Skadi::ControllerDelegate.event expects Hash as second parameter, got #{properties.class.name}" unless properties.is_a?(Hash)

  event = { name:, properties:, sensitive: }

  @events << event
end

#new_visit?TrueClass, FalseClass

Whether the current request has recorded a new visit

Returns:

  • (TrueClass, FalseClass)


15
# File 'lib/skadi/controller_delegate.rb', line 15

def new_visit? = @new_visit

#user_agentSkadi::UserAgent

The parsed user agent

Returns:



11
# File 'lib/skadi/controller_delegate.rb', line 11

def user_agent = @_user_agent ||= UserAgent.new(request.user_agent || "")

#user_consent!(consent) ⇒ Object

Set consent for tracking by logged in user

Parameters:

  • consent (TrueClass, FalseClass)


214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/skadi/controller_delegate.rb', line 214

def user_consent!(consent)
  tracked_user_id = @visit&.user_id || logged_in_user&.id

  if consent
    cookie_manager.track_users = true

    unless tracked_user_id.nil?
      if @visit
        @visit.user_id = tracked_user_id
      else
        # Build the visit without the request, because the current request is likely not the original first request
        @visit = Visit.build_from(nil, tracked_user_id)
        @view.visit = @visit if @view
      end
    end
  else
    cookie_manager.track_users = false

    # If there is a logged in user, we delete the user id from any rows that match
    unless tracked_user_id.nil?
      # Note: this needs a DB update because there may be other visits outside the visit limit
      Skadi::Visit.where(user_id: tracked_user_id).update_all(user_id: nil)

      # Update the local instance of the current visit so it doesn't get re-set when saved
      @visit.user_id = nil if @visit
    end
  end
end

#user_consent?Boolean

Returns:

  • (Boolean)


243
244
245
246
247
248
249
# File 'lib/skadi/controller_delegate.rb', line 243

def user_consent?
  cookie_value = cookie_manager.track_users
  return cookie_value unless cookie_value.nil?

  # There is no explicit consent or opt-out, so we use the configured default value
  return Skadi.configuration.track_users
end