diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a1f27a97c51..f73cb5343fd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -159,7 +159,6 @@ class ApplicationController < ActionController::Base use_responsive_layout: use_responsive_layout?, use_rce_enhancements: (@context.is_a?(User) ? @domain_root_account : @context).try(:feature_enabled?, :rce_enhancements), rce_auto_save: @context.try(:feature_enabled?, :rce_auto_save), - DIRECT_SHARE_ENABLED: !@context.is_a?(Group) && @domain_root_account.try(:feature_enabled?, :direct_share), help_link_name: help_link_name, help_link_icon: help_link_icon, use_high_contrast: @current_user.try(:prefers_high_contrast?), @@ -172,22 +171,14 @@ class ApplicationController < ActionController::Base collapse_global_nav: @current_user.try(:collapse_global_nav?), show_feedback_link: show_feedback_link? }, - FEATURES: { - assignment_bulk_edit: @domain_root_account&.feature_enabled?(:assignment_bulk_edit), - assignment_bulk_edit_phase_2: Account.site_admin.feature_enabled?(:assignment_bulk_edit_phase_2), - canvas_k6_theme: @context.try(:feature_enabled?, :canvas_k6_theme), - cc_in_rce_video_tray: Account.site_admin.feature_enabled?(:cc_in_rce_video_tray), - featured_help_links: Account.site_admin.feature_enabled?(:featured_help_links), - responsive_admin_settings: !!@domain_root_account&.feature_enabled?(:responsive_admin_settings), - responsive_awareness: !!@domain_root_account&.feature_enabled?(:responsive_awareness), - responsive_misc: !!@domain_root_account&.feature_enabled?(:responsive_misc), - product_tours: !!@domain_root_account&.feature_enabled?(:product_tours), - module_dnd: !!@domain_root_account&.feature_enabled?(:module_dnd), - files_dnd: !!@domain_root_account&.feature_enabled?(:files_dnd), - unpublished_courses: !!@domain_root_account&.feature_enabled?(:unpublished_courses) - }, } @js_env[:KILL_JOY] = @domain_root_account.kill_joy? if @domain_root_account&.kill_joy? + + cached_features = cached_js_env_account_features + @js_env[:DIRECT_SHARE_ENABLED] = cached_features.delete(:direct_share) && !@context.is_a?(Group) + @js_env[:FEATURES] = cached_features.merge( + canvas_k6_theme: @context.try(:feature_enabled?, :canvas_k6_theme) + ) @js_env[:current_user] = @current_user ? Rails.cache.fetch(['user_display_json', @current_user].cache_key, :expires_in => 1.hour) { user_display_json(@current_user, :profile, [:avatar_is_fallback]) } : {} @js_env[:page_view_update_url] = page_view_path(@page_view.id, page_view_token: @page_view.token) if @page_view @js_env[:IS_LARGE_ROSTER] = true if !@js_env[:IS_LARGE_ROSTER] && @context.respond_to?(:large_roster?) && @context.large_roster? @@ -214,6 +205,29 @@ class ApplicationController < ActionController::Base end helper_method :js_env + # put feature checks on Account.site_admin and @domain_root_account that we're loading for every page in here + # so altogether we can get them faster the vast majority of the time + JS_ENV_SITE_ADMIN_FEATURES = [:assignment_bulk_edit_phase_2, :cc_in_rce_video_tray, :featured_help_links].freeze + JS_ENV_ROOT_ACCOUNT_FEATURES = [ + :direct_share, :assignment_bulk_edit, :responsive_admin_settings, :responsive_awareness, + :responsive_misc, :product_tours, :module_dnd, :files_dnd, :unpublished_courses + ].freeze + JS_ENV_FEATURES_HASH = Digest::MD5.hexdigest([JS_ENV_SITE_ADMIN_FEATURES + JS_ENV_ROOT_ACCOUNT_FEATURES].sort.join(",")).freeze + def cached_js_env_account_features + # can be invalidated by a flag change on either site admin or the domain root account + Rails.cache.fetch(["js_env_account_features", JS_ENV_FEATURES_HASH, + Account.site_admin.cache_key(:feature_flags), @domain_root_account&.cache_key(:feature_flags)].cache_key) do + results = {} + JS_ENV_SITE_ADMIN_FEATURES.each do |f| + results[f] = Account.site_admin.feature_enabled?(f) + end + JS_ENV_ROOT_ACCOUNT_FEATURES.each do |f| + results[f] = !!@domain_root_account&.feature_enabled?(f) + end + results + end + end + def add_to_js_env(hash, jsenv, overwrite) hash.each do |k,v| if jsenv[k] && !overwrite diff --git a/app/models/feature_flag.rb b/app/models/feature_flag.rb index 9d3909b017e..7abed08880b 100644 --- a/app/models/feature_flag.rb +++ b/app/models/feature_flag.rb @@ -54,6 +54,7 @@ class FeatureFlag < ActiveRecord::Base if self.context self.class.connection.after_transaction_commit { self.context.feature_flag_cache.delete(self.context.feature_flag_cache_key(feature)) } self.context.touch if Feature.definitions[feature].try(:touch_context) + self.context.clear_cache_key(:feature_flags) if self.context.is_a?(Account) if ::Rails.env.development? && self.context.is_a?(Account) && Account.all_special_accounts.include?(self.context) Account.clear_special_account_cache!(true) end diff --git a/lib/canvas/cache_register.rb b/lib/canvas/cache_register.rb index 0c52cd0e54b..50bbdd49035 100644 --- a/lib/canvas/cache_register.rb +++ b/lib/canvas/cache_register.rb @@ -27,7 +27,7 @@ module Canvas # (which is far less often than the many times per day users are currently being touched) ALLOWED_TYPES = { - 'Account' => %w{account_chain role_overrides global_navigation}, + 'Account' => %w{account_chain role_overrides global_navigation feature_flags}, 'Course' => %w{account_associations}, 'User' => %w{enrollments groups account_users todo_list submissions user_services}, 'Assignment' => %w{availability}, diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 390874e4b2e..f25fec7520f 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -128,7 +128,7 @@ RSpec.describe ApplicationController do describe "DIRECT_SHARE_ENABLED feature flag" do it "sets the env var to true when FF is enabled" do - root_account = double(global_id: 1, open_registration?: true, settings: {}) + root_account = double(global_id: 1, open_registration?: true, settings: {}, cache_key: "key") allow(root_account).to receive(:kill_joy?) allow(root_account).to receive(:feature_enabled?).and_return(false) allow(root_account).to receive(:feature_enabled?).with(:direct_share).and_return(true) @@ -138,7 +138,7 @@ RSpec.describe ApplicationController do end it "sets the env var to false when the context is a group" do - root_account = double(global_id: 1, open_registration?: true, settings: {}) + root_account = double(global_id: 1, open_registration?: true, settings: {}, cache_key: "key") allow(root_account).to receive(:kill_joy?) allow(root_account).to receive(:feature_enabled?).and_return(false) allow(root_account).to receive(:feature_enabled?).with(:direct_share).and_return(true) @@ -149,7 +149,7 @@ RSpec.describe ApplicationController do end it "sets the env var to false when FF is disabled" do - root_account = double(global_id: 1, open_registration?: true, settings: {}) + root_account = double(global_id: 1, open_registration?: true, settings: {}, cache_key: "key") allow(root_account).to receive(:kill_joy?) allow(root_account).to receive(:feature_enabled?).and_return(false) allow(HostUrl).to receive_messages(file_host: 'files.example.com') @@ -183,7 +183,7 @@ RSpec.describe ApplicationController do end it 'gets appropriate settings from the root account' do - root_account = double(global_id: 1, feature_enabled?: false, open_registration?: true, settings: {}) + root_account = double(global_id: 1, feature_enabled?: false, open_registration?: true, settings: {}, cache_key: "key") allow(root_account).to receive(:kill_joy?).and_return(false) allow(HostUrl).to receive_messages(file_host: 'files.example.com') controller.instance_variable_set(:@domain_root_account, root_account) @@ -192,7 +192,7 @@ RSpec.describe ApplicationController do end it 'disables fun when set' do - root_account = double(global_id: 1, feature_enabled?: false, open_registration?: true, settings: {}) + root_account = double(global_id: 1, feature_enabled?: false, open_registration?: true, settings: {}, cache_key: "key") allow(root_account).to receive(:kill_joy?).and_return(true) allow(HostUrl).to receive_messages(file_host: 'files.example.com') controller.instance_variable_set(:@domain_root_account, root_account)