lapsoss 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/lapsoss/adapters/appsignal_adapter.rb +18 -12
- data/lib/lapsoss/adapters/base.rb +19 -0
- data/lib/lapsoss/adapters/concerns/envelope_builder.rb +127 -0
- data/lib/lapsoss/adapters/concerns/http_delivery.rb +130 -0
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +65 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +21 -21
- data/lib/lapsoss/adapters/rollbar_adapter.rb +64 -122
- data/lib/lapsoss/adapters/sentry_adapter.rb +77 -143
- data/lib/lapsoss/backtrace_processor.rb +2 -1
- data/lib/lapsoss/breadcrumb.rb +59 -0
- data/lib/lapsoss/client.rb +2 -2
- data/lib/lapsoss/configuration.rb +23 -19
- data/lib/lapsoss/event.rb +90 -96
- data/lib/lapsoss/fingerprinter.rb +5 -2
- data/lib/lapsoss/merged_scope.rb +1 -6
- data/lib/lapsoss/rails_error_subscriber.rb +3 -4
- data/lib/lapsoss/scope.rb +1 -6
- data/lib/lapsoss/scrubber.rb +15 -11
- data/lib/lapsoss/user_context.rb +15 -5
- data/lib/lapsoss/utils.rb +0 -2
- data/lib/lapsoss/validators.rb +111 -76
- data/lib/lapsoss/version.rb +1 -1
- data/lib/tasks/cassettes.rake +50 -0
- metadata +15 -7
- data/CHANGELOG.md +0 -5
- data/lib/lapsoss/middleware.rb +0 -6
- data/lib/lapsoss/sampling.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c0faf08671ef4e1b0b36ccc9e29900b8e8b5e4a78ac61ffea6269294efee5881
|
4
|
+
data.tar.gz: b2c76c33817f3a939388970d21c5fc341ad6ce46e0ce20ef9b71ca5b5c565ae0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cfb22826215ffc2473410d6e984ecbd93f74ec0c011e742d7246549b29c1d77b9cb81c774cab311748c0b39cbfd9b9eccfdbb4324aa6da7a632ba8634eb3b19
|
7
|
+
data.tar.gz: f641a491be8007a0bc6780094803724f66efc79aa1e10cadbb1ac96c84a8ff30af119c763e1611dc3e2218305c4d0b5958b6f5c5f2863c7b5694b5ff7c9f0856
|
data/README.md
CHANGED
@@ -8,7 +8,6 @@ module Lapsoss
|
|
8
8
|
class AppsignalAdapter < Base
|
9
9
|
PUSH_API_URI = "https://push.appsignal.com"
|
10
10
|
ERRORS_API_URI = "https://appsignal-endpoint.net"
|
11
|
-
JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
|
12
11
|
|
13
12
|
def initialize(name, settings = {})
|
14
13
|
super
|
@@ -17,7 +16,20 @@ module Lapsoss
|
|
17
16
|
@app_name = settings[:app_name] || ENV.fetch("APPSIGNAL_APP_NAME", nil)
|
18
17
|
@environment = Lapsoss.configuration.environment
|
19
18
|
|
20
|
-
|
19
|
+
# Just log if keys look unusual but don't fail
|
20
|
+
if @push_api_key.present?
|
21
|
+
validate_api_key!(@push_api_key, "AppSignal push API key", format: :uuid)
|
22
|
+
end
|
23
|
+
|
24
|
+
if @frontend_api_key.present?
|
25
|
+
validate_api_key!(@frontend_api_key, "AppSignal frontend API key", format: :uuid)
|
26
|
+
end
|
27
|
+
|
28
|
+
if @push_api_key.blank? && @frontend_api_key.blank?
|
29
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss::AppsignalAdapter] No API keys provided, adapter disabled"
|
30
|
+
@enabled = false
|
31
|
+
return
|
32
|
+
end
|
21
33
|
|
22
34
|
@push_client = create_http_client(PUSH_API_URI) if @push_api_key
|
23
35
|
@errors_client = create_http_client(ERRORS_API_URI) if @frontend_api_key
|
@@ -30,10 +42,10 @@ module Lapsoss
|
|
30
42
|
return unless payload
|
31
43
|
|
32
44
|
path = "/errors?api_key=#{@frontend_api_key}"
|
33
|
-
headers =
|
45
|
+
headers = default_headers(content_type: json_content_type)
|
34
46
|
|
35
47
|
begin
|
36
|
-
@errors_client.post(path, body: JSON.
|
48
|
+
@errors_client.post(path, body: ActiveSupport::JSON.encode(payload), headers: headers)
|
37
49
|
rescue DeliveryError => e
|
38
50
|
# Log the error and potentially notify error handler
|
39
51
|
Lapsoss.configuration.logger&.error("[Lapsoss::AppsignalAdapter] Failed to deliver event: #{e.message}")
|
@@ -120,15 +132,9 @@ module Lapsoss
|
|
120
132
|
(hash || {}).transform_keys(&:to_s).transform_values(&:to_s)
|
121
133
|
end
|
122
134
|
|
135
|
+
# No longer need strict validation
|
123
136
|
def validate_settings!
|
124
|
-
|
125
|
-
raise ValidationError, "AppSignal API key is required (either push_api_key or frontend_api_key)"
|
126
|
-
end
|
127
|
-
|
128
|
-
validate_api_key!(@push_api_key, "AppSignal push API key", format: :uuid) if @push_api_key
|
129
|
-
validate_api_key!(@frontend_api_key, "AppSignal frontend API key", format: :uuid) if @frontend_api_key
|
130
|
-
validate_presence!(@app_name, "AppSignal app name") if @app_name
|
131
|
-
validate_environment!(@environment, "AppSignal environment") if @environment
|
137
|
+
# Validation moved to initialize with logging
|
132
138
|
end
|
133
139
|
end
|
134
140
|
end
|
@@ -5,6 +5,9 @@ module Lapsoss
|
|
5
5
|
class Base
|
6
6
|
include Validators
|
7
7
|
|
8
|
+
USER_AGENT = "lapsoss/#{Lapsoss::VERSION}".freeze
|
9
|
+
JSON_CONTENT_TYPE = "application/json; charset=UTF-8".freeze
|
10
|
+
|
8
11
|
attr_reader :name, :settings
|
9
12
|
|
10
13
|
def initialize(name, settings = {})
|
@@ -80,6 +83,22 @@ module Lapsoss
|
|
80
83
|
|
81
84
|
HttpClient.new(uri, transport_config)
|
82
85
|
end
|
86
|
+
|
87
|
+
def user_agent
|
88
|
+
USER_AGENT
|
89
|
+
end
|
90
|
+
|
91
|
+
def json_content_type
|
92
|
+
JSON_CONTENT_TYPE
|
93
|
+
end
|
94
|
+
|
95
|
+
def default_headers(content_type: nil, gzip: false, extra: {})
|
96
|
+
headers = { "User-Agent" => user_agent }
|
97
|
+
headers["Content-Type"] = content_type if content_type
|
98
|
+
headers["Content-Encoding"] = "gzip" if gzip
|
99
|
+
headers.merge!(extra) if extra && !extra.empty?
|
100
|
+
headers
|
101
|
+
end
|
83
102
|
end
|
84
103
|
end
|
85
104
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/json"
|
6
|
+
require "active_support/core_ext/numeric/bytes"
|
7
|
+
require "active_support/gzip"
|
8
|
+
require "securerandom"
|
9
|
+
|
10
|
+
module Lapsoss
|
11
|
+
module Adapters
|
12
|
+
module Concerns
|
13
|
+
module EnvelopeBuilder
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
GZIP_THRESHOLD = 30.kilobytes
|
17
|
+
|
18
|
+
included do
|
19
|
+
class_attribute :envelope_format, default: :json
|
20
|
+
class_attribute :compress_threshold, default: GZIP_THRESHOLD
|
21
|
+
end
|
22
|
+
|
23
|
+
# Build envelope with common structure
|
24
|
+
def build_envelope_wrapper(event)
|
25
|
+
envelope = {
|
26
|
+
id: event.fingerprint.presence || SecureRandom.uuid,
|
27
|
+
timestamp: format_timestamp(event.timestamp),
|
28
|
+
environment: event.environment.presence || "production",
|
29
|
+
level: map_level(event.level),
|
30
|
+
platform: "ruby",
|
31
|
+
sdk: sdk_info
|
32
|
+
}
|
33
|
+
|
34
|
+
# Add event-specific data
|
35
|
+
envelope.merge!(build_event_data(event))
|
36
|
+
|
37
|
+
# Add context data
|
38
|
+
envelope.merge!(
|
39
|
+
tags: event.tags.presence,
|
40
|
+
user: event.user_context.presence,
|
41
|
+
extra: event.extra.presence,
|
42
|
+
breadcrumbs: format_breadcrumbs(event.breadcrumbs)
|
43
|
+
).compact_blank
|
44
|
+
end
|
45
|
+
|
46
|
+
# Format timestamp using AS helpers
|
47
|
+
def format_timestamp(time)
|
48
|
+
time = Time.current if time.blank?
|
49
|
+
time.in_time_zone("UTC").iso8601
|
50
|
+
end
|
51
|
+
|
52
|
+
# Format breadcrumbs consistently
|
53
|
+
def format_breadcrumbs(breadcrumbs)
|
54
|
+
return nil if breadcrumbs.blank?
|
55
|
+
|
56
|
+
breadcrumbs.map do |crumb|
|
57
|
+
{
|
58
|
+
timestamp: format_timestamp(crumb[:timestamp]),
|
59
|
+
type: crumb[:type].presence || "default",
|
60
|
+
message: crumb[:message],
|
61
|
+
data: crumb.except(:timestamp, :type, :message).presence
|
62
|
+
}.compact_blank
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# SDK info for all adapters
|
67
|
+
def sdk_info
|
68
|
+
{
|
69
|
+
name: "lapsoss",
|
70
|
+
version: Lapsoss::VERSION,
|
71
|
+
packages: [ {
|
72
|
+
name: "lapsoss-ruby",
|
73
|
+
version: Lapsoss::VERSION
|
74
|
+
} ]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Serialize and optionally compress
|
79
|
+
def serialize_payload(data, compress: :auto)
|
80
|
+
json = ActiveSupport::JSON.encode(data)
|
81
|
+
|
82
|
+
should_compress = case compress
|
83
|
+
when :auto then json.bytesize >= compress_threshold
|
84
|
+
when true then true
|
85
|
+
else false
|
86
|
+
end
|
87
|
+
|
88
|
+
if should_compress
|
89
|
+
[ ActiveSupport::Gzip.compress(json), true ]
|
90
|
+
else
|
91
|
+
[ json, false ]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Override in adapter for specific event data
|
98
|
+
def build_event_data(event)
|
99
|
+
case event.type
|
100
|
+
in :exception
|
101
|
+
build_exception_data(event)
|
102
|
+
in :message
|
103
|
+
build_message_data(event)
|
104
|
+
else
|
105
|
+
{}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def build_exception_data(event)
|
110
|
+
{
|
111
|
+
exception: {
|
112
|
+
type: event.exception_type,
|
113
|
+
message: event.exception_message,
|
114
|
+
backtrace: event.backtrace_frames&.map(&:to_h)
|
115
|
+
}.compact_blank
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_message_data(event)
|
120
|
+
{
|
121
|
+
message: event.message
|
122
|
+
}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/module/attribute_accessors"
|
5
|
+
require "active_support/notifications"
|
6
|
+
require "active_support/core_ext/numeric/bytes"
|
7
|
+
|
8
|
+
module Lapsoss
|
9
|
+
module Adapters
|
10
|
+
module Concerns
|
11
|
+
module HttpDelivery
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
included do
|
15
|
+
class_attribute :api_endpoint, instance_writer: false
|
16
|
+
class_attribute :api_path, default: "/", instance_writer: false
|
17
|
+
|
18
|
+
# Memoized git info using AS
|
19
|
+
mattr_accessor :git_info_cache, default: {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Unified HTTP delivery with instrumentation
|
23
|
+
def deliver(event)
|
24
|
+
return unless enabled?
|
25
|
+
|
26
|
+
payload = build_payload(event)
|
27
|
+
return if payload.blank?
|
28
|
+
|
29
|
+
body, compressed = serialize_payload(payload)
|
30
|
+
headers = build_delivery_headers(compressed: compressed)
|
31
|
+
|
32
|
+
ActiveSupport::Notifications.instrument("deliver.lapsoss",
|
33
|
+
adapter: self.class.name,
|
34
|
+
event_type: event.type,
|
35
|
+
compressed: compressed,
|
36
|
+
size: body.bytesize
|
37
|
+
) do
|
38
|
+
response = http_client.post(api_path, body: body, headers: headers)
|
39
|
+
handle_response(response)
|
40
|
+
end
|
41
|
+
rescue => error
|
42
|
+
handle_delivery_error(error)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Common headers for all adapters
|
46
|
+
def build_delivery_headers(compressed: false, content_type: "application/json")
|
47
|
+
{
|
48
|
+
"User-Agent" => user_agent,
|
49
|
+
"Content-Type" => content_type,
|
50
|
+
"Content-Encoding" => ("gzip" if compressed),
|
51
|
+
"X-Lapsoss-Version" => Lapsoss::VERSION
|
52
|
+
}.merge(adapter_specific_headers).compact_blank
|
53
|
+
end
|
54
|
+
|
55
|
+
# Override for adapter-specific headers
|
56
|
+
def adapter_specific_headers
|
57
|
+
{}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Git info with AS memoization
|
61
|
+
def git_branch
|
62
|
+
self.class.git_info_cache[:branch] ||= begin
|
63
|
+
`git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip.presence
|
64
|
+
rescue
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def git_sha
|
70
|
+
self.class.git_info_cache[:sha] ||= begin
|
71
|
+
`git rev-parse HEAD 2>/dev/null`.strip.presence
|
72
|
+
rescue
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Common response handling
|
78
|
+
def handle_response(response)
|
79
|
+
code = response.respond_to?(:status) ? response.status.to_i : response.code.to_i
|
80
|
+
case code
|
81
|
+
when 200..299
|
82
|
+
ActiveSupport::Notifications.instrument("success.lapsoss",
|
83
|
+
adapter: self.class.name,
|
84
|
+
response_code: code
|
85
|
+
)
|
86
|
+
true
|
87
|
+
when 429
|
88
|
+
raise DeliveryError.new("Rate limit exceeded", response: response)
|
89
|
+
when 401, 403
|
90
|
+
raise DeliveryError.new("Authentication failed", response: response)
|
91
|
+
when 400..499
|
92
|
+
handle_client_error(response)
|
93
|
+
else
|
94
|
+
raise DeliveryError.new("Server error: #{code}", response: response)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_client_error(response)
|
99
|
+
body = ActiveSupport::JSON.decode(response.body) rescue {}
|
100
|
+
message = body["message"].presence || body["error"].presence || "Bad request"
|
101
|
+
raise DeliveryError.new("Client error: #{message}", response: response)
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_delivery_error(error)
|
105
|
+
ActiveSupport::Notifications.instrument("error.lapsoss",
|
106
|
+
adapter: self.class.name,
|
107
|
+
error: error.class.name,
|
108
|
+
message: error.message
|
109
|
+
)
|
110
|
+
|
111
|
+
Lapsoss.configuration.logger&.error("[#{self.class.name}] Delivery failed: #{error.message}")
|
112
|
+
Lapsoss.configuration.error_handler&.call(error)
|
113
|
+
|
114
|
+
raise error if error.is_a?(DeliveryError)
|
115
|
+
raise DeliveryError.new("Delivery failed: #{error.message}", cause: error)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def http_client
|
121
|
+
@http_client ||= create_http_client(api_endpoint)
|
122
|
+
end
|
123
|
+
|
124
|
+
def user_agent
|
125
|
+
"Lapsoss/#{Lapsoss::VERSION} Ruby/#{RUBY_VERSION} Rails/#{Rails.version if defined?(Rails)}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
5
|
+
|
6
|
+
module Lapsoss
|
7
|
+
module Adapters
|
8
|
+
module Concerns
|
9
|
+
module LevelMapping
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
# Single source of truth for level mappings
|
13
|
+
LEVEL_MAPPINGS = {
|
14
|
+
sentry: {
|
15
|
+
debug: "debug",
|
16
|
+
info: "info",
|
17
|
+
warn: "warning",
|
18
|
+
warning: "warning",
|
19
|
+
error: "error",
|
20
|
+
fatal: "fatal"
|
21
|
+
}.with_indifferent_access,
|
22
|
+
|
23
|
+
rollbar: {
|
24
|
+
debug: "debug",
|
25
|
+
info: "info",
|
26
|
+
warning: "warning",
|
27
|
+
error: "error",
|
28
|
+
fatal: "critical"
|
29
|
+
}.with_indifferent_access,
|
30
|
+
|
31
|
+
bugsnag: {
|
32
|
+
debug: "info",
|
33
|
+
info: "info",
|
34
|
+
warning: "warning",
|
35
|
+
error: "error",
|
36
|
+
fatal: "error"
|
37
|
+
}.with_indifferent_access,
|
38
|
+
|
39
|
+
appsignal: {
|
40
|
+
debug: "debug",
|
41
|
+
info: "info",
|
42
|
+
warning: "warning",
|
43
|
+
error: "error",
|
44
|
+
fatal: "error",
|
45
|
+
critical: "error"
|
46
|
+
}.with_indifferent_access
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
included do
|
50
|
+
# Define which mapping this adapter uses
|
51
|
+
class_attribute :level_mapping_type, default: :sentry
|
52
|
+
end
|
53
|
+
|
54
|
+
# Map level using the adapter's configured mapping
|
55
|
+
def map_level(level)
|
56
|
+
mapping = LEVEL_MAPPINGS[self.class.level_mapping_type]
|
57
|
+
mapping[level] || mapping[:info]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Map severity (alias for bugsnag compatibility)
|
61
|
+
alias_method :map_severity, :map_level
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -1,18 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "json"
|
3
|
+
require "active_support/json"
|
4
4
|
|
5
5
|
module Lapsoss
|
6
6
|
module Adapters
|
7
7
|
class InsightHubAdapter < Base
|
8
8
|
API_URI = "https://notify.bugsnag.com"
|
9
|
-
JSON_CONTENT_TYPE = "application/json"
|
10
9
|
|
11
10
|
def initialize(name, settings = {})
|
12
11
|
super
|
13
12
|
@api_key = settings[:api_key] || ENV.fetch("INSIGHT_HUB_API_KEY", nil)
|
14
13
|
|
15
|
-
|
14
|
+
if @api_key.blank?
|
15
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss::InsightHubAdapter] No API key provided, adapter disabled"
|
16
|
+
@enabled = false
|
17
|
+
return
|
18
|
+
else
|
19
|
+
validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric)
|
20
|
+
end
|
16
21
|
|
17
22
|
@client = create_http_client(API_URI)
|
18
23
|
@backtrace_processor = BacktraceProcessor.new
|
@@ -24,11 +29,15 @@ module Lapsoss
|
|
24
29
|
payload = build_payload(event)
|
25
30
|
return unless payload
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
headers = default_headers(
|
33
|
+
content_type: json_content_type,
|
34
|
+
extra: {
|
35
|
+
"Bugsnag-Api-Key" => @api_key,
|
36
|
+
"Bugsnag-Payload-Version" => "5"
|
37
|
+
}
|
38
|
+
)
|
39
|
+
|
40
|
+
response = @client.post("/", body: ActiveSupport::JSON.encode(payload), headers: headers)
|
32
41
|
|
33
42
|
handle_response(response, event)
|
34
43
|
rescue StandardError => e
|
@@ -116,16 +125,7 @@ module Lapsoss
|
|
116
125
|
end
|
117
126
|
|
118
127
|
def build_breadcrumbs(event)
|
119
|
-
|
120
|
-
|
121
|
-
breadcrumbs.map do |crumb|
|
122
|
-
{
|
123
|
-
timestamp: crumb[:timestamp]&.iso8601,
|
124
|
-
name: crumb[:message],
|
125
|
-
type: crumb[:type] || "manual",
|
126
|
-
metaData: crumb[:data] || {}
|
127
|
-
}
|
128
|
-
end
|
128
|
+
Breadcrumb.for_insight_hub(event.context[:breadcrumbs] || [])
|
129
129
|
end
|
130
130
|
|
131
131
|
def build_user_data(event)
|
@@ -161,7 +161,7 @@ module Lapsoss
|
|
161
161
|
true
|
162
162
|
when 400
|
163
163
|
body = begin
|
164
|
-
JSON.
|
164
|
+
ActiveSupport::JSON.decode(response.body)
|
165
165
|
rescue
|
166
166
|
{}
|
167
167
|
end
|
@@ -177,9 +177,9 @@ module Lapsoss
|
|
177
177
|
end
|
178
178
|
end
|
179
179
|
|
180
|
+
# No longer need strict validation
|
180
181
|
def validate_settings!
|
181
|
-
|
182
|
-
validate_api_key!(@api_key, "Insight Hub API key", format: :alphanumeric) if @api_key
|
182
|
+
# Validation moved to initialize with logging
|
183
183
|
end
|
184
184
|
|
185
185
|
def handle_delivery_error(error, response = nil)
|