FullStory updates

This change
1. moves the feature flag from RootAccount to SiteAdmin, as it should
   not be manipulated by any of our institutions
2. hides the mobile login qr code from fullstory using their fs-exclude
   class name
3. hides the users' avatars by adding a data-fs-exclude attribute to all
   instances of the instui Avatar component, which we an then filter on
   in the fullstory UI (because instui doesn't let us add a className)
4. hides the users' avatars that are not Avatar by adding .fs-exclude

Note: I am hiding all <img> elements via the FullStory UI, but this does
not hide Avatar, which puts the image in as a background-image

closes UXS-97
flag=enable_fullstory

test plan:
  - you can really only test in beta where fullstory can capture
    sessions and see that user avatars and the qr code are hidden

Change-Id: Ic8b73a2d7cd0474c1fd9a5337f747b76e5f67d06
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/232666
QA-Review: David Tan <dtan@instructure.com>
Reviewed-by: Augusto Callejas <acallejas@instructure.com>
Reviewed-by: Alex Anderson <raanderson@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Ed Schiebel 2020-04-02 14:05:42 -04:00
parent 40e53e026b
commit 531b3a359a
28 changed files with 76 additions and 48 deletions

View File

@ -70,7 +70,7 @@ 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)
fullstory_init(Account.site_admin, session)
respond_to do |format|
if (oauth = session[:oauth2])

View File

@ -62,7 +62,7 @@ module AvatarHelper
end
end
link_opts = {}
link_opts[:class] = 'avatar '+opts[:class].to_s
link_opts[:class] = 'fs-exclude avatar '+opts[:class].to_s
link_opts[:style] = "background-image: url(#{avatar_url})"
link_opts[:style] += ";width: #{opts[:size]}px;height: #{opts[:size]}px" if opts[:size]
link_opts[:href] = url if url

View File

@ -27,7 +27,13 @@ export default function UserLink({size, avatar_url, name, avatarName, ...propsTo
theme={{mediumPadding: '0', mediumHeight: '1rem'}}
{...propsToPassOnToLink}
>
<Avatar size={size} name={avatarName} src={avatar_url} margin="0 x-small xxx-small 0" />
<Avatar
size={size}
name={avatarName}
src={avatar_url}
margin="0 x-small xxx-small 0"
data-fs-exclude
/>
{name}
</Button>
)

View File

@ -211,6 +211,7 @@ export default class ActAsModal extends React.Component {
src={user.avatar_image_url}
size="small"
margin="medium 0 x-small 0"
data-fs-exclude
/>
</View>
<View as="div" textAlign="center">

View File

@ -112,6 +112,7 @@ exports[`ActAsModal renders with panda svgs, user avatar, table, and proceed but
textAlign="center"
>
<Avatar
data-fs-exclude={true}
inline={true}
margin="medium 0 x-small 0"
name="foo"

View File

@ -54,6 +54,7 @@ export default function CommentRow(props) {
name={author ? author.shortName : I18n.t('Anonymous')}
src={author ? author.avatarUrl : ''}
margin="0 small 0 0"
data-fs-exclude
/>
</div>
<div className="comment-text-comment-container">
@ -89,4 +90,3 @@ export default function CommentRow(props) {
CommentRow.propTypes = {
comment: SubmissionComment.shape.isRequired
}

View File

@ -73,7 +73,7 @@ export default class StudentTray extends React.Component {
aria-label={I18n.t("Go to %{name}'s profile", {name})}
target="_blank"
>
<Avatar size="x-large" name={name} src={this.props.student.avatarUrl} />
<Avatar size="x-large" name={name} src={this.props.student.avatarUrl} data-fs-exclude />
</Link>
</View>
)

View File

@ -105,7 +105,13 @@ export default class StudentsTable extends React.Component {
const displayName = student.shortName || student.name || I18n.t('User')
return (
<>
<Avatar name={student.name} src={student.avatarUrl} size="small" margin="0 small 0 0" />
<Avatar
name={student.name}
src={student.avatarUrl}
size="small"
margin="0 small 0 0"
data-fs-exclude
/>
{displayName}
</>
)

View File

@ -151,6 +151,7 @@ export default function ReceivedTable({shares, onPreview, onImport, onRemove, on
size="small"
name={share.sender.display_name}
src={share.sender.avatar_image_url}
data-fs-exclude
/>{' '}
{share.sender.display_name}
</Table.Cell>

View File

@ -44,7 +44,7 @@ class Avatar extends React.Component {
href={`/courses/${this.props.courseId}/users/${user._id}`}
aria-label={I18n.t("Go to %{name}'s profile", {name})}
>
<InstUIAvatar size="x-large" name={name} src={user.avatar_url} />
<InstUIAvatar size="x-large" name={name} src={user.avatar_url} data-fs-exclude />
</Link>
{canMasquerade && (
<Text size="x-small" weight="bold" as="div">

View File

@ -109,6 +109,7 @@ export default class SubmissionCommentListItem extends React.Component {
alt={I18n.t('Avatar for %{author}', {author: this.props.author})}
src={this.props.authorAvatarUrl}
margin="0 x-small 0 0"
data-fs-exclude
/>
</Link>

View File

@ -39,7 +39,7 @@ import {extractSimilarityInfo} from '../../../grading/helpers/SubmissionHelper'
function renderAvatar(name, avatarUrl) {
return (
<div id="SubmissionTray__Avatar">
<Avatar name={name} src={avatarUrl} size="auto" />
<Avatar name={name} src={avatarUrl} size="auto" data-fs-exclude />
</div>
)
}

View File

@ -137,6 +137,7 @@ export default class MobileGlobalMenu extends React.Component {
name={this.props.current_user.display_name}
src={this.props.current_user.avatar_image_url}
size="x-small"
data-fs-exclude
/>
</Flex.Item>
<Flex.Item>

View File

@ -87,6 +87,7 @@ export default function ProfileTray(props) {
size="x-large"
inline={false}
margin="auto"
data-fs-exclude
/>
<div style={{wordBreak: 'break-word'}}>
<Heading level="h3" as="h2">

View File

@ -51,7 +51,9 @@ export function QRLoginModal({onDismiss, refreshInterval, pollInterval}) {
function renderQRCode() {
const body = imagePng ? (
<Img data-testid="qr-code-image" src={`data:image/png;base64, ${imagePng}`} />
<span className="fs-exclude">
<Img data-testid="qr-code-image" src={`data:image/png;base64, ${imagePng}`} />
</span>
) : (
<Spinner
data-testid="qr-code-spinner"

View File

@ -225,6 +225,7 @@ export default class CourseItemRow extends Component {
size="small"
name={this.props.author.display_name || I18n.t('Unknown')}
src={this.props.author.avatar_image_url}
data-fs-exclude
/>
</div>
)}

View File

@ -223,6 +223,7 @@ export default class CreateOrUpdateUserModal extends React.Component {
size="small"
name={this.state.data.user.name}
src={this.props.user.avatar_url}
data-fs-exclude
/>{' '}
{I18n.t('Edit User Details')}
</span>

View File

@ -33,7 +33,7 @@ export default function UserSearchSelectorItem({user}) {
return (
<Flex>
<Flex.Item>
<Avatar name={name} src={avatar_url} />
<Avatar name={name} src={avatar_url} data-fs-exclude />
</Flex.Item>
<Flex.Item padding="0 0 0 small" grow>
<Flex direction="column">

View File

@ -1,27 +1,20 @@
{{!-- same markup as ApplicationHelper#avatar, essensially --}}
{{#if this}}
{{#if anonymous}}
<a
class="avatar"
style="background-image: url(/images/messages/avatar-50.png)"
>
<span class="screenreader-only">{{#t}}Student{{/t}}</span>
</a>
{{else}}
{{#ifAny avatar_url avatar_image_url}}
{{#if alreadyEscaped}}
<a
{{#ifAny url html_url}}href="{{{or url html_url}}}"{{/ifAny}}
class="avatar"
style="background-image: url({{{or avatar_url avatar_image_url}}})">
<span class="screenreader-only">{{{or display_name short_name}}}</span></a>
{{else}}
<a
{{#ifAny url html_url}}href="{{or url html_url}}"{{/ifAny}}
class="avatar"
style="background-image: url({{or avatar_url avatar_image_url}})"
><span class="screenreader-only">{{or display_name short_name}}</span></a>
{{/if}}
{{/ifAny}}
{{/if}}
{{#if anonymous}}
<a class="avatar" style="background-image: url(/images/messages/avatar-50.png)">
<span class="screenreader-only">{{#t}}Student{{/t}}</span>
</a>
{{else}}
{{#ifAny avatar_url avatar_image_url}}
{{#if alreadyEscaped}}
<a {{#ifAny url html_url}}href="{{{or url html_url}}}" {{/ifAny}} class="fs-exclude avatar"
style="background-image: url({{{or avatar_url avatar_image_url}}})">
<span class="screenreader-only">{{{or display_name short_name}}}</span></a>
{{else}}
<a {{#ifAny url html_url}}href="{{or url html_url}}" {{/ifAny}} class="fs-exclude avatar"
style="background-image: url({{or avatar_url avatar_image_url}})"><span
class="screenreader-only">{{or display_name short_name}}</span></a>
{{/if}}
{{/ifAny}}
{{/if}}
{{/if}}

View File

@ -1,9 +1,9 @@
<td class="center">
<a href="{{html_url}}" class="avatar" title="{{name}}"><img src="{{avatar_url}}" alt="{{name}}"></a>
<a href="{{html_url}}" class="fs-exclude avatar" title="{{name}}"><img src="{{avatar_url}}" alt="{{name}}"></a>
</td>
<td>
<a href="{{html_url}}" data-user-id="{{id}}" class="roster_user_name">{{name}}</a>
</td>
<td>
{{login_id}}
</td>
</td>

View File

@ -99,7 +99,6 @@
<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 %>

View File

@ -19,6 +19,7 @@
<%
fullstory_key = fullstory_app_key
if (fullstory_key) %>
<!-- fullstory snippet -->
<script>
window['_fs_debug'] = false;
window['_fs_host'] = 'fullstory.com';

View File

@ -56,7 +56,7 @@
<li class="menu-item ic-app-header__menu-list-item <%= ' ic-app-header__menu-list-item--active' if active_path?('/profile') %>">
<a id="global_nav_profile_link" role="button" href="/profile" class="ic-app-header__menu-list-link">
<div class="menu-item-icon-container">
<div aria-hidden="true" class="ic-avatar <% if @real_current_user && @real_current_user != @current_user %>ic-avatar--fake-student<% end %>">
<div aria-hidden="true" class="fs-exclude ic-avatar <% if @real_current_user && @real_current_user != @current_user %>ic-avatar--fake-student<% end %>">
<img src="<%= @current_user.try { |usr| avatar_image_attrs(usr).first } %>" alt="<%= @current_user.short_name %>" />
</div>
<span class="menu-item__badge"></span>

View File

@ -62,4 +62,4 @@ enable_fullstory:
display_name: Enable fullstory
description: |-
Include FullStory recording of the user's session
applies_to: RootAccount
applies_to: SiteAdmin

View File

@ -316,6 +316,7 @@ exports[`prefers to render feedback if it and the location are available 1`] = `
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
data-fs-exclude={true}
inline={true}
name="Dr. David Bowman"
onImageLoaded={[Function]}
@ -3332,6 +3333,7 @@ exports[`renders Note correctly with everything 1`] = `
}
>
<Avatar
data-fs-exclude={true}
inline={true}
name="Jane"
onImageLoaded={[Function]}
@ -3521,6 +3523,7 @@ exports[`renders Note correctly without Course 1`] = `
}
>
<Avatar
data-fs-exclude={true}
inline={true}
name="Jane"
onImageLoaded={[Function]}
@ -5425,6 +5428,7 @@ exports[`renders feedback anonymously according to the assignment settings 1`] =
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
data-fs-exclude={true}
inline={true}
name="?"
onImageLoaded={[Function]}
@ -5600,6 +5604,7 @@ exports[`renders feedback if available 1`] = `
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
data-fs-exclude={true}
inline={true}
name="Boyd Crowder"
onImageLoaded={[Function]}
@ -5776,6 +5781,7 @@ exports[`renders media feedback if available 1`] = `
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
data-fs-exclude={true}
inline={true}
name="Howard Stern"
onImageLoaded={[Function]}
@ -6178,6 +6184,7 @@ exports[`renders user-created Todo correctly 1`] = `
}
>
<Avatar
data-fs-exclude={true}
inline={true}
name="Jane"
onImageLoaded={[Function]}

View File

@ -282,7 +282,12 @@ export class PlannerItem extends Component {
return <IconPeerReviewLine />
default:
return (
<Avatar name={currentUser.displayName || '?'} src={currentUser.avatarUrl} size="small" />
<Avatar
name={currentUser.displayName || '?'}
src={currentUser.avatarUrl}
size="small"
data-fs-exclude
/>
)
}
}
@ -480,6 +485,7 @@ export class PlannerItem extends Component {
name={feedback.author_name || '?'}
src={feedback.author_avatar_url}
size="small"
data-fs-exclude
/>
</span>
<span className={styles.feedbackComment}>

View File

@ -112,7 +112,7 @@ function Attribution({name, avatarUrl, profileUrl}) {
return (
<Flex>
<Flex.Item margin="xx-small">
<Avatar name={name} src={avatarUrl} size="small" />
<Avatar name={name} src={avatarUrl} size="small" data-fs-exclude />
</Flex.Item>
<Flex.Item margin="xx-small" shrink>
<Button

View File

@ -22,19 +22,19 @@ require 'spec_helper'
include FullStoryHelper
before :each do
@domain_root_account = Account.default
@site_admin_account = Account.site_admin
@session = {}
end
context "with feature enabled" do
before :each do
@domain_root_account.enable_feature!(:enable_fullstory)
@site_admin_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)
fullstory_init(@site_admin_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
@ -44,7 +44,7 @@ require 'spec_helper'
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)
fullstory_init(@site_admin_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
@ -54,7 +54,7 @@ require 'spec_helper'
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)
fullstory_init(@site_admin_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
@ -64,13 +64,13 @@ require 'spec_helper'
context "with feature disabled" do
before :each do
@domain_root_account.disable_feature!(:enable_fullstory)
@site_admin_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)
fullstory_init(@site_admin_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