Module: Shipeasy::SDK::See

Defined in:
lib/shipeasy/sdk/see.rb

Defined Under Namespace

Modules: Context Classes: Built, Chain, ControlFlowChain, ControlFlowTail, Limiter, NullChain, Violation

Constant Summary collapse

SEE_MAX_MESSAGE =

---- Limits (mirror core.ts; kept in sync with the worker's /collect) ----

500
SEE_MAX_STACK =
8000
SEE_MAX_SUBJECT =

used for subject, outcome, error_type

200
SEE_MAX_EXTRA_VALUE =
200
SEE_MAX_EXTRA_KEYS =
20
SEE_DEDUP_WINDOW_MS =
30_000
SEE_MAX_PER_PROCESS =
25
DEFAULT_SUBJECT =

Default consequence parts when a chain omits them.

"app".freeze
DEFAULT_OUTCOME =
"hit an error".freeze
EXPECTED_IVAR =

Marker attribute stamped onto an exception by control_flow_exception().

:@__shipeasy_see_expected

Class Method Summary collapse

Class Method Details

.build_event(problem, subject, outcome, extras, sdk_version:, env:, backtrace_cleaner: nil) ⇒ Object

Build the type:"error" event accepted by POST /collect.

backtrace_cleaner (optional) is a callable that takes the raw backtrace array and returns an application-only subset (framework/gem frames stripped) — e.g. ->(bt) { Rails.backtrace_cleaner.clean(bt) }. It is applied before the stack is joined; if it strips every frame (a stack that is purely framework/gem code) we fall back to the raw backtrace so an error is never left with an empty stack. We never invent the filtering rules — the callable is supplied by the framework's own cleaner.



132
133
134
135
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
166
167
# File 'lib/shipeasy/sdk/see.rb', line 132

def build_event(problem, subject, outcome, extras, sdk_version:, env:, backtrace_cleaner: nil)
  stack = nil

  if problem.is_a?(Violation)
    error_type = problem.name
    message    = problem.name
    kind       = "violation"
  elsif problem.is_a?(Exception)
    error_type = problem.class.name || "Error"
    message    = (problem.message.to_s.empty? ? error_type : problem.message)
    bt = clean_backtrace(problem.backtrace, backtrace_cleaner)
    stack = bt.join("\n") if bt && !bt.empty?
    kind = "caught"
  else
    error_type = "Error"
    message    = problem.to_s
    kind       = "caught"
  end

  ev = {
    "type"        => "error",
    "kind"        => kind,
    "error_type"  => truncate(error_type, SEE_MAX_SUBJECT),
    "message"     => truncate(message, SEE_MAX_MESSAGE),
    "subject"     => truncate(subject, SEE_MAX_SUBJECT),
    "outcome"     => truncate(outcome, SEE_MAX_SUBJECT),
    "side"        => "server",
    "sdk_version" => sdk_version,
    "ts"          => (Time.now.to_f * 1000).to_i,
  }
  ev["stack"] = truncate(stack, SEE_MAX_STACK) if stack
  clean = sanitize_extras(extras)
  ev["extras"] = clean if clean
  ev["env"] = env if env && !env.to_s.empty?
  ev
end

.clean_backtrace(bt, cleaner) ⇒ Object

Apply the framework-provided backtrace cleaner to a raw backtrace, returning an application-only frame array. No cleaner (or no backtrace) ⇒ the input is returned untouched. If the cleaner raises, or strips every frame, fall back to the raw backtrace — a cleaned-to-empty stack would lose all debugging signal for an error that lives entirely in framework code. Never raises.



175
176
177
178
179
180
181
182
# File 'lib/shipeasy/sdk/see.rb', line 175

def clean_backtrace(bt, cleaner)
  return bt if cleaner.nil? || bt.nil? || bt.empty?

  cleaned = cleaner.call(bt)
  cleaned && !cleaned.empty? ? cleaned : bt
rescue StandardError
  bt
end

.expected?(err) ⇒ Boolean

Returns:

  • (Boolean)


104
105
106
107
108
109
# File 'lib/shipeasy/sdk/see.rb', line 104

def expected?(err)
  err.instance_variable_defined?(EXPECTED_IVAR) &&
    !err.instance_variable_get(EXPECTED_IVAR).nil?
rescue StandardError
  false
end

.mark_expected(err, because, extras = nil) ⇒ Object

Best-effort stamp marking an exception as expected control flow.



94
95
96
97
98
99
100
101
102
# File 'lib/shipeasy/sdk/see.rb', line 94

def mark_expected(err, because, extras = nil)
  mark = { "because" => because.to_s }
  clean = sanitize_extras(extras)
  mark["extras"] = clean if clean
  err.instance_variable_set(EXPECTED_IVAR, mark)
rescue StandardError
  # Frozen / builtin objects that reject ivars: best effort only.
  nil
end

.sanitize_extras(extras) ⇒ Object

Drop nil values, keep only String/Numeric(finite)/boolean, truncate string values to 200 chars, cap at 20 keys (insertion order). Returns nil if nothing is kept. Keys are stringified.



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
# File 'lib/shipeasy/sdk/see.rb', line 67

def sanitize_extras(extras)
  return nil unless extras.is_a?(Hash)
  return nil if extras.empty?

  out = {}
  extras.each do |k, v|
    break if out.size >= SEE_MAX_EXTRA_KEYS
    next if v.nil?

    case v
    when true, false
      out[k.to_s] = v
    when String
      out[k.to_s] = truncate(v, SEE_MAX_EXTRA_VALUE)
    when Numeric
      # Reject NaN / Infinity (not representable in JSON).
      next if v.respond_to?(:finite?) && !v.finite?

      out[k.to_s] = v
    else
      next
    end
  end
  out.empty? ? nil : out
end

.top_stack_line(stack) ⇒ Object



219
220
221
222
223
224
225
226
227
# File 'lib/shipeasy/sdk/see.rb', line 219

def top_stack_line(stack)
  return "" if stack.nil? || stack.empty?

  stack.each_line do |line|
    s = line.strip
    return s[0, 200] if s.start_with?("File ") || s.start_with?("at ") || s.include?("line ") || s.include?(":in ")
  end
  ""
end

.truncate(str, limit) ⇒ Object



59
60
61
62
# File 'lib/shipeasy/sdk/see.rb', line 59

def truncate(str, limit)
  s = str.to_s
  s.length <= limit ? s : s[0, limit]
end