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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c6f63828ebac1dc0ff437bc6bc2371170c244fe0ff5195ce8ddc0f2cec21731
4
- data.tar.gz: d0678fb7bda03a75851b6ab06590cd9ab7bd6464878367a7d5e48d9631d26be4
3
+ metadata.gz: c0faf08671ef4e1b0b36ccc9e29900b8e8b5e4a78ac61ffea6269294efee5881
4
+ data.tar.gz: b2c76c33817f3a939388970d21c5fc341ad6ce46e0ce20ef9b71ca5b5c565ae0
5
5
  SHA512:
6
- metadata.gz: 6e725dfbd5841bfd68af3deaa61d5106f966ada89b7e47515eb2d8e1b04327501fa51ba475ff6e6deb971cf3a2370b87b3da7da0f539f35e109b1d555897e55d
7
- data.tar.gz: 3a2ece11a9adaf5738a05c90da27096e7060be9e7fa93f90a64616f8bb1776040dcf27d1f375bb5c21593e3580f2af63edeed53296582db77088067e2181b474
6
+ metadata.gz: 5cfb22826215ffc2473410d6e984ecbd93f74ec0c011e742d7246549b29c1d77b9cb81c774cab311748c0b39cbfd9b9eccfdbb4324aa6da7a632ba8634eb3b19
7
+ data.tar.gz: f641a491be8007a0bc6780094803724f66efc79aa1e10cadbb1ac96c84a8ff30af119c763e1611dc3e2218305c4d0b5958b6f5c5f2863c7b5694b5ff7c9f0856
data/README.md CHANGED
@@ -31,6 +31,11 @@ Lapsoss.configure do |config|
31
31
  end
32
32
  ```
33
33
 
34
+ ## Requirements
35
+
36
+ - Ruby 3.3+
37
+ - Rails 7.2+
38
+
34
39
  ## Installation
35
40
 
36
41
  ```ruby
@@ -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
- validate_settings!
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 = { "Content-Type" => JSON_CONTENT_TYPE }
45
+ headers = default_headers(content_type: json_content_type)
34
46
 
35
47
  begin
36
- @errors_client.post(path, body: JSON.generate(payload), headers: headers)
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
- unless @push_api_key || @frontend_api_key
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
- validate_settings!
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
- response = @client.post("/", body: payload.to_json, headers: {
28
- "Content-Type" => JSON_CONTENT_TYPE,
29
- "Bugsnag-Api-Key" => @api_key,
30
- "Bugsnag-Payload-Version" => "5"
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
- breadcrumbs = event.context[:breadcrumbs] || []
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.parse(response.body)
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
- validate_presence!(@api_key, "Insight Hub API key")
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)