mousereco 0.0.2

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 (167) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +34 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/mousereco/mousereco.js +15 -0
  6. data/app/assets/javascripts/mousereco/pageviews.js +244 -0
  7. data/app/assets/javascripts/mousereco/tracker.js.erb +312 -0
  8. data/app/assets/stylesheets/mousereco/bootstrap.min.css +9 -0
  9. data/app/assets/stylesheets/mousereco/mousereco.css +39 -0
  10. data/app/controllers/mousereco/api/v1/application_controller.rb +18 -0
  11. data/app/controllers/mousereco/api/v1/events_controller.rb +17 -0
  12. data/app/controllers/mousereco/api/v1/pageviews_controller.rb +21 -0
  13. data/app/controllers/mousereco/application_controller.rb +5 -0
  14. data/app/controllers/mousereco/pageviews_controller.rb +8 -0
  15. data/app/controllers/mousereco/trackers_controller.rb +6 -0
  16. data/app/controllers/mousereco/visitors_controller.rb +5 -0
  17. data/app/helpers/mousereco/application_helper.rb +4 -0
  18. data/app/helpers/mousereco/pageview_helper.rb +7 -0
  19. data/app/models/mousereco/click.rb +4 -0
  20. data/app/models/mousereco/event.rb +13 -0
  21. data/app/models/mousereco/mousemove.rb +4 -0
  22. data/app/models/mousereco/pageview.rb +15 -0
  23. data/app/models/mousereco/scroll.rb +4 -0
  24. data/app/models/mousereco/visitor.rb +13 -0
  25. data/app/services/mousereco/events_service.rb +21 -0
  26. data/app/services/mousereco/pageviews_service.rb +14 -0
  27. data/app/views/layouts/mousereco/application.html.haml +21 -0
  28. data/app/views/mousereco/pageviews/page_html.html.haml +1 -0
  29. data/app/views/mousereco/pageviews/show.html.haml +18 -0
  30. data/app/views/mousereco/visitors/index.html.haml +12 -0
  31. data/config/routes.rb +25 -0
  32. data/db/migrate/20140128001612_create_mousereco_events.rb +14 -0
  33. data/db/migrate/20140128001832_create_mousereco_pageviews.rb +17 -0
  34. data/db/migrate/20140128001950_create_mousereco_visitors.rb +9 -0
  35. data/db/migrate/20140130215132_add_limit_to_timestamps.rb +12 -0
  36. data/lib/generators/mousereco/install/install_generator.rb +22 -0
  37. data/lib/mousereco/engine.rb +9 -0
  38. data/lib/mousereco/version.rb +3 -0
  39. data/lib/mousereco.rb +4 -0
  40. data/lib/tasks/mousereco_tasks.rake +4 -0
  41. data/spec/api/mousereco/events_api_spec.rb +37 -0
  42. data/spec/api/mousereco/pageviews_api_spec.rb +58 -0
  43. data/spec/controllers/mousereco/pageviews_controller_spec.rb +12 -0
  44. data/spec/controllers/mousereco/visitors_controller_spec.rb +12 -0
  45. data/spec/dummy/Gemfile +31 -0
  46. data/spec/dummy/Gemfile.lock +174 -0
  47. data/spec/dummy/Rakefile +6 -0
  48. data/spec/dummy/app/assets/javascripts/application.js +14 -0
  49. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  50. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  51. data/spec/dummy/app/controllers/home_controller.rb +4 -0
  52. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  53. data/spec/dummy/app/views/home/index.html.haml +2 -0
  54. data/spec/dummy/app/views/layouts/application.html.haml +17 -0
  55. data/spec/dummy/bin/bundle +3 -0
  56. data/spec/dummy/bin/rails +4 -0
  57. data/spec/dummy/bin/rake +4 -0
  58. data/spec/dummy/config/application.rb +27 -0
  59. data/spec/dummy/config/boot.rb +4 -0
  60. data/spec/dummy/config/database.yml +25 -0
  61. data/spec/dummy/config/environment.rb +5 -0
  62. data/spec/dummy/config/environments/development.rb +29 -0
  63. data/spec/dummy/config/environments/production.rb +80 -0
  64. data/spec/dummy/config/environments/test.rb +36 -0
  65. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  66. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  67. data/spec/dummy/config/initializers/inflections.rb +16 -0
  68. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  69. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  70. data/spec/dummy/config/initializers/session_store.rb +3 -0
  71. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  72. data/spec/dummy/config/locales/en.yml +23 -0
  73. data/spec/dummy/config/routes.rb +7 -0
  74. data/spec/dummy/config.ru +4 -0
  75. data/spec/dummy/db/development.sqlite3 +0 -0
  76. data/spec/dummy/db/migrate/20140128221416_create_mousereco_events.mousereco.rb +15 -0
  77. data/spec/dummy/db/migrate/20140128221417_create_mousereco_pageviews.mousereco.rb +18 -0
  78. data/spec/dummy/db/migrate/20140128221418_create_mousereco_visitors.mousereco.rb +10 -0
  79. data/spec/dummy/db/migrate/20140130215835_add_limit_to_timestamps.mousereco.rb +13 -0
  80. data/spec/dummy/db/schema.rb +48 -0
  81. data/spec/dummy/db/seeds.rb +7 -0
  82. data/spec/dummy/db/test.sqlite3 +0 -0
  83. data/spec/dummy/log/development.log +13721 -0
  84. data/spec/dummy/log/test.log +13232 -0
  85. data/spec/dummy/public/404.html +58 -0
  86. data/spec/dummy/public/422.html +58 -0
  87. data/spec/dummy/public/500.html +57 -0
  88. data/spec/dummy/public/favicon.ico +0 -0
  89. data/spec/dummy/public/robots.txt +5 -0
  90. data/spec/dummy/public/test.html +59 -0
  91. data/spec/dummy/tmp/cache/assets/development/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  92. data/spec/dummy/tmp/cache/assets/development/sprockets/1510a9c9dc22b2be9eed26a8ed2dabe8 +0 -0
  93. data/spec/dummy/tmp/cache/assets/development/sprockets/16d2919a6d361fd8c6e82b279d64067d +0 -0
  94. data/spec/dummy/tmp/cache/assets/development/sprockets/1d3e49d5614179160c0b571a9927dcc1 +0 -0
  95. data/spec/dummy/tmp/cache/assets/development/sprockets/1de21ea3d9fadc1a1c3269614b19495b +0 -0
  96. data/spec/dummy/tmp/cache/assets/development/sprockets/2533caf6d0459a8624fd40d33cddde85 +0 -0
  97. data/spec/dummy/tmp/cache/assets/development/sprockets/29cde272ab8d39abcb99bfc3cba51e1b +0 -0
  98. data/spec/dummy/tmp/cache/assets/development/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  99. data/spec/dummy/tmp/cache/assets/development/sprockets/2fd3dabf7baee6c0702bc29f86d2a70c +0 -0
  100. data/spec/dummy/tmp/cache/assets/development/sprockets/2ffb0b37de65c4bc12c9759935bcfa4a +0 -0
  101. data/spec/dummy/tmp/cache/assets/development/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  102. data/spec/dummy/tmp/cache/assets/development/sprockets/40091f436d9a77d1d9a9de7f3da46098 +0 -0
  103. data/spec/dummy/tmp/cache/assets/development/sprockets/48139bef1023ab80e22b4891191c5c3a +0 -0
  104. data/spec/dummy/tmp/cache/assets/development/sprockets/4ea33e825e6e38bd22dccab488a0255b +0 -0
  105. data/spec/dummy/tmp/cache/assets/development/sprockets/54d5f50daadb633077b6cd9dd9fb72cc +0 -0
  106. data/spec/dummy/tmp/cache/assets/development/sprockets/5817965bb0355e7b0da40f9b36153ef6 +0 -0
  107. data/spec/dummy/tmp/cache/assets/development/sprockets/5cbf24a4616e0252f26d7949679a42fe +0 -0
  108. data/spec/dummy/tmp/cache/assets/development/sprockets/5d1aff6b78ca0eab106290139d3410a3 +0 -0
  109. data/spec/dummy/tmp/cache/assets/development/sprockets/6d498a2bfb1bc8566133e4eb5c0ccd35 +0 -0
  110. data/spec/dummy/tmp/cache/assets/development/sprockets/727930fb0f32b87625179e6a77770d16 +0 -0
  111. data/spec/dummy/tmp/cache/assets/development/sprockets/7be9086c960bb4384c9d23a52e489f29 +0 -0
  112. data/spec/dummy/tmp/cache/assets/development/sprockets/7cd76d5e9ba9b8658678d1455acfab74 +0 -0
  113. data/spec/dummy/tmp/cache/assets/development/sprockets/7d94acf804831ce74c7bf5455c1782f0 +0 -0
  114. data/spec/dummy/tmp/cache/assets/development/sprockets/8f8936f164170e095c5ca2d0c2ae6127 +0 -0
  115. data/spec/dummy/tmp/cache/assets/development/sprockets/912559948251ede2d7fcd0cbd58654da +0 -0
  116. data/spec/dummy/tmp/cache/assets/development/sprockets/9d48f1ca00d652a327f6e8f7c7fc0d40 +0 -0
  117. data/spec/dummy/tmp/cache/assets/development/sprockets/a9cf04e553d11e0103cf1c870e363d7d +0 -0
  118. data/spec/dummy/tmp/cache/assets/development/sprockets/b02bb55c99ea1e7b80cf52548ee0d1c5 +0 -0
  119. data/spec/dummy/tmp/cache/assets/development/sprockets/b9b341a8be874e4eee26dce66499bc80 +0 -0
  120. data/spec/dummy/tmp/cache/assets/development/sprockets/c6793056549a28ef6b4d0a4a8929f7e3 +0 -0
  121. data/spec/dummy/tmp/cache/assets/development/sprockets/cac5a12e0a520ce86636e0443f3da875 +0 -0
  122. data/spec/dummy/tmp/cache/assets/development/sprockets/cb21171d87e8bb8b243a86594263deff +0 -0
  123. data/spec/dummy/tmp/cache/assets/development/sprockets/cc5195a4891bb336aeb52b296f4971e9 +0 -0
  124. data/spec/dummy/tmp/cache/assets/development/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  125. data/spec/dummy/tmp/cache/assets/development/sprockets/d44cfedd9e50cf9fd20463a31bad4ce3 +0 -0
  126. data/spec/dummy/tmp/cache/assets/development/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  127. data/spec/dummy/tmp/cache/assets/development/sprockets/e6c6c2db7ca06e62d0b8d81c9ee3fcf0 +0 -0
  128. data/spec/dummy/tmp/cache/assets/development/sprockets/e73e1a39e822bb2763fbb90a3d41498f +0 -0
  129. data/spec/dummy/tmp/cache/assets/development/sprockets/eae4c7b940819b55676cea6e03251739 +0 -0
  130. data/spec/dummy/tmp/cache/assets/development/sprockets/f6f1db9d7bdcc747aae3fe9f3ec43dab +0 -0
  131. data/spec/dummy/tmp/cache/assets/development/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  132. data/spec/dummy/tmp/cache/assets/development/sprockets/fe8b16f9af7353e9609c7fe6259d6912 +0 -0
  133. data/spec/dummy/tmp/cache/assets/test/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  134. data/spec/dummy/tmp/cache/assets/test/sprockets/1d3e49d5614179160c0b571a9927dcc1 +0 -0
  135. data/spec/dummy/tmp/cache/assets/test/sprockets/2154a1002f636a71bd688ee3b7a40295 +0 -0
  136. data/spec/dummy/tmp/cache/assets/test/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  137. data/spec/dummy/tmp/cache/assets/test/sprockets/2ffb0b37de65c4bc12c9759935bcfa4a +0 -0
  138. data/spec/dummy/tmp/cache/assets/test/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  139. data/spec/dummy/tmp/cache/assets/test/sprockets/40091f436d9a77d1d9a9de7f3da46098 +0 -0
  140. data/spec/dummy/tmp/cache/assets/test/sprockets/417edfca088d0fa2811b10666b3adea4 +0 -0
  141. data/spec/dummy/tmp/cache/assets/test/sprockets/48139bef1023ab80e22b4891191c5c3a +0 -0
  142. data/spec/dummy/tmp/cache/assets/test/sprockets/4ea33e825e6e38bd22dccab488a0255b +0 -0
  143. data/spec/dummy/tmp/cache/assets/test/sprockets/54d5f50daadb633077b6cd9dd9fb72cc +0 -0
  144. data/spec/dummy/tmp/cache/assets/test/sprockets/5817965bb0355e7b0da40f9b36153ef6 +0 -0
  145. data/spec/dummy/tmp/cache/assets/test/sprockets/5cbf24a4616e0252f26d7949679a42fe +0 -0
  146. data/spec/dummy/tmp/cache/assets/test/sprockets/6d498a2bfb1bc8566133e4eb5c0ccd35 +0 -0
  147. data/spec/dummy/tmp/cache/assets/test/sprockets/7be9086c960bb4384c9d23a52e489f29 +0 -0
  148. data/spec/dummy/tmp/cache/assets/test/sprockets/7d94acf804831ce74c7bf5455c1782f0 +0 -0
  149. data/spec/dummy/tmp/cache/assets/test/sprockets/9fe452757d2a02edb37d0d7901709d9c +0 -0
  150. data/spec/dummy/tmp/cache/assets/test/sprockets/a9cf04e553d11e0103cf1c870e363d7d +0 -0
  151. data/spec/dummy/tmp/cache/assets/test/sprockets/b02bb55c99ea1e7b80cf52548ee0d1c5 +0 -0
  152. data/spec/dummy/tmp/cache/assets/test/sprockets/b9b341a8be874e4eee26dce66499bc80 +0 -0
  153. data/spec/dummy/tmp/cache/assets/test/sprockets/c7cac2711d4e453b8698081414f6bc73 +0 -0
  154. data/spec/dummy/tmp/cache/assets/test/sprockets/cac5a12e0a520ce86636e0443f3da875 +0 -0
  155. data/spec/dummy/tmp/cache/assets/test/sprockets/cb21171d87e8bb8b243a86594263deff +0 -0
  156. data/spec/dummy/tmp/cache/assets/test/sprockets/cc5195a4891bb336aeb52b296f4971e9 +0 -0
  157. data/spec/dummy/tmp/cache/assets/test/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  158. data/spec/dummy/tmp/cache/assets/test/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  159. data/spec/dummy/tmp/cache/assets/test/sprockets/e6c6c2db7ca06e62d0b8d81c9ee3fcf0 +0 -0
  160. data/spec/dummy/tmp/cache/assets/test/sprockets/f6f1db9d7bdcc747aae3fe9f3ec43dab +0 -0
  161. data/spec/dummy/tmp/cache/assets/test/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  162. data/spec/dummy/tmp/cache/assets/test/sprockets/fe8b16f9af7353e9609c7fe6259d6912 +0 -0
  163. data/spec/dummy/tmp/pids/server.pid +1 -0
  164. data/spec/fabricators/pageview.rb +3 -0
  165. data/spec/fabricators/visitor.rb +3 -0
  166. data/spec/spec_helper.rb +20 -0
  167. metadata +393 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YTI3YjRjZmNhZWMwN2M3NGUwYTcyMmFlOWI2ZDg0ZDlhNjg1ZjhkNw==
5
+ data.tar.gz: !binary |-
6
+ NDJjZjQ1NTJhMWI5ZGM1MGIxOWI2NWI1YzIzNDMzMGJjZWFjYWJlNg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NWY4YWJhYjIyNjFhYTM4ZjNmYzVkNzI0YThmYjI2ZjQzMmQ5ZDc4M2U3OGZl
10
+ OWM2NmRlN2E2YjkyYmQzNzVlZDAzNDRkZGY1ZGI3NjA0MWVmYjI3MzM5MDY1
11
+ ZjVjZTRjZTUzZDIzZjljN2NmNDY5MjBmYTNlYTlmYTFhZmMyMjA=
12
+ data.tar.gz: !binary |-
13
+ NTkzMWEzM2VlMTEyYTM1OWI0YmJlMmZlZTFkMDFhYjE4MmIzMTQ3ZGEwODU4
14
+ MWE0NmQyYjE3ZDI3N2VhNzc5YTAwOGZhZThkMjE1ODI5YzE1MTdiY2Y2YTNm
15
+ ZDY5YjQ1M2I1MDE3MTdjN2VmOTNmOTc0ODIzZDJlYmJkYTZkODQ=
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Paweł Nguyen
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,34 @@
1
+ # Mousereco
2
+
3
+ Mousereco is a mouse recording Rails engine with a web interface used to replay user visits
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'mousereco'
10
+
11
+ Execute:
12
+
13
+ $ bundle
14
+
15
+ And then run install generator:
16
+
17
+ $ rails generate mousereco:install
18
+
19
+ It will:
20
+ - add a tracker js code to your application.js file
21
+ - mount Mousereco at ```/mousereco``` route
22
+ - install and run migrations needed for Mousereco
23
+
24
+ ## Usage
25
+
26
+ By default Mousereco engine will be mounted at ```/mousereco``` path in your application. You can replay your recordings there
27
+
28
+ ## Contributing
29
+
30
+ 1. Fork it
31
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
32
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
33
+ 4. Push to the branch (`git push origin my-new-feature`)
34
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Mousereco'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,15 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require jquery
14
+ //= require jquery_ujs
15
+ //= require mousereco/pageviews
@@ -0,0 +1,244 @@
1
+ var mouseRecorder = mouseRecorder || {};
2
+ mouseRecorder.modules = mouseRecorder.modules || {};
3
+ mouseRecorder.views = mouseRecorder.views || {};
4
+
5
+
6
+
7
+ (function(namespace) {
8
+
9
+ var EVENTS = {
10
+ MOVE: 'mousemove',
11
+ SCROLL: 'scroll'
12
+ },
13
+ playDelay = 500,
14
+ duartionInterval = 100,
15
+ Player = function(events, $iframe){
16
+ this.mouseEvents = [];
17
+ this.scrollEvents = [];
18
+ this.currentMouseEvent = 0;
19
+ this.currentScrollEvent = 0;
20
+
21
+ this.currentMouseEventTimestamp = events[0].timestamp - playDelay;
22
+ this.mouseRealTimestamp = 0;
23
+ this.mousePauseTimeout = 0;
24
+
25
+ this.currentScrollEventTimestamp = events[0].timestamp - playDelay;
26
+ this.scrollRealTimestamp = 0;
27
+ this.scrollPauseTimeout = 0;
28
+
29
+ this.currentPlayPosition = 0;
30
+
31
+ this.$iframe = $iframe;
32
+ this.$mouse = null;
33
+
34
+ this.getDuration(events);
35
+ this.groupEvents(events);
36
+ this.createMouse();
37
+ };
38
+ Player.prototype = {
39
+ getDuration: function(events) {
40
+ if(events === undefined) {
41
+ return this.duration;
42
+ }
43
+ else {
44
+ this.duration = playDelay + (events[events.length - 1].timestamp - events[0].timestamp);
45
+ }
46
+ },
47
+ getPlayPosition: function() {
48
+ return Math.min(this.currentPlayPosition, this.duration);
49
+ },
50
+ groupEvents: function(events) {
51
+ for(var i in events) {
52
+ events[i].type = events[i].type.toLowerCase();
53
+ if(events[i].type == EVENTS.SCROLL) {
54
+ this.scrollEvents.push(events[i]);
55
+ }
56
+ else {
57
+ this.mouseEvents.push(events[i]);
58
+ }
59
+ }
60
+ },
61
+ createMouse: function() {
62
+ if(!this.$mouse) {
63
+ this.$iframe.append('<div id="mouse" style="max-width:50px;min-width:10px;position:absolute;top:' + 0 + 'px;left:' + 0 + 'px;color:blue;font-size:10px;z-index:99999;background-color:black;">Mouse</div>');
64
+ this.$mouse = this.$iframe.find('#mouse');
65
+ }
66
+ },
67
+ play: function() {
68
+ this.paused = false;
69
+ this.playScroll();
70
+ this.playMouse();
71
+ this.playDuration();
72
+ },
73
+ pause: function() {
74
+ this.paused = true;
75
+ clearTimeout(this.scrollTimeout);
76
+ clearTimeout(this.mouseTimeout);
77
+ clearInterval(this.durationTicker);
78
+ this.$mouse.stop();
79
+ this.mousePauseTimeout += (new Date()).getTime() - this.mouseRealTimestamp;
80
+ this.scrollPauseTimeout += (new Date()).getTime() - this.scrollRealTimestamp;
81
+ },
82
+ playDuration: function() {
83
+ var self = this,
84
+ $self = $(this);
85
+ this.durationTicker = setInterval(function() {
86
+ self.currentPlayPosition += duartionInterval;
87
+ $self.trigger('duration.update');
88
+ self.end();
89
+ }, duartionInterval);
90
+ },
91
+ playMouse: function() {
92
+ if(!this.paused && this.mouseEvents[this.currentMouseEvent]) {
93
+ var self = this;
94
+ if(this.currentMouseEvent === 0) {
95
+ var event = this.mouseEvents[this.currentMouseEvent],
96
+ timeout = event.timestamp - this.currentMouseEventTimestamp;
97
+ this.mouseRealTimestamp = (new Date()).getTime();
98
+ this.mouseTimeout = setTimeout(function() {
99
+ if(!self.paused) {
100
+ self.currentMouseEvent++;
101
+ self.currentMouseEventTimestamp = event.timestamp;
102
+ self.showEvent(event);
103
+ self.playMouse();
104
+ }
105
+ }, timeout);
106
+ }
107
+ else {
108
+ var currentEvent = this.mouseEvents[this.currentMouseEvent],
109
+ previousEvent = this.mouseEvents[this.currentMouseEvent - 1],
110
+ tmpPauseTimestamp = this.mousePauseTimeout;
111
+ this.mouseRealTimestamp = (new Date()).getTime();
112
+ this.$mouse.animate({
113
+ top: currentEvent.y,
114
+ left: currentEvent.x
115
+ }, currentEvent.timestamp - previousEvent.timestamp - tmpPauseTimestamp, function() {
116
+ if(!self.paused) {
117
+ self.mousePauseTimeout = 0;
118
+ self.currentMouseEvent++;
119
+ self.currentMouseEventTimestamp = currentEvent.timestamp;
120
+ self.showEvent(currentEvent);
121
+ self.playMouse();
122
+ }
123
+ });
124
+ }
125
+ }
126
+ },
127
+ playScroll: function() {
128
+ if(!this.paused && this.scrollEvents[this.currentScrollEvent]) {
129
+ var self = this,
130
+ event = this.scrollEvents[this.currentScrollEvent],
131
+ timeout = event.timestamp - this.currentScrollEventTimestamp - this.scrollPauseTimeout;
132
+ this.scrollRealTimestamp = (new Date()).getTime();
133
+ this.scrollTimeout = setTimeout(function() {
134
+ if(!self.paused) {
135
+ self.scrollPauseTimeout = 0;
136
+ self.currentScrollEvent++;
137
+ self.currentScrollEventTimestamp = event.timestamp;
138
+ self.showEvent(event);
139
+ self.playScroll();
140
+ }
141
+ }, timeout);
142
+ }
143
+ },
144
+ end: function() {
145
+ if(this.currentPlayPosition >= this.duration) {
146
+ this.pause();
147
+ // @TODO emit finished event
148
+ }
149
+ },
150
+ showEvent: function(event) {
151
+ switch(event.type) {
152
+ case EVENTS.SCROLL:
153
+ this.$iframe.scrollTop(event.y);
154
+ this.$iframe.scrollLeft(event.x);
155
+ break;
156
+ default:
157
+ this.$iframe.append('<div style="position:absolute;top:' + event.y + 'px;left:' + event.x + 'px;color:red;z-index:99998;">' + this.currentMouseEvent + '-' + event.type + '</div>');
158
+ break;
159
+ }
160
+ }
161
+ };
162
+
163
+ namespace.Player = Player;
164
+ })(mouseRecorder.modules);
165
+
166
+
167
+ (function(namespace) {
168
+
169
+ var EVENTS = 'data-events',
170
+ WIDTH = 'data-window-width',
171
+ HEIGHT = 'data-window-height',
172
+ URL = 'data-url',
173
+ Pageviews = function(events) {
174
+ this.$events = $(events);
175
+ this.$iframe = this.getIframe();
176
+ this.$progressbar = $('.progress-bar');
177
+ this.events = this.parseEvents();
178
+ this.init();
179
+
180
+ };
181
+ Pageviews.prototype = {
182
+ parseEvents: function() {
183
+ return $.parseJSON(this.$events.attr(EVENTS));
184
+ },
185
+ getIframe: function() {
186
+ var $iframe = $('#pageview_window');
187
+ $iframe.attr('src', this.$events.attr(URL));
188
+ $iframe.attr('width', this.$events.attr(WIDTH));
189
+ $iframe.attr('height', this.$events.attr(HEIGHT));
190
+ return $iframe;
191
+ },
192
+ formatTime: function(timeMs) {
193
+ var time = timeMs / 100,
194
+ h = Math.floor(time / (10 * 60 * 60)),
195
+ m = Math.floor(time / (10 * 60)) % 60,
196
+ s = Math.floor(time / 10) % 60,
197
+ ms = Math.floor(time % 10);
198
+
199
+ return this.padTime(m) + ':' + this.padTime(s) + ':' + ms;
200
+ },
201
+ padTime: function(time) {
202
+ return time < 10 ? '0' + time : time;
203
+ },
204
+ getTime: function(current, duration) {
205
+ return this.formatTime(current) + ' / ' + this.formatTime(duration);
206
+ },
207
+ init: function() {
208
+ var player,
209
+ self = this;
210
+ this.$iframe.on('load', $.proxy(function() {
211
+ this.player = player = new mouseRecorder.modules.Player(this.events, this.$iframe.contents().find('body'));
212
+ var duration = player.getDuration(),
213
+ $label = self.$progressbar.next();
214
+ $label.text(self.getTime(0, duration));
215
+ $(this.player).on('duration.update', function() {
216
+ self.$progressbar.width(Math.round((this.getPlayPosition() / this.getDuration() * 100 * 100))/100 + '%');
217
+ $label.text(self.getTime(this.getPlayPosition(), duration));
218
+ });
219
+ }, this));
220
+ $('button.play').on('click', function() {
221
+ if(player) {
222
+ var $el = $(this);
223
+ if($el.hasClass('play')) {
224
+ $el.toggleClass('play').text('pause');
225
+ player.play()
226
+ }
227
+ else {
228
+ $el.toggleClass('play').text('play');
229
+ player.pause();
230
+ }
231
+ }
232
+ });
233
+ }
234
+ };
235
+
236
+ namespace.Pageviews = Pageviews;
237
+ })(mouseRecorder.views);
238
+
239
+
240
+ $(document).on('ready', function() {
241
+ if($('body').hasClass('pageviews')) {
242
+ var pageviews = new mouseRecorder.views.Pageviews('#events');
243
+ }
244
+ });
@@ -0,0 +1,312 @@
1
+ (function(){
2
+ var MouseRecorder = {};
3
+
4
+
5
+ /**
6
+ * MouseRecorder.Events
7
+ */
8
+ MouseRecorder.Events = {
9
+ CLICK: 'click',
10
+ MOVE: 'mousemove',
11
+ SCROLL: 'scroll',
12
+ OVER: 'mouseover'
13
+ },
14
+
15
+
16
+ /**
17
+ * MouseRecorder.Tracker
18
+ */
19
+ (function(namespace){
20
+
21
+ var instance,
22
+ mousePositionTrackInterval = 100,
23
+ mouseScrollTrackInterval = 100,
24
+ mouseOverTrackInterval = 250,
25
+ body = document.getElementsByTagName('body')[0],
26
+ Tracker = function() {
27
+ this.pusher = new MouseRecorder.Pusher();
28
+ this.pusher.html();
29
+
30
+ this.coords = {};
31
+ this.initListeners();
32
+ };
33
+ Tracker.prototype = {
34
+ on: {},
35
+ initListeners: function() {
36
+ MouseRecorder.Util.addEventListener(window, MouseRecorder.Events.OVER, this.withInterval(MouseRecorder.Events.OVER, mouseOverTrackInterval, MouseRecorder.Events.MOVE), this)
37
+ MouseRecorder.Util.addEventListener(window, MouseRecorder.Events.SCROLL, this.withInterval(MouseRecorder.Events.SCROLL, mouseScrollTrackInterval), this);
38
+ MouseRecorder.Util.addEventListener(document, MouseRecorder.Events.MOVE, this.withInterval(MouseRecorder.Events.MOVE, mousePositionTrackInterval), this);
39
+ MouseRecorder.Util.addEventListener(document, MouseRecorder.Events.CLICK, this.pusher.receive, this.pusher);
40
+ },
41
+ withInterval: function(eventType, interval, asEvent) {
42
+ var self = this,
43
+ sourceEvent = eventType,
44
+ destEvent = (asEvent === undefined ? eventType : asEvent);
45
+ setInterval(function() {
46
+ if(self.coords[sourceEvent] && self.coords[sourceEvent].update === true) {
47
+ self.coords[sourceEvent].update = false;
48
+ self.pusher.receive(self.coords[sourceEvent], destEvent);
49
+ }
50
+ }, interval);
51
+ return function(event) {
52
+ var data = self.on[sourceEvent](event);
53
+ self.coords[sourceEvent] = self.coords[sourceEvent] || {};
54
+ self.coords[sourceEvent].update = true;
55
+ self.coords[sourceEvent].pageX = data[0];
56
+ self.coords[sourceEvent].pageY = data[1];
57
+ };
58
+ }
59
+ };
60
+ Tracker.prototype.on[MouseRecorder.Events.SCROLL] = function(event) {
61
+ return [body.scrollLeft, body.scrollTop];
62
+ };
63
+ Tracker.prototype.on[MouseRecorder.Events.MOVE] = function(event) {
64
+ return [event.pageX, event.pageY];
65
+ };
66
+ Tracker.prototype.on[MouseRecorder.Events.OVER] = function(event) {
67
+ return [event.pageX, event.pageY];
68
+ }
69
+
70
+ namespace.Tracker = {
71
+ instance: function() {
72
+ if(!instance) {
73
+ instance = new Tracker();
74
+ }
75
+ return instance;
76
+ }
77
+ };
78
+ })(MouseRecorder);
79
+
80
+
81
+ /**
82
+ * MouseRecorder.Pusher
83
+ */
84
+ (function(namespace) {
85
+
86
+ <% url_helpers = Mousereco::Engine.routes.url_helpers %>
87
+ var eventsPath = '<%= url_helpers.api_v1_events_path %>',
88
+ htmlPath = '<%= url_helpers.api_v1_pageviews_path %>',
89
+ cookie = 'MouseRecorder_visitorKey',
90
+ pushTimeout = 2500,
91
+ Pusher = function(){
92
+ this.url = MouseRecorder.Util.getUrl();
93
+ this.visitorKey = this.getVisitorKey();
94
+ this.pageviewKey = MouseRecorder.Util.getUID();
95
+ this.events = [];
96
+ this.pushTimer = undefined;
97
+ };
98
+ Pusher.prototype = {
99
+ preflight: function() {
100
+ // @TODO this.push(this.basicData(), preflightUrl);
101
+ },
102
+ html: function() {
103
+ var data = this.basicData();
104
+ var window_size = MouseRecorder.Util.getWindowSize();
105
+ data.window_height = window_size.height;
106
+ data.window_width = window_size.width;
107
+ data.page_html = MouseRecorder.Util.getHTML();
108
+ this.push(data, htmlPath);
109
+ },
110
+ receive: function(event, type) {
111
+ var type = type || MouseRecorder.Events.CLICK,
112
+ push = (type == MouseRecorder.Events.CLICK);
113
+ this.events.push({
114
+ x: event.pageX,
115
+ y: event.pageY,
116
+ timestamp: (new Date()).getTime(),
117
+ type: type
118
+ });
119
+ if(push) {
120
+ this.prePush();
121
+ }
122
+ else {
123
+ var self = this;
124
+ if(!this.pushTimer) {
125
+ this.pushTimer = setTimeout(function() {
126
+ clearTimeout(self.pushTimer);
127
+ self.pushTimer = undefined;
128
+ self.prePush();
129
+ }, pushTimeout);
130
+ }
131
+ }
132
+ },
133
+ prePush: function() {
134
+ var data = this.basicData();
135
+ data.events = this.events;
136
+ this.events = [];
137
+ this.push(data, eventsPath);
138
+ },
139
+ push: function(data, url, callback) {
140
+ var request = false,
141
+ self = this;
142
+ if (window.ActiveXObject) {
143
+ var types = ['MSXML2.XmlHttp.5.0', 'MSXML2.XmlHttp.4.0', 'MSXML2.XmlHttp.3.0', 'MSXML2.XmlHttp.2.0', 'Microsoft.XmlHttp'];
144
+ for(var i in types) {
145
+ try {
146
+ request = new ActiveXObject(types[i]);
147
+ break;
148
+ } catch(e){}
149
+ }
150
+ }
151
+ else if (window.XMLHttpRequest) {
152
+ request = new XMLHttpRequest();
153
+ }
154
+
155
+ if(request) {
156
+ request.open('POST', url, true);
157
+ if(callback) {
158
+ request.onreadystatechange = function() {
159
+ if(this.status === 200 && this.readyState === 4) {
160
+ callback.apply(self, JSON.parse(this.responseText));
161
+ }
162
+ };
163
+ }
164
+ request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
165
+ request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
166
+ if(request.readyState != 4) {
167
+ request.send(MouseRecorder.Util.toString(data));
168
+ }
169
+ }
170
+ },
171
+ basicData: function() {
172
+ return {
173
+ url: this.url,
174
+ visitor_key: this.visitorKey,
175
+ pageview_key: this.pageviewKey,
176
+ timestamp: (new Date()).getTime()
177
+ };
178
+ },
179
+ getVisitorKey: function() {
180
+ var visitorKeyCookie = MouseRecorder.CookieMonster.get(cookie);
181
+ if(visitorKeyCookie === null || visitorKeyCookie === undefined || visitorKeyCookie.length === 0) {
182
+ visitorKeyCookie = MouseRecorder.Util.getUID();
183
+ }
184
+ MouseRecorder.CookieMonster.set(cookie, visitorKeyCookie, 60 * 60, '/')
185
+ return visitorKeyCookie;
186
+ }
187
+ };
188
+
189
+ namespace.Pusher = Pusher;
190
+ })(MouseRecorder);
191
+
192
+
193
+ /**
194
+ * MouseRecorder.Util
195
+ */
196
+ (function(namespace) {
197
+ namespace.Util = {
198
+ addEventListener: function(el, event, callback, scope) {
199
+ var _callback = function() {
200
+ callback.apply(scope, arguments);
201
+ };
202
+ if(el.addEventListener != undefined) {
203
+ el.addEventListener(event, _callback, false);
204
+ }
205
+ else if(el.attachEvent != undefined) {
206
+ el.attachEvent('on' + event, _callback);
207
+ }
208
+ else {
209
+ el['on' + event] = _callback;
210
+ }
211
+ },
212
+ getUID: function() {
213
+ return ((((1+Math.random())*0x10000)|0).toString(16) + (new Date().getTime()).toString(16)).substring(1);
214
+ },
215
+ getUrl: function() {
216
+ return document.URL;
217
+ },
218
+ toString: function(obj, index) {
219
+ var params = [];
220
+ for(var i in obj) {
221
+ var key = (index == undefined) ? i : index + '[' + i + ']';
222
+ if(Array.isArray(obj[i])) {
223
+ params.push(this.toString(obj[i], key));
224
+ }
225
+ else if(typeof obj[i] === 'object') {
226
+ params.push(this.toString(obj[i], key));
227
+ }
228
+ else {
229
+ params.push(key + '=' + obj[i]);
230
+ }
231
+ }
232
+ return params.join('&');
233
+ },
234
+ getHTML: function() {
235
+ var html = document.documentElement.outerHTML.replace(/\s+/g, ' ').replace(/\r?\n|\r/g, ' ').replace(
236
+ /<head>(.*)<\/head>/, '<base href="' + this.getUrlRoot() + '">' + document.getElementsByTagName('head')[0].innerHTML);
237
+ return encodeURIComponent(html.replace(/\s+/g, ' ').replace(/\r?\n|\r/g, ' '))
238
+ },
239
+ getUrlRoot: function() {
240
+ return this.getUrl().match(/(.*\/)/)[0];
241
+ },
242
+ getWindowSize: function() {
243
+ return {width: window.innerWidth, height: window.innerHeight};
244
+ }
245
+ };
246
+ })(MouseRecorder);
247
+
248
+
249
+ /**
250
+ * MouseRecorder.CookieMonster
251
+ */
252
+ (function(namespace){
253
+ /*!
254
+ * cookie-monster - a simple cookie library
255
+ * v0.2.0
256
+ * https://github.com/jgallen23/cookie-monster
257
+ * copyright Greg Allen 2013
258
+ * MIT License
259
+ */
260
+ var monster = {
261
+ set: function(name, value, seconds, path) {
262
+ var date = new Date(),
263
+ expires = '',
264
+ type = typeof(value),
265
+ valueToUse = '';
266
+ path = path || "/";
267
+ if (seconds) {
268
+ date.setTime(date.getTime() + seconds * 1000);
269
+ expires = "; expires=" + date.toUTCString();
270
+ }
271
+ if (type === "object" && type !== "undefined") {
272
+ if(!("JSON" in window)) throw "Bummer, your browser doesn't support JSON parsing.";
273
+ valueToUse = JSON.stringify({v:value});
274
+ } else {
275
+ valueToUse = encodeURIComponent(value);
276
+ }
277
+
278
+ document.cookie = name + "=" + valueToUse + expires + "; path=" + path;
279
+ },
280
+ get: function(name) {
281
+ var nameEQ = name + "=",
282
+ ca = document.cookie.split(';'),
283
+ value = '',
284
+ firstChar = '',
285
+ parsed={};
286
+ for (var i = 0; i < ca.length; i++) {
287
+ var c = ca[i];
288
+ while (c.charAt(0) == ' ') c = c.substring(1, c.length);
289
+ if (c.indexOf(nameEQ) === 0) {
290
+ value = c.substring(nameEQ.length, c.length);
291
+ firstChar = value.substring(0, 1);
292
+ if(firstChar=="{"){
293
+ parsed = JSON.parse(value);
294
+ if("v" in parsed) return parsed.v;
295
+ }
296
+ if (value=="undefined") return undefined;
297
+ return decodeURIComponent(value);
298
+ }
299
+ }
300
+ return null;
301
+ },
302
+ remove: function(name) {
303
+ this.set(name, "", -1);
304
+ }
305
+ };
306
+
307
+ namespace.CookieMonster = monster;
308
+ })(MouseRecorder);
309
+
310
+
311
+ MouseRecorder.Tracker.instance();
312
+ })();