lapsoss 0.1.0 → 0.3.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
  6. data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
  7. data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
  8. data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
  9. data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
  10. data/lib/lapsoss/backtrace_frame.rb +35 -214
  11. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  12. data/lib/lapsoss/backtrace_processor.rb +37 -37
  13. data/lib/lapsoss/client.rb +2 -6
  14. data/lib/lapsoss/configuration.rb +25 -22
  15. data/lib/lapsoss/current.rb +9 -1
  16. data/lib/lapsoss/event.rb +30 -6
  17. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  18. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  19. data/lib/lapsoss/exclusion_filter.rb +156 -0
  20. data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
  21. data/lib/lapsoss/fingerprinter.rb +9 -13
  22. data/lib/lapsoss/http_client.rb +42 -8
  23. data/lib/lapsoss/merged_scope.rb +63 -0
  24. data/lib/lapsoss/middleware/base.rb +15 -0
  25. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  26. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  27. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  28. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  29. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  30. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  31. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  32. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  33. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  34. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  35. data/lib/lapsoss/middleware.rb +0 -347
  36. data/lib/lapsoss/pipeline.rb +1 -73
  37. data/lib/lapsoss/pipeline_builder.rb +69 -0
  38. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  39. data/lib/lapsoss/rails_middleware.rb +78 -0
  40. data/lib/lapsoss/railtie.rb +22 -50
  41. data/lib/lapsoss/registry.rb +34 -20
  42. data/lib/lapsoss/release_providers.rb +110 -0
  43. data/lib/lapsoss/release_tracker.rb +112 -207
  44. data/lib/lapsoss/router.rb +3 -5
  45. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  46. data/lib/lapsoss/sampling/base.rb +11 -0
  47. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  48. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  49. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  50. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  51. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  52. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  53. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  54. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  55. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  56. data/lib/lapsoss/sampling.rb +0 -326
  57. data/lib/lapsoss/scope.rb +17 -57
  58. data/lib/lapsoss/scrubber.rb +16 -18
  59. data/lib/lapsoss/user_context.rb +18 -198
  60. data/lib/lapsoss/user_context_integrations.rb +39 -0
  61. data/lib/lapsoss/user_context_middleware.rb +50 -0
  62. data/lib/lapsoss/user_context_provider.rb +93 -0
  63. data/lib/lapsoss/utils.rb +13 -0
  64. data/lib/lapsoss/validators.rb +14 -27
  65. data/lib/lapsoss/version.rb +1 -1
  66. data/lib/lapsoss.rb +12 -25
  67. metadata +106 -21
@@ -13,10 +13,13 @@ module Lapsoss
13
13
  @cache_duration = configuration[:cache_duration] || 300 # 5 minutes
14
14
  @cached_release_info = nil
15
15
  @cache_timestamp = nil
16
+
17
+ # Auto-register rails_app_version provider if available
18
+ add_rails_app_version_provider if defined?(Rails) && Rails.application.respond_to?(:version)
16
19
  end
17
20
 
18
21
  def get_release_info
19
- now = Time.now
22
+ now = Time.zone.now
20
23
 
21
24
  # Return cached info if still valid
22
25
  if @cached_release_info && @cache_timestamp && (now - @cache_timestamp) < @cache_duration
@@ -28,34 +31,26 @@ module Lapsoss
28
31
 
29
32
  # Add custom version providers
30
33
  @version_providers.each do |provider|
31
- begin
32
- if provider_info = provider.call
33
- release_info.merge!(provider_info)
34
- end
35
- rescue StandardError => e
36
- warn "Release provider failed: #{e.message}"
34
+ if provider_info = provider.call
35
+ release_info.merge!(provider_info)
37
36
  end
37
+ rescue StandardError => e
38
+ warn "Release provider failed: #{e.message}"
38
39
  end
39
40
 
40
41
  # Add Git information
41
- if @git_enabled
42
- if git_info = detect_git_info
43
- release_info.merge!(git_info)
44
- end
42
+ if @git_enabled && (git_info = detect_git_info)
43
+ release_info.merge!(git_info)
45
44
  end
46
45
 
47
46
  # Add environment information
48
- if @environment_enabled
49
- if env_info = detect_environment_info
50
- release_info.merge!(env_info)
51
- end
47
+ if @environment_enabled && (env_info = detect_environment_info)
48
+ release_info.merge!(env_info)
52
49
  end
53
50
 
54
51
  # Add deployment information
55
- if @deployment_enabled
56
- if deployment_info = detect_deployment_info
57
- release_info.merge!(deployment_info)
58
- end
52
+ if @deployment_enabled && (deployment_info = detect_deployment_info)
53
+ release_info.merge!(deployment_info)
59
54
  end
60
55
 
61
56
  # Generate release ID if not provided
@@ -77,6 +72,19 @@ module Lapsoss
77
72
  @cache_timestamp = nil
78
73
  end
79
74
 
75
+ def add_rails_app_version_provider
76
+ add_version_provider do
77
+ if defined?(Rails) && Rails.application.respond_to?(:version)
78
+ version = Rails.application.version
79
+ {
80
+ rails_app_version: version.to_s,
81
+ rails_app_version_cache_key: version.to_cache_key,
82
+ rails_app_environment: Rails.application.env
83
+ }
84
+ end
85
+ end
86
+ end
87
+
80
88
  private
81
89
 
82
90
  def detect_git_info
@@ -99,9 +107,7 @@ module Lapsoss
99
107
 
100
108
  # Get commit timestamp
101
109
  commit_timestamp = execute_git_command("log -1 --format=%ct")
102
- if commit_timestamp && !commit_timestamp.empty?
103
- git_info[:commit_timestamp] = Time.at(commit_timestamp.to_i)
104
- end
110
+ git_info[:commit_timestamp] = Time.zone.at(commit_timestamp.to_i) if commit_timestamp.present?
105
111
 
106
112
  # Get commit message
107
113
  commit_message = execute_git_command("log -1 --format=%s")
@@ -113,11 +119,11 @@ module Lapsoss
113
119
 
114
120
  # Get tag if on a tag
115
121
  tag = execute_git_command("describe --exact-match --tags HEAD 2>/dev/null")
116
- git_info[:tag] = tag if tag && !tag.empty?
122
+ git_info[:tag] = tag if tag.present?
117
123
 
118
124
  # Get latest tag
119
125
  latest_tag = execute_git_command("describe --tags --abbrev=0 2>/dev/null")
120
- git_info[:latest_tag] = latest_tag if latest_tag && !latest_tag.empty?
126
+ git_info[:latest_tag] = latest_tag if latest_tag.present?
121
127
 
122
128
  # Get commits since latest tag
123
129
  if latest_tag
@@ -131,9 +137,7 @@ module Lapsoss
131
137
 
132
138
  # Get remote URL
133
139
  remote_url = execute_git_command("config --get remote.origin.url")
134
- if remote_url
135
- git_info[:remote_url] = sanitize_remote_url(remote_url)
136
- end
140
+ git_info[:remote_url] = sanitize_remote_url(remote_url) if remote_url
137
141
 
138
142
  git_info
139
143
  rescue StandardError => e
@@ -145,9 +149,22 @@ module Lapsoss
145
149
  def detect_environment_info
146
150
  env_info = {}
147
151
 
148
- # Application version from common environment variables
149
- env_info[:app_version] = ENV["APP_VERSION"] if ENV["APP_VERSION"]
150
- env_info[:version] = ENV["VERSION"] if ENV["VERSION"]
152
+ # Try rails_app_version first if available
153
+ if defined?(Rails) && Rails.application.respond_to?(:version)
154
+ env_info[:version] = Rails.application.version.to_s
155
+ env_info[:app_version] = Rails.application.version.to_s
156
+
157
+ # Add detailed version info
158
+ version_obj = Rails.application.version
159
+ env_info[:version_major] = version_obj.major
160
+ env_info[:version_minor] = version_obj.minor
161
+ env_info[:version_patch] = version_obj.patch
162
+ env_info[:version_prerelease] = version_obj.prerelease? if version_obj.respond_to?(:prerelease?)
163
+ else
164
+ # Fallback to environment variables
165
+ env_info[:app_version] = ENV["APP_VERSION"] if ENV["APP_VERSION"]
166
+ env_info[:version] = ENV["VERSION"] if ENV["VERSION"]
167
+ end
151
168
 
152
169
  # Environment detection
153
170
  env_info[:environment] = detect_environment
@@ -191,6 +208,9 @@ module Lapsoss
191
208
  end
192
209
 
193
210
  def detect_environment
211
+ # Try rails_app_version first if available
212
+ return Rails.application.env if defined?(Rails) && Rails.application.respond_to?(:env)
213
+
194
214
  return ENV["RAILS_ENV"] if ENV["RAILS_ENV"]
195
215
  return ENV["RACK_ENV"] if ENV["RACK_ENV"]
196
216
  return ENV["NODE_ENV"] if ENV["NODE_ENV"]
@@ -198,9 +218,7 @@ module Lapsoss
198
218
  return ENV["ENV"] if ENV["ENV"]
199
219
 
200
220
  # Try to detect from Rails if available
201
- if defined?(Rails) && Rails.respond_to?(:env)
202
- return Rails.env.to_s
203
- end
221
+ return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
204
222
 
205
223
  # Default fallback
206
224
  "unknown"
@@ -212,57 +230,57 @@ module Lapsoss
212
230
  # GitHub Actions
213
231
  if ENV["GITHUB_ACTIONS"]
214
232
  ci_info[:provider] = "github_actions"
215
- ci_info[:run_id] = ENV["GITHUB_RUN_ID"]
216
- ci_info[:run_number] = ENV["GITHUB_RUN_NUMBER"]
217
- ci_info[:workflow] = ENV["GITHUB_WORKFLOW"]
218
- ci_info[:actor] = ENV["GITHUB_ACTOR"]
219
- ci_info[:repository] = ENV["GITHUB_REPOSITORY"]
220
- ci_info[:ref] = ENV["GITHUB_REF"]
221
- ci_info[:sha] = ENV["GITHUB_SHA"]
233
+ ci_info[:run_id] = ENV.fetch("GITHUB_RUN_ID", nil)
234
+ ci_info[:run_number] = ENV.fetch("GITHUB_RUN_NUMBER", nil)
235
+ ci_info[:workflow] = ENV.fetch("GITHUB_WORKFLOW", nil)
236
+ ci_info[:actor] = ENV.fetch("GITHUB_ACTOR", nil)
237
+ ci_info[:repository] = ENV.fetch("GITHUB_REPOSITORY", nil)
238
+ ci_info[:ref] = ENV.fetch("GITHUB_REF", nil)
239
+ ci_info[:sha] = ENV.fetch("GITHUB_SHA", nil)
222
240
  end
223
241
 
224
242
  # GitLab CI
225
243
  if ENV["GITLAB_CI"]
226
244
  ci_info[:provider] = "gitlab_ci"
227
- ci_info[:pipeline_id] = ENV["CI_PIPELINE_ID"]
228
- ci_info[:job_id] = ENV["CI_JOB_ID"]
229
- ci_info[:job_name] = ENV["CI_JOB_NAME"]
230
- ci_info[:commit_sha] = ENV["CI_COMMIT_SHA"]
231
- ci_info[:commit_ref] = ENV["CI_COMMIT_REF_NAME"]
232
- ci_info[:project_url] = ENV["CI_PROJECT_URL"]
245
+ ci_info[:pipeline_id] = ENV.fetch("CI_PIPELINE_ID", nil)
246
+ ci_info[:job_id] = ENV.fetch("CI_JOB_ID", nil)
247
+ ci_info[:job_name] = ENV.fetch("CI_JOB_NAME", nil)
248
+ ci_info[:commit_sha] = ENV.fetch("CI_COMMIT_SHA", nil)
249
+ ci_info[:commit_ref] = ENV.fetch("CI_COMMIT_REF_NAME", nil)
250
+ ci_info[:project_url] = ENV.fetch("CI_PROJECT_URL", nil)
233
251
  end
234
252
 
235
253
  # Jenkins
236
254
  if ENV["JENKINS_URL"]
237
255
  ci_info[:provider] = "jenkins"
238
- ci_info[:build_number] = ENV["BUILD_NUMBER"]
239
- ci_info[:build_id] = ENV["BUILD_ID"]
240
- ci_info[:job_name] = ENV["JOB_NAME"]
241
- ci_info[:build_url] = ENV["BUILD_URL"]
242
- ci_info[:git_commit] = ENV["GIT_COMMIT"]
243
- ci_info[:git_branch] = ENV["GIT_BRANCH"]
256
+ ci_info[:build_number] = ENV.fetch("BUILD_NUMBER", nil)
257
+ ci_info[:build_id] = ENV.fetch("BUILD_ID", nil)
258
+ ci_info[:job_name] = ENV.fetch("JOB_NAME", nil)
259
+ ci_info[:build_url] = ENV.fetch("BUILD_URL", nil)
260
+ ci_info[:git_commit] = ENV.fetch("GIT_COMMIT", nil)
261
+ ci_info[:git_branch] = ENV.fetch("GIT_BRANCH", nil)
244
262
  end
245
263
 
246
264
  # CircleCI
247
265
  if ENV["CIRCLECI"]
248
266
  ci_info[:provider] = "circleci"
249
- ci_info[:build_num] = ENV["CIRCLE_BUILD_NUM"]
250
- ci_info[:workflow_id] = ENV["CIRCLE_WORKFLOW_ID"]
251
- ci_info[:job] = ENV["CIRCLE_JOB"]
252
- ci_info[:project_reponame] = ENV["CIRCLE_PROJECT_REPONAME"]
253
- ci_info[:sha1] = ENV["CIRCLE_SHA1"]
254
- ci_info[:branch] = ENV["CIRCLE_BRANCH"]
267
+ ci_info[:build_num] = ENV.fetch("CIRCLE_BUILD_NUM", nil)
268
+ ci_info[:workflow_id] = ENV.fetch("CIRCLE_WORKFLOW_ID", nil)
269
+ ci_info[:job] = ENV.fetch("CIRCLE_JOB", nil)
270
+ ci_info[:project_reponame] = ENV.fetch("CIRCLE_PROJECT_REPONAME", nil)
271
+ ci_info[:sha1] = ENV.fetch("CIRCLE_SHA1", nil)
272
+ ci_info[:branch] = ENV.fetch("CIRCLE_BRANCH", nil)
255
273
  end
256
274
 
257
275
  # Travis CI
258
276
  if ENV["TRAVIS"]
259
277
  ci_info[:provider] = "travis"
260
- ci_info[:build_id] = ENV["TRAVIS_BUILD_ID"]
261
- ci_info[:build_number] = ENV["TRAVIS_BUILD_NUMBER"]
262
- ci_info[:job_id] = ENV["TRAVIS_JOB_ID"]
263
- ci_info[:commit] = ENV["TRAVIS_COMMIT"]
264
- ci_info[:branch] = ENV["TRAVIS_BRANCH"]
265
- ci_info[:tag] = ENV["TRAVIS_TAG"]
278
+ ci_info[:build_id] = ENV.fetch("TRAVIS_BUILD_ID", nil)
279
+ ci_info[:build_number] = ENV.fetch("TRAVIS_BUILD_NUMBER", nil)
280
+ ci_info[:job_id] = ENV.fetch("TRAVIS_JOB_ID", nil)
281
+ ci_info[:commit] = ENV.fetch("TRAVIS_COMMIT", nil)
282
+ ci_info[:branch] = ENV.fetch("TRAVIS_BRANCH", nil)
283
+ ci_info[:tag] = ENV.fetch("TRAVIS_TAG", nil)
266
284
  end
267
285
 
268
286
  ci_info
@@ -273,11 +291,11 @@ module Lapsoss
273
291
 
274
292
  {
275
293
  platform: "heroku",
276
- app_name: ENV["HEROKU_APP_NAME"],
277
- dyno: ENV["DYNO"],
278
- slug_commit: ENV["HEROKU_SLUG_COMMIT"],
279
- release_version: ENV["HEROKU_RELEASE_VERSION"],
280
- slug_description: ENV["HEROKU_SLUG_DESCRIPTION"]
294
+ app_name: ENV.fetch("HEROKU_APP_NAME", nil),
295
+ dyno: ENV.fetch("DYNO", nil),
296
+ slug_commit: ENV.fetch("HEROKU_SLUG_COMMIT", nil),
297
+ release_version: ENV.fetch("HEROKU_RELEASE_VERSION", nil),
298
+ slug_description: ENV.fetch("HEROKU_SLUG_DESCRIPTION", nil)
281
299
  }
282
300
  end
283
301
 
@@ -287,17 +305,17 @@ module Lapsoss
287
305
  if ENV["AWS_EXECUTION_ENV"]
288
306
  info[:platform] = "aws"
289
307
  info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
290
- info[:region] = ENV["AWS_REGION"] || ENV["AWS_DEFAULT_REGION"]
291
- info[:function_name] = ENV["AWS_LAMBDA_FUNCTION_NAME"]
292
- info[:function_version] = ENV["AWS_LAMBDA_FUNCTION_VERSION"]
308
+ info[:region] = ENV["AWS_REGION"] || ENV.fetch("AWS_DEFAULT_REGION", nil)
309
+ info[:function_name] = ENV.fetch("AWS_LAMBDA_FUNCTION_NAME", nil)
310
+ info[:function_version] = ENV.fetch("AWS_LAMBDA_FUNCTION_VERSION", nil)
293
311
  end
294
312
 
295
313
  # EC2 metadata (if available)
296
314
  if ENV["EC2_INSTANCE_ID"]
297
315
  info[:platform] = "aws_ec2"
298
316
  info[:instance_id] = ENV["EC2_INSTANCE_ID"]
299
- info[:instance_type] = ENV["EC2_INSTANCE_TYPE"]
300
- info[:availability_zone] = ENV["EC2_AVAILABILITY_ZONE"]
317
+ info[:instance_type] = ENV.fetch("EC2_INSTANCE_TYPE", nil)
318
+ info[:availability_zone] = ENV.fetch("EC2_AVAILABILITY_ZONE", nil)
301
319
  end
302
320
 
303
321
  info
@@ -309,18 +327,18 @@ module Lapsoss
309
327
  if ENV["GOOGLE_CLOUD_PROJECT"]
310
328
  info[:platform] = "gcp"
311
329
  info[:project] = ENV["GOOGLE_CLOUD_PROJECT"]
312
- info[:region] = ENV["GOOGLE_CLOUD_REGION"]
313
- info[:function_name] = ENV["FUNCTION_NAME"]
314
- info[:function_signature_type] = ENV["FUNCTION_SIGNATURE_TYPE"]
330
+ info[:region] = ENV.fetch("GOOGLE_CLOUD_REGION", nil)
331
+ info[:function_name] = ENV.fetch("FUNCTION_NAME", nil)
332
+ info[:function_signature_type] = ENV.fetch("FUNCTION_SIGNATURE_TYPE", nil)
315
333
  end
316
334
 
317
335
  # App Engine
318
336
  if ENV["GAE_APPLICATION"]
319
337
  info[:platform] = "gcp_app_engine"
320
338
  info[:application] = ENV["GAE_APPLICATION"]
321
- info[:service] = ENV["GAE_SERVICE"]
322
- info[:version] = ENV["GAE_VERSION"]
323
- info[:runtime] = ENV["GAE_RUNTIME"]
339
+ info[:service] = ENV.fetch("GAE_SERVICE", nil)
340
+ info[:version] = ENV.fetch("GAE_VERSION", nil)
341
+ info[:runtime] = ENV.fetch("GAE_RUNTIME", nil)
324
342
  end
325
343
 
326
344
  info
@@ -332,9 +350,9 @@ module Lapsoss
332
350
  if ENV["WEBSITE_SITE_NAME"]
333
351
  info[:platform] = "azure"
334
352
  info[:site_name] = ENV["WEBSITE_SITE_NAME"]
335
- info[:resource_group] = ENV["WEBSITE_RESOURCE_GROUP"]
336
- info[:subscription_id] = ENV["WEBSITE_OWNER_NAME"]
337
- info[:sku] = ENV["WEBSITE_SKU"]
353
+ info[:resource_group] = ENV.fetch("WEBSITE_RESOURCE_GROUP", nil)
354
+ info[:subscription_id] = ENV.fetch("WEBSITE_OWNER_NAME", nil)
355
+ info[:sku] = ENV.fetch("WEBSITE_SKU", nil)
338
356
  end
339
357
 
340
358
  info
@@ -346,8 +364,8 @@ module Lapsoss
346
364
  if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
347
365
  info[:platform] = "docker"
348
366
  info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
349
- info[:image] = ENV["DOCKER_IMAGE"]
350
- info[:tag] = ENV["DOCKER_TAG"]
367
+ info[:image] = ENV.fetch("DOCKER_IMAGE", nil)
368
+ info[:tag] = ENV.fetch("DOCKER_TAG", nil)
351
369
  end
352
370
 
353
371
  info
@@ -358,11 +376,11 @@ module Lapsoss
358
376
 
359
377
  if ENV["KUBERNETES_SERVICE_HOST"]
360
378
  info[:platform] = "kubernetes"
361
- info[:namespace] = ENV["KUBERNETES_NAMESPACE"]
362
- info[:pod_name] = ENV["HOSTNAME"]
363
- info[:service_account] = ENV["KUBERNETES_SERVICE_ACCOUNT"]
364
- info[:cluster_name] = ENV["CLUSTER_NAME"]
365
- info[:node_name] = ENV["NODE_NAME"]
379
+ info[:namespace] = ENV.fetch("KUBERNETES_NAMESPACE", nil)
380
+ info[:pod_name] = ENV.fetch("HOSTNAME", nil)
381
+ info[:service_account] = ENV.fetch("KUBERNETES_SERVICE_ACCOUNT", nil)
382
+ info[:cluster_name] = ENV.fetch("CLUSTER_NAME", nil)
383
+ info[:node_name] = ENV.fetch("NODE_NAME", nil)
366
384
  end
367
385
 
368
386
  info
@@ -393,16 +411,14 @@ module Lapsoss
393
411
  ]
394
412
 
395
413
  formats.each do |format|
396
- begin
397
- return Time.strptime(time_str, format)
398
- rescue ArgumentError
399
- next
400
- end
414
+ return Time.strptime(time_str, format)
415
+ rescue ArgumentError
416
+ next
401
417
  end
402
418
 
403
419
  # Try parsing as integer (Unix timestamp)
404
420
  begin
405
- return Time.at(time_str.to_i) if time_str.match?(/^\d+$/)
421
+ return Time.zone.at(time_str.to_i) if time_str.match?(/^\d+$/)
406
422
  rescue ArgumentError
407
423
  nil
408
424
  end
@@ -439,115 +455,4 @@ module Lapsoss
439
455
  end
440
456
  end
441
457
  end
442
-
443
- # Built-in release providers for common scenarios
444
- class ReleaseProviders
445
- def self.from_file(file_path)
446
- lambda do
447
- return nil unless File.exist?(file_path)
448
-
449
- content = File.read(file_path).strip
450
- return nil if content.empty?
451
-
452
- # Try to parse as JSON first
453
- begin
454
- JSON.parse(content)
455
- rescue JSON::ParserError
456
- # Treat as plain text version
457
- { version: content }
458
- end
459
- end
460
- end
461
-
462
- def self.from_ruby_constant(constant_name)
463
- lambda do
464
- begin
465
- constant = Object.const_get(constant_name)
466
- { version: constant.to_s }
467
- rescue NameError
468
- nil
469
- end
470
- end
471
- end
472
-
473
- def self.from_gemfile_lock
474
- lambda do
475
- return nil unless File.exist?("Gemfile.lock")
476
-
477
- content = File.read("Gemfile.lock")
478
-
479
- # Extract gems with versions
480
- gems = {}
481
- content.scan(/^\s{4}(\w+)\s+\(([^)]+)\)/).each do |name, version|
482
- gems[name] = version
483
- end
484
-
485
- { gems: gems }
486
- end
487
- end
488
-
489
- def self.from_package_json
490
- lambda do
491
- return nil unless File.exist?("package.json")
492
-
493
- begin
494
- package_info = JSON.parse(File.read("package.json"))
495
- {
496
- version: package_info["version"],
497
- name: package_info["name"],
498
- dependencies: package_info["dependencies"]&.keys
499
- }.compact
500
- rescue JSON::ParserError
501
- nil
502
- end
503
- end
504
- end
505
-
506
- def self.from_rails_application
507
- lambda do
508
- return nil unless defined?(Rails) && Rails.respond_to?(:application)
509
-
510
- app = Rails.application
511
- return nil unless app
512
-
513
- info = {
514
- rails_version: Rails.version,
515
- environment: Rails.env,
516
- root: Rails.root.to_s
517
- }
518
-
519
- # Get application version if defined
520
- if app.class.respond_to?(:version)
521
- info[:app_version] = app.class.version
522
- end
523
-
524
- # Get application name
525
- if app.class.respond_to?(:name)
526
- info[:app_name] = app.class.name
527
- end
528
-
529
- info
530
- end
531
- end
532
-
533
- def self.from_capistrano
534
- lambda do
535
- # Check for Capistrano deployment files
536
- %w[REVISION current/REVISION].each do |file|
537
- next unless File.exist?(file)
538
-
539
- revision = File.read(file).strip
540
- next if revision.empty?
541
-
542
- return {
543
- revision: revision,
544
- deployed_at: File.mtime(file),
545
- deployment_method: "capistrano"
546
- }
547
- end
548
-
549
- nil
550
- end
551
- end
552
- end
553
458
  end
@@ -9,11 +9,9 @@ module Lapsoss
9
9
  # @param event [Lapsoss::Event] The event to process.
10
10
  def process_event(event)
11
11
  Registry.instance.active.each do |adapter|
12
- begin
13
- adapter.capture(event)
14
- rescue => e
15
- handle_adapter_error(adapter, event, e)
16
- end
12
+ adapter.capture(event)
13
+ rescue StandardError => e
14
+ handle_adapter_error(adapter, event, e)
17
15
  end
18
16
  end
19
17
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class AdaptiveSampler < Base
6
+ def initialize(target_rate: 1.0, adjustment_period: 60)
7
+ @target_rate = target_rate
8
+ @adjustment_period = adjustment_period
9
+ @current_rate = target_rate
10
+ @events_count = 0
11
+ @last_adjustment = Time.zone.now
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def sample?(_event, _hint = {})
16
+ @mutex.synchronize do
17
+ @events_count += 1
18
+
19
+ # Adjust rate periodically
20
+ now = Time.zone.now
21
+ if now - @last_adjustment > @adjustment_period
22
+ adjust_rate
23
+ @last_adjustment = now
24
+ @events_count = 0
25
+ end
26
+ end
27
+
28
+ @current_rate > rand
29
+ end
30
+
31
+ attr_reader :current_rate
32
+
33
+ private
34
+
35
+ def adjust_rate
36
+ # Simple adaptive logic - can be enhanced based on system metrics
37
+ # For now, just ensure we don't drift too far from target
38
+ if @events_count > 100 # High volume
39
+ @current_rate = [ @current_rate * 0.9, @target_rate * 0.1 ].max
40
+ elsif @events_count < 10 # Low volume
41
+ @current_rate = [ @current_rate * 1.1, @target_rate ].min
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class Base
6
+ def sample?(_event, _hint = {})
7
+ true
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class CompositeSampler < Base
6
+ def initialize(app = nil, samplers: [], strategy: :all)
7
+ @app = app
8
+ @samplers = samplers
9
+ @strategy = strategy
10
+ end
11
+
12
+ def sample?(event, hint = {})
13
+ case @strategy
14
+ when :all
15
+ @samplers.all? { |sampler| sampler.sample?(event, hint) }
16
+ when :any
17
+ @samplers.any? { |sampler| sampler.sample?(event, hint) }
18
+ when :first
19
+ @samplers.first&.sample?(event, hint) || true
20
+ else
21
+ raise ArgumentError, "Unknown strategy: #{@strategy}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Lapsoss
6
+ module Sampling
7
+ class ConsistentHashSampler < Base
8
+ def initialize(rate:, key_extractor: nil)
9
+ @rate = rate
10
+ @key_extractor = key_extractor || method(:default_key_extractor)
11
+ @threshold = (rate * 0xFFFFFFFF).to_i
12
+ end
13
+
14
+ def sample?(event, hint = {})
15
+ key = @key_extractor.call(event, hint)
16
+ return @rate > rand unless key
17
+
18
+ hash_value = Digest::MD5.hexdigest(key.to_s)[0, 8].to_i(16)
19
+ hash_value <= @threshold
20
+ end
21
+
22
+ private
23
+
24
+ def default_key_extractor(event, _hint)
25
+ # Use fingerprint for consistent sampling
26
+ event.fingerprint
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class ExceptionTypeSampler < Base
6
+ def initialize(rates: {})
7
+ @rates = rates
8
+ @default_rate = rates.fetch(:default, 1.0)
9
+ end
10
+
11
+ def sample?(event, _hint = {})
12
+ return @default_rate > rand unless event.exception
13
+
14
+ exception_class = event.exception.class
15
+ rate = find_rate_for_exception(exception_class)
16
+ rate > rand
17
+ end
18
+
19
+ private
20
+
21
+ def find_rate_for_exception(exception_class)
22
+ # Check exact class match first
23
+ return @rates[exception_class] if @rates.key?(exception_class)
24
+
25
+ # Check inheritance hierarchy
26
+ @rates.each do |klass, rate|
27
+ return rate if klass.is_a?(Class) && exception_class <= klass
28
+ end
29
+
30
+ # Check string/regex patterns
31
+ @rates.each do |pattern, rate|
32
+ case pattern
33
+ when String
34
+ return rate if exception_class.name.include?(pattern)
35
+ when Regexp
36
+ return rate if exception_class.name.match?(pattern)
37
+ end
38
+ end
39
+
40
+ @default_rate
41
+ end
42
+ end
43
+ end
44
+ end