searchjoy 0.5.1 → 1.0.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: a837b7470769cd1d7589023ab3f1ffed51b683ea67016d3f642aa26b9edcab3e
4
- data.tar.gz: 54239d751a05741b0b8fbfcedbc6d130af6a6153499ada75e282bfad7acec716
3
+ metadata.gz: 807861a09661dcbcf5a2f4c580e189e3df4d7f67e3a1dbb863408ce7fc1091c3
4
+ data.tar.gz: b901df59229c2a6daa3781a44b66b89d87d171aeede2acbd17f9ef135e78c7fd
5
5
  SHA512:
6
- metadata.gz: 8f35d980883ded923995f9340867b1685bcb710e66976ce8ce72e18033de94155c49ff1b53b8c4ba2fdb3d4c1f365bd0db8ee8dd0e70de39730e9f8f4124fb36
7
- data.tar.gz: 191ee153cd9941773f2c388b38816513e1319ec7f29f5a6d59ad8d5182e310080d7260a9ebfecc7e4dda8801f61dbaab3576426912848787a713c60ef8c92782
6
+ metadata.gz: 4c7d96778f812145a132f8f756ac2ca1bb88429758db180402f47b0f56d68b9dfe63e461668b28a51162018f7f97f49c6f94100e3430a79bf54056cd2b4fcdbb
7
+ data.tar.gz: 744e8fff8679dfc197d4439c7cda43cc21b23f38e12aec883a1b095481ae4f3db597b3f26bc2c409c51c07928a4f68a4a5114b387905b5c0c6f119611dbac7b1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.0.0 (2022-05-10)
2
+
3
+ - Added support for multiple conversions per search
4
+ - Added experimental support for Propshaft
5
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
6
+
1
7
  ## 0.5.1 (2020-09-07)
2
8
 
3
9
  - Use `datetime` type in migration
@@ -57,3 +63,44 @@ Breaking
57
63
  ## 0.1.0 (2016-08-23)
58
64
 
59
65
  - Fixed Searchkick integration for `execute: false`
66
+
67
+ ## 0.0.10 (2016-04-20)
68
+
69
+ - Fixed error when block passed to `search`
70
+
71
+ ## 0.0.9 (2016-03-12)
72
+
73
+ - Fixed error with routes
74
+
75
+ ## 0.0.8 (2015-12-12)
76
+
77
+ - Added `user_id` to searches
78
+
79
+ ## 0.0.7 (2014-08-18)
80
+
81
+ - Fixed error with Searchkick 0.8
82
+
83
+ ## 0.0.6 (2013-12-19)
84
+
85
+ - Fixed error with Rails 3
86
+
87
+ ## 0.0.5 (2013-11-01)
88
+
89
+ - Renamed to Searchjoy
90
+
91
+ ## 0.0.4 (2013-10-31)
92
+
93
+ - Renamed `converted` to `convert`
94
+
95
+ ## 0.0.3 (2013-10-31)
96
+
97
+ - Added `conversion_name` option
98
+ - Added `converted` method
99
+
100
+ ## 0.0.2 (2013-10-31)
101
+
102
+ - Added dashboard
103
+
104
+ ## 0.0.1 (2013-10-24)
105
+
106
+ - First release
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2020 Andrew Kane
1
+ Copyright (c) 2013-2022 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -4,7 +4,7 @@ Search analytics made easy
4
4
 
5
5
  [See it in action](https://searchjoy.dokkuapp.com/)
6
6
 
7
- [![Screenshot](https://searchjoy.dokkuapp.com/assets/searchjoy-a9d4c01356a822bc1dba1eecf7ac76387de009fc9902eb9a880057069212be80.png)](https://searchjoy.dokkuapp.com/)
7
+ [![Screenshot](https://searchjoy.dokkuapp.com/assets/searchjoy-7be12d922ca8b31b7d7440e618b0c666698a4b15752653a0c5c45e3dd2737142.png)](https://searchjoy.dokkuapp.com/)
8
8
 
9
9
  - view searches in real-time
10
10
  - track conversions week over week
@@ -14,7 +14,7 @@ Works with any search platform, including Elasticsearch, Sphinx, and Solr
14
14
 
15
15
  :cupid: An amazing companion to [Searchkick](https://github.com/ankane/searchkick)
16
16
 
17
- [![Build Status](https://travis-ci.org/ankane/searchjoy.svg?branch=master)](https://travis-ci.org/ankane/searchjoy)
17
+ [![Build Status](https://github.com/ankane/searchjoy/workflows/build/badge.svg?branch=master)](https://github.com/ankane/searchjoy/actions)
18
18
 
19
19
  ## Installation
20
20
 
@@ -70,8 +70,6 @@ Then, pass the values to the `track` option.
70
70
  Item.search("apple", track: {user_id: 1, source: "web"})
71
71
  ```
72
72
 
73
- It’s that easy.
74
-
75
73
  ## Track Conversions
76
74
 
77
75
  First, choose a conversion metric. At Instacart, an item added to the cart from the search results page counts as a conversion.
@@ -121,12 +119,18 @@ ENV["SEARCHJOY_PASSWORD"] = "secret"
121
119
 
122
120
  ## Data Retention
123
121
 
124
- Delete older data with:
122
+ Data should only be retained for as long as it’s needed. Delete older data with:
125
123
 
126
124
  ```ruby
127
125
  Searchjoy::Search.where("created_at < ?", 1.year.ago).in_batches.delete_all
128
126
  ```
129
127
 
128
+ You can use [Rollup](https://github.com/ankane/rollup) to aggregate important data before you do.
129
+
130
+ ```ruby
131
+ Searchjoy::Search.rollup("Searches")
132
+ ```
133
+
130
134
  Delete data for a specific user with:
131
135
 
132
136
  ```ruby
@@ -167,6 +171,46 @@ Show the conversion name in the live stream
167
171
  Searchjoy.conversion_name = ->(model) { model.name }
168
172
  ```
169
173
 
174
+ ## Upgrading
175
+
176
+ ### 1.0
177
+
178
+ Searchjoy now supports multiple conversions per search :tada:
179
+
180
+ Before updating the gem, create a migration with:
181
+
182
+ ```ruby
183
+ create_table :searchjoy_conversions do |t|
184
+ t.references :search
185
+ t.references :convertable, polymorphic: true, index: {name: "index_searchjoy_conversions_on_convertable"}
186
+ t.datetime :created_at
187
+ end
188
+ ```
189
+
190
+ Deploy and run the migration, then update the gem.
191
+
192
+ You can optionally backfill the conversions table
193
+
194
+ ```ruby
195
+ Searchjoy.backfill_conversions
196
+ ```
197
+
198
+ And optionally remove `convertable` from searches
199
+
200
+ ```ruby
201
+ remove_reference :searchjoy_searches, :convertable, polymorphic: true
202
+ ```
203
+
204
+ You can stay with single conversions (and skip all the previous steps) by creating an initializer with:
205
+
206
+ ```ruby
207
+ Searchjoy.multiple_conversions = false
208
+ ```
209
+
210
+ ## History
211
+
212
+ View the [changelog](https://github.com/ankane/searchjoy/blob/master/CHANGELOG.md)
213
+
170
214
  ## Contributing
171
215
 
172
216
  Everyone is encouraged to help improve this project. Here are a few ways you can help:
@@ -23,18 +23,18 @@ module Searchjoy
23
23
  duration = @time_range.last - @time_range.first
24
24
  period =
25
25
  if duration < 3.days # shows 48-72 data points (ends at current time)
26
- "hour"
26
+ :hour
27
27
  elsif duration < 60.days
28
- "day"
28
+ :day
29
29
  elsif duration < 60.weeks # to make it easy to compare to same time last year
30
- "week"
30
+ :week
31
31
  else
32
- "month"
32
+ :month
33
33
  end
34
34
 
35
35
  relation = Searchjoy::Search.where(search_type: params[:search_type])
36
36
  @searches_by_week = relation.group_by_period(period, :created_at, time_zone: @time_zone, range: @time_range).count
37
- @conversions_by_week = relation.where("converted_at is not null").group_by_period(period, :created_at, time_zone: @time_zone, range: @time_range).count
37
+ @conversions_by_week = relation.where.not(converted_at: nil).group_by_period(period, :created_at, time_zone: @time_zone, range: @time_range).count
38
38
  @top_searches = @searches.first(5)
39
39
  @bad_conversion_rate = @searches.sort_by { |s| [s["conversion_rate"].to_f, s["query"]] }.first(5).select { |s| s["conversion_rate"] < 50 }
40
40
  @conversion_rate_by_week = {}
@@ -47,7 +47,12 @@ module Searchjoy
47
47
  end
48
48
 
49
49
  def recent
50
- @searches = Searchjoy::Search.includes(:convertable).order("created_at desc").limit(50)
50
+ @searches = Searchjoy::Search.order(created_at: :desc).limit(50)
51
+ if Searchjoy.multiple_conversions
52
+ @searches.includes!(conversions: :convertable)
53
+ else
54
+ @searches.includes!(:convertable)
55
+ end
51
56
  render layout: false
52
57
  end
53
58
 
@@ -81,7 +86,14 @@ module Searchjoy
81
86
 
82
87
  def set_searches
83
88
  @limit = params[:limit] || Searchjoy.top_searches
84
- @searches = Searchjoy::Search.connection.select_all(Searchjoy::Search.select("normalized_query, COUNT(*) as searches_count, COUNT(converted_at) as conversions_count, AVG(results_count) as avg_results_count").where(created_at: @time_range, search_type: @search_type).group("normalized_query").order("searches_count desc, normalized_query asc").limit(@limit).to_sql).to_a
89
+ relation = Searchjoy::Search
90
+ .select("normalized_query, COUNT(*) as searches_count, COUNT(converted_at) as conversions_count, AVG(results_count) as avg_results_count")
91
+ .where(created_at: @time_range, search_type: @search_type)
92
+ .group(:normalized_query)
93
+ .order(searches_count: :desc, normalized_query: :asc)
94
+ .limit(@limit)
95
+ # get array of hashes for performance
96
+ @searches = Searchjoy::Search.connection.select_all(relation.to_sql).to_a
85
97
  @searches.each do |search|
86
98
  search["conversion_rate"] = 100 * search["conversions_count"].to_i / search["searches_count"].to_f
87
99
  end
@@ -0,0 +1,8 @@
1
+ module Searchjoy
2
+ class Conversion < ActiveRecord::Base
3
+ self.table_name = "searchjoy_conversions"
4
+
5
+ belongs_to :search
6
+ belongs_to :convertable, polymorphic: true, optional: true
7
+ end
8
+ end
@@ -4,14 +4,34 @@ module Searchjoy
4
4
 
5
5
  belongs_to :convertable, polymorphic: true, optional: true
6
6
  belongs_to :user, optional: true
7
+ has_many :conversions
7
8
 
8
9
  before_save :set_normalized_query
9
10
 
10
11
  def convert(convertable = nil)
11
- unless converted?
12
- self.converted_at = Time.now
13
- self.convertable = convertable
14
- save(validate: false)
12
+ return unless Searchjoy.multiple_conversions || !converted?
13
+
14
+ # use transaction to keep consistent
15
+ self.class.transaction do
16
+ # make time consistent
17
+ now = Time.now
18
+
19
+ if Searchjoy.multiple_conversions
20
+ conversion =
21
+ conversions.create!(
22
+ convertable: convertable,
23
+ created_at: now
24
+ )
25
+ end
26
+
27
+ unless converted?
28
+ self.converted_at = now
29
+ # check id instead of association
30
+ self.convertable = convertable if respond_to?(:convertable_id=)
31
+ save(validate: false)
32
+ end
33
+
34
+ conversion
15
35
  end
16
36
  end
17
37
 
@@ -288,7 +288,11 @@
288
288
  }
289
289
  </style>
290
290
 
291
- <%= javascript_include_tag "searchjoy/application" %>
291
+ <% if defined?(Propshaft::Railtie) %>
292
+ <%= javascript_include_tag "chartkick", "Chart.bundle", "searchjoy/litepicker", "searchjoy/application" %>
293
+ <% else %>
294
+ <%= javascript_include_tag "searchjoy/application" %>
295
+ <% end %>
292
296
  </head>
293
297
  <body>
294
298
  <div class="container">
@@ -15,7 +15,8 @@
15
15
  <td style="width: 35%; color: #5cb85c;">
16
16
  <% if search.converted_at %>
17
17
  <strong>✓</strong>
18
- <%= search.convertable ? (Searchjoy.conversion_name ? Searchjoy.conversion_name.call(search.convertable) : "#{search.convertable_type} #{search.convertable_id}") : "Converted" %>
18
+ <% convertable = Searchjoy.multiple_conversions ? search.conversions.min_by(&:created_at)&.convertable : search.convertable %>
19
+ <%= convertable ? (Searchjoy.conversion_name ? Searchjoy.conversion_name.call(convertable) : "#{convertable.model_name.name} #{convertable.id}") : "Converted" %>
19
20
  <% end %>
20
21
  </td>
21
22
  <td style="width: 20%;" class="num">
@@ -16,7 +16,7 @@
16
16
  }
17
17
 
18
18
  function fetchRecentSearches() {
19
- load(document.getElementById("stream"), "<%= searches_recent_path %>")
19
+ load(document.getElementById("stream"), <%= raw json_escape(searches_recent_path.to_json) %>)
20
20
  setTimeout(fetchRecentSearches, 5 * 1000);
21
21
  }
22
22
  fetchRecentSearches();
@@ -7,12 +7,17 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
7
7
  t.string :normalized_query
8
8
  t.integer :results_count
9
9
  t.datetime :created_at
10
- t.references :convertable, polymorphic: true, index: {name: "index_searchjoy_searches_on_convertable"}
11
10
  t.datetime :converted_at
12
11
  end
13
12
 
14
13
  add_index :searchjoy_searches, [:created_at]
15
14
  add_index :searchjoy_searches, [:search_type, :created_at]
16
15
  add_index :searchjoy_searches, [:search_type, :normalized_query, :created_at], name: "index_searchjoy_searches_on_search_type_query" # autogenerated name is too long
16
+
17
+ create_table :searchjoy_conversions do |t|
18
+ t.references :search
19
+ t.references :convertable, polymorphic: true, index: {name: "index_searchjoy_conversions_on_convertable"}
20
+ t.datetime :created_at
21
+ end
17
22
  end
18
23
  end
@@ -1,3 +1,3 @@
1
1
  module Searchjoy
2
- VERSION = "0.5.1"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/searchjoy.rb CHANGED
@@ -23,11 +23,36 @@ module Searchjoy
23
23
  mattr_accessor :query_name
24
24
  mattr_accessor :query_url
25
25
 
26
+ # multiple conversions
27
+ mattr_accessor :multiple_conversions
28
+ self.multiple_conversions = true
29
+
26
30
  def self.attach_to_searchkick!
27
31
  Searchkick::Query.prepend(Searchjoy::Track::Query)
28
32
  Searchkick::MultiSearch.prepend(Searchjoy::Track::MultiSearch)
29
33
  Searchkick::Results.send(:attr_accessor, :search)
30
34
  end
35
+
36
+ def self.backfill_conversions
37
+ Searchjoy::Search.where.not(converted_at: nil).left_joins(:conversions).where(searchjoy_conversions: {id: nil}).find_in_batches do |searches|
38
+ conversions =
39
+ searches.map do |search|
40
+ {
41
+ search_id: search.id,
42
+ convertable_id: search.convertable_id,
43
+ convertable_type: search.convertable_type,
44
+ created_at: search.converted_at
45
+ }
46
+ end
47
+ if ActiveRecord::VERSION::MAJOR >= 6
48
+ Searchjoy::Conversion.insert_all(conversions)
49
+ else
50
+ Searchjoy::Conversion.transaction do
51
+ Searchjoy::Conversion.create!(conversions)
52
+ end
53
+ end
54
+ end
55
+ end
31
56
  end
32
57
 
33
58
  if defined?(Rails)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchjoy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-08 00:00:00.000000000 Z
11
+ date: 2022-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chartkick
@@ -44,86 +44,16 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '5'
47
+ version: '5.2'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '5'
55
- - !ruby/object:Gem::Dependency
56
- name: bundler
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: minitest
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: sqlite3
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: searchkick
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- description:
126
- email: andrew@chartkick.com
54
+ version: '5.2'
55
+ description:
56
+ email: andrew@ankane.org
127
57
  executables: []
128
58
  extensions: []
129
59
  extra_rdoc_files: []
@@ -134,6 +64,7 @@ files:
134
64
  - app/assets/javascripts/searchjoy/application.js
135
65
  - app/assets/javascripts/searchjoy/litepicker.js
136
66
  - app/controllers/searchjoy/searches_controller.rb
67
+ - app/models/searchjoy/conversion.rb
137
68
  - app/models/searchjoy/search.rb
138
69
  - app/views/layouts/searchjoy/application.html.erb
139
70
  - app/views/searchjoy/searches/index.html.erb
@@ -152,7 +83,7 @@ homepage: https://github.com/ankane/searchjoy
152
83
  licenses:
153
84
  - MIT
154
85
  metadata: {}
155
- post_install_message:
86
+ post_install_message:
156
87
  rdoc_options: []
157
88
  require_paths:
158
89
  - lib
@@ -160,15 +91,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
91
  requirements:
161
92
  - - ">="
162
93
  - !ruby/object:Gem::Version
163
- version: '2.4'
94
+ version: '2.6'
164
95
  required_rubygems_version: !ruby/object:Gem::Requirement
165
96
  requirements:
166
97
  - - ">="
167
98
  - !ruby/object:Gem::Version
168
99
  version: '0'
169
100
  requirements: []
170
- rubygems_version: 3.1.2
171
- signing_key:
101
+ rubygems_version: 3.3.7
102
+ signing_key:
172
103
  specification_version: 4
173
104
  summary: Search analytics made easy
174
105
  test_files: []