rails_pulse 0.2.4 → 0.2.5.pre.pre.3

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -12
  3. data/Rakefile +142 -8
  4. data/app/assets/stylesheets/rails_pulse/components/table.css +16 -1
  5. data/app/assets/stylesheets/rails_pulse/components/tags.css +7 -2
  6. data/app/assets/stylesheets/rails_pulse/components/utilities.css +3 -0
  7. data/app/controllers/concerns/chart_table_concern.rb +2 -1
  8. data/app/controllers/rails_pulse/application_controller.rb +11 -1
  9. data/app/controllers/rails_pulse/assets_controller.rb +18 -2
  10. data/app/controllers/rails_pulse/job_runs_controller.rb +37 -0
  11. data/app/controllers/rails_pulse/jobs_controller.rb +80 -0
  12. data/app/controllers/rails_pulse/operations_controller.rb +43 -31
  13. data/app/controllers/rails_pulse/queries_controller.rb +1 -1
  14. data/app/controllers/rails_pulse/requests_controller.rb +3 -9
  15. data/app/controllers/rails_pulse/routes_controller.rb +1 -1
  16. data/app/controllers/rails_pulse/tags_controller.rb +31 -5
  17. data/app/helpers/rails_pulse/application_helper.rb +32 -1
  18. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +15 -1
  19. data/app/helpers/rails_pulse/status_helper.rb +16 -0
  20. data/app/helpers/rails_pulse/tags_helper.rb +39 -1
  21. data/app/javascript/rails_pulse/controllers/chart_controller.js +112 -8
  22. data/app/models/concerns/rails_pulse/taggable.rb +25 -2
  23. data/app/models/rails_pulse/charts/operations_chart.rb +33 -0
  24. data/app/models/rails_pulse/dashboard/charts/p95_response_time.rb +1 -2
  25. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
  26. data/app/models/rails_pulse/job.rb +85 -0
  27. data/app/models/rails_pulse/job_run.rb +76 -0
  28. data/app/models/rails_pulse/jobs/cards/average_duration.rb +85 -0
  29. data/app/models/rails_pulse/jobs/cards/base.rb +70 -0
  30. data/app/models/rails_pulse/jobs/cards/failure_rate.rb +85 -0
  31. data/app/models/rails_pulse/jobs/cards/total_jobs.rb +74 -0
  32. data/app/models/rails_pulse/jobs/cards/total_runs.rb +48 -0
  33. data/app/models/rails_pulse/operation.rb +16 -3
  34. data/app/models/rails_pulse/queries/cards/average_query_times.rb +3 -3
  35. data/app/models/rails_pulse/queries/cards/execution_rate.rb +1 -1
  36. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
  37. data/app/models/rails_pulse/queries/tables/index.rb +2 -1
  38. data/app/models/rails_pulse/query.rb +10 -1
  39. data/app/models/rails_pulse/routes/cards/average_response_times.rb +3 -2
  40. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
  41. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
  42. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +1 -1
  43. data/app/models/rails_pulse/routes/tables/index.rb +2 -1
  44. data/app/models/rails_pulse/summary.rb +10 -3
  45. data/app/services/rails_pulse/summary_service.rb +46 -0
  46. data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
  47. data/app/views/layouts/rails_pulse/application.html.erb +23 -0
  48. data/app/views/rails_pulse/components/_active_filters.html.erb +7 -6
  49. data/app/views/rails_pulse/components/_page_header.html.erb +8 -7
  50. data/app/views/rails_pulse/components/_table.html.erb +7 -4
  51. data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
  52. data/app/views/rails_pulse/job_runs/_operations.html.erb +78 -0
  53. data/app/views/rails_pulse/job_runs/index.html.erb +3 -0
  54. data/app/views/rails_pulse/job_runs/show.html.erb +51 -0
  55. data/app/views/rails_pulse/jobs/_job_runs_table.html.erb +35 -0
  56. data/app/views/rails_pulse/jobs/_table.html.erb +43 -0
  57. data/app/views/rails_pulse/jobs/index.html.erb +34 -0
  58. data/app/views/rails_pulse/jobs/show.html.erb +49 -0
  59. data/app/views/rails_pulse/operations/_operation_analysis_application.html.erb +29 -27
  60. data/app/views/rails_pulse/operations/_operation_analysis_view.html.erb +11 -9
  61. data/app/views/rails_pulse/operations/show.html.erb +10 -8
  62. data/app/views/rails_pulse/queries/_table.html.erb +3 -3
  63. data/app/views/rails_pulse/requests/_table.html.erb +6 -6
  64. data/app/views/rails_pulse/routes/_table.html.erb +3 -3
  65. data/app/views/rails_pulse/routes/show.html.erb +1 -1
  66. data/app/views/rails_pulse/tags/_tag_manager.html.erb +7 -14
  67. data/config/brakeman.ignore +213 -0
  68. data/config/brakeman.yml +68 -0
  69. data/config/initializers/rails_pulse.rb +52 -0
  70. data/config/routes.rb +6 -0
  71. data/db/rails_pulse_migrate/20250113000000_add_jobs_to_rails_pulse.rb +95 -0
  72. data/db/rails_pulse_migrate/20250122000000_add_query_fingerprinting.rb +150 -0
  73. data/db/rails_pulse_migrate/20250202000000_add_index_to_request_uuid.rb +14 -0
  74. data/db/rails_pulse_schema.rb +186 -103
  75. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +186 -103
  76. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +30 -1
  77. data/lib/generators/rails_pulse/templates/rails_pulse.rb +31 -0
  78. data/lib/rails_pulse/active_job_extensions.rb +13 -0
  79. data/lib/rails_pulse/adapters/delayed_job_plugin.rb +25 -0
  80. data/lib/rails_pulse/adapters/sidekiq_middleware.rb +41 -0
  81. data/lib/rails_pulse/cleanup_service.rb +65 -0
  82. data/lib/rails_pulse/configuration.rb +80 -7
  83. data/lib/rails_pulse/engine.rb +34 -3
  84. data/lib/rails_pulse/extensions/active_record.rb +82 -0
  85. data/lib/rails_pulse/job_run_collector.rb +172 -0
  86. data/lib/rails_pulse/middleware/request_collector.rb +20 -43
  87. data/lib/rails_pulse/subscribers/operation_subscriber.rb +11 -5
  88. data/lib/rails_pulse/tracker.rb +82 -0
  89. data/lib/rails_pulse/version.rb +1 -1
  90. data/lib/rails_pulse.rb +2 -0
  91. data/lib/rails_pulse_server.ru +107 -0
  92. data/lib/tasks/rails_pulse_benchmark.rake +382 -0
  93. data/public/rails-pulse-assets/rails-pulse-icons.js +3 -2
  94. data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
  95. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  96. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  97. data/public/rails-pulse-assets/rails-pulse.js +1 -1
  98. data/public/rails-pulse-assets/rails-pulse.js.map +3 -3
  99. metadata +37 -9
  100. data/app/models/rails_pulse/requests/charts/operations_chart.rb +0 -35
  101. data/db/migrate/20250930105043_install_rails_pulse_tables.rb +0 -23
@@ -3,128 +3,211 @@
3
3
  # Load with: rails db:schema:load:rails_pulse or db:prepare
4
4
 
5
5
  RailsPulse::Schema = lambda do |connection|
6
+ adapter = connection.adapter_name.downcase
6
7
  # Skip if all tables already exist to prevent conflicts
7
- required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries ]
8
+ required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_jobs, :rails_pulse_job_runs, :rails_pulse_summaries ]
8
9
 
9
- if ENV["CI"] == "true"
10
- existing_tables = required_tables.select { |table| connection.table_exists?(table) }
11
- missing_tables = required_tables - existing_tables
12
- puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
13
- puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
10
+ # Check which tables already exist
11
+ existing_tables = required_tables.select { |table| connection.table_exists?(table) }
12
+ missing_tables = required_tables - existing_tables
13
+
14
+ # Always log for transparency (not just in CI)
15
+ if existing_tables.any?
16
+ puts "[RailsPulse::Schema] Existing tables detected: #{existing_tables.join(', ')}"
17
+ end
18
+
19
+ if missing_tables.any?
20
+ puts "[RailsPulse::Schema] Creating missing tables: #{missing_tables.join(', ')}"
14
21
  end
15
22
 
16
- return if required_tables.all? { |table| connection.table_exists?(table) }
23
+ # If all tables exist, skip creation entirely
24
+ if missing_tables.empty?
25
+ puts "[RailsPulse::Schema] All Rails Pulse tables already exist. Skipping schema load."
26
+ return
27
+ end
28
+
29
+ unless connection.table_exists?(:rails_pulse_routes)
30
+ connection.create_table :rails_pulse_routes do |t|
31
+ t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
32
+ t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
33
+ t.text :tags, comment: "JSON array of tags for filtering and categorization"
34
+ t.timestamps
35
+ end
36
+
37
+ connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
38
+ end
17
39
 
18
- connection.create_table :rails_pulse_routes do |t|
19
- t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
20
- t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
21
- t.text :tags, comment: "JSON array of tags for filtering and categorization"
22
- t.timestamps
40
+ unless connection.table_exists?(:rails_pulse_queries)
41
+ connection.create_table :rails_pulse_queries do |t|
42
+ t.string :hashed_sql, limit: 32, null: false, comment: "MD5 hash of normalized SQL for fast lookups and uniqueness"
43
+ t.text :normalized_sql, null: false, comment: "Full normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
44
+ t.datetime :analyzed_at, comment: "When query analysis was last performed"
45
+ t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
46
+ t.text :issues, comment: "JSON array of detected performance issues"
47
+ t.text :metadata, comment: "JSON object containing query complexity metrics"
48
+ t.text :query_stats, comment: "JSON object with query characteristics analysis"
49
+ t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
50
+ t.text :index_recommendations, comment: "JSON array of database index recommendations"
51
+ t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
52
+ t.text :suggestions, comment: "JSON array of optimization recommendations"
53
+ t.text :tags, comment: "JSON array of tags for filtering and categorization"
54
+ t.timestamps
55
+ end
56
+
57
+ connection.add_index :rails_pulse_queries, :hashed_sql, unique: true, name: "index_rails_pulse_queries_on_hashed_sql"
23
58
  end
24
59
 
25
- connection.add_index :rails_pulse_routes, [ :method, :path ], unique: true, name: "index_rails_pulse_routes_on_method_and_path"
26
-
27
- connection.create_table :rails_pulse_queries do |t|
28
- t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
29
- t.datetime :analyzed_at, comment: "When query analysis was last performed"
30
- t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
31
- t.text :issues, comment: "JSON array of detected performance issues"
32
- t.text :metadata, comment: "JSON object containing query complexity metrics"
33
- t.text :query_stats, comment: "JSON object with query characteristics analysis"
34
- t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
35
- t.text :index_recommendations, comment: "JSON array of database index recommendations"
36
- t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
37
- t.text :suggestions, comment: "JSON array of optimization recommendations"
38
- t.text :tags, comment: "JSON array of tags for filtering and categorization"
39
- t.timestamps
60
+ unless connection.table_exists?(:rails_pulse_requests)
61
+ connection.create_table :rails_pulse_requests do |t|
62
+ t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
63
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
64
+ t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
65
+ t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
66
+ t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
67
+ t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
68
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
69
+ t.text :tags, comment: "JSON array of tags for filtering and categorization"
70
+ t.timestamps
71
+ end
72
+
73
+ connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
74
+ connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
75
+ connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
40
76
  end
41
77
 
42
- connection.add_index :rails_pulse_queries, :normalized_sql, unique: true, name: "index_rails_pulse_queries_on_normalized_sql", length: 191
43
-
44
- connection.create_table :rails_pulse_requests do |t|
45
- t.references :route, null: false, foreign_key: { to_table: :rails_pulse_routes }, comment: "Link to the route"
46
- t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Total request duration in milliseconds"
47
- t.integer :status, null: false, comment: "HTTP status code (e.g., 200, 500)"
48
- t.boolean :is_error, null: false, default: false, comment: "True if status >= 500"
49
- t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
50
- t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
51
- t.timestamp :occurred_at, null: false, comment: "When the request started"
52
- t.text :tags, comment: "JSON array of tags for filtering and categorization"
53
- t.timestamps
78
+ unless connection.table_exists?(:rails_pulse_jobs)
79
+ connection.create_table :rails_pulse_jobs do |t|
80
+ t.string :name, null: false, comment: "Job class name"
81
+ t.string :queue_name, comment: "Default queue"
82
+ t.text :description, comment: "Optional description"
83
+ t.integer :runs_count, null: false, default: 0, comment: "Cache of total runs"
84
+ t.integer :failures_count, null: false, default: 0, comment: "Cache of failed runs"
85
+ t.integer :retries_count, null: false, default: 0, comment: "Cache of retried runs"
86
+ t.decimal :avg_duration, precision: 15, scale: 6, comment: "Average duration in milliseconds"
87
+ t.text :tags, comment: "JSON array of tags"
88
+ t.timestamps
89
+ end
90
+
91
+ connection.add_index :rails_pulse_jobs, :name, unique: true, name: "index_rails_pulse_jobs_on_name"
92
+ connection.add_index :rails_pulse_jobs, :queue_name, name: "index_rails_pulse_jobs_on_queue"
93
+ connection.add_index :rails_pulse_jobs, :runs_count, name: "index_rails_pulse_jobs_on_runs_count"
54
94
  end
55
95
 
56
- connection.add_index :rails_pulse_requests, :occurred_at, name: "index_rails_pulse_requests_on_occurred_at"
57
- connection.add_index :rails_pulse_requests, :request_uuid, unique: true, name: "index_rails_pulse_requests_on_request_uuid"
58
- connection.add_index :rails_pulse_requests, [ :route_id, :occurred_at ], name: "index_rails_pulse_requests_on_route_id_and_occurred_at"
59
-
60
- connection.create_table :rails_pulse_operations do |t|
61
- t.references :request, null: false, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
62
- t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
63
- t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
64
- t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
65
- t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
66
- t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
67
- t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
68
- t.timestamp :occurred_at, null: false, comment: "When the request started"
69
- t.timestamps
96
+ unless connection.table_exists?(:rails_pulse_job_runs)
97
+ connection.create_table :rails_pulse_job_runs do |t|
98
+ t.references :job, null: false, foreign_key: { to_table: :rails_pulse_jobs }, comment: "Link to job definition"
99
+ t.string :run_id, null: false, comment: "Adapter specific run id"
100
+ t.decimal :duration, precision: 15, scale: 6, comment: "Execution duration in milliseconds"
101
+ t.string :status, null: false, comment: "Execution status"
102
+ t.string :error_class, comment: "Error class name"
103
+ t.text :error_message, comment: "Error message"
104
+ t.integer :attempts, null: false, default: 0, comment: "Retry attempts"
105
+ t.timestamp :occurred_at, null: false, comment: "When the job started"
106
+ t.timestamp :enqueued_at, comment: "When the job was enqueued"
107
+ t.text :arguments, comment: "Serialized arguments"
108
+ t.string :adapter, comment: "Queue adapter"
109
+ t.text :tags, comment: "Execution tags"
110
+ t.timestamps
111
+ end
112
+
113
+ connection.add_index :rails_pulse_job_runs, :run_id, unique: true, name: "index_rails_pulse_job_runs_on_run_id"
114
+ connection.add_index :rails_pulse_job_runs, [ :job_id, :occurred_at ], name: "index_rails_pulse_job_runs_on_job_and_occurred"
115
+ connection.add_index :rails_pulse_job_runs, :occurred_at, name: "index_rails_pulse_job_runs_on_occurred_at"
116
+ connection.add_index :rails_pulse_job_runs, :status, name: "index_rails_pulse_job_runs_on_status"
117
+ connection.add_index :rails_pulse_job_runs, [ :job_id, :status ], name: "index_rails_pulse_job_runs_on_job_and_status"
70
118
  end
71
119
 
72
- connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
73
- connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
74
- connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
75
- connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
76
- connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
77
-
78
- connection.create_table :rails_pulse_summaries do |t|
79
- # Time fields
80
- t.datetime :period_start, null: false, comment: "Start of the aggregation period"
81
- t.datetime :period_end, null: false, comment: "End of the aggregation period"
82
- t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
83
-
84
- # Polymorphic association to handle both routes and queries
85
- t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
86
- # This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
87
- # and summarizable_id (route_id or query_id)
88
-
89
- # Universal metrics
90
- t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
91
- t.float :avg_duration, comment: "Average duration in milliseconds"
92
- t.float :min_duration, comment: "Minimum duration in milliseconds"
93
- t.float :max_duration, comment: "Maximum duration in milliseconds"
94
- t.float :p50_duration, comment: "50th percentile duration"
95
- t.float :p95_duration, comment: "95th percentile duration"
96
- t.float :p99_duration, comment: "99th percentile duration"
97
- t.float :total_duration, comment: "Total duration in milliseconds"
98
- t.float :stddev_duration, comment: "Standard deviation of duration"
99
-
100
- # Request/Route specific metrics
101
- t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
102
- t.integer :success_count, default: 0, comment: "Number of successful responses"
103
- t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
104
- t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
105
- t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
106
- t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
107
-
108
- t.timestamps
120
+ unless connection.table_exists?(:rails_pulse_operations)
121
+ connection.create_table :rails_pulse_operations do |t|
122
+ t.references :request, null: true, foreign_key: { to_table: :rails_pulse_requests }, comment: "Link to the request"
123
+ t.references :job_run, null: true, foreign_key: { to_table: :rails_pulse_job_runs }, comment: "Link to a background job execution"
124
+ t.references :query, foreign_key: { to_table: :rails_pulse_queries }, index: true, comment: "Link to the normalized SQL query"
125
+ t.string :operation_type, null: false, comment: "Type of operation (e.g., database, view, gem_call)"
126
+ t.string :label, null: false, comment: "Descriptive name (e.g., SELECT FROM users WHERE id = 1, render layout)"
127
+ t.decimal :duration, precision: 15, scale: 6, null: false, comment: "Operation duration in milliseconds"
128
+ t.string :codebase_location, comment: "File and line number (e.g., app/models/user.rb:25)"
129
+ t.float :start_time, null: false, default: 0.0, comment: "Operation start time in milliseconds"
130
+ t.timestamp :occurred_at, null: false, comment: "When the request started"
131
+ t.timestamps
132
+ end
133
+
134
+ connection.add_index :rails_pulse_operations, :operation_type, name: "index_rails_pulse_operations_on_operation_type"
135
+ connection.add_index :rails_pulse_operations, :occurred_at, name: "index_rails_pulse_operations_on_occurred_at"
136
+ connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
137
+ connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
138
+ connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
139
+
140
+ if adapter.include?("postgres") || adapter.include?("mysql")
141
+ connection.add_check_constraint :rails_pulse_operations,
142
+ "(request_id IS NOT NULL OR job_run_id IS NOT NULL)",
143
+ name: "rails_pulse_operations_request_or_job_run"
144
+ end
109
145
  end
110
146
 
111
- # Unique constraint and indexes for summaries
112
- connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
113
- unique: true,
114
- name: "idx_pulse_summaries_unique"
115
- connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
116
- connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
147
+ unless connection.table_exists?(:rails_pulse_summaries)
148
+ connection.create_table :rails_pulse_summaries do |t|
149
+ # Time fields
150
+ t.datetime :period_start, null: false, comment: "Start of the aggregation period"
151
+ t.datetime :period_end, null: false, comment: "End of the aggregation period"
152
+ t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
153
+
154
+ # Polymorphic association to handle both routes and queries
155
+ t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
156
+ # This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
157
+ # and summarizable_id (route_id or query_id)
158
+
159
+ # Universal metrics
160
+ t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
161
+ t.float :avg_duration, comment: "Average duration in milliseconds"
162
+ t.float :min_duration, comment: "Minimum duration in milliseconds"
163
+ t.float :max_duration, comment: "Maximum duration in milliseconds"
164
+ t.float :p50_duration, comment: "50th percentile duration"
165
+ t.float :p95_duration, comment: "95th percentile duration"
166
+ t.float :p99_duration, comment: "99th percentile duration"
167
+ t.float :total_duration, comment: "Total duration in milliseconds"
168
+ t.float :stddev_duration, comment: "Standard deviation of duration"
169
+
170
+ # Request/Route specific metrics
171
+ t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
172
+ t.integer :success_count, default: 0, comment: "Number of successful responses"
173
+ t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
174
+ t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
175
+ t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
176
+ t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
177
+
178
+ t.timestamps
179
+ end
180
+
181
+ # Unique constraint and indexes for summaries
182
+ connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
183
+ unique: true,
184
+ name: "idx_pulse_summaries_unique"
185
+ connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
186
+ connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
187
+ end
117
188
 
118
189
  # Add indexes to existing tables for efficient aggregation
119
- connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
120
- connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
190
+ unless connection.index_exists?(:rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation")
191
+ connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
192
+ end
193
+
194
+ unless connection.index_exists?(:rails_pulse_requests, :created_at, name: "idx_requests_created_at")
195
+ connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
196
+ end
121
197
 
122
- connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
123
- connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
198
+ unless connection.index_exists?(:rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation")
199
+ connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
200
+ end
201
+
202
+ unless connection.index_exists?(:rails_pulse_operations, :created_at, name: "idx_operations_created_at")
203
+ connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
204
+ end
124
205
 
125
- if ENV["CI"] == "true"
126
- created_tables = required_tables.select { |table| connection.table_exists?(table) }
127
- puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
206
+ # Log successful creation
207
+ created_tables = required_tables.select { |table| connection.table_exists?(table) }
208
+ newly_created = created_tables - existing_tables
209
+ if newly_created.any?
210
+ puts "[RailsPulse::Schema] Successfully created tables: #{newly_created.join(', ')}"
128
211
  end
129
212
  end
130
213