rails_pulse 0.2.5.pre.4 → 0.2.5.pre.5

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.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_pulse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5.pre.4
4
+ version: 0.2.5.pre.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Pulse
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-18 00:00:00.000000000 Z
10
+ date: 2026-01-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -139,6 +139,34 @@ dependencies:
139
139
  - - ">="
140
140
  - !ruby/object:Gem::Version
141
141
  version: '1.0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: minitest
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '5.0'
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '5.0'
156
+ - !ruby/object:Gem::Dependency
157
+ name: ostruct
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ type: :development
164
+ prerelease: false
165
+ version_requirements: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
142
170
  description: Ruby on Rails performance monitoring tool that provides insights into
143
171
  your application's performance, helping you identify bottlenecks and optimize your
144
172
  code for better efficiency.
@@ -333,9 +361,7 @@ files:
333
361
  - config/importmap.rb
334
362
  - config/initializers/rails_pulse.rb
335
363
  - config/routes.rb
336
- - db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb
337
- - db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb
338
- - db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb
364
+ - db/rails_pulse_migrate/20260117000000_optimize_rails_pulse_indexes.rb
339
365
  - db/rails_pulse_schema.rb
340
366
  - lib/generators/rails_pulse/convert_to_migrations_generator.rb
341
367
  - lib/generators/rails_pulse/install_generator.rb
@@ -1,95 +0,0 @@
1
- # Add background job tracking to Rails Pulse
2
- class AddJobsToRailsPulse < ActiveRecord::Migration[7.0]
3
- def up
4
- # Create jobs table for storing job definitions
5
- unless table_exists?(:rails_pulse_jobs)
6
- create_table :rails_pulse_jobs do |t|
7
- t.string :name, null: false, comment: "Job class name"
8
- t.string :queue_name, comment: "Default queue"
9
- t.text :description, comment: "Optional description"
10
- t.integer :runs_count, null: false, default: 0, comment: "Cache of total runs"
11
- t.integer :failures_count, null: false, default: 0, comment: "Cache of failed runs"
12
- t.integer :retries_count, null: false, default: 0, comment: "Cache of retried runs"
13
- t.decimal :avg_duration, precision: 15, scale: 6, comment: "Average duration in milliseconds"
14
- t.text :tags, comment: "JSON array of tags"
15
- t.timestamps
16
- end
17
-
18
- add_index :rails_pulse_jobs, :name, unique: true, name: "index_rails_pulse_jobs_on_name"
19
- add_index :rails_pulse_jobs, :queue_name, name: "index_rails_pulse_jobs_on_queue"
20
- add_index :rails_pulse_jobs, :runs_count, name: "index_rails_pulse_jobs_on_runs_count"
21
- end
22
-
23
- # Create job_runs table for individual job executions
24
- unless table_exists?(:rails_pulse_job_runs)
25
- create_table :rails_pulse_job_runs do |t|
26
- t.references :job, null: false, foreign_key: { to_table: :rails_pulse_jobs }, comment: "Link to job definition"
27
- t.string :run_id, null: false, comment: "Adapter specific run id"
28
- t.decimal :duration, precision: 15, scale: 6, comment: "Execution duration in milliseconds"
29
- t.string :status, null: false, comment: "Execution status"
30
- t.string :error_class, comment: "Error class name"
31
- t.text :error_message, comment: "Error message"
32
- t.integer :attempts, null: false, default: 0, comment: "Retry attempts"
33
- t.timestamp :occurred_at, null: false, comment: "When the job started"
34
- t.timestamp :enqueued_at, comment: "When the job was enqueued"
35
- t.text :arguments, comment: "Serialized arguments"
36
- t.string :adapter, comment: "Queue adapter"
37
- t.text :tags, comment: "Execution tags"
38
- t.timestamps
39
- end
40
-
41
- add_index :rails_pulse_job_runs, :run_id, unique: true, name: "index_rails_pulse_job_runs_on_run_id"
42
- add_index :rails_pulse_job_runs, [ :job_id, :occurred_at ], name: "index_rails_pulse_job_runs_on_job_and_occurred"
43
- add_index :rails_pulse_job_runs, :occurred_at, name: "index_rails_pulse_job_runs_on_occurred_at"
44
- add_index :rails_pulse_job_runs, :status, name: "index_rails_pulse_job_runs_on_status"
45
- add_index :rails_pulse_job_runs, [ :job_id, :status ], name: "index_rails_pulse_job_runs_on_job_and_status"
46
- end
47
-
48
- # Add job_run_id to operations table if it doesn't exist
49
- if table_exists?(:rails_pulse_operations) && !column_exists?(:rails_pulse_operations, :job_run_id)
50
- # Make request_id nullable to allow job operations
51
- change_column_null :rails_pulse_operations, :request_id, true
52
-
53
- # Add job_run_id reference
54
- add_reference :rails_pulse_operations, :job_run,
55
- null: true,
56
- foreign_key: { to_table: :rails_pulse_job_runs },
57
- comment: "Link to a background job execution"
58
-
59
- # Add check constraint for PostgreSQL and MySQL to ensure either request_id or job_run_id is present
60
- adapter = connection.adapter_name.downcase
61
- if adapter.include?("postgres") || adapter.include?("mysql")
62
- execute <<-SQL
63
- ALTER TABLE rails_pulse_operations
64
- ADD CONSTRAINT rails_pulse_operations_request_or_job_run
65
- CHECK (request_id IS NOT NULL OR job_run_id IS NOT NULL)
66
- SQL
67
- end
68
- end
69
- end
70
-
71
- def down
72
- # Remove check constraint first
73
- adapter = connection.adapter_name.downcase
74
- if adapter.include?("postgres") || adapter.include?("mysql")
75
- execute <<-SQL
76
- ALTER TABLE rails_pulse_operations
77
- DROP CONSTRAINT IF EXISTS rails_pulse_operations_request_or_job_run
78
- SQL
79
- end
80
-
81
- # Remove job_run_id from operations
82
- if column_exists?(:rails_pulse_operations, :job_run_id)
83
- remove_reference :rails_pulse_operations, :job_run, foreign_key: { to_table: :rails_pulse_job_runs }
84
- end
85
-
86
- # Make request_id non-nullable again
87
- if column_exists?(:rails_pulse_operations, :request_id)
88
- change_column_null :rails_pulse_operations, :request_id, false
89
- end
90
-
91
- # Drop job tables
92
- drop_table :rails_pulse_job_runs if table_exists?(:rails_pulse_job_runs)
93
- drop_table :rails_pulse_jobs if table_exists?(:rails_pulse_jobs)
94
- end
95
- end
@@ -1,150 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Add query fingerprinting to handle long SQL queries
4
- # Uses MD5 hash of normalized SQL as unique identifier
5
- class AddQueryFingerprinting < ActiveRecord::Migration[7.0]
6
- def up
7
- return unless table_exists?(:rails_pulse_queries)
8
-
9
- # Add hashed_sql column if it doesn't exist
10
- unless column_exists?(:rails_pulse_queries, :hashed_sql)
11
- say "Adding hashed_sql column to rails_pulse_queries..."
12
- add_column :rails_pulse_queries, :hashed_sql, :string, limit: 32
13
-
14
- # Backfill existing records with MD5 hash
15
- say "Backfilling query hashes for existing records..."
16
- backfill_query_hashes
17
-
18
- # Make it required and unique
19
- say "Adding constraints and indexes..."
20
- change_column_null :rails_pulse_queries, :hashed_sql, false
21
- add_index :rails_pulse_queries, :hashed_sql, unique: true,
22
- name: "index_rails_pulse_queries_on_hashed_sql"
23
-
24
- # Remove old index
25
- say "Removing old normalized_sql index..."
26
- if index_exists?(:rails_pulse_queries, :normalized_sql, name: "index_rails_pulse_queries_on_normalized_sql")
27
- remove_index :rails_pulse_queries, :normalized_sql, name: "index_rails_pulse_queries_on_normalized_sql"
28
- end
29
-
30
- # Change normalized_sql to text (remove 1000 char limit)
31
- say "Changing normalized_sql to text type (removing length limit)..."
32
- change_column :rails_pulse_queries, :normalized_sql, :text
33
-
34
- say "Query fingerprinting migration completed successfully!", :green
35
- else
36
- say "Query fingerprinting already applied. Skipping.", :yellow
37
- end
38
- end
39
-
40
- def down
41
- # Prevent rollback if there are queries longer than 1000 characters
42
- if has_long_queries?
43
- raise ActiveRecord::IrreversibleMigration,
44
- "Cannot rollback: normalized_sql contains queries longer than 1000 characters. " \
45
- "Rolling back would truncate data."
46
- end
47
-
48
- return unless column_exists?(:rails_pulse_queries, :hashed_sql)
49
-
50
- say "Rolling back query fingerprinting changes..."
51
-
52
- # Restore varchar limit (safe because we checked for long queries)
53
- change_column :rails_pulse_queries, :normalized_sql, :string, limit: 1000
54
-
55
- # Restore old index
56
- add_index :rails_pulse_queries, :normalized_sql, unique: true,
57
- name: "index_rails_pulse_queries_on_normalized_sql", length: 191
58
-
59
- # Remove new index
60
- if index_exists?(:rails_pulse_queries, :hashed_sql, name: "index_rails_pulse_queries_on_hashed_sql")
61
- remove_index :rails_pulse_queries, :hashed_sql, name: "index_rails_pulse_queries_on_hashed_sql"
62
- end
63
-
64
- # Remove hashed_sql column
65
- remove_column :rails_pulse_queries, :hashed_sql
66
-
67
- say "Rollback completed.", :green
68
- end
69
-
70
- private
71
-
72
- def backfill_query_hashes
73
- adapter = connection.adapter_name.downcase
74
-
75
- if adapter.include?("postgres") || adapter.include?("mysql")
76
- # Use database MD5 function for better performance
77
- execute <<-SQL
78
- UPDATE rails_pulse_queries
79
- SET hashed_sql = MD5(normalized_sql)
80
- WHERE hashed_sql IS NULL
81
- SQL
82
- else
83
- # SQLite - use Ruby MD5 (slower but works)
84
- require "digest"
85
- RailsPulse::Query.where(hashed_sql: nil).find_each do |query|
86
- query.update_column(:hashed_sql, Digest::MD5.hexdigest(query.normalized_sql))
87
- end
88
- end
89
-
90
- # Handle potential duplicates (queries with same normalized SQL)
91
- handle_duplicate_hashes
92
- end
93
-
94
- def handle_duplicate_hashes
95
- # Group queries by hash and find duplicates
96
- query_groups = RailsPulse::Query
97
- .select(:hashed_sql)
98
- .group(:hashed_sql)
99
- .having("COUNT(*) > 1")
100
- .pluck(:hashed_sql)
101
-
102
- return if query_groups.empty?
103
-
104
- say "Found #{query_groups.size} duplicate query groups. Merging...", :yellow
105
-
106
- query_groups.each do |hash|
107
- # Get all queries with this hash, ordered by creation time
108
- queries = RailsPulse::Query.where(hashed_sql: hash).order(:created_at).to_a
109
- keep_query = queries.first
110
- duplicate_queries = queries[1..]
111
-
112
- duplicate_queries.each do |dup_query|
113
- # Count operations before merge
114
- operations_count = RailsPulse::Operation.where(query_id: dup_query.id).count
115
-
116
- if operations_count > 0
117
- say " Merging #{operations_count} operations from query ##{dup_query.id} into ##{keep_query.id}"
118
-
119
- # Reassign operations to the kept query
120
- RailsPulse::Operation.where(query_id: dup_query.id).update_all(query_id: keep_query.id)
121
- end
122
-
123
- # Delete the duplicate query
124
- dup_query.delete
125
- end
126
- end
127
-
128
- say "Merged #{query_groups.size} duplicate query groups successfully.", :green
129
- end
130
-
131
- def has_long_queries?
132
- # Check if any queries exceed 1000 characters
133
- adapter = connection.adapter_name.downcase
134
-
135
- if adapter.include?("postgres")
136
- result = execute("SELECT EXISTS(SELECT 1 FROM rails_pulse_queries WHERE LENGTH(normalized_sql) > 1000)")
137
- # Handle both Rails 7.2 and 8.0 result formats
138
- result.first.is_a?(Hash) ? result.first["exists"] == "t" : result.first[0] == "t"
139
- elsif adapter.include?("mysql")
140
- result = execute("SELECT EXISTS(SELECT 1 FROM rails_pulse_queries WHERE LENGTH(normalized_sql) > 1000) as result")
141
- # Handle both result formats
142
- result.first.is_a?(Hash) ? result.first["result"] == 1 : result.first[0] == 1
143
- else
144
- # SQLite
145
- result = execute("SELECT COUNT(*) as count FROM rails_pulse_queries WHERE LENGTH(normalized_sql) > 1000")
146
- count = result.first.is_a?(Hash) ? result.first["count"] : result.first[0]
147
- count.to_i > 0
148
- end
149
- end
150
- end
@@ -1,14 +0,0 @@
1
- # Add index to rails_pulse_requests.request_uuid for efficient lookups
2
- class AddIndexToRequestUuid < ActiveRecord::Migration[7.0]
3
- def up
4
- unless index_exists?(:rails_pulse_requests, :request_uuid)
5
- add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
6
- end
7
- end
8
-
9
- def down
10
- if index_exists?(:rails_pulse_requests, :request_uuid)
11
- remove_index :rails_pulse_requests, name: "index_rails_pulse_requests_on_request_uuid"
12
- end
13
- end
14
- end