Enable FullStory, with throttling

closes UX-31
flag=enable_fullstory

test plan:
  1. LOG OUT BETWEEN TESTS
  2. to determine if fullstory was enabled, you can:
     a. use devtools to look for the fullstory script
        in the HEAD. It will be after the
        <!-- fullstory snippet --> comment, making it easy to find
     b. lok for a network request for edge.fullstory.com/s/fs.js
  3. to your dynamic_settings.yml, add
     config:
       canvas:
         fullstory:
           sampling_rate: 1.0
           app_key: 'anything'

  - do not enable the flag
  - log in
  > expect fullstory not to be enabled

  - enable the flag
  - log in
  > expect fullstory to be enabled

  - change sampling_rate to 0.0
  - log in
  > expect fullstory not to be enabled

Change-Id: I27885ebafea3066a8996a45f990118584f2bf56c
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/230591
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
Reviewed-by: Gary Mei <gmei@instructure.com>
QA-Review: Anju Reddy <areddy@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Ed Schiebel 2020-03-18 15:39:02 -04:00
parent c0325eb466
commit efc6a1247a
7 changed files with 184 additions and 0 deletions

View File

@ -16,6 +16,8 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Login::Shared
include FullStoryHelper
def reset_session_for_login
reset_session_saving_keys(:return_to,
:oauth,
@ -68,6 +70,8 @@ module Login::Shared
session[:require_terms] = true if @domain_root_account.require_acceptance_of_terms?(user)
@current_user = user
fullstory_init(@domain_root_account, session)
respond_to do |format|
if (oauth = session[:oauth2])
provider = Canvas::Oauth::Provider.new(oauth[:client_id], oauth[:redirect_uri], oauth[:scopes], oauth[:purpose])

View File

@ -0,0 +1,42 @@
#
# Copyright (C) 2020 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module FullStoryHelper
# the feature determines if fullstory is turned on
# then we enabled it only for every nth login
def fullstory_init(account, session)
fullstory_enabled = account.feature_enabled?(:enable_fullstory) rescue false
return unless fullstory_enabled
# this session is already hooked up
return if session.key?(:fullstory_enabled)
fsconfig = Canvas::DynamicSettings.find('fullstory', tree: 'config', service: 'canvas')
rate = fsconfig[:sampling_rate].to_f
sample = rand()
session[:fullstory_enabled] = rate >= 0.0 && rate <= 1.0 && sample < rate
end
def fullstory_app_key
Canvas::DynamicSettings.find('fullstory', tree: 'config', service: 'canvas')[:app_key] rescue nil
end
def fullstory_enabled_for_session?(session)
!!session[:fullstory_enabled]
end
end

View File

@ -98,4 +98,9 @@
<% end %>
<title><%= @page_title || (yield :page_title).presence || t('default_page_title', "Canvas LMS") %></title>
<%= render partial: 'layouts/google_analytics_snippet' %>
<!-- fullstory snippet -->
<% if fullstory_enabled_for_session?(session) %>
<%= render :partial => "shared/fullstory_snippet" %>
<% end %>
</head>

View File

@ -0,0 +1,44 @@
<%
# Copyright (C) 2020 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
%>
<%
fullstory_key = fullstory_app_key
if (fullstory_key) %>
<script>
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';
window['_fs_script'] = 'edge.fullstory.com/s/fs.js';
window['_fs_org'] = '<%=raw(fullstory_key)%>';
window['_fs_namespace'] = 'FS';
(function(m,n,e,t,l,o,g,y){
if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].');} return;}
g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[];
o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src='https://'+_fs_script;
y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);
g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)};
g.shutdown=function(){g("rec",!1)};g.restart=function(){g("rec",!0)};
g.log = function(a,b){g("log",[a,b])};
g.consent=function(a){g("consent",!arguments.length||a)};
g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};
g.clearUserCookie=function(){};
g._w={};y='XMLHttpRequest';g._w[y]=m[y];y='fetch';g._w[y]=m[y];
if(m[y])m[y]=function(){return g._w[y].apply(this,arguments)};
g._v="1.1.1";
})(window,document,window['_fs_namespace'],'script','user');
</script>
<% end %>

View File

@ -37,6 +37,9 @@ development:
ios-pandata-secret: teamrocketblastoffatthespeedoflight
android-pandata-key: ANDROID_pandata_key
android-pandata-secret: surrendernoworpreparetofight
fullstory:
sampling_rate: '0.0' # randomly inject this fraction of the time
app_key: 'xyzzy'
private:
canvas:

View File

@ -57,3 +57,9 @@ uxs_4_omg_a_scary_blueprint_checkbox:
Show a checkbox that optionally lets an account admin publish courses
automatically after associating and syncing with a blueprint course
applies_to: RootAccount
enable_fullstory:
state: hidden
display_name: Enable fullstory
description: |-
Include FullStory recording of the user's session
applies_to: RootAccount

View File

@ -0,0 +1,80 @@
#
# Copyright (C) 2020 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require 'spec_helper'
describe "fullstory" do
include FullStoryHelper
before :each do
@domain_root_account = Account.default
@session = {}
end
context "with feature enabled" do
before :each do
@domain_root_account.enable_feature!(:enable_fullstory)
end
it 'is enabled if login is sampled' do
allow(FullStoryHelper).to receive(:rand).and_return(0.5)
override_dynamic_settings(config: {canvas: { fullstory: {sampling_rate: 1, app_key: '12345'} } }) do
fullstory_init(@domain_root_account, @session)
expect(fullstory_app_key).to eql('12345')
expect(@session[:fullstory_enabled]).to be_truthy
expect(fullstory_enabled_for_session?(@session)).to be_truthy
end
end
it 'is disabled if login is not sampled' do
allow(FullStoryHelper).to receive(:rand).and_return(0.5)
override_dynamic_settings(config: {canvas: { fullstory: {sampling_rate: 0, app_key: '12345'} } }) do
fullstory_init(@domain_root_account, @session)
expect(fullstory_app_key).to eql('12345')
expect(@session[:fullstory_enabled]).to be_falsey
expect(fullstory_enabled_for_session?(@session)).to be_falsey
end
end
it "doesn't explode if the dynamic settings are missing" do
allow(FullStoryHelper).to receive(:rand).and_return(0.5)
override_dynamic_settings(config: {canvas: { fullstory: nil } }) do
fullstory_init(@domain_root_account, @session)
expect(fullstory_app_key).to be_nil
expect(@session[:fullstory_enabled]).to be_falsey
expect(fullstory_enabled_for_session?(@session)).to be_falsey
end
end
end
context "with feature disabled" do
before :each do
@domain_root_account.disable_feature!(:enable_fullstory)
end
it 'is disabled' do
allow(FullStoryHelper).to receive(:rand).and_return(0.5)
override_dynamic_settings(config: {canvas: { fullstory: {sampling_rate: 1, app_key: '12345'} } }) do
fullstory_init(@domain_root_account, @session)
expect(fullstory_app_key).to eql('12345')
expect(@session[:fullstory_enabled]).to be_falsey
expect(fullstory_enabled_for_session?(@session)).to be_falsey
end
end
end
end