mousereco 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+ })();