diff --git a/db/manifest b/db/manifest index c7537c01a..232cb12ef 100755 --- a/db/manifest +++ b/db/manifest @@ -142,3 +142,4 @@ order_event_session.sql emails.sql email_batch.sql user_progress_tracking2.sql +bands_did_session.sql diff --git a/db/up/bands_did_session.sql b/db/up/bands_did_session.sql new file mode 100644 index 000000000..62bb9222c --- /dev/null +++ b/db/up/bands_did_session.sql @@ -0,0 +1,2 @@ +ALTER TABLE bands ADD COLUMN did_real_session boolean default false; + diff --git a/ruby/Gemfile b/ruby/Gemfile index 5ed51d3d0..c1f1b078e 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -54,7 +54,8 @@ group :test do gem 'spork', '0.9.0' gem 'database_cleaner', '0.7.0' gem 'faker' - gem 'resque_spec' + gem 'resque_spec', :path => "/home/jam/src/resque_spec/" + gem 'timecop' end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index d6d49ac15..ef426e9a8 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -40,6 +40,7 @@ require "jam_ruby/resque/scheduled/audiomixer_retry" require "jam_ruby/resque/scheduled/icecast_config_retry" require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/resque/scheduled/cleanup_facebook_signup" +require "jam_ruby/resque/google_analytics_event" require "jam_ruby/mq_router" require "jam_ruby/base_manager" require "jam_ruby/connection_manager" diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index bce468fd9..210753784 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -263,6 +263,12 @@ module JamRuby name end + def in_real_session?(session) + b_members = self.users.sort_by(&:id).map(&:id) + s_members = session.users.sort_by(&:id).map(&:id) + (b_members - s_members).blank? + end + private def require_at_least_one_genre diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index c0a6c124b..51a6befd5 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -18,6 +18,7 @@ module JamRuby validate :can_join_music_session, :if => :joining_session? after_save :require_at_least_one_track_when_in_session, :if => :joining_session? after_create :did_create + after_save :report_add_participant include AASM IDLE_STATE = :idle @@ -127,6 +128,17 @@ module JamRuby self.user.update_lat_lng(self.ip_address) if self.user && self.ip_address end + def report_add_participant + if self.music_session_id_changed? && + self.music_session.present? && + self.connected? && + self.as_musician? && + 0 < (count = self.music_session.connected_participant_count) + GoogleAnalyticsEvent.report_session_participant(count) + end + true + end + private def require_at_least_one_track_when_in_session if tracks.count == 0 diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 5a1090b28..2640ef36c 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -24,6 +24,8 @@ module JamRuby has_many :recordings, :class_name => "JamRuby::Recording", :inverse_of => :music_session belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id" + after_create :started_session + validate :require_at_least_one_genre, :limit_max_genres after_save :sync_music_session_history @@ -401,6 +403,18 @@ module JamRuby self.save!(:validate => false) end + def connected_participant_count + Connection.where(:music_session_id => self.id, + :aasm_state => Connection::CONNECT_STATE.to_s, + :as_musician => true) + .count + end + + def started_session + GoogleAnalyticsEvent.track_session_duration(self) + GoogleAnalyticsEvent.track_band_real_session(self) + end + private def require_at_least_one_genre diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 645ccf6b8..58ef9d126 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -146,6 +146,8 @@ module JamRuby recording.band = music_session.band if recording.save + GoogleAnalyticsEvent.report_band_recording(recording.band) + music_session.connections.each do |connection| connection.tracks.each do |track| recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) diff --git a/ruby/lib/jam_ruby/resque/google_analytics_event.rb b/ruby/lib/jam_ruby/resque/google_analytics_event.rb index 30c08084b..deb85cde1 100644 --- a/ruby/lib/jam_ruby/resque/google_analytics_event.rb +++ b/ruby/lib/jam_ruby/resque/google_analytics_event.rb @@ -1,43 +1,107 @@ -class GoogleAnalyticsEvent +require 'resque' - @queue = 'google_analytics_event' +module JamRuby + class GoogleAnalyticsEvent - @@log = Logging.logger[GoogleAnalyticsEvent] + @queue = :google_analytics_event - def self.perform(category, action) + CAT_SESS_SIZE = 'SessionSize' + ACTION_SESS_SIZE = 'Size' + CAT_SESS_DUR = 'SessionDuration' + ACTION_SESS_DUR = 'Duration' + CAT_BAND = 'Band' + ACTION_BAND_SESS = 'Session' + ACTION_BAND_REC = 'Recording' - @@log.info("starting (#{category}, #{action})") + @@log = Logging.logger[GoogleAnalyticsEvent] - run(category, action) + SESSION_INTERVALS = [1, 5, 10, 15, 30, 45, 60, 90, 120, 180] # minutes + QUEUE_BAND_TRACKER = :band_tracker + QUEUE_SESSION_TRACKER = :session_tracker - @@log.info("done (#{category}, #{action})") + class SessionDurationTracker + @queue = QUEUE_SESSION_TRACKER - end - - def self.enqueue(category, event) - begin - Resque.enqueue(AudioMixer, category, event) - true - rescue - # implies redis is down. but since there is no retry logic with this, we should at least log a warn in case we've configured something wrong - @@log.warn("unable to enqueue") - false + def self.perform(args={}) + session_id, interval_idx = args['session_id'], args['interval_idx'].to_i + return unless session_id && session = MusicSession.find(session_id) + GoogleAnalyticsEvent.enqueue(CAT_SESS_DUR, ACTION_SESS_DUR, SESSION_INTERVALS[interval_idx]) + interval_idx += 1 + + if SESSION_INTERVALS.count-1 > interval_idx + next_time = session.created_at + SESSION_INTERVALS[interval_idx].minutes + Resque.enqueue_at(next_time, self, :session_id => session_id, :interval_idx => interval_idx) + end + end end - end - def self.run(category, action) + def self.track_session_duration(session) + Resque.enqueue_at(SESSION_INTERVALS[0].minute.from_now, + SessionDurationTracker, + :session_id => session.id, + :interval_idx => 0) + end - raise "no google analytics tracking ID" unless APP_CONFIG.ga_ua + class BandSessionTracker + @queue = QUEUE_BAND_TRACKER - params = { + def self.perform(session_id) + return unless session = MusicSession.find(session_id) + band = session.band + if band.in_real_session?(session) + band.update_attribute(:did_real_session, true) + GoogleAnalyticsEvent.enqueue(CAT_BAND, ACTION_BAND_SESS, nil) + end if band + end + end + + BAND_SESSION_MIN_DURATION = 15 # minutes + + def self.track_band_real_session(session) + if session.band && !session.band.did_real_session? + Resque.enqueue_at(BAND_SESSION_MIN_DURATION.minutes.from_now, + BandSessionTracker, + session.id) + end + end + + def self.report_band_recording(band) + if band && 1 == Recording.where(:band_id => band.id).count + self.enqueue(CAT_BAND, ACTION_BAND_REC) + end + end + + def self.report_session_participant(participant_count) + self.enqueue(CAT_SESS_SIZE, ACTION_SESS_SIZE, participant_count) + end + + def self.enqueue(category, event, data=nil) + begin + Resque.enqueue(GoogleAnalyticsEvent, category, event, data) + true + rescue + # implies redis is down. but since there is no retry logic with this, we should at least log a warn in case we've configured something wrong + @@log.warn("unable to enqueue") + false + end + end + + def self.perform(category, action, data) + @@log.info("starting (#{category}, #{action})") + raise "no google analytics tracking ID" unless APP_CONFIG.ga_ua + params = { v: APP_CONFIG.ga_ua_version, tid: APP_CONFIG.ga_ua, cid: APP_CONFIG.ga_anonymous_client_id, t: "event", ec: category, - ea: action - } + ea: action, + el: 'data', + ev: data.to_s + } + RestClient.post(APP_CONFIG.ga_endpoint, params: params, timeout: 8, open_timeout: 8) + @@log.info("done (#{category}, #{action})") + end - RestClient.post(APP_CONFIG.ga_endpoint, params: params, timeout: 8, open_timeout: 8) end -end \ No newline at end of file +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 0c32a5dbb..2b5b85d88 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -160,6 +160,7 @@ FactoryGirl.define do association :owner, factory: :user association :music_session, factory: :music_session + association :band, factory: :band factory :recording_with_track do before(:create) { |recording| diff --git a/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb b/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb new file mode 100644 index 000000000..aeef82f0a --- /dev/null +++ b/ruby/spec/jam_ruby/resque/google_analytics_event_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe GoogleAnalyticsEvent do + + let(:ga) { GoogleAnalyticsEvent.new } + + describe "track band analytics" do + it 'reports first recording' do + ResqueSpec.reset! + user = FactoryGirl.create(:user) + band = FactoryGirl.create(:band) + music_session = FactoryGirl.create(:music_session, + :creator => user, + :musician_access => true, + :band => band) + recording = Recording.start(music_session, user) + expect(Recording.where(:band_id => band.id).count).to eq(1) + + GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_BAND, + GoogleAnalyticsEvent::ACTION_BAND_REC, + nil) + end + + it 'reports first real session' do + ResqueSpec.reset! + JamRuby::GoogleAnalyticsEvent::BandSessionTracker.should have_schedule_size_of(0) + user = FactoryGirl.create(:user) + user1 = FactoryGirl.create(:user) + band = FactoryGirl.create(:band) + band.users << user + band.users << user1 + band.reload + music_session = FactoryGirl.create(:music_session, :creator => user, + :musician_access => true, :band => band) + expect(band.band_musicians.count).to eq(2) + expect(band.did_real_session).to eq(false) + connection = FactoryGirl.create(:connection, :user => user, :as_musician => true, + :aasm_state => Connection::CONNECT_STATE.to_s, + :music_session => music_session) + connection = FactoryGirl.create(:connection, :user => user1, :as_musician => true, + :aasm_state => Connection::CONNECT_STATE.to_s, + :music_session => music_session) + music_session.reload + expect(music_session.connected_participant_count).to eq(2) + expect(band.did_real_session).to eq(false) + + ResqueSpec.queues["#{GoogleAnalyticsEvent::QUEUE_BAND_TRACKER}_scheduled"].select do |qq| + qq[:class] == GoogleAnalyticsEvent::BandSessionTracker.name + end.count.should eq(1) + # GoogleAnalyticsEvent::BandSessionTracker.should have_schedule_size_of_at_least(1) + GoogleAnalyticsEvent.should_not have_queued(GoogleAnalyticsEvent::CAT_BAND, GoogleAnalyticsEvent::ACTION_BAND_SESS, nil) + Timecop.freeze((GoogleAnalyticsEvent::BAND_SESSION_MIN_DURATION + 1).minutes.from_now) + + qname = "#{ResqueSpec.queue_name(JamRuby::GoogleAnalyticsEvent::BandSessionTracker)}_scheduled" + expect(ResqueSpec.peek(qname).present?).to eq(true) + ResqueSpec.perform_next(qname) + GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_BAND, + GoogleAnalyticsEvent::ACTION_BAND_SESS, + nil) + band.reload + expect(band.did_real_session).to eq(true) + end + + end + + describe "track session analytics" do + before :each do + ResqueSpec.reset! + end + it 'reports size increment' do + user = FactoryGirl.create(:user) + music_session = FactoryGirl.create(:music_session, + :creator => user, + :musician_access => true) + connection = FactoryGirl.create(:connection, :user => user, + :as_musician => true, + :aasm_state => Connection::CONNECT_STATE.to_s, + :music_session => music_session) + GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_SESS_SIZE, + GoogleAnalyticsEvent::ACTION_SESS_SIZE, + music_session.connected_participant_count) + end + + it 'reports duration' do + user = FactoryGirl.create(:user) + JamRuby::GoogleAnalyticsEvent::SessionDurationTracker.should have_schedule_size_of(0) + music_session = FactoryGirl.create(:music_session, + :creator => user, + :musician_access => true) + GoogleAnalyticsEvent::SessionDurationTracker.should have_schedule_size_of(1) + + GoogleAnalyticsEvent::SESSION_INTERVALS.each do |interval| + Timecop.travel((interval + 1).minutes.from_now) + qname = "#{ResqueSpec.queue_name(JamRuby::GoogleAnalyticsEvent::SessionDurationTracker)}_scheduled" + next unless ResqueSpec.peek(qname).present? + ResqueSpec.perform_next(qname) + GoogleAnalyticsEvent.should have_queued(GoogleAnalyticsEvent::CAT_SESS_DUR, + GoogleAnalyticsEvent::ACTION_SESS_DUR, + interval) + end + GoogleAnalyticsEvent.should have_queue_size_of(GoogleAnalyticsEvent::SESSION_INTERVALS.count - 1) + end + end + +end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index afe08c312..c29b007e6 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -26,6 +26,9 @@ require 'spork' require 'database_cleaner' require 'factories' +require 'timecop' +require 'resque_spec/scheduler' + # uncomment this to see active record logs #ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) @@ -68,6 +71,7 @@ Spork.prefork do # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| + config.color_enabled = true config.treat_symbols_as_metadata_keys_with_true_values = true config.run_all_when_everything_filtered = true @@ -105,6 +109,32 @@ Spork.prefork do # the seed, which is printed after each run. # --seed 1234 config.order = 'random' + + REDIS_PID = "#{Rails.root}/tmp/pids/redis-test.pid" + REDIS_CACHE_PATH = "#{Rails.root}/tmp/cache/" + config.before(:suite) do + redis_options = { + "--daemonize" => 'yes', + "--pidfile" => REDIS_PID, + "--port" => 9736, + "--timeout" => 300, + "--save 900" => 1, + "--save 300" => 1, + "--save 60" => 10000, + "--dbfilename" => "dump.rdb", + "--dir" => REDIS_CACHE_PATH, + "--loglevel" => "debug", + "--logfile" => "stdout", + "--databases" => 16 + }.map { |k, v| "#{k} #{v}" }.join(" ") + `redis-server #{redis_options}` + end + config.after(:suite) do + %x{ + cat #{REDIS_PID} | xargs kill -QUIT + rm -f #{REDIS_CACHE_PATH}dump.rdb + } + end end end diff --git a/web/app/assets/javascripts/hoverRecording.js b/web/app/assets/javascripts/hoverRecording.js index 20a5b3994..3555b626e 100644 --- a/web/app/assets/javascripts/hoverRecording.js +++ b/web/app/assets/javascripts/hoverRecording.js @@ -73,7 +73,7 @@ instrumentHtml = '
'; $.each(val.instrument_ids, function(index, val) { instrumentHtml += '  '; - }) + }); instrumentHtml += '
'; musicianHtml += instrumentHtml; diff --git a/web/app/assets/stylesheets/client/hoverBubble.css.scss b/web/app/assets/stylesheets/client/hoverBubble.css.scss index 42faa0190..3d2efd754 100644 --- a/web/app/assets/stylesheets/client/hoverBubble.css.scss +++ b/web/app/assets/stylesheets/client/hoverBubble.css.scss @@ -4,7 +4,7 @@ background-color:#242323; border:solid 1px #ed3618; position:absolute; - z-index:999; + z-index:1100; &.musician-bubble { diff --git a/web/spec/features/bands_spec.rb b/web/spec/features/bands_spec.rb index d8e2b9b78..81166d368 100644 --- a/web/spec/features/bands_spec.rb +++ b/web/spec/features/bands_spec.rb @@ -16,16 +16,16 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do #end end + let(:fan) { FactoryGirl.create(:fan) } let(:user) { FactoryGirl.create(:user) } let(:finder) { FactoryGirl.create(:user) } before(:each) do UserMailer.deliveries.clear - navigate_band_setup end - def navigate_band_setup - sign_in_poltergeist(user) + def navigate_band_setup login=user + sign_in_poltergeist(login) wait_until_curtain_gone find('div.homecard.profile').trigger(:click) find('#profile-bands-link').trigger(:click) @@ -33,26 +33,143 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do expect(page).to have_selector('#band-setup-title') end - it "have validation errors shown, but then can navigate past and eventually save" do - find('#btn-band-setup-next').trigger(:click) - find('#tdBandName .error-text li', text: "can't be blank") - find('#tdBandBiography .error-text li', text: "can't be blank") - find('#tdBandGenres .error-text li', text: "At least 1 genre is required.") + def complete_band_setup_form(band, biography, params={}) + navigate_band_setup unless URI.parse(current_url).fragment == '/band/setup/new' + params['band-name'] ||= band || "Default band name" + params['band-biography'] ||= biography || "Default band biography" within('#band-setup-form') do - fill_in "band-name", with: "The Band" - fill_in "band-biography", with: "Biography" + params.each do |field, value| + fill_in field, with: "#{value}" + end first('#band-genres input[type="checkbox"]').trigger(:click) end sleep 1 # work around race condition find('#btn-band-setup-next').trigger(:click) find('h2', text: 'Step 2: Add Band Members') - find('#btn-band-setup-save').trigger(:click) - find('#band-profile-name', text: "The Band") - find('#band-profile-biography', text: "Biography") end + + context "band profile - new band setup" do + it "displays 'Set up your band' link to user" do + sign_in_poltergeist user + view_profile_of user + find('#profile-bands-link').trigger(:click) + expect(page).to have_selector('#band-setup-link') + end + + it "does not display band setup link when viewed by other user" do + in_client(fan) do + sign_in_poltergeist fan + view_profile_of user + find('#profile-bands-link').trigger(:click) + + expect(page).to_not have_selector('#band-setup-link') + end + end + + it "indicates required fields and user may eventually complete" do + navigate_band_setup + find('#btn-band-setup-next').trigger(:click) + expect(page).to have_selector('#tdBandName .error-text li', text: "can't be blank") + expect(page).to have_selector('#tdBandBiography .error-text li', text: "can't be blank") + expect(page).to have_selector('#tdBandGenres .error-text li', text: "At least 1 genre is required.") + + complete_band_setup_form("Band name", "Band biography") + + expect(page).to have_selector('#band-profile-name', text: "Band name") + expect(page).to have_selector('#band-profile-biography', text: "Band biography") + + end + + it "limits genres to 3" do + navigate_band_setup + within('#band-setup-form') do + fill_in 'band-name', with: "whatever" + fill_in 'band-biography', with: "a good story" + all('#band-genres input[type="checkbox"]').each_with_index do |cb, i| + cb.trigger(:click) unless i > 3 + end + end + sleep 1 + find('#btn-band-setup-next').trigger(:click) + expect(page).to have_selector('#tdBandGenres .error-text li', text: "No more than 3 genres are allowed.") + end + + it "handles max-length field input" do + pending "update this after VRFS-1610 is resolved" + max = { + name: 1024, + bio: 4000, + website: 1024 # unsure what the max is, see VRFS-1610 + } + navigate_band_setup + band_name = 'a'*(max[:name] + 1) + band_bio = 'b'*(max[:bio] + 1) + band_website = 'c'*(max[:website] + 1) + complete_band_setup_form(band_name, band_bio, 'band-website' => band_website) + + expect(page).to have_selector('#band-profile-name', text: band_name.slice(0, max[:name])) + expect(page).to have_selector('#band-profile-biography', text: band_bio.slice(0, max[:bio])) + end + + it "handles special characters in text fields" do + pending "update this after VRFS-1609 is resolved" + navigate_band_setup + band_name = garbage(3) + ' ' + garbage(50) + band_bio = garbage(500) + band_website = garbage(500) + complete_band_setup_form(band_name, band_bio, 'band-website' => band_website) + + expect(page).to have_selector('#band-profile-name', text: band_name) + expect(page).to have_selector('#band-profile-biography', text: band_bio) + end + + it "another user receives invite notification during Band Setup" + end + + + context "about view" do + it "displays the band's information to another user" + #photo + #name + #website address + #country, state, city + #biography/description + #genres chosen + #number of followers, recordings, sessions + #actions: follow button + + it "allows a user to follow the band" + end + + context "members view" do + it "photo and name links to the musician's profile page" + it "displays photo, name, location, instruments played" + it "displays a hover bubble containing more info on musician" + it "displays any pending band invitations when viewed by current band member" + + end + + context "history view" do + it "shows public info" + it "does not show private info to non-band user" + it "shows private info to band user" + end + + context "social view" do + it "displays musicians and fans who follow band" + end + + context "band profile - editing" do + it "about page shows the current band's info when 'Edit Profile' is clicked" + it "members page shows 'Edit Members' button and user can remove member" + it "non-member cannot Edit Profile" + it "non-member cannot Edit Members" + end + + it "band shows up in sidebar search result" end diff --git a/web/spec/support/utilities.rb b/web/spec/support/utilities.rb index b57e7fc02..7cd3869c9 100644 --- a/web/spec/support/utilities.rb +++ b/web/spec/support/utilities.rb @@ -415,6 +415,13 @@ def assert_all_tracks_seen(users=[]) end end +def view_profile_of user + id = user.kind_of?(JamRuby::User) ? user.id : user + # assume some user signed in already (allows reuse in multi-user tests) + visit "/client#/profile/#{id}" + wait_until_curtain_gone +end + def show_user_menu page.execute_script("$('ul.shortcuts').show()") #page.execute_script("JK.UserDropdown.menuHoverIn()") @@ -430,4 +437,15 @@ def send_key(selector, keycode = 13) keypress_script = "var e = $.Event('keyup', { keyCode: #{keycode} }); jQuery('#{selector}').trigger(e);" page.driver.execute_script(keypress_script) +end + +def special_characters + ["?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}"] +end + +def garbage length + output = '' + length.times { output << special_characters.sample } + output.gsub!(/<[\/|!|\?]/, '/<') # security risk -- avoids inputting tags until VRFS-1609 resolved + output.slice(0, length) end \ No newline at end of file