sleek 0.0.1

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 (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