Class: Alplus::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/alplus/client.rb

Overview

Orchestrates one configuration's capture pipeline: builds the wire item, wraps it in an envelope, and hands it to the background worker (or, in test mode, sends it synchronously through the in-memory transport).

Every public method is fail-safe: a bug anywhere in envelope building or dispatch is caught and logged, never raised into the host app (issue #14 story 8). The event id is always generated and returned first, so a caller can show "reference id err_..." even if the event was dropped.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config, sleeper: config.sleeper) ⇒ Client

sleeper: is forwarded to the default Transport (never used when config.transport/config.test_mode supply a different one) purely as a test seam: with the real Kernel#sleep, a spec exercising a retried send would otherwise wait on real backoff wall-clock time. Defaults to config.sleeper so Alplus.configure { |c| c.sleeper = ... } reaches the module-level singleton client too, not just a directly constructed Client.new.



20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/alplus/client.rb', line 20

def initialize(config, sleeper: config.sleeper)
  @config = config
  @transport = config.transport || (config.test_mode ? TestTransport.new : Transport.new(config, sleeper: sleeper))
  @worker = Worker.new(config, @transport, kind: :error)
  # A SEPARATE worker (own queue, own thread) from `@worker` (issue
  # #12 fix): see `Worker`'s class doc for why sharing one lane with
  # error delivery was a defect (head-of-line blocking + silent drops
  # under an error storm, exactly when crash-free data matters).
  @session_worker = Worker.new(config, @transport, kind: :session)

  @pending_window =
    PendingWindow.new(config.resolved_post_error_log_window_ms) { |item| deliver_item(item) }
end

Instance Attribute Details

#transportObject (readonly)

Returns the value of attribute transport.



34
35
36
# File 'lib/alplus/client.rb', line 34

def transport
  @transport
end

Instance Method Details

#capture_exception(exception, level: "error", context: nil, contexts: nil, tags: nil, breadcrumbs: nil, user: Alplus::UNSET, mechanism: "generic", fingerprint: nil) ⇒ Object

The enabled/sampled gate runs BEFORE dedup registration (issue #15 defect): a sampled-out or disabled capture must not occupy the dedup slot, or it would suppress the NEXT (real) in-window capture of the same error.

Dedup (issue #15) then runs BEFORE the scope merge / envelope build: the same exception object captured twice within the dedup window (e.g. by RackMiddleware auto-capture and a manual rescue further up the stack) returns the first call's id and is never re-queued — see Dedup.

contexts:/fingerprint: and the ambient Scope (set_user, set_tag, set_context, add_breadcrumb) are issue #17: an explicit per-call user:/tags:/contexts:/breadcrumbs: here wins over whatever the ambient scope carries, field-by-field — see ScopeMerge.merge. user: defaults to the Alplus::UNSET sentinel, not nil, so a caller CAN pass user: nil to explicitly clear the ambient user for one capture.



54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/alplus/client.rb', line 54

def capture_exception(exception, level: "error", context: nil, contexts: nil, tags: nil, breadcrumbs: nil, user: Alplus::UNSET, mechanism: "generic", fingerprint: nil)
  mark_session_outcome(level)
  fresh_id = Id.generate_event_id
  return fresh_id unless enabled?
  return fresh_id if excluded?(exception)

  resolved = Dedup.resolve(exception, fresh_id)
  return resolved[:id] if resolved[:duplicate]

  id = resolved[:id]
  dispatch(id) { build_item(:exception_item, exception: exception, id: id, level: level, context: context, contexts: contexts, tags: tags, breadcrumbs: breadcrumbs, user: user, mechanism: mechanism, fingerprint: fingerprint) }
  id
end

#capture_message(message, level: "info", context: nil, contexts: nil, tags: nil, breadcrumbs: nil, user: Alplus::UNSET, mechanism: "generic", fingerprint: nil) ⇒ Object



68
69
70
71
72
73
74
75
# File 'lib/alplus/client.rb', line 68

def capture_message(message, level: "info", context: nil, contexts: nil, tags: nil, breadcrumbs: nil, user: Alplus::UNSET, mechanism: "generic", fingerprint: nil)
  mark_session_outcome(level)
  id = Id.generate_event_id
  return id unless enabled?

  dispatch(id) { build_item(:message_item, message: message, id: id, level: level, context: context, contexts: contexts, tags: tags, breadcrumbs: breadcrumbs, user: user, mechanism: mechanism, fingerprint: fingerprint) }
  id
end

#flush(timeout: 2) ⇒ Object

Flushes BOTH delivery lanes (issue #12: error and session are independent workers). A slow/stuck error lane still bounds this call by timeout for the session lane's own drain -- each Worker#flush call gets the full timeout budget rather than splitting it, since a caller flushing wants both drained, not a race between them.



82
83
84
85
86
87
88
89
90
91
# File 'lib/alplus/client.rb', line 82

def flush(timeout: 2)
  # Seal the post-error log window first: flush means "send now with
  # whatever after-lines were collected so far", never "wait".
  @pending_window.seal_all!
  error_flushed = @worker.flush(timeout: timeout)
  session_flushed = @session_worker.flush(timeout: timeout)
  error_flushed && session_flushed
rescue StandardError
  false
end

#notify_log_breadcrumb(crumb) ⇒ Object

Offers a log-line breadcrumb to every exception item currently inside its post-error log window on this thread (issue #47). Never raises.



95
96
97
# File 'lib/alplus/client.rb', line 95

def notify_log_breadcrumb(crumb)
  @pending_window.notify_log_breadcrumb(crumb)
end

#report_session(session) ⇒ Object

Reports a closed Session (issue #12) to POST /e/sessions, on its OWN background Worker (own queue, own thread) so a slow/failing /e/errors delivery can never delay or drop a queued session, or vice versa -- see Worker's class doc. Unlike capture_exception/ capture_message, never sampled or deduped — an accurate crash-free percentage needs every session counted, not a sample of them. Only gated on config.valid? (configured + enabled). Fail-safe: never raises.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/alplus/client.rb', line 107

def report_session(session)
  return false unless @config.valid?

  envelope = Envelope.wrap(config: @config, item: Envelope.session_item(session: session, config: @config))

  if @config.test_mode
    @transport.send_envelope(envelope, kind: :session)
  else
    @session_worker.enqueue(envelope)
  end
rescue StandardError => e
  @config.logger&.warn("[alplus] session report failed internally; dropped: #{e.class}: #{e.message}")
  false
end