Class: Errsight::Scope

Inherits:
Object
  • Object
show all
Defined in:
lib/errsight/scope.rb

Overview

Holds the user, tags, and breadcrumbs that should be attached to events captured while this scope is on top of the hub stack.

A scope is owned by a single thread of execution (a Rails request, a Sidekiq job, or an ad-hoc Errsight.with_scope block). Pushing a new scope forks a deep copy of the parent so child mutations don’t bleed back up the stack.

Breadcrumbs are split into two ring buffers — manual app crumbs and auto-collected DB crumbs — so a high-query request can’t evict the user’s manual context. The public ‘breadcrumbs` accessor returns a merged, timestamp-sorted view; consumers see one stream.

Constant Summary collapse

MAX_USER_BREADCRUMBS =
50
MAX_DB_BREADCRUMBS =
30
MAX_USER_BREADCRUMBS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeScope

Returns a new instance of Scope.



22
23
24
25
26
27
# File 'lib/errsight/scope.rb', line 22

def initialize
  @user             = nil
  @tags             = {}
  @user_breadcrumbs = []
  @db_breadcrumbs   = []
end

Instance Attribute Details

#tagsObject (readonly)

Returns the value of attribute tags.



20
21
22
# File 'lib/errsight/scope.rb', line 20

def tags
  @tags
end

#userObject (readonly)

Returns the value of attribute user.



20
21
22
# File 'lib/errsight/scope.rb', line 20

def user
  @user
end

Class Method Details

.from_h(hash) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/errsight/scope.rb', line 130

def self.from_h(hash)
  scope = new
  return scope unless hash.is_a?(Hash)
  tags = hash["tags"].is_a?(Hash) ? hash["tags"].transform_keys(&:to_s).transform_values(&:to_s) : {}
  # dup each crumb so the rehydrated scope doesn't alias entries inside
  # the caller's job payload — add_breadcrumb mutates @user_breadcrumbs
  # in place and we don't want that mutation to flow back into job args.
  crumbs = hash["breadcrumbs"].is_a?(Array) ? hash["breadcrumbs"].map { |c| c.is_a?(Hash) ? c.dup : c } : []
  # send: replace_state is protected so external callers can't reach in,
  # but a class-method factory needs to bypass that to build a new scope.
  # DB breadcrumbs are intentionally not propagated; they start empty.
  scope.send(:replace_state, hash["user"], tags, crumbs, [])
  scope
end

Instance Method Details

#add_breadcrumb(category:, message:, level: :info, data: nil) ⇒ Object



60
61
62
63
# File 'lib/errsight/scope.rb', line 60

def add_breadcrumb(category:, message:, level: :info, data: nil)
  @user_breadcrumbs << build_crumb(category, message, level, data)
  @user_breadcrumbs.shift while @user_breadcrumbs.size > MAX_USER_BREADCRUMBS
end

#add_db_breadcrumb(message:, data: nil) ⇒ Object

Internal API for auto-instrumentation (sql.active_record subscriber today; future http subscribers will use the same ring or get their own). Separate cap from manual crumbs so a runaway-query request can’t push out the app code’s own context.



69
70
71
72
# File 'lib/errsight/scope.rb', line 69

def add_db_breadcrumb(message:, data: nil)
  @db_breadcrumbs << build_crumb("db", message, :info, data)
  @db_breadcrumbs.shift while @db_breadcrumbs.size > MAX_DB_BREADCRUMBS
end

Merged, timestamp-sorted view across both rings. ISO-8601 strings sort lexicographically the same as chronologically, so a string sort is correct without parsing back into Time.



32
33
34
35
36
# File 'lib/errsight/scope.rb', line 32

def breadcrumbs
  return @user_breadcrumbs if @db_breadcrumbs.empty?
  return @db_breadcrumbs   if @user_breadcrumbs.empty?
  (@user_breadcrumbs + @db_breadcrumbs).sort_by { |b| b[:timestamp] }
end

#clear_breadcrumbsObject



74
75
76
77
# File 'lib/errsight/scope.rb', line 74

def clear_breadcrumbs
  @user_breadcrumbs = []
  @db_breadcrumbs   = []
end

#clear_tagsObject



56
57
58
# File 'lib/errsight/scope.rb', line 56

def clear_tags
  @tags = {}
end

#clear_userObject



42
43
44
# File 'lib/errsight/scope.rb', line 42

def clear_user
  @user = nil
end

#dupObject

Deep-ish copy used when pushing a child scope. Hashes/arrays are dup’d so child mutations don’t bleed back up the stack.



104
105
106
107
108
109
110
111
112
# File 'lib/errsight/scope.rb', line 104

def dup
  copy = Scope.new
  copy.send(:replace_state,
            @user&.dup,
            @tags.dup,
            @user_breadcrumbs.map(&:dup),
            @db_breadcrumbs.map(&:dup))
  copy
end

#merge!(other) ⇒ Object

Overlay another scope’s state onto this one. Used by Sidekiq server middleware to layer payload scope (user/tags shipped by the enqueuer) on top of process-wide root state (e.g. Errsight.set_tag(“region”,…) called once at boot). User from ‘other` wins; tags are merged with `other` taking precedence on key collisions; breadcrumbs are appended in order and clipped to the limit.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/errsight/scope.rb', line 85

def merge!(other)
  return self unless other.is_a?(Scope)
  @user = other.user if other.user
  @tags.merge!(other.tags) unless other.tags.empty?
  other_user = other.instance_variable_get(:@user_breadcrumbs)
  other_db   = other.instance_variable_get(:@db_breadcrumbs)
  unless other_user.empty?
    @user_breadcrumbs.concat(other_user.map(&:dup))
    @user_breadcrumbs.shift while @user_breadcrumbs.size > MAX_USER_BREADCRUMBS
  end
  unless other_db.empty?
    @db_breadcrumbs.concat(other_db.map(&:dup))
    @db_breadcrumbs.shift while @db_breadcrumbs.size > MAX_DB_BREADCRUMBS
  end
  self
end

#set_tag(key, value) ⇒ Object



46
47
48
49
# File 'lib/errsight/scope.rb', line 46

def set_tag(key, value)
  return if key.nil?
  @tags[key.to_s] = value.to_s
end

#set_tags(tags) ⇒ Object



51
52
53
54
# File 'lib/errsight/scope.rb', line 51

def set_tags(tags)
  return unless tags.is_a?(Hash)
  tags.each { |k, v| set_tag(k, v) }
end

#set_user(user) ⇒ Object



38
39
40
# File 'lib/errsight/scope.rb', line 38

def set_user(user)
  @user = user.is_a?(Hash) ? user : nil
end

#to_hObject

Serialize for cross-process propagation (Sidekiq client middleware will stash this in the job payload so the server middleware can rehydrate it before the job runs).

Only manual user breadcrumbs travel across process boundaries. The receiving worker collects its own DB breadcrumbs from its own queries — propagating the parent’s would mix DB events from two unrelated connection states and confuse debugging.



122
123
124
125
126
127
128
# File 'lib/errsight/scope.rb', line 122

def to_h
  hash = {}
  hash["user"]        = @user             unless @user.nil?
  hash["tags"]        = @tags             unless @tags.empty?
  hash["breadcrumbs"] = @user_breadcrumbs unless @user_breadcrumbs.empty?
  hash
end