mdash 0.1.0 → 0.2.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: f04e567a5a7a3c4f5c350db0a238c0f1193b9a312b60069159c740a6c64a309b
4
- data.tar.gz: 4e22e7a1d14be1045162856737f72e181ba86b7f7041e2241e59dc44c0d55c45
3
+ metadata.gz: 99e02fcb481a90b671b153894b496e402c72e5507ec3c5905de47290395e5161
4
+ data.tar.gz: 63a4ea369081f75e2515677df81f35261637da50195586dc9bdf4c0c39cd90e0
5
5
  SHA512:
6
- metadata.gz: 9ea1ac9cbc669d1673cfc5cec7b3eebd935c08ea2ae8a7703d926cf7aefa9f867ed47bf6b195bd26c52508f1fdf1f940a161c27ce2f4443cd1603c5df568fd4c
7
- data.tar.gz: cdbe84f79f38cf298fd22bd0ef792025dfe152c57eeb6daa649bf9911ad927e40898b36bc1de8855293188dac4f9ec7756907aad7822161c13ffb350c207e1bc
6
+ metadata.gz: 67a995b78d3f381fa6c54333ed805fc16ebd8ffa6ed9e3e39522a9bd552dde5f77e9f9985e5bfe9c96c1e15f2b5ae33b3ff149f059cd03d69e16d9e04e892567
7
+ data.tar.gz: 54fcde0b2ef3cb0fc2784e632efb9e66a9b409c456bf24a58284f73f4a5f6e156ec9f42ac7f27645c6354c424d10a9d00bcc838ea1ac7ab32ea9d067e35198d4
data/README.md CHANGED
@@ -35,9 +35,301 @@ Mount the Mdash engine in your routes file
35
35
  mount Mdash::Engine => "/mdash"
36
36
  ```
37
37
 
38
+ #### Configuration
39
+
40
+ There are three main sections to be configured to properly setup Mdash in your Rails app.
41
+
42
+ ##### Site Name
43
+
44
+ This is the name of your site that will be displayed in the Mdash dashboard when setting up widgets.
45
+
46
+ ```ruby
47
+ config.site_name = "Your Site Name"
48
+ ```
49
+
50
+ ##### Secret
51
+
52
+ This is the secret key that will be used to authenticate requests from Mdash to your app.
53
+ In the future we plan to support authing against Mdash and storing the secret on our service but for now direct connections are the only way
54
+
55
+ Requirements:
56
+ - Must be a string
57
+ - Must be at least 10 characters long but really use a longer one, please
58
+ - Ideally store this in your Rails credentials file or a secret manager
59
+
60
+ ```ruby
61
+ config.secret = "a-really-long-secret-key"
62
+ ```
63
+
64
+ ##### Exported Metrics
65
+
66
+ This is where you define the metrics that you want to expose to Mdash.
67
+ Mdash will have predefined templates for common metrics but you can also define your own custom metrics.
68
+
69
+ A metric is defined by the following attributes:
70
+ - `model` - The name of the model that the metric is based on
71
+ - `metrics` - A hash of metrics that you want to expose
72
+ - `aggregation` - The type of aggregation to perform on the model
73
+ - `aggregation_field` - The field to aggregate on, defaults to `id`
74
+ - `period` - The period over which to aggregate the metric
75
+ - `periods` - The number of periods to aggregate over
76
+ - `modifier` - A modifier to apply to the aggregation
77
+
78
+ Valid aggregations are:
79
+ - `:count` - Count the number of records
80
+ - `:sum` - Sum the values of a column
81
+ - `:average` - Average the values of a column
82
+
83
+ If you define an aggregation of sum or average you should also define the field to aggregate on with the `aggregation_field` attribute.
84
+ - String
85
+ - Default is `id`
86
+
87
+ Valid period values are:
88
+ - `nil` - No period, aggregate over all time
89
+ - `:hour` - Aggregate over an hour
90
+ - `:day` - Aggregate over a day
91
+ - `:week` - Aggregate over a week
92
+ - `:month` - Aggregate over a month
93
+ - `:year` - Aggregate over a year
94
+
95
+ If you define a period you can also define the number of periods to aggregate over with the `periods` attribute.
96
+ - Integer > 1
97
+ - Default is 1
98
+ - If periods is set then we will return an object of values even if periods is 1
99
+
100
+ If you define a modifier you can also define the modifier with the `modifier` attribute.
101
+ - String
102
+ - Default is nil
103
+ - Must be a method that exists on the model
104
+
105
+ ##### Example Config
106
+
107
+ Here's a real world example from https://timewith.xyz
108
+
109
+ ```ruby
110
+ Mdash.configure do |config|
111
+ config.site_name = "Timewith"
112
+ config.secret = Rails.application.credentials.dig(:mdash, :secret)
113
+
114
+ config.exported_metrics = {
115
+ users: {
116
+ model: "Account",
117
+ metrics: {
118
+ total: {
119
+ aggregation: :count,
120
+ },
121
+ recent_signups: {
122
+ aggregation: :count,
123
+ period: :week,
124
+ },
125
+ weekly_signups: {
126
+ aggregation: :count,
127
+ period: :week,
128
+ periods: 12,
129
+ }
130
+ }
131
+ },
132
+ profiles: {
133
+ model: "Profile",
134
+ metrics: {
135
+ total: {
136
+ aggregation: :count,
137
+ }
138
+ }
139
+ },
140
+ events: {
141
+ model: "Event",
142
+ metrics: {
143
+ total: {
144
+ aggregation: :count,
145
+ }
146
+ }
147
+ },
148
+ bookings: {
149
+ model: "Booking",
150
+ metrics: {
151
+ total: {
152
+ aggregation: :count,
153
+ },
154
+ recent: {
155
+ aggregation: :count,
156
+ period: :week,
157
+ },
158
+ weekly: {
159
+ aggregation: :count,
160
+ period: :week,
161
+ periods: 12,
162
+ },
163
+ cancelled_total: {
164
+ aggregation: :count,
165
+ modifier: "cancelled",
166
+ },
167
+ cancelled_recent: {
168
+ aggregation: :count,
169
+ period: :week,
170
+ modifier: "cancelled",
171
+ },
172
+ }
173
+ }
174
+ }
175
+ end
176
+ ```
177
+
178
+ ## Consuming
179
+
180
+ The simplest way to consume the metrics from Mdash is to use the Mdash App (coming soon).
181
+
182
+ If you want to consume the metrics in your own app you can use the Mdash API.
183
+
184
+ #### API
185
+
186
+ The Mdash API is a simple RESTful API that allows you to fetch the metrics that you have defined in your Rails app.
187
+
188
+ ##### Authentication
189
+
190
+ To authenticate with the Mdash API you need to include the secret key that you defined in your Rails app as a header "X-Mdash-Token"
191
+
192
+ ##### Announce
193
+
194
+ The announce endpoint contains a list of all the valid metrics that you have defined in your Rails app.
195
+
196
+ ```http
197
+ GET /mdash/announce
198
+ ```
199
+
200
+ ```json
201
+ {
202
+ "site_name": "Timewith",
203
+ "metrics": [
204
+ {
205
+ "name": "users_total",
206
+ "model": "Account",
207
+ "aggregation": "count",
208
+ "aggregation_field": "id",
209
+ "period": null,
210
+ "periods": null,
211
+ "modifier": null
212
+ },
213
+ {
214
+ "name": "users_recent_signups",
215
+ "model": "Account",
216
+ "aggregation": "count",
217
+ "aggregation_field": "id",
218
+ "period": "week",
219
+ "periods": null,
220
+ "modifier": null
221
+ },
222
+ {
223
+ "name": "users_weekly_signups",
224
+ "model": "Account",
225
+ "aggregation": "count",
226
+ "aggregation_field": "id",
227
+ "period": "week",
228
+ "periods": 12,
229
+ "modifier": null
230
+ },
231
+ {
232
+ "name": "profiles_total",
233
+ "model": "Profile",
234
+ "aggregation": "count",
235
+ "aggregation_field": "id",
236
+ "period": null,
237
+ "periods": null,
238
+ "modifier": null
239
+ },
240
+ {
241
+ "name": "events_total",
242
+ "model": "Event",
243
+ "aggregation": "count",
244
+ "aggregation_field": "id",
245
+ "period": null,
246
+ "periods": null,
247
+ "modifier": null
248
+ },
249
+ {
250
+ "name": "bookings_total",
251
+ "model": "Booking",
252
+ "aggregation": "count",
253
+ "aggregation_field": "id",
254
+ "period": null,
255
+ "periods": null,
256
+ "modifier": null
257
+ },
258
+ {
259
+ "name": "bookings_recent",
260
+ "model": "Booking",
261
+ "aggregation": "count",
262
+ "aggregation_field": "id",
263
+ "period": "week",
264
+ "periods": null,
265
+ "modifier": null
266
+ },
267
+ {
268
+ "name": "bookings_weekly",
269
+ "model": "Booking",
270
+ "aggregation": "count",
271
+ "aggregation_field": "id",
272
+ "period": "week",
273
+ "periods": 12,
274
+ "modifier": null
275
+ },
276
+ {
277
+ "name": "bookings_cancelled_total",
278
+ "model": "Booking",
279
+ "aggregation": "count",
280
+ "aggregation_field": "id",
281
+ "period": null,
282
+ "periods": null,
283
+ "modifier": "cancelled"
284
+ },
285
+ {
286
+ "name": "bookings_cancelled_recent",
287
+ "model": "Booking",
288
+ "aggregation": "count",
289
+ "aggregation_field": "id",
290
+ "period": "week",
291
+ "periods": null,
292
+ "modifier": "cancelled"
293
+ }
294
+ ]
295
+ }
296
+ ```
297
+
298
+ ##### Stats
299
+
300
+ The stats endpoint allows you to fetch the values of the metrics that you have defined in your Rails app.
301
+ Stats contains the rollup values for each metric.
302
+ Last updated is the time that the stats were last updated (ie. if they're stale or cached)
303
+
304
+ ```http
305
+ GET /mdash/stats
306
+ ```
307
+
308
+ ```json
309
+ {
310
+ "stats": {
311
+ "users_total": 1,
312
+ "users_recent_signups": 0,
313
+ "users_weekly_signups": {
314
+ "2025-01-05": 1
315
+ },
316
+ "profiles_total": 2,
317
+ "events_total": 2,
318
+ "bookings_total": 4,
319
+ "bookings_recent": 0,
320
+ "bookings_weekly": {
321
+ "2025-01-05": 4
322
+ },
323
+ "bookings_cancelled_total": 0,
324
+ "bookings_cancelled_recent": 0
325
+ },
326
+ "last_updated": "2025-01-05T00:00:00Z"
327
+ }
328
+ ```
329
+
38
330
  ## Contributing
39
331
 
40
- Bug reports and pull requests are welcome on GitHub at https://github.com/imothee/mdash. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/imothee/policygen/blob/main/CODE_OF_CONDUCT.md).
332
+ Bug reports and pull requests are welcome on GitHub at https://github.com/imothee/mdash-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/imothee/policygen/blob/main/CODE_OF_CONDUCT.md).
41
333
 
42
334
  ## License
43
335
 
@@ -1,18 +1,22 @@
1
1
  module Mdash
2
2
  class AnnounceController < ApplicationController
3
3
  def index
4
- available = Mdash.config.metrics.each_with_object({}) do |metric, hash|
5
- hash[metric.id] = {
4
+ metrics = Mdash.config.metrics.map { |metric|
5
+ {
6
+ name: metric.id,
6
7
  model: metric.model,
7
8
  aggregation: metric.aggregation,
8
- aggregation_column: metric.aggregation_column,
9
+ aggregation_field: metric.aggregation_field,
9
10
  period: metric.period,
10
11
  periods: metric.periods,
11
12
  modifier: metric.modifier
12
13
  }
13
- end
14
+ }
14
15
 
15
- render json: available.as_json
16
+ render json: {
17
+ site_name: Mdash.config.site_name,
18
+ metrics: metrics
19
+ }.as_json
16
20
  end
17
21
  end
18
22
  end
@@ -1,8 +1,10 @@
1
1
  module Mdash
2
2
  class StatsController < ApplicationController
3
3
  def index
4
- @stats = Mdash.stats
5
- render json: @stats
4
+ render json: {
5
+ stats: Mdash.stats,
6
+ last_updated: Mdash.last_updated
7
+ }.as_json
6
8
  end
7
9
  end
8
10
  end
@@ -7,7 +7,7 @@ module Mdash
7
7
  # Valid periods
8
8
  PERIODS = %i[hour day week month year].freeze
9
9
 
10
- attr_reader :id, :model, :aggregation, :aggregation_column, :period, :periods, :modifier
10
+ attr_reader :id, :model, :aggregation, :aggregation_field, :period, :periods, :modifier
11
11
 
12
12
  def self.all(configuration: nil)
13
13
  configuration ||= Mdash.config
@@ -20,11 +20,11 @@ module Mdash
20
20
  end
21
21
  end
22
22
 
23
- def initialize(id:, model:, aggregation:, aggregation_column: :id, period: nil, periods: nil, modifier: nil)
23
+ def initialize(id:, model:, aggregation:, aggregation_field: :id, period: nil, periods: nil, modifier: nil)
24
24
  @id = id.to_sym
25
25
  @model = model.to_sym
26
26
  @aggregation = aggregation.to_sym
27
- @aggregation_column = aggregation_column.to_sym
27
+ @aggregation_field = aggregation_field.to_sym
28
28
  @period = period&.to_sym
29
29
  @periods = periods
30
30
  @modifier = modifier
@@ -34,6 +34,8 @@ module Mdash
34
34
  return false unless AGGREGATIONS.include?(@aggregation)
35
35
  return false unless PERIODS.include?(@period) if @period.present?
36
36
 
37
+ return false if @periods.present? && !@periods.positive?
38
+
37
39
  true
38
40
  end
39
41
 
@@ -82,9 +84,9 @@ module Mdash
82
84
  def aggregation_query(query)
83
85
  case @aggregation
84
86
  when :sum
85
- query.sum(@aggregation_column)
87
+ query.sum(@aggregation_field)
86
88
  when :avg
87
- query.average(@aggregation_column)
89
+ query.average(@aggregation_field)
88
90
  when :count
89
91
  query.count
90
92
  end
@@ -1,6 +1,10 @@
1
1
  # Use this setup block to configure all options available in Mdash
2
2
  Mdash.configure do |config|
3
+ config.site_name = "<%= Rails.application.class.module_parent.name %>"
3
4
  config.secret = "<%= SecureRandom.hex(32) %>"
4
5
 
6
+ # How long should we cache metrics for so we don't hit the database too often?
7
+ # config.cache_expiry = 5.minutes
8
+
5
9
  config.exported_metrics = {}
6
10
  end
@@ -1,19 +1,33 @@
1
1
  module Mdash
2
2
  class Configuration
3
+ attr_accessor :site_name
3
4
  attr_accessor :secret
5
+ attr_accessor :cache_expiry
4
6
  attr_accessor :exported_metrics
5
7
 
8
+ def initialize
9
+ @cache_expiry = 5.minutes
10
+ end
11
+
6
12
  def metrics
7
- @metrics ||= exported_metrics.flat_map do |prefix, params|
8
- model = params[:model]
9
- params[:metrics].map do |k, metric_params|
10
- metric = Metric.new(id: "#{prefix}_#{k}", model: model, **metric_params)
13
+ @metrics ||= exported_metrics.each_with_object([]) do |(prefix, params), arr|
14
+ # Check if the model exists, if not return
15
+ next Rails.logger.warn("Invalid model: #{params[:model]}") unless params[:model].to_s.classify.safe_constantize
16
+
17
+ params[:metrics].each do |k, metric_params|
18
+ id = "#{prefix}_#{k}".to_sym
19
+ # Check if the metric is a hash
20
+ next Rails.logger.warn("Metric is not a hash: #{id}") unless metric_params.is_a?(Hash)
21
+ # Check if metric id is unique
22
+ next Rails.logger.warn("Duplicate metric: #{id}") if arr.any? { |m| m.id == id }
23
+
24
+ # Create the metric
25
+ metric = Metric.new(id:, model: params[:model], **metric_params)
26
+
11
27
  # Check if the metric is valid
12
- unless metric.valid?
13
- Rails.logger.warn("Invalid metric: #{metric.id}")
14
- return nil
15
- end
16
- metric
28
+ next Rails.logger.warn("Invalid metric: #{id}") unless metric.valid?
29
+
30
+ arr << metric
17
31
  end
18
32
  end
19
33
  end
data/lib/mdash/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Mdash
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/mdash.rb CHANGED
@@ -18,15 +18,18 @@ module Mdash
18
18
  def stats
19
19
  # Check the last time stats were updated
20
20
  # If it's been more than 5 minutes, update the stats
21
- if @last_updated.nil? || @last_updated < 5.minutes.ago
22
- @stats = config.metrics.reduce({}) do |stats, metric|
21
+ if @last_updated.nil? || @last_updated < config.cache_expiry.ago
22
+ @stats = config.metrics.each_with_object({}) do |metric, stats|
23
23
  stats[metric.id] = metric.data
24
- stats
25
24
  end
26
25
  @last_updated = Time.now
27
26
  end
28
27
  @stats
29
28
  end
29
+
30
+ def last_updated
31
+ @last_updated
32
+ end
30
33
  end
31
34
  end
32
35
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mdash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Marks