solid_errors 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ <div class="inline-flex items-center justify-start flex-wrap gap-3 whitespace-nowrap">
2
+ <% if current_controller.errors? && !current_action.errors_index? %>
3
+ <%= link_to errors_path, class: button_classes(icon: true) do %>
4
+ <%= bootstrap_svg "arrow-left" %>
5
+ <span>Back to errors</span>
6
+ <% end %>
7
+ <% end %>
8
+
9
+ <%= button_to error_path(error), method: :patch, class: button_classes(type: :primary_ghost), params: { error: { resolved_at: Time.now } } do %>
10
+ Resolve Error<span class="sr-only"> #<%= error.id %></span>
11
+ <% end %>
12
+ </div>
@@ -0,0 +1,24 @@
1
+ <tr class="even:bg-gray-50 align-top">
2
+ <td scope="col" class="whitespace-wrap py-4 pl-4 pr-3 font-medium text-gray-900 sm:pl-3">
3
+ <div>
4
+ <%= error.emoji %>
5
+ <%= link_to error_path(error), class: "text-blue-400 underline inline-flex items-baseline gap-1" do %>
6
+ <strong><code><%= error.exception_class %></code></strong>
7
+ <% end %>
8
+ from
9
+ <em><code><%= error.source %></code></em>
10
+ </div>
11
+ <pre class="ml-6 mt-4"><%= error.message %></pre>
12
+ </td>
13
+ <td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-gray-500 text-right">
14
+ <%= error.occurrences.size %>
15
+ </td>
16
+ <td scope="col" class="whitespace-nowrap px-3 py-4 pt-7 text-gray-500 text-right">
17
+ <%= time_ago_in_words error.occurrences.maximum(:created_at), scope: 'datetime.distance_in_words.short' %>
18
+ </td>
19
+ <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-3">
20
+ <%= button_to error_path(error), method: :patch, class: button_classes(type: :primary_ghost), params: { error: { resolved_at: Time.now } } do %>
21
+ Resolve<span class="sr-only">, Error #<%= error.id %></span>
22
+ <% end %>
23
+ </td>
24
+ </tr>
@@ -0,0 +1,18 @@
1
+ <table class="min-w-full divide-y divide-gray-300">
2
+ <thead>
3
+ <tr>
4
+ <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3">Error</th>
5
+ <th scope="col" class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900">Count</th>
6
+ <th scope="col" class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900">Last</th>
7
+ <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-3">
8
+ <span class="sr-only">Resolve</span>
9
+ </th>
10
+ </tr>
11
+ </thead>
12
+
13
+ <tbody class="bg-white">
14
+ <%= render partial: "solid_errors/errors/error", collection: @errors %>
15
+ </tbody>
16
+ </table>
17
+
18
+ <%#= @errors.count %>
@@ -0,0 +1,101 @@
1
+ <%= tag.section id: dom_id(@error), class: "space-y-6" do %>
2
+ <div class="flex justify-between items-center">
3
+ <%= tag.h1 class: "font-bold flex items-center text-2xl gap-2" do %>
4
+ <%= @error.emoji %>
5
+ <code><%= @error.exception_class %></code>
6
+ from
7
+ <em><code><%= @error.source %></code></em>
8
+ <% end %>
9
+
10
+ <small class="text-base">
11
+ #<code><%= @error.id %></code>
12
+ </small>
13
+ </div>
14
+
15
+ <pre class=""><%= @error.message %></pre>
16
+
17
+ <dl class="flex-1 grid grid-cols-2 gap-x-4">
18
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
19
+ <dt class="font-bold">
20
+ <%= SolidErrors::Error.human_attribute_name(:first_seen) %>
21
+ </dt>
22
+ <dd class="inline-flex items-center gap-1">
23
+ <% first_seen_at = @error.occurrences.minimum(:created_at) %>
24
+
25
+ <abbr title="<%= first_seen_at.iso8601 %>" class="cursor-help">
26
+ <%= time_tag first_seen_at, "#{time_ago_in_words(first_seen_at)} ago" %>
27
+ </abbr>
28
+ </dd>
29
+ </div>
30
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
31
+ <dt class="font-bold">
32
+ <%= SolidErrors::Error.human_attribute_name(:last_seen) %>
33
+ </dt>
34
+ <dd class="inline-flex items-center gap-1">
35
+ <% last_seen_at = @error.occurrences.maximum(:created_at) %>
36
+
37
+ <abbr title="<%= last_seen_at.iso8601 %>" class="cursor-help">
38
+ <%= time_tag last_seen_at, "#{time_ago_in_words(last_seen_at)} ago" %>
39
+ </abbr>
40
+ </dd>
41
+ </div>
42
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
43
+ <dt class="font-bold">
44
+ <%= SolidErrors::Error.human_attribute_name(:occurrences) %>
45
+ </dt>
46
+ <dd class="inline-flex items-center gap-1">
47
+ <%= @error.occurrences.size %>
48
+ </dd>
49
+ </div>
50
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
51
+ <dt class="font-bold">
52
+ <%= SolidErrors::Error.human_attribute_name(:exception_class) %>
53
+ </dt>
54
+ <dd class="inline-flex items-center gap-1">
55
+ <code><%= @error.exception_class %></code>
56
+ </dd>
57
+ </div>
58
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
59
+ <dt class="font-bold">
60
+ <%= SolidErrors::Error.human_attribute_name(:severity) %>
61
+ </dt>
62
+ <dd class="inline-flex items-center gap-1">
63
+ <span class="<%= badge_classes %>">
64
+ <%= @error.severity %>
65
+ </span>
66
+ </dd>
67
+ </div>
68
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
69
+ <dt class="font-bold">
70
+ <%= SolidErrors::Error.human_attribute_name(:source) %>
71
+ </dt>
72
+ <dd class="inline-flex items-center gap-1">
73
+ <em><%= @error.source %></em>
74
+ </dd>
75
+ </div>
76
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
77
+ <dt class="font-bold">
78
+ <%= SolidErrors::Error.human_attribute_name(:project_root) %>
79
+ </dt>
80
+ <dd class="inline-flex items-center gap-1">
81
+ <span><%= SolidErrors::BacktraceLine::RAILS_ROOT %></span>
82
+ </dd>
83
+ </div>
84
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
85
+ <dt class="font-bold">
86
+ <%= SolidErrors::Error.human_attribute_name(:gem_root) %>
87
+ </dt>
88
+ <dd class="inline-flex items-center gap-1">
89
+ <span><%= Gem.path.join("\n") %></span>
90
+ </dd>
91
+ </div>
92
+ </dl>
93
+
94
+ <%= render "solid_errors/errors/actions", error: @error %>
95
+ <% end %>
96
+
97
+ <hr class="mt-4 pb-4">
98
+
99
+ <%= render "solid_errors/occurrences/collection",
100
+ occurrences: @error.occurrences,
101
+ titled: true %>
@@ -0,0 +1,25 @@
1
+ <% titled ||= false %>
2
+
3
+ <section id="occurrences" class="sm:rounded-md mt-4 -mx-2 sm:mx-0">
4
+ <% if titled %>
5
+ <div class="flex justify-between items-center mb-3 border-b">
6
+ <h2 class="font-bold text-2xl">
7
+ Occurrences
8
+ </h2>
9
+ <p class="text-right">
10
+ Total: <strong><%= occurrences.size %></strong>
11
+ </p>
12
+ </div>
13
+ <% end %>
14
+
15
+ <div class="min-w-full">
16
+ <% if occurrences.any? %>
17
+ <%= render occurrences %>
18
+ <% else %>
19
+ <div class="flex items-center justify-center gap-2">
20
+ <%= bootstrap_svg "list-ul" %>
21
+ <span class="text-2xl">No occurrences yet&hellip;</span>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ </section>
@@ -0,0 +1,46 @@
1
+ <% seen_at = occurrence.created_at %>
2
+ <% backtrace = occurrence.parsed_backtrace %>
3
+
4
+ <%= tag.section id: dom_id(occurrence), class: "" do %>
5
+ <%= tag.details open: occurrence_counter.zero? do %>
6
+ <summary class="hover:bg-gray-50 p-2 rounded cursor-pointer">
7
+ <%= time_tag seen_at, seen_at %>
8
+ (<em><%= time_ago_in_words(seen_at) %> ago</em>)
9
+ </summary>
10
+ <div class="">
11
+ <dl class="ml-6">
12
+ <% occurrence.context&.each do |key, value| %>
13
+ <div class="flex items-center justify-between flex-wrap gap-x-2">
14
+ <dt class="font-bold">
15
+ <%= SolidErrors::Occurrence.human_attribute_name(key) %>
16
+ </dt>
17
+ <dd class="">
18
+ <%= value %>
19
+ </dd>
20
+ </div>
21
+ <% end %>
22
+ <div class="">
23
+ <dt class="font-bold">
24
+ <%= SolidErrors::Occurrence.human_attribute_name(:backtrace) %>
25
+ </dt>
26
+ <dd class="">
27
+ <% backtrace.lines.each_with_index do |line, i| %>
28
+ <%= tag.details open: line.application? || i.zero? do %>
29
+ <summary class="hover:bg-gray-50 px-2 py-1 rounded cursor-pointer">
30
+ <span class="text-gray-500"><%= File.dirname(line.filtered_file) %>/</span><span class="text-blue-500"><%= File.basename(line.filtered_file) %></span>:<span class="text-gray-900"><%= line.filtered_number %></span>
31
+ <span class="text-gray-500">in</span>
32
+ <code class="text-green-500"><%= line.filtered_method %></code>
33
+ </summary>
34
+ <div><pre class="debug_dump"><code class="torchlight"><% line.source.each do |n, code| %>
35
+ <div class="line"><span class="mr-2 text-right select-none text-gray-600"><%= n %></span><span><%= code %></span></div>
36
+ <% end %></code></pre></div>
37
+ <% end %>
38
+ <% end %>
39
+ </dd>
40
+ </div>
41
+ </dl>
42
+ </div>
43
+ <% end %>
44
+ <% end %>
45
+
46
+ <hr class="mt-4 pb-4">
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ SolidErrors::Engine.routes.draw do
2
+ root to: "errors#index"
3
+
4
+ resources :errors, only: [:index, :show, :update]
5
+ end
@@ -0,0 +1,48 @@
1
+ module SolidErrors
2
+ class Backtrace
3
+ # Holder for an Array of Backtrace::Line instances.
4
+ attr_reader :lines, :application_lines
5
+
6
+ def self.parse(ruby_backtrace, opts = {})
7
+ ruby_lines = ruby_backtrace.to_a
8
+
9
+ lines = ruby_lines.collect do |unparsed_line|
10
+ BacktraceLine.parse(unparsed_line.to_s, opts)
11
+ end.compact
12
+
13
+ instance = new(lines)
14
+ end
15
+
16
+ def initialize(lines)
17
+ self.lines = lines
18
+ self.application_lines = lines.select(&:application?)
19
+ end
20
+
21
+ # Convert Backtrace to array.
22
+ #
23
+ # Returns array containing backtrace lines.
24
+ def to_ary
25
+ lines.take(1000).map { |l| { :number => l.filtered_number, :file => l.filtered_file, :method => l.filtered_method, :source => l.source } }
26
+ end
27
+ alias :to_a :to_ary
28
+
29
+ # JSON support.
30
+ #
31
+ # Returns JSON representation of backtrace.
32
+ def as_json(options = {})
33
+ to_ary
34
+ end
35
+
36
+ def to_s
37
+ lines.map(&:to_s).join("\n")
38
+ end
39
+
40
+ def inspect
41
+ "<Backtrace: " + lines.collect { |line| line.inspect }.join(", ") + ">"
42
+ end
43
+
44
+ private
45
+
46
+ attr_writer :lines, :application_lines
47
+ end
48
+ end
@@ -0,0 +1,113 @@
1
+ module SolidErrors
2
+ class BacktraceLine
3
+ # Backtrace line regexp (optionally allowing leading X: for windows support).
4
+ INPUT_FORMAT = %r{^((?:[a-zA-Z]:)?[^:]+):(\d+)(?::in `([^']+)')?$}.freeze
5
+ STRING_EMPTY = ''.freeze
6
+ GEM_ROOT = '[GEM_ROOT]'.freeze
7
+ PROJECT_ROOT = '[PROJECT_ROOT]'.freeze
8
+ PROJECT_ROOT_CACHE = {}
9
+ GEM_ROOT_CACHE = {}
10
+ RELATIVE_ROOT = Regexp.new('^\.\/').freeze
11
+ RAILS_ROOT = ::Rails.root.to_s.dup.freeze
12
+ ROOT_REGEXP = Regexp.new("^#{ Regexp.escape(RAILS_ROOT) }").freeze
13
+ BACKTRACE_FILTERS = [
14
+ lambda { |line|
15
+ return line unless defined?(Gem)
16
+ GEM_ROOT_CACHE[line] ||= Gem.path.reduce(line) do |line, path|
17
+ line.sub(path, GEM_ROOT)
18
+ end
19
+ },
20
+ lambda { |line|
21
+ c = (PROJECT_ROOT_CACHE[RAILS_ROOT] ||= {})
22
+ return c[line] if c.has_key?(line)
23
+ c[line] ||= line.sub(ROOT_REGEXP, PROJECT_ROOT)
24
+ },
25
+ lambda { |line| line.sub(RELATIVE_ROOT, STRING_EMPTY) }
26
+ ].freeze
27
+
28
+ attr_reader :file
29
+ attr_reader :number
30
+ attr_reader :method
31
+ attr_reader :filtered_file, :filtered_number, :filtered_method
32
+
33
+ # Parses a single line of a given backtrace
34
+ #
35
+ # @param [String] unparsed_line The raw line from +caller+ or some backtrace.
36
+ #
37
+ # @return The parsed backtrace line.
38
+ def self.parse(unparsed_line, opts = {})
39
+ filtered_line = BACKTRACE_FILTERS.reduce(unparsed_line) do |line, proc|
40
+ proc.call(line)
41
+ end
42
+
43
+ if filtered_line
44
+ match = unparsed_line.match(INPUT_FORMAT) || [].freeze
45
+ fmatch = filtered_line.match(INPUT_FORMAT) || [].freeze
46
+
47
+ file, number, method = match[1], match[2], match[3]
48
+ filtered_args = [fmatch[1], fmatch[2], fmatch[3]]
49
+ new(file, number, method, *filtered_args, opts.fetch(:source_radius, 2))
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def initialize(file, number, method, filtered_file = file,
56
+ filtered_number = number, filtered_method = method,
57
+ source_radius = 2)
58
+ self.filtered_file = filtered_file
59
+ self.filtered_number = filtered_number
60
+ self.filtered_method = filtered_method
61
+ self.file = file
62
+ self.number = number
63
+ self.method = method
64
+ self.source_radius = source_radius
65
+ end
66
+
67
+ # Reconstructs the line in a readable fashion.
68
+ def to_s
69
+ "#{filtered_file}:#{filtered_number}:in `#{filtered_method}'"
70
+ end
71
+
72
+ def ==(other)
73
+ to_s == other.to_s
74
+ end
75
+
76
+ def inspect
77
+ "<Line:#{to_s}>"
78
+ end
79
+
80
+ # Determines if this line is part of the application trace or not.
81
+ def application?
82
+ (filtered_file =~ /^\[PROJECT_ROOT\]/i) && !(filtered_file =~ /^\[PROJECT_ROOT\]\/vendor/i)
83
+ end
84
+
85
+ def source
86
+ @source ||= get_source(file, number, source_radius)
87
+ end
88
+
89
+ private
90
+
91
+ attr_writer :file, :number, :method, :filtered_file, :filtered_number, :filtered_method
92
+
93
+ attr_accessor :source_radius
94
+
95
+ # Open source file and read line(s).
96
+ #
97
+ # Returns an array of line(s) from source file.
98
+ def get_source(file, number, radius = 2)
99
+ return {} unless file && File.exist?(file)
100
+
101
+ before = after = radius
102
+ start = (number.to_i - 1) - before
103
+ start = 0 and before = 1 if start <= 0
104
+ duration = before + 1 + after
105
+
106
+ l = 0
107
+ File.open(file) do |f|
108
+ start.times { f.gets ; l += 1 }
109
+ return Hash[duration.times.map { (line = f.gets) ? [(l += 1), line] : nil }.compact]
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidErrors
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace SolidErrors
6
+
7
+ rake_tasks do
8
+ load "solid_errors/tasks.rb"
9
+ end
10
+
11
+ config.solid_errors = ActiveSupport::OrderedOptions.new
12
+
13
+ initializer "solid_errors.config" do
14
+ config.solid_errors.each do |name, value|
15
+ SolidErrors.public_send("#{name}=", value)
16
+ end
17
+ end
18
+
19
+ initializer "solid_errors.active_record.error_subscriber" do
20
+ Rails.error.subscribe(SolidErrors::Subscriber.new)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ module SolidErrors
2
+ class Subscriber
3
+ IGNORED_ERRORS = ['ActionController::RoutingError',
4
+ 'AbstractController::ActionNotFound',
5
+ 'ActionController::MethodNotAllowed',
6
+ 'ActionController::UnknownHttpMethod',
7
+ 'ActionController::NotImplemented',
8
+ 'ActionController::UnknownFormat',
9
+ 'ActionController::InvalidAuthenticityToken',
10
+ 'ActionController::InvalidCrossOriginRequest',
11
+ 'ActionDispatch::Http::Parameters::ParseError',
12
+ 'ActionController::BadRequest',
13
+ 'ActionController::ParameterMissing',
14
+ 'ActiveRecord::RecordNotFound',
15
+ 'ActionController::UnknownAction',
16
+ 'ActionDispatch::Http::MimeNegotiation::InvalidType',
17
+ 'Rack::QueryParser::ParameterTypeError',
18
+ 'Rack::QueryParser::InvalidParameterError',
19
+ 'CGI::Session::CookieStore::TamperedWithCookie',
20
+ 'Mongoid::Errors::DocumentNotFound',
21
+ 'Sinatra::NotFound',
22
+ 'Sidekiq::JobRetry::Skip'].map(&:freeze).freeze
23
+
24
+
25
+ def report(error, handled:, severity:, context:, source: nil)
26
+ return if ignore_by_class?(error.class.name)
27
+
28
+ error_attributes = {
29
+ exception_class: error.class.name,
30
+ message: s(error.message),
31
+ severity: severity,
32
+ source: source
33
+ }
34
+ if (record = SolidErrors::Error.find_by(error_attributes))
35
+ record.update!(resolved_at: nil, updated_at: Time.now)
36
+ else
37
+ record = SolidErrors::Error.create!(error_attributes)
38
+ end
39
+
40
+ SolidErrors::Occurrence.create(
41
+ error_id: record.id,
42
+ backtrace: error.backtrace.join("\n"),
43
+ context: s(context)
44
+ )
45
+ end
46
+
47
+ def s(data)
48
+ Sanitizer.sanitize(data)
49
+ end
50
+
51
+ def ignore_by_class?(error_class_name)
52
+ IGNORED_ERRORS.any? do |ignored_class|
53
+ ignored_class_name = ignored_class.respond_to?(:name) ? ignored_class.name : ignored_class
54
+
55
+ ignored_class_name == error_class_name
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidErrors
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/solid_errors.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "solid_errors/version"
4
+ require_relative "solid_errors/subscriber"
5
+ require_relative "solid_errors/backtrace"
6
+ require_relative "solid_errors/backtrace_line"
7
+ require_relative "solid_errors/engine"
4
8
 
5
9
  module SolidErrors
6
- class Error < StandardError; end
7
- # Your code goes here...
10
+ mattr_accessor :connects_to
8
11
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_errors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
11
  date: 2024-01-14 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
13
27
  description:
14
28
  email:
15
29
  - stephen.margheim@gmail.com
@@ -17,24 +31,32 @@ executables: []
17
31
  extensions: []
18
32
  extra_rdoc_files: []
19
33
  files:
20
- - ".standard.yml"
21
- - CHANGELOG.md
22
- - CODE_OF_CONDUCT.md
23
- - Gemfile
24
- - LICENSE.txt
25
34
  - README.md
26
35
  - Rakefile
36
+ - app/controllers/solid_errors/errors_controller.rb
37
+ - app/models/solid_errors/error.rb
38
+ - app/models/solid_errors/occurrence.rb
39
+ - app/models/solid_errors/record.rb
40
+ - app/views/layouts/application.html.erb
41
+ - app/views/solid_errors/errors/_actions.html.erb
42
+ - app/views/solid_errors/errors/_error.html.erb
43
+ - app/views/solid_errors/errors/index.html.erb
44
+ - app/views/solid_errors/errors/show.html.erb
45
+ - app/views/solid_errors/occurrences/_collection.html.erb
46
+ - app/views/solid_errors/occurrences/_occurrence.html.erb
47
+ - config/routes.rb
27
48
  - lib/solid_errors.rb
49
+ - lib/solid_errors/backtrace.rb
50
+ - lib/solid_errors/backtrace_line.rb
51
+ - lib/solid_errors/engine.rb
52
+ - lib/solid_errors/subscriber.rb
28
53
  - lib/solid_errors/version.rb
29
- - sig/solid_errors.rbs
30
- - solid_errors.gemspec
31
54
  homepage: https://github.com/fractaledmind/solid_errors
32
55
  licenses:
33
56
  - MIT
34
57
  metadata:
35
58
  homepage_uri: https://github.com/fractaledmind/solid_errors
36
59
  source_code_uri: https://github.com/fractaledmind/solid_errors
37
- changelog_uri: https://github.com/fractaledmind/solid_errors/blob/main/CHANGELOG.md
38
60
  post_install_message:
39
61
  rdoc_options: []
40
62
  require_paths:
data/.standard.yml DELETED
@@ -1,3 +0,0 @@
1
- # For available configuration options, see:
2
- # https://github.com/testdouble/standard
3
- ruby_version: 2.6
data/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [Unreleased]
2
-
3
- ## [0.1.0] - 2024-01-14
4
-
5
- - Initial release