moesif_rack 1.3.9 → 1.4.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +176 -125
- data/lib/moesif_rack/app_config.rb +9 -0
- data/lib/moesif_rack/client_ip.rb +2 -2
- data/lib/moesif_rack/moesif_middleware.rb +119 -76
- data/moesif_capture_outgoing/httplog/http_log.rb +59 -4
- data/test/moesif_rack_test.rb +3 -1
- metadata +22 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8bde32f3f60b62bfe1dfd5aef52ba6c56f21db3e2f8ac78743972c227ff1d2b
|
4
|
+
data.tar.gz: d0fe24cc62aa794dab07b04fa6ce943f85d015db045eb654cd66e9827e9b97fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0db637f28ba8d87d7e5e8a81b9642eeeb64f2c701d862c941e90ac0bdc5a2d15584f597df82420b736020dcd8f426c87e1e727cb9dcf081214e9aeecc07fdcd9
|
7
|
+
data.tar.gz: 134f36829793337b6d78a7a51dedddeadd2d4e2fc1353e5b0ae5962f7ddb72f08562c5ff9f5f93248b7ee80db14a987f4935c58eb152b0229bfae9f7ea9e128c
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -22,7 +22,7 @@ gem install moesif_rack
|
|
22
22
|
and if you have a `Gemfile` in your project, please add this line to
|
23
23
|
|
24
24
|
```
|
25
|
-
gem 'moesif_rack', '~> 1.
|
25
|
+
gem 'moesif_rack', '~> 1.4.2'
|
26
26
|
|
27
27
|
```
|
28
28
|
|
@@ -126,12 +126,10 @@ Optional.
|
|
126
126
|
identify_user is a Proc that takes env, headers, and body as arguments and returns a user_id string. This helps us attribute requests to unique users. Even though Moesif can automatically retrieve the user_id without this, this is highly recommended to ensure accurate attribution.
|
127
127
|
|
128
128
|
```ruby
|
129
|
-
|
130
129
|
moesif_options['identify_user'] = Proc.new { |env, headers, body|
|
131
130
|
|
132
|
-
#
|
133
|
-
|
134
|
-
'my_user_id'
|
131
|
+
# Add your custom code that returns a string for user id
|
132
|
+
'12345'
|
135
133
|
}
|
136
134
|
|
137
135
|
```
|
@@ -145,13 +143,24 @@ identify_company is a Proc that takes env, headers, and body as arguments and re
|
|
145
143
|
|
146
144
|
moesif_options['identify_company'] = Proc.new { |env, headers, body|
|
147
145
|
|
148
|
-
#
|
149
|
-
|
150
|
-
'my_company_id'
|
146
|
+
# Add your custom code that returns a string for company id
|
147
|
+
'67890'
|
151
148
|
}
|
152
149
|
|
153
150
|
```
|
154
151
|
|
152
|
+
#### __`identify_session`__
|
153
|
+
|
154
|
+
Optional. A Proc that takes env, headers, body and returns a string.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
|
158
|
+
moesif_options['identify_session'] = Proc.new { |env, headers, body|
|
159
|
+
# Add your custom code that returns a string for session/API token
|
160
|
+
'XXXXXXXXX'
|
161
|
+
}
|
162
|
+
```
|
163
|
+
|
155
164
|
#### __`get_metadata`__
|
156
165
|
|
157
166
|
Optional.
|
@@ -162,31 +171,15 @@ metadata to this event.
|
|
162
171
|
```ruby
|
163
172
|
|
164
173
|
moesif_options['get_metadata'] = Proc.new { |env, headers, body|
|
165
|
-
|
166
|
-
#snip
|
174
|
+
# Add your custom code that returns a dictionary
|
167
175
|
value = {
|
168
|
-
'
|
169
|
-
'
|
176
|
+
'datacenter' => 'westus',
|
177
|
+
'deployment_version' => 'v1.2.3'
|
170
178
|
}
|
171
|
-
|
172
179
|
value
|
173
180
|
}
|
174
181
|
```
|
175
182
|
|
176
|
-
#### __`identify_session`__
|
177
|
-
|
178
|
-
Optional. A Proc that takes env, headers, body and returns a string.
|
179
|
-
|
180
|
-
```ruby
|
181
|
-
|
182
|
-
moesif_options['identify_session'] = Proc.new { |env, headers, body|
|
183
|
-
|
184
|
-
#snip
|
185
|
-
|
186
|
-
'the_session_token'
|
187
|
-
}
|
188
|
-
|
189
|
-
```
|
190
183
|
|
191
184
|
#### __`mask_data`__
|
192
185
|
|
@@ -196,9 +189,8 @@ With mask_data, you can make modifications to headers or body of the event befor
|
|
196
189
|
```ruby
|
197
190
|
|
198
191
|
moesif_options['mask_data'] = Proc.new { |event_model|
|
199
|
-
|
200
|
-
|
201
|
-
|
192
|
+
# Add your custom code that returns a event_model after modifying any fields
|
193
|
+
event_model.response.body.password = nil
|
202
194
|
event_model
|
203
195
|
}
|
204
196
|
|
@@ -213,9 +205,7 @@ Optional. A Proc that takes env, headers, body and returns a boolean.
|
|
213
205
|
```ruby
|
214
206
|
|
215
207
|
moesif_options['skip'] = Proc.new { |env, headers, body|
|
216
|
-
|
217
|
-
#snip
|
218
|
-
|
208
|
+
# Add your custom code that returns true to skip logging the API call
|
219
209
|
false
|
220
210
|
}
|
221
211
|
|
@@ -232,7 +222,7 @@ Optional. Boolean. Default false. If true, it will print out debug messages. In
|
|
232
222
|
|
233
223
|
Optional. Boolean. Default true. If false, will not log request and response body to Moesif.
|
234
224
|
|
235
|
-
#### __`
|
225
|
+
#### __`capture_outgoing_requests`__
|
236
226
|
Optional. boolean, Default `false`. Set to `true` to capture all outgoing API calls from your app to third parties like Stripe, Github or to your own dependencies while using [Net::HTTP](https://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html) package. The options below is applied to outgoing API calls. When the request is outgoing, for options functions that take request and response as input arguments, the request and response objects passed in are [Request](https://www.rubydoc.info/stdlib/net/Net/HTTPRequest) request and [Response](https://www.rubydoc.info/stdlib/net/Net/HTTPResponse) response objects.
|
237
227
|
|
238
228
|
|
@@ -245,9 +235,8 @@ identify_user_outgoing is a Proc that takes request and response as arguments an
|
|
245
235
|
|
246
236
|
moesif_options['identify_user_outgoing'] = Proc.new { |request, response|
|
247
237
|
|
248
|
-
#
|
249
|
-
|
250
|
-
'the_user_id'
|
238
|
+
# Add your custom code that returns a string for user id
|
239
|
+
'12345'
|
251
240
|
}
|
252
241
|
|
253
242
|
```
|
@@ -261,9 +250,8 @@ identify_company_outgoing is a Proc that takes request and response as arguments
|
|
261
250
|
|
262
251
|
moesif_options['identify_company_outgoing'] = Proc.new { |request, response|
|
263
252
|
|
264
|
-
#
|
265
|
-
|
266
|
-
'the_company_id'
|
253
|
+
# Add your custom code that returns a string for company id
|
254
|
+
'67890'
|
267
255
|
}
|
268
256
|
|
269
257
|
```
|
@@ -279,12 +267,11 @@ metadata to this event.
|
|
279
267
|
|
280
268
|
moesif_options['get_metadata_outgoing'] = Proc.new { |request, response|
|
281
269
|
|
282
|
-
#
|
270
|
+
# Add your custom code that returns a dictionary
|
283
271
|
value = {
|
284
|
-
'
|
285
|
-
'
|
272
|
+
'datacenter' => 'westus',
|
273
|
+
'deployment_version' => 'v1.2.3'
|
286
274
|
}
|
287
|
-
|
288
275
|
value
|
289
276
|
}
|
290
277
|
```
|
@@ -297,9 +284,8 @@ Optional. A Proc that takes request, response and returns a string.
|
|
297
284
|
|
298
285
|
moesif_options['identify_session_outgoing'] = Proc.new { |request, response|
|
299
286
|
|
300
|
-
|
301
|
-
|
302
|
-
'the_session_token'
|
287
|
+
# Add your custom code that returns a string for session/API token
|
288
|
+
'XXXXXXXXX'
|
303
289
|
}
|
304
290
|
|
305
291
|
```
|
@@ -312,8 +298,7 @@ Optional. A Proc that takes request, response and returns a boolean. If `true` w
|
|
312
298
|
|
313
299
|
moesif_options['skip_outgoing'] = Proc.new{ |request, response|
|
314
300
|
|
315
|
-
#
|
316
|
-
|
301
|
+
# Add your custom code that returns true to skip logging the API call
|
317
302
|
false
|
318
303
|
}
|
319
304
|
|
@@ -328,8 +313,8 @@ With mask_data_outgoing, you can make modifications to headers or body of the ev
|
|
328
313
|
|
329
314
|
moesif_options['mask_data_outgoing'] = Proc.new { |event_model|
|
330
315
|
|
331
|
-
#
|
332
|
-
|
316
|
+
# Add your custom code that returns a event_model after modifying any fields
|
317
|
+
event_model.response.body.password = nil
|
333
318
|
event_model
|
334
319
|
}
|
335
320
|
|
@@ -341,103 +326,169 @@ Optional. Boolean. Default true. If false, will not log request and response bod
|
|
341
326
|
|
342
327
|
## Update User
|
343
328
|
|
344
|
-
###
|
345
|
-
|
346
|
-
The metadata field can be any
|
329
|
+
### Update a Single User
|
330
|
+
Create or update a user profile in Moesif.
|
331
|
+
The metadata field can be any customer demographic or other info you want to store.
|
332
|
+
Only the `user_id` field is required.
|
333
|
+
This method is a convenient helper that calls the Moesif API lib.
|
334
|
+
For details, visit the [Ruby API Reference](https://www.moesif.com/docs/api?ruby#update-a-user).
|
347
335
|
|
348
336
|
```ruby
|
349
|
-
metadata =
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
337
|
+
metadata = {
|
338
|
+
:email => 'john@acmeinc.com',
|
339
|
+
:first_name => 'John',
|
340
|
+
:last_name => 'Doe',
|
341
|
+
:title => 'Software Engineer',
|
342
|
+
:salesInfo => {
|
343
|
+
:stage => 'Customer',
|
344
|
+
:lifetime_value => 24000,
|
345
|
+
:accountOwner => 'mary@contoso.com',
|
346
|
+
}
|
347
|
+
}
|
357
348
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
349
|
+
# Campaign object is optional, but useful if you want to track ROI of acquisition channels
|
350
|
+
# See https://www.moesif.com/docs/api#users for campaign schema
|
351
|
+
campaign = MoesifApi::CampaignModel.new()
|
352
|
+
campaign.utm_source = "google"
|
353
|
+
campaign.utm_medium = "cpc"
|
354
|
+
campaign.utm_campaign = "adwords"
|
355
|
+
campaign.utm_term = "api+tooling"
|
356
|
+
campaign.utm_content = "landing"
|
357
|
+
|
358
|
+
# Only user_id is required.
|
359
|
+
# metadata can be any custom object
|
360
|
+
user = MoesifApi::UserModel.new()
|
361
|
+
user.user_id = "12345"
|
362
|
+
user.company_id = "67890" # If set, associate user with a company object
|
363
|
+
user.campaign = campaign
|
364
|
+
user.metadata = metadata
|
363
365
|
|
364
366
|
update_user = MoesifRack::MoesifMiddleware.new(@app, @options).update_user(user_model)
|
365
367
|
```
|
366
368
|
|
367
|
-
###
|
368
|
-
|
369
|
-
|
369
|
+
### Update Users in Batch
|
370
|
+
Similar to update_user, but used to update a list of users in one batch.
|
371
|
+
Only the `user_id` field is required.
|
372
|
+
This method is a convenient helper that calls the Moesif API lib.
|
373
|
+
For details, visit the [Ruby API Reference](https://www.moesif.com/docs/api?ruby#update-users-in-batch).
|
370
374
|
|
371
375
|
```ruby
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
376
|
+
users = []
|
377
|
+
|
378
|
+
metadata = {
|
379
|
+
:email => 'john@acmeinc.com',
|
380
|
+
:first_name => 'John',
|
381
|
+
:last_name => 'Doe',
|
382
|
+
:title => 'Software Engineer',
|
383
|
+
:salesInfo => {
|
384
|
+
:stage => 'Customer',
|
385
|
+
:lifetime_value => 24000,
|
386
|
+
:accountOwner => 'mary@contoso.com',
|
387
|
+
}
|
388
|
+
}
|
389
|
+
|
390
|
+
# Campaign object is optional, but useful if you want to track ROI of acquisition channels
|
391
|
+
# See https://www.moesif.com/docs/api#users for campaign schema
|
392
|
+
campaign = MoesifApi::CampaignModel.new()
|
393
|
+
campaign.utm_source = "google"
|
394
|
+
campaign.utm_medium = "cpc"
|
395
|
+
campaign.utm_campaign = "adwords"
|
396
|
+
campaign.utm_term = "api+tooling"
|
397
|
+
campaign.utm_content = "landing"
|
398
|
+
|
399
|
+
# Only user_id is required.
|
400
|
+
# metadata can be any custom object
|
401
|
+
user = MoesifApi::UserModel.new()
|
402
|
+
user.user_id = "12345"
|
403
|
+
user.company_id = "67890" # If set, associate user with a company object
|
404
|
+
user.campaign = campaign
|
405
|
+
user.metadata = metadata
|
406
|
+
|
407
|
+
users << user
|
408
|
+
|
409
|
+
response = MoesifRack::MoesifMiddleware.new(@app, @options).update_users_batch(users)
|
392
410
|
```
|
393
411
|
|
394
412
|
## Update Company
|
395
413
|
|
396
|
-
###
|
397
|
-
|
398
|
-
The metadata field can be any
|
414
|
+
### Update a Single Company
|
415
|
+
Create or update a company profile in Moesif.
|
416
|
+
The metadata field can be any company demographic or other info you want to store.
|
417
|
+
Only the `company_id` field is required.
|
418
|
+
This method is a convenient helper that calls the Moesif API lib.
|
419
|
+
For details, visit the [Ruby API Reference](https://www.moesif.com/docs/api?ruby#update-a-company).
|
399
420
|
|
400
421
|
```ruby
|
401
|
-
metadata =
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
422
|
+
metadata = {
|
423
|
+
:org_name => 'Acme, Inc',
|
424
|
+
:plan_name => 'Free',
|
425
|
+
:deal_stage => 'Lead',
|
426
|
+
:mrr => 24000,
|
427
|
+
:demographics => {
|
428
|
+
:alexa_ranking => 500000,
|
429
|
+
:employee_count => 47
|
430
|
+
}
|
431
|
+
}
|
409
432
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
433
|
+
# Campaign object is optional, but useful if you want to track ROI of acquisition channels
|
434
|
+
# See https://www.moesif.com/docs/api#update-a-company for campaign schema
|
435
|
+
campaign = MoesifApi::CampaignModel.new()
|
436
|
+
campaign.utm_source = "google"
|
437
|
+
campaign.utm_medium = "cpc"
|
438
|
+
campaign.utm_campaign = "adwords"
|
439
|
+
campaign.utm_term = "api+tooling"
|
440
|
+
campaign.utm_content = "landing"
|
441
|
+
|
442
|
+
# Only company_id is required.
|
443
|
+
# metadata can be any custom object
|
444
|
+
company = MoesifApi::CompanyModel.new()
|
445
|
+
company.company_id = "67890"
|
446
|
+
company.company_domain = "acmeinc.com" # If domain is set, Moesif will enrich your profiles with publicly available info
|
447
|
+
company.campaign = campaign
|
448
|
+
company.metadata = metadata
|
414
449
|
|
415
450
|
update_company = MoesifRack::MoesifMiddleware.new(@app, @options).update_company(company_model)
|
416
451
|
```
|
417
452
|
|
418
|
-
###
|
419
|
-
|
420
|
-
|
453
|
+
### Update Companies in Batch
|
454
|
+
Similar to update_company, but used to update a list of companies in one batch.
|
455
|
+
Only the `company_id` field is required.
|
456
|
+
This method is a convenient helper that calls the Moesif API lib.
|
457
|
+
For details, visit the [Ruby API Reference](https://www.moesif.com/docs/api?ruby#update-companies-in-batch).
|
421
458
|
|
422
459
|
```ruby
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
company_model_B = { "company_id" => "67890",
|
436
|
-
"company_domain" => "acmeinc.com",
|
437
|
-
"metadata" => metadata }
|
460
|
+
companies = []
|
461
|
+
|
462
|
+
metadata = {
|
463
|
+
:org_name => 'Acme, Inc',
|
464
|
+
:plan_name => 'Free',
|
465
|
+
:deal_stage => 'Lead',
|
466
|
+
:mrr => 24000,
|
467
|
+
:demographics => {
|
468
|
+
:alexa_ranking => 500000,
|
469
|
+
:employee_count => 47
|
470
|
+
}
|
471
|
+
}
|
438
472
|
|
439
|
-
|
440
|
-
|
473
|
+
# Campaign object is optional, but useful if you want to track ROI of acquisition channels
|
474
|
+
# See https://www.moesif.com/docs/api#update-a-company for campaign schema
|
475
|
+
campaign = MoesifApi::CampaignModel.new()
|
476
|
+
campaign.utm_source = "google"
|
477
|
+
campaign.utm_medium = "cpc"
|
478
|
+
campaign.utm_campaign = "adwords"
|
479
|
+
campaign.utm_term = "api+tooling"
|
480
|
+
campaign.utm_content = "landing"
|
481
|
+
|
482
|
+
# Only company_id is required.
|
483
|
+
# metadata can be any custom object
|
484
|
+
company = MoesifApi::CompanyModel.new()
|
485
|
+
company.company_id = "67890"
|
486
|
+
company.company_domain = "acmeinc.com" # If domain is set, Moesif will enrich your profiles with publicly available info
|
487
|
+
company.campaign = campaign
|
488
|
+
company.metadata = metadata
|
489
|
+
|
490
|
+
companies << company
|
491
|
+
response = MoesifRack::MoesifMiddleware.new(@app, @options).update_companies_batch(companies)
|
441
492
|
```
|
442
493
|
|
443
494
|
## How to test
|
@@ -19,7 +19,12 @@ class AppConfig
|
|
19
19
|
puts 'Error getting application configuration, with status code:'
|
20
20
|
puts e.response_code
|
21
21
|
end
|
22
|
+
rescue => e
|
23
|
+
if debug
|
24
|
+
puts e.to_s
|
25
|
+
end
|
22
26
|
end
|
27
|
+
rescue
|
23
28
|
end
|
24
29
|
|
25
30
|
def parse_configuration(config_api_response, debug)
|
@@ -118,4 +123,8 @@ class AppConfig
|
|
118
123
|
return nil
|
119
124
|
end
|
120
125
|
end
|
126
|
+
|
127
|
+
def calculate_weight(sample_rate)
|
128
|
+
return sample_rate == 0 ? 1 : (100 / sample_rate).floor
|
129
|
+
end
|
121
130
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
|
2
2
|
def is_ip?(value)
|
3
|
+
ipv4 = /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/
|
4
|
+
ipv6 = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/
|
3
5
|
if
|
4
|
-
ipv4 = /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/
|
5
|
-
ipv6 = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/
|
6
6
|
# We use !! to convert the return value to a boolean
|
7
7
|
!!(value =~ ipv4 or value=~ ipv6)
|
8
8
|
end
|
@@ -6,6 +6,8 @@ require_relative './client_ip.rb'
|
|
6
6
|
require_relative './app_config.rb'
|
7
7
|
require_relative './update_user.rb'
|
8
8
|
require_relative './update_company.rb'
|
9
|
+
require 'zlib'
|
10
|
+
require 'stringio'
|
9
11
|
|
10
12
|
module MoesifRack
|
11
13
|
|
@@ -30,25 +32,29 @@ module MoesifRack
|
|
30
32
|
@config = @app_config.get_config(@api_controller, @debug)
|
31
33
|
@config_etag = nil
|
32
34
|
@sampling_percentage = 100
|
33
|
-
@
|
35
|
+
@last_config_download_time = Time.now.utc
|
36
|
+
@last_worker_run = Time.now.utc
|
34
37
|
@config_dict = Hash.new
|
35
38
|
@disable_transaction_id = options['disable_transaction_id'] || false
|
36
39
|
@log_body = options.fetch('log_body', true)
|
40
|
+
@batch_size = options['batch_size'] || 25
|
41
|
+
@batch_max_time = options['batch_max_time'] || 2
|
42
|
+
@events_queue = Queue.new
|
43
|
+
@event_response_config_etag = nil
|
44
|
+
start_worker()
|
45
|
+
|
37
46
|
begin
|
38
47
|
if !@config.nil?
|
39
|
-
@config_etag, @sampling_percentage, @
|
48
|
+
@config_etag, @sampling_percentage, @last_config_download_time = @app_config.parse_configuration(@config, @debug)
|
40
49
|
end
|
41
50
|
rescue => exception
|
42
|
-
|
43
|
-
|
44
|
-
puts exception.to_s
|
45
|
-
end
|
51
|
+
log_debug 'Error while parsing application configuration on initialization'
|
52
|
+
log_debug exception.to_s
|
46
53
|
end
|
47
54
|
@capture_outoing_requests = options['capture_outoing_requests']
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
55
|
+
@capture_outgoing_requests = options['capture_outgoing_requests']
|
56
|
+
if @capture_outoing_requests || @capture_outgoing_requests
|
57
|
+
log_debug 'Start Capturing outgoing requests'
|
52
58
|
require_relative '../../moesif_capture_outgoing/httplog.rb'
|
53
59
|
MoesifCaptureOutgoing.start_capture_outgoing(options)
|
54
60
|
end
|
@@ -70,12 +76,84 @@ module MoesifRack
|
|
70
76
|
CompanyHelper.new.update_companies_batch(@api_controller, @debug, company_profiles)
|
71
77
|
end
|
72
78
|
|
73
|
-
def
|
74
|
-
|
79
|
+
def start_with_json(body)
|
80
|
+
body.start_with?('{') || body.start_with?('[')
|
81
|
+
end
|
82
|
+
|
83
|
+
def decompress_body(body)
|
84
|
+
Zlib::GzipReader.new(StringIO.new(body)).read
|
85
|
+
end
|
86
|
+
|
87
|
+
def transform_headers(headers)
|
88
|
+
Hash[headers.map { |k, v| [k.downcase, v]}]
|
89
|
+
end
|
90
|
+
|
91
|
+
def base64_encode_body(body)
|
92
|
+
return Base64.encode64(body), 'base64'
|
93
|
+
end
|
75
94
|
|
95
|
+
def log_debug(message)
|
76
96
|
if @debug
|
77
|
-
puts
|
97
|
+
puts("#{Time.now.to_s} [Moesif Middleware] PID #{Process.pid} TID #{Thread.current.object_id} #{message}")
|
78
98
|
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_body(body, headers)
|
102
|
+
begin
|
103
|
+
if start_with_json(body)
|
104
|
+
parsed_body = JSON.parse(body)
|
105
|
+
transfer_encoding = 'json'
|
106
|
+
elsif headers.key?('content-encoding') && ((headers['content-encoding'].downcase).include? "gzip")
|
107
|
+
uncompressed_string = decompress_body(body)
|
108
|
+
parsed_body, transfer_encoding = base64_encode_body(uncompressed_string)
|
109
|
+
else
|
110
|
+
parsed_body, transfer_encoding = base64_encode_body(body)
|
111
|
+
end
|
112
|
+
rescue
|
113
|
+
parsed_body, transfer_encoding = base64_encode_body(body)
|
114
|
+
end
|
115
|
+
return parsed_body, transfer_encoding
|
116
|
+
end
|
117
|
+
|
118
|
+
def start_worker
|
119
|
+
Thread::new do
|
120
|
+
@last_worker_run = Time.now.utc
|
121
|
+
loop do
|
122
|
+
begin
|
123
|
+
until @events_queue.empty? do
|
124
|
+
batch_events = []
|
125
|
+
until batch_events.size == @batch_size || @events_queue.empty? do
|
126
|
+
batch_events << @events_queue.pop
|
127
|
+
end
|
128
|
+
log_debug("Sending #{batch_events.size.to_s} events to Moesif")
|
129
|
+
event_api_response = @api_controller.create_events_batch(batch_events)
|
130
|
+
@event_response_config_etag = event_api_response[:x_moesif_config_etag]
|
131
|
+
log_debug(event_api_response.to_s)
|
132
|
+
log_debug("Events successfully sent to Moesif")
|
133
|
+
end
|
134
|
+
|
135
|
+
if @events_queue.empty?
|
136
|
+
log_debug("No events to read from the queue")
|
137
|
+
end
|
138
|
+
|
139
|
+
sleep @batch_max_time
|
140
|
+
rescue MoesifApi::APIException => e
|
141
|
+
if e.response_code.between?(401, 403)
|
142
|
+
puts "Unathorized accesss sending event to Moesif. Please verify your Application Id."
|
143
|
+
log_debug(e.to_s)
|
144
|
+
end
|
145
|
+
log_debug("Error sending event to Moesif, with status code #{e.response_code.to_s}")
|
146
|
+
rescue => e
|
147
|
+
log_debug(e.to_s)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def call env
|
154
|
+
start_time = Time.now.utc.iso8601
|
155
|
+
|
156
|
+
log_debug('Calling Moesif middleware')
|
79
157
|
|
80
158
|
status, headers, body = @app.call env
|
81
159
|
end_time = Time.now.utc.iso8601
|
@@ -85,7 +163,7 @@ module MoesifRack
|
|
85
163
|
complex_copy = env.dup
|
86
164
|
|
87
165
|
req_headers = {}
|
88
|
-
complex_copy.select {|k,v| k.start_with?
|
166
|
+
complex_copy.select {|k,v| k.start_with?('HTTP_', 'CONTENT_') }.each do |key, val|
|
89
167
|
new_key = key.sub(/^HTTP_/, '')
|
90
168
|
new_key = new_key.sub('_', '-')
|
91
169
|
req_headers[new_key] = val
|
@@ -98,12 +176,7 @@ module MoesifRack
|
|
98
176
|
|
99
177
|
if @log_body
|
100
178
|
if req_body_string && req_body_string.length != 0
|
101
|
-
|
102
|
-
req_body = JSON.parse(req_body_string)
|
103
|
-
rescue
|
104
|
-
req_body = Base64.encode64(req_body_string)
|
105
|
-
req_body_transfer_encoding = 'base64'
|
106
|
-
end
|
179
|
+
req_body, req_body_transfer_encoding = parse_body(req_body_string, transform_headers(req_headers))
|
107
180
|
end
|
108
181
|
end
|
109
182
|
|
@@ -115,12 +188,7 @@ module MoesifRack
|
|
115
188
|
|
116
189
|
if @log_body
|
117
190
|
if rsp_body_string && rsp_body_string.length != 0
|
118
|
-
|
119
|
-
rsp_body = JSON.parse(rsp_body_string)
|
120
|
-
rescue
|
121
|
-
rsp_body = Base64.encode64(rsp_body_string)
|
122
|
-
rsp_body_transfer_encoding = 'base64'
|
123
|
-
end
|
191
|
+
rsp_body, rsp_body_transfer_encoding = parse_body(rsp_body_string, transform_headers(rsp_headers))
|
124
192
|
end
|
125
193
|
end
|
126
194
|
|
@@ -180,43 +248,31 @@ module MoesifRack
|
|
180
248
|
event_model.direction = "Incoming"
|
181
249
|
|
182
250
|
if @identify_user
|
183
|
-
|
184
|
-
puts "calling identify user proc"
|
185
|
-
end
|
251
|
+
log_debug "calling identify user proc"
|
186
252
|
event_model.user_id = @identify_user.call(env, headers, body)
|
187
253
|
end
|
188
254
|
|
189
255
|
if @identify_company
|
190
|
-
|
191
|
-
puts "calling identify company proc"
|
192
|
-
end
|
256
|
+
log_debug "calling identify company proc"
|
193
257
|
event_model.company_id = @identify_company.call(env, headers, body)
|
194
258
|
end
|
195
259
|
|
196
260
|
if @get_metadata
|
197
|
-
|
198
|
-
puts "calling get_metadata proc"
|
199
|
-
end
|
261
|
+
log_debug "calling get_metadata proc"
|
200
262
|
event_model.metadata = @get_metadata.call(env, headers, body)
|
201
263
|
end
|
202
264
|
|
203
265
|
if @identify_session
|
204
|
-
|
205
|
-
puts "calling identify session proc"
|
206
|
-
end
|
266
|
+
log_debug "calling identify session proc"
|
207
267
|
event_model.session_token = @identify_session.call(env, headers, body)
|
208
268
|
end
|
209
269
|
if @mask_data
|
210
|
-
|
211
|
-
puts "calling mask_data proc"
|
212
|
-
end
|
270
|
+
log_debug "calling mask_data proc"
|
213
271
|
event_model = @mask_data.call(event_model)
|
214
272
|
end
|
215
273
|
|
216
|
-
|
217
|
-
|
218
|
-
puts event_model.to_json
|
219
|
-
end
|
274
|
+
log_debug "sending data to moesif"
|
275
|
+
log_debug event_model.to_json
|
220
276
|
# Perform the API call through the SDK function
|
221
277
|
begin
|
222
278
|
@random_percentage = Random.rand(0.00..100.00)
|
@@ -224,44 +280,35 @@ module MoesifRack
|
|
224
280
|
begin
|
225
281
|
@sampling_percentage = @app_config.get_sampling_percentage(@config, event_model.user_id, event_model.company_id, @debug)
|
226
282
|
rescue => exception
|
227
|
-
|
228
|
-
|
229
|
-
puts exception.to_s
|
230
|
-
end
|
283
|
+
log_debug 'Error while getting sampling percentage, assuming default behavior'
|
284
|
+
log_debug exception.to_s
|
231
285
|
@sampling_percentage = 100
|
232
286
|
end
|
233
287
|
|
234
288
|
if @sampling_percentage > @random_percentage
|
235
|
-
|
236
|
-
|
289
|
+
event_model.weight = @app_config.calculate_weight(@sampling_percentage)
|
290
|
+
# Add Event to the queue
|
291
|
+
@events_queue << event_model
|
292
|
+
log_debug("Event added to the queue ")
|
293
|
+
if Time.now.utc > (@last_config_download_time + 60)
|
294
|
+
start_worker()
|
295
|
+
end
|
237
296
|
|
238
|
-
if
|
297
|
+
if !@event_response_config_etag.nil? && !@config_etag.nil? && @config_etag != @event_response_config_etag && Time.now.utc > (@last_config_download_time + 300)
|
239
298
|
begin
|
240
299
|
@config = @app_config.get_config(@api_controller, @debug)
|
241
|
-
@config_etag, @sampling_percentage, @
|
300
|
+
@config_etag, @sampling_percentage, @last_config_download_time = @app_config.parse_configuration(@config, @debug)
|
242
301
|
rescue => exception
|
243
|
-
|
244
|
-
|
245
|
-
puts exception.to_s
|
246
|
-
end
|
302
|
+
log_debug 'Error while updating the application configuration'
|
303
|
+
log_debug exception.to_s
|
247
304
|
end
|
248
305
|
end
|
249
|
-
if @debug
|
250
|
-
puts("Event successfully sent to Moesif")
|
251
|
-
end
|
252
306
|
else
|
253
|
-
|
254
|
-
puts("Skipped Event due to sampling percentage: " + @sampling_percentage.to_s + " and random percentage: " + @random_percentage.to_s)
|
255
|
-
end
|
256
|
-
end
|
257
|
-
rescue MoesifApi::APIException => e
|
258
|
-
if e.response_code.between?(401, 403)
|
259
|
-
puts "Unathorized accesss sending event to Moesif. Please verify your Application Id."
|
260
|
-
end
|
261
|
-
if @debug
|
262
|
-
puts "Error sending event to Moesif, with status code: "
|
263
|
-
puts e.response_code
|
307
|
+
log_debug("Skipped Event due to sampling percentage: " + @sampling_percentage.to_s + " and random percentage: " + @random_percentage.to_s)
|
264
308
|
end
|
309
|
+
rescue => exception
|
310
|
+
log_debug "Error adding event to the queue "
|
311
|
+
log_debug exception.to_s
|
265
312
|
end
|
266
313
|
|
267
314
|
end
|
@@ -275,11 +322,7 @@ module MoesifRack
|
|
275
322
|
end
|
276
323
|
|
277
324
|
if !should_skip
|
278
|
-
|
279
|
-
process_send.call
|
280
|
-
else
|
281
|
-
Thread.start(&process_send)
|
282
|
-
end
|
325
|
+
process_send.call
|
283
326
|
end
|
284
327
|
|
285
328
|
[status, headers, body]
|
@@ -3,6 +3,7 @@ require 'rack'
|
|
3
3
|
require 'moesif_api'
|
4
4
|
require 'json'
|
5
5
|
require 'base64'
|
6
|
+
require_relative '../../lib/moesif_rack/app_config.rb'
|
6
7
|
|
7
8
|
module MoesifCaptureOutgoing
|
8
9
|
|
@@ -23,6 +24,22 @@ module MoesifCaptureOutgoing
|
|
23
24
|
@skip_outgoing = options['skip_outgoing']
|
24
25
|
@mask_data_outgoing = options['mask_data_outgoing']
|
25
26
|
@log_body_outgoing = options.fetch('log_body_outgoing', true)
|
27
|
+
@app_config = AppConfig.new
|
28
|
+
@config = @app_config.get_config(@api_controller, @debug)
|
29
|
+
@config_etag = nil
|
30
|
+
@sampling_percentage = 100
|
31
|
+
@last_updated_time = Time.now.utc
|
32
|
+
@config_dict = Hash.new
|
33
|
+
begin
|
34
|
+
if !@config.nil?
|
35
|
+
@config_etag, @sampling_percentage, @last_updated_time = @app_config.parse_configuration(@config, @debug)
|
36
|
+
end
|
37
|
+
rescue => exception
|
38
|
+
if @debug
|
39
|
+
puts 'Error while parsing application configuration on initialization'
|
40
|
+
puts exception.to_s
|
41
|
+
end
|
42
|
+
end
|
26
43
|
end
|
27
44
|
|
28
45
|
def call (url, request, request_time, response, response_time)
|
@@ -157,11 +174,45 @@ module MoesifCaptureOutgoing
|
|
157
174
|
|
158
175
|
# Send Event to Moesif
|
159
176
|
begin
|
160
|
-
|
161
|
-
|
162
|
-
|
177
|
+
@random_percentage = Random.rand(0.00..100.00)
|
178
|
+
begin
|
179
|
+
@sampling_percentage = @app_config.get_sampling_percentage(@config, event_model.user_id, event_model.company_id, @debug)
|
180
|
+
rescue => exception
|
181
|
+
if @debug
|
182
|
+
puts 'Error while getting sampling percentage, assuming default behavior'
|
183
|
+
puts exception.to_s
|
184
|
+
end
|
185
|
+
@sampling_percentage = 100
|
186
|
+
end
|
187
|
+
|
188
|
+
if @sampling_percentage > @random_percentage
|
189
|
+
event_model.weight = @app_config.calculate_weight(@sampling_percentage)
|
190
|
+
if @debug
|
191
|
+
puts 'Sending Outgoing Request Data to Moesif'
|
192
|
+
puts event_model.to_json
|
193
|
+
end
|
194
|
+
event_api_response = @api_controller.create_event(event_model)
|
195
|
+
event_response_config_etag = event_api_response[:x_moesif_config_etag]
|
196
|
+
|
197
|
+
if !event_response_config_etag.nil? && !@config_etag.nil? && @config_etag != event_response_config_etag && Time.now.utc > @last_updated_time + 300
|
198
|
+
begin
|
199
|
+
@config = @app_config.get_config(@api_controller, @debug)
|
200
|
+
@config_etag, @sampling_percentage, @last_updated_time = @app_config.parse_configuration(@config, @debug)
|
201
|
+
rescue => exception
|
202
|
+
if @debug
|
203
|
+
puts 'Error while updating the application configuration'
|
204
|
+
puts exception.to_s
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
if @debug
|
209
|
+
puts("Event successfully sent to Moesif")
|
210
|
+
end
|
211
|
+
else
|
212
|
+
if @debug
|
213
|
+
puts("Skipped outgoing Event due to sampling percentage: " + @sampling_percentage.to_s + " and random percentage: " + @random_percentage.to_s)
|
214
|
+
end
|
163
215
|
end
|
164
|
-
@api_controller.create_event(event_model)
|
165
216
|
rescue MoesifApi::APIException => e
|
166
217
|
if e.response_code.between?(401, 403)
|
167
218
|
puts "Unathorized accesss sending event to Moesif. Please verify your Application Id."
|
@@ -170,6 +221,10 @@ module MoesifCaptureOutgoing
|
|
170
221
|
puts "Error sending event to Moesif, with status code: "
|
171
222
|
puts e.response_code
|
172
223
|
end
|
224
|
+
rescue => e
|
225
|
+
if debug
|
226
|
+
puts e.to_s
|
227
|
+
end
|
173
228
|
end
|
174
229
|
else
|
175
230
|
if @debug
|
data/test/moesif_rack_test.rb
CHANGED
@@ -10,7 +10,7 @@ class MoesifRackTest < Test::Unit::TestCase
|
|
10
10
|
@options = { 'application_id' => 'Your Moesif Application Id',
|
11
11
|
'debug' => true,
|
12
12
|
'disable_transaction_id' => true,
|
13
|
-
'
|
13
|
+
'capture_outgoing_requests' => true,
|
14
14
|
'get_metadata' => Proc.new {|request, response|
|
15
15
|
{
|
16
16
|
'foo' => 'abc',
|
@@ -108,6 +108,8 @@ class MoesifRackTest < Test::Unit::TestCase
|
|
108
108
|
|
109
109
|
def test_log_event
|
110
110
|
response = @moesif_rack_app.call(Rack::MockRequest.env_for("https://acmeinc.com/items/42752/reviews"))
|
111
|
+
# Sleep to allow queue to flush for testing purpose
|
112
|
+
sleep 5
|
111
113
|
assert_equal response, @app.call(nil)
|
112
114
|
end
|
113
115
|
|
metadata
CHANGED
@@ -1,28 +1,34 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: moesif_rack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3
|
4
|
+
version: 1.4.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Moesif, Inc
|
8
8
|
- Xing Wang
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-06-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: test-unit
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
18
|
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3.1'
|
21
|
+
- - ">="
|
19
22
|
- !ruby/object:Gem::Version
|
20
23
|
version: 3.1.5
|
21
|
-
type: :
|
24
|
+
type: :development
|
22
25
|
prerelease: false
|
23
26
|
version_requirements: !ruby/object:Gem::Requirement
|
24
27
|
requirements:
|
25
28
|
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '3.1'
|
31
|
+
- - ">="
|
26
32
|
- !ruby/object:Gem::Version
|
27
33
|
version: 3.1.5
|
28
34
|
- !ruby/object:Gem::Dependency
|
@@ -31,15 +37,21 @@ dependencies:
|
|
31
37
|
requirements:
|
32
38
|
- - "~>"
|
33
39
|
- !ruby/object:Gem::Version
|
34
|
-
version: 1.2
|
40
|
+
version: '1.2'
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.2.12
|
35
44
|
type: :runtime
|
36
45
|
prerelease: false
|
37
46
|
version_requirements: !ruby/object:Gem::Requirement
|
38
47
|
requirements:
|
39
48
|
- - "~>"
|
40
49
|
- !ruby/object:Gem::Version
|
41
|
-
version: 1.2
|
42
|
-
|
50
|
+
version: '1.2'
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.2.12
|
54
|
+
description: Rack/Rails middleware to log API calls to Moesif API analytics and monitoring
|
43
55
|
email: xing@moesif.com
|
44
56
|
executables: []
|
45
57
|
extensions: []
|
@@ -61,7 +73,7 @@ homepage: https://moesif.com
|
|
61
73
|
licenses:
|
62
74
|
- Apache-2.0
|
63
75
|
metadata: {}
|
64
|
-
post_install_message:
|
76
|
+
post_install_message:
|
65
77
|
rdoc_options: []
|
66
78
|
require_paths:
|
67
79
|
- lib
|
@@ -76,9 +88,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
88
|
- !ruby/object:Gem::Version
|
77
89
|
version: '0'
|
78
90
|
requirements: []
|
79
|
-
rubyforge_project:
|
91
|
+
rubyforge_project:
|
80
92
|
rubygems_version: 2.7.7
|
81
|
-
signing_key:
|
93
|
+
signing_key:
|
82
94
|
specification_version: 4
|
83
95
|
summary: moesif_rack
|
84
96
|
test_files: []
|