sleek 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +256 -0
  7. data/Rakefile +6 -0
  8. data/lib/sleek/base.rb +52 -0
  9. data/lib/sleek/core_ext/range.rb +44 -0
  10. data/lib/sleek/core_ext/time.rb +2 -0
  11. data/lib/sleek/event.rb +37 -0
  12. data/lib/sleek/filter.rb +24 -0
  13. data/lib/sleek/interval.rb +41 -0
  14. data/lib/sleek/queries/average.rb +21 -0
  15. data/lib/sleek/queries/count.rb +17 -0
  16. data/lib/sleek/queries/count_unique.rb +21 -0
  17. data/lib/sleek/queries/maximum.rb +21 -0
  18. data/lib/sleek/queries/minimum.rb +21 -0
  19. data/lib/sleek/queries/query.rb +105 -0
  20. data/lib/sleek/queries/sum.rb +21 -0
  21. data/lib/sleek/queries/targetable.rb +13 -0
  22. data/lib/sleek/queries.rb +8 -0
  23. data/lib/sleek/query_collection.rb +25 -0
  24. data/lib/sleek/timeframe.rb +85 -0
  25. data/lib/sleek/version.rb +3 -0
  26. data/lib/sleek.rb +23 -0
  27. data/sleek.gemspec +28 -0
  28. data/sleek.png +0 -0
  29. data/spec/lib/sleek/base_spec.rb +48 -0
  30. data/spec/lib/sleek/event_spec.rb +21 -0
  31. data/spec/lib/sleek/filter_spec.rb +26 -0
  32. data/spec/lib/sleek/interval_spec.rb +24 -0
  33. data/spec/lib/sleek/queries/average_spec.rb +13 -0
  34. data/spec/lib/sleek/queries/count_spec.rb +13 -0
  35. data/spec/lib/sleek/queries/count_unique_spec.rb +15 -0
  36. data/spec/lib/sleek/queries/maximum_spec.rb +13 -0
  37. data/spec/lib/sleek/queries/minimum_spec.rb +13 -0
  38. data/spec/lib/sleek/queries/query_spec.rb +226 -0
  39. data/spec/lib/sleek/queries/sum_spec.rb +13 -0
  40. data/spec/lib/sleek/queries/targetable_spec.rb +29 -0
  41. data/spec/lib/sleek/query_collection_spec.rb +27 -0
  42. data/spec/lib/sleek/timeframe_spec.rb +86 -0
  43. data/spec/lib/sleek_spec.rb +10 -0
  44. data/spec/spec_helper.rb +17 -0
  45. metadata +203 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ services:
6
+ - mongodb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sleek.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Gosha Arinich
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,256 @@
1
+ ![Sleek](sleek.png)
2
+
3
+ [![Build Status](https://travis-ci.org/goshakkk/sleek.png)](https://travis-ci.org/goshakkk/sleek)
4
+
5
+ Sleek is a gem for doing analytics. It allows you to easily collect and
6
+ analyze events that happen in your app.
7
+
8
+ ## Installation
9
+
10
+ The easiest way to install Sleek is to add it to your Gemfile:
11
+
12
+ ```ruby
13
+ gem "sleek"
14
+ ```
15
+
16
+ Then, install it:
17
+
18
+ ```
19
+ $ bundle install
20
+ ```
21
+
22
+ Sleek requires MongoDB to work and assumes that you have Mongoid
23
+ configured already.
24
+
25
+ ## Getting started
26
+
27
+ ### Namespacing
28
+
29
+ Namespaces are a great way to organize entirely different buckets of
30
+ data inside a single application. In Sleek, everything is namespaced.
31
+
32
+ Creating a namespaced instance of Sleek is easy:
33
+
34
+ ```ruby
35
+ sleek = Sleek[:my_namespace]
36
+ ```
37
+
38
+ You then would just call everything on this instance.
39
+
40
+ ### Sending an Event
41
+
42
+ The heart of analytics is in recording events. Events are things that
43
+ happen in your app that you want to track. Events are stored in event
44
+ buckets.
45
+
46
+ In order to send an event, you would simply need to call
47
+ `sleek.record`, passing the event bucket name and the event
48
+ payload.
49
+
50
+ ```ruby
51
+ sleek.record(:purchases, {
52
+ customer: { id: 1, name: "First Last", email: "first@last.com" },
53
+ items: [{ sku: "TSTITM1", name: "Test Item 1", price: 1999 }],
54
+ total: 1999
55
+ })
56
+ ```
57
+
58
+ ### Analyzing Events
59
+
60
+ #### Simple count
61
+
62
+ There are a few methods of analyzing your data. The simplest one is
63
+ counting. It, you guessed it, would count how many times the event has
64
+ occurred.
65
+
66
+ ```ruby
67
+ sleek.queries.count(:purchases)
68
+ # => 42
69
+ ```
70
+
71
+ #### Average
72
+
73
+ In order to calculate average value, it's needed to additionally specify
74
+ what property should the average be calculated based on:
75
+
76
+ ```ruby
77
+ sleek.queries.average(:purchases, target_property: :total)
78
+ # => 1999
79
+ ```
80
+
81
+ #### Query with timeframe
82
+
83
+ You can limit the scope of events that analysis is run on by adding the
84
+ `:timeframe` option to any query call.
85
+
86
+ ```ruby
87
+ sleek.queries.count(:purchases, timeframe: :this_day)
88
+ # => 10
89
+ ```
90
+
91
+ #### Query with interval
92
+
93
+ Some kinds of applications may need to analyze trends in the data. Using
94
+ intervals, you can break a timeframe into minutes, hours, days, weeks,
95
+ or months. One can do so by passing the `:interval` option to any query
96
+ call. Using `:interval` also requires that you specify `:timeframe`.
97
+
98
+ ```ruby
99
+ sleek.queries.count(:purchases, timeframe: :this_2_days, interval: :daily)
100
+ # => [
101
+ # {:timeframe=>#<Sleek::Timeframe 2013-01-01 00:00:00 UTC..2013-01-02 00:00:00 UTC>, :value=>10},
102
+ # {:timeframe=>#<Sleek::Timeframe 2013-01-02 00:00:00 UTC..2013-01-03 00:00:00 UTC>, :value=>24}
103
+ # ]
104
+ ```
105
+
106
+ ## Data analysis in more detail
107
+
108
+ ### Metrics
109
+
110
+ The word "metrics" is used to describe analysis queries which return a
111
+ single numeric value.
112
+
113
+ ### Count
114
+
115
+ Count just counts the number of events recorded.
116
+
117
+ ```ruby
118
+ sleek.queries.count(:bucket)
119
+ # => 42
120
+ ```
121
+
122
+ ### Count unique
123
+
124
+ It counts how many events have an unique value for a given property.
125
+
126
+ ```ruby
127
+ sleek.queries.count_unique(:bucket, params)
128
+ ```
129
+
130
+ You must pass the target property name in params like this:
131
+
132
+ ```ruby
133
+ sleek.queries.count_unique(:purchases, target_property: "customer.id")
134
+ # => 30
135
+ ```
136
+
137
+ ### Minimum
138
+
139
+ It finds the minimum numeric value for a given property. All non-numeric
140
+ values are ignored. If none of property values are numeric, the
141
+ exception will be raised.
142
+
143
+ ```ruby
144
+ sleek.queries.minimum(:bucket, params)
145
+ ```
146
+
147
+ You must pass the target property name in params like this:
148
+
149
+ ```ruby
150
+ sleek.queries.minimum(:purchases, target_property: "total")
151
+ # => 10_99
152
+ ```
153
+
154
+ ### Maximum
155
+
156
+ It finds the maximum numeric value for a given property. All non-numeric
157
+ values are ignored. If none of property values are numeric, the
158
+ exception will be raised.
159
+
160
+ ```ruby
161
+ sleek.queries.maximum(:bucket, params)
162
+ ```
163
+
164
+ You must pass the target property name in params like this:
165
+
166
+ ```ruby
167
+ sleek.queries.maximum(:purchases, target_property: "total")
168
+ # => 199_99
169
+ ```
170
+
171
+ ### Average
172
+
173
+ The average query finds the average value for a given property. All
174
+ non-numeric values are ignored. If none of property values are numeric,
175
+ the exception will be raised.
176
+
177
+ ```ruby
178
+ sleek.queries.average(:bucket, params)
179
+ ```
180
+
181
+ You must pass the target property name in params like this:
182
+
183
+ ```ruby
184
+ sleek.queries.average(:purchases, target_property: "total")
185
+ # => 49_35
186
+ ```
187
+
188
+ ### Sum
189
+
190
+ The sum query sums all the numeric values for a given property. All
191
+ non-numeric values are ignored. If none of property values are numeric,
192
+ the exception will be raised.
193
+
194
+ ```ruby
195
+ sleek.queries.sum(:bucket, params)
196
+ ```
197
+
198
+ You must pass the target property name in params like this:
199
+
200
+ ```ruby
201
+ sleek.queries.sum(:purchases, target_property: "total")
202
+ # => 2_072_70
203
+ ```
204
+
205
+ ## Filters
206
+
207
+ To limit the scope of events used in analysis you can use a filter. To
208
+ do so, you just pass the `:filter` option to the query.
209
+
210
+ A single filter is a 3-element array, consisting of:
211
+
212
+ * `property_name` - the property name to filter.
213
+ * `operator` - the name of the operator to apply.
214
+ * `value` - the value used in operator to compare to property value.
215
+
216
+ Operators: eq, ne, lt, lte, gt, gte, in.
217
+
218
+ You can pass either a single filter or an array of filters.
219
+
220
+ ```ruby
221
+ sleek.queries.count(:purchases, filters: [:total, :gt, 1599])
222
+ # => 20
223
+ ```
224
+
225
+ ## Series
226
+
227
+ Series allow you to analyze trends in metrics over time. They break a
228
+ timeframe into intervals and compute the metric for those intervals.
229
+
230
+ Calculating series is simply done by adding the `:timeframe` and
231
+ `:interval` options to the metric query.
232
+
233
+ Valid intervals are:
234
+
235
+ * `:hourly`
236
+ * `:daily`
237
+ * `:weekly`
238
+ * `:monthly`
239
+
240
+ ## Other
241
+
242
+ ### Deleting buckets
243
+
244
+ ```ruby
245
+ sleek.delete_bucket(:purchases)
246
+ ```
247
+
248
+ ### Deleting property from all events in the bucket
249
+
250
+ ```ruby
251
+ sleek.delete_property(:purchases, :some_property)
252
+ ```
253
+
254
+ ## License
255
+
256
+ [MIT](LICENSE).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task default: :spec
data/lib/sleek/base.rb ADDED
@@ -0,0 +1,52 @@
1
+ module Sleek
2
+ class Base
3
+ attr_reader :namespace
4
+
5
+ # Internal: Initialize Sleek with namespace.
6
+ #
7
+ # namespace - the Symbol namespace name.
8
+ def initialize(namespace)
9
+ @namespace = namespace
10
+ end
11
+
12
+ # Public: Record an event.
13
+ #
14
+ # bucket - the String name of bucket.
15
+ # payload - the Hash of event data.
16
+ def record(bucket, payload)
17
+ Event.create_with_namespace(namespace, bucket, payload)
18
+ end
19
+
20
+ # Public: Get `QueriesCollection` for the namespace.
21
+ def queries
22
+ @queries ||= QueryCollection.new(namespace)
23
+ end
24
+
25
+ # Public: Delete event bucket.
26
+ #
27
+ # bucket - the String bucket name.
28
+ def delete_bucket(bucket)
29
+ events(bucket).delete_all
30
+ end
31
+
32
+ # Public: Delete specific property from all events in the bucket.
33
+ #
34
+ # bucket - the String bucket name.
35
+ # property - the String property name.
36
+ def delete_property(bucket, property)
37
+ events(bucket).unset("d.#{property}")
38
+ end
39
+
40
+ # Internal: Get events associated with current namespace and,
41
+ # optionally, specified bucket.
42
+ def events(bucket = nil)
43
+ evts = Event.where(namespace: namespace)
44
+ evts = evts.where(bucket: bucket) if bucket.present?
45
+ evts
46
+ end
47
+
48
+ def inspect
49
+ "#<Sleek::Base ns=#{namespace}>"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ class Range
2
+ # Public: Convert both ends of range to integers.
3
+ def to_i_range
4
+ self.begin.to_i..self.end.to_i
5
+ end
6
+
7
+ # Public: Convert both ends of range to times.
8
+ def to_time_range
9
+ Time.at(self.begin)..Time.at(self.end)
10
+ end
11
+
12
+ def int_range?
13
+ self.begin.is_a?(Integer)
14
+ end
15
+
16
+ # Public: Check if range elements are times.
17
+ def time_range?
18
+ self.begin.is_a?(Time)
19
+ end
20
+
21
+ # Public: Calculate the differentce between ends of the range.
22
+ def difference
23
+ self.end - self.begin
24
+ end
25
+
26
+ # Public: Make up a range for previous n periods.
27
+ # Start of new range would be start of current - difference between
28
+ # start and end * number of periods, end of new range would be start of
29
+ # current.
30
+ #
31
+ # Example
32
+ #
33
+ # (1200..1300).previous
34
+ # # => 1100..1200
35
+ def previous(n = 1)
36
+ new_begin = self.begin - difference * n
37
+ new_end = self.end - difference * n
38
+ new_begin..new_end
39
+ end
40
+
41
+ def -(what)
42
+ (self.begin - what)..(self.end - what)
43
+ end
44
+ end
@@ -0,0 +1,2 @@
1
+ class Time
2
+ end
@@ -0,0 +1,37 @@
1
+ module Sleek
2
+ class EventMetadata
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps::Created::Short
5
+
6
+ field :t, type: Time, as: :timestamp
7
+ embedded_in :event
8
+
9
+ before_create :set_timestamp
10
+
11
+ def set_timestamp
12
+ self.timestamp = created_at unless timestamp
13
+ end
14
+ end
15
+
16
+ class Event
17
+ include Mongoid::Document
18
+
19
+ field :ns, type: Symbol, as: :namespace
20
+ field :b, type: String, as: :bucket
21
+ field :d, type: Hash, as: :data
22
+ embeds_one :sleek, store_as: "s", class_name: 'Sleek::EventMetadata', cascade_callbacks: true
23
+ accepts_nested_attributes_for :sleek
24
+
25
+ validates :namespace, presence: true
26
+ validates :bucket, presence: true
27
+
28
+ after_initialize { build_sleek }
29
+
30
+ def self.create_with_namespace(namespace, bucket, payload)
31
+ sleek = payload.delete(:sleek)
32
+ event = create(namespace: namespace, bucket: bucket, data: payload)
33
+ event.sleek.update_attributes sleek
34
+ event
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ module Sleek
2
+ class Filter
3
+ attr_reader :property_name, :operator, :value
4
+
5
+ def initialize(property_name, operator, value)
6
+ @property_name = "d.#{property_name}"
7
+ @operator = operator.to_sym
8
+ @value = value
9
+
10
+ unless [:eq, :ne, :lt, :lte, :gt, :gte, :in].include? @operator
11
+ raise ArgumentError, "unsupported operator - #{operator}"
12
+ end
13
+ end
14
+
15
+ def apply(criteria)
16
+ criteria.send(operator, property_name => value)
17
+ end
18
+
19
+ def ==(other)
20
+ other.is_a?(Filter) && property_name == other.property_name &&
21
+ operator == other.operator && value == other.value
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ module Sleek
2
+ class Interval
3
+ def self.interval_value(desc)
4
+ case desc
5
+ when :hourly
6
+ 1.hour
7
+ when :daily
8
+ 1.day
9
+ when :weekly
10
+ 1.week
11
+ when :monthly
12
+ 1.month
13
+ else
14
+ raise ArgumentError, "invalid interval description"
15
+ end
16
+ end
17
+
18
+ attr_reader :interval, :timeframe
19
+
20
+ # Internal: Initialize an interval.
21
+ #
22
+ # interval_desc - the Symbol description of the interval.
23
+ # Possible values: :hourly, :daily, :weekly,
24
+ # :monthly.
25
+ # timeframe - the Timeframe object.
26
+ def initialize(interval_desc, timeframe)
27
+ @interval = self.class.interval_value(interval_desc)
28
+ @timeframe = timeframe
29
+ end
30
+
31
+ # Internal: Split the timeframe into intervals.
32
+ #
33
+ # Returns an Array of Timeframe objects.
34
+ def timeframes
35
+ @timeframes ||= timeframe.to_time_range.to_i_range.each_slice(interval)
36
+ .to_a[0..-2]
37
+ .map { |tf| (tf.first..(tf.first + interval)).to_time_range }
38
+ .map { |tf| Timeframe.new(tf) }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Average query.
4
+ #
5
+ # Finds the average value for a given property.
6
+ #
7
+ # target_property - the String name of target property on event.
8
+ #
9
+ # Examples
10
+ #
11
+ # sleek.queries.average(:purchases, target_property: "total")
12
+ # # => 49_35
13
+ class Average < Query
14
+ include Targetable
15
+
16
+ def perform(events)
17
+ events.avg target_property
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Count query.
4
+ #
5
+ # Simply counts events.
6
+ #
7
+ # Examples
8
+ #
9
+ # sleek.queries.count(:purchases)
10
+ # # => 42
11
+ class Count < Query
12
+ def perform(events)
13
+ events.count
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Count unique query.
4
+ #
5
+ # Counts how many events have unique value for a given property.
6
+ #
7
+ # target_property - the String name of target property on event.
8
+ #
9
+ # Examples
10
+ #
11
+ # sleek.queries.count_unique(:purchases, target_property: "customer.email")
12
+ # # => 4
13
+ class CountUnique < Query
14
+ include Targetable
15
+
16
+ def perform(events)
17
+ events.distinct(target_property).count
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Maximum query.
4
+ #
5
+ # Finds the maximum value for a given property.
6
+ #
7
+ # target_property - the String name of target property on event.
8
+ #
9
+ # Examples
10
+ #
11
+ # sleek.queries.maximum(:purchases, target_property: "total")
12
+ # # => 199_99
13
+ class Maximum < Query
14
+ include Targetable
15
+
16
+ def perform(events)
17
+ events.max target_property
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Sleek
2
+ module Queries
3
+ # Internal: Minimum query.
4
+ #
5
+ # Finds the minimum value for a given property.
6
+ #
7
+ # target_property - the String name of target property on event.
8
+ #
9
+ # Examples
10
+ #
11
+ # sleek.queries.minimum(:purchases, target_property: "total")
12
+ # # => 19_99
13
+ class Minimum < Query
14
+ include Targetable
15
+
16
+ def perform(events)
17
+ events.min target_property
18
+ end
19
+ end
20
+ end
21
+ end