diff --git a/app/controllers/admin/billboards_controller.rb b/app/controllers/admin/billboards_controller.rb index b10d75ef0038..06e85daf4a51 100644 --- a/app/controllers/admin/billboards_controller.rb +++ b/app/controllers/admin/billboards_controller.rb @@ -59,7 +59,7 @@ def destroy def billboard_params params.permit(:organization_id, :body_markdown, :placement_area, :target_geolocations, :published, :approved, :name, :display_to, :tag_list, :type_of, - :exclude_article_ids, :audience_segment_id, :priority, + :exclude_article_ids, :audience_segment_id, :priority, :browser_context, :render_mode, :template, :custom_display_label, :requires_cookies) end diff --git a/app/controllers/api/v1/billboards_controller.rb b/app/controllers/api/v1/billboards_controller.rb index c4fcdd7cb398..70db4d6d6d23 100644 --- a/app/controllers/api/v1/billboards_controller.rb +++ b/app/controllers/api/v1/billboards_controller.rb @@ -48,7 +48,7 @@ def require_admin def permitted_params params.permit :approved, :body_markdown, :creator_id, :display_to, - :name, :organization_id, :placement_area, :published, + :name, :organization_id, :placement_area, :published, :browser_context, :tag_list, :type_of, :exclude_article_ids, :weight, :requires_cookies, :audience_segment_type, :audience_segment_id, :priority, :special_behavior, :custom_display_label, :template, :render_mode, :preferred_article_ids, diff --git a/app/controllers/billboards_controller.rb b/app/controllers/billboards_controller.rb index 175c5d80e884..0fa31f4a2090 100644 --- a/app/controllers/billboards_controller.rb +++ b/app/controllers/billboards_controller.rb @@ -33,6 +33,7 @@ def show user_tags: user_tags, cookies_allowed: cookies_allowed?, location: client_geolocation, + user_agent: request.user_agent, ) if @billboard && !session_current_user_id diff --git a/app/javascript/packs/admin/billboards.jsx b/app/javascript/packs/admin/billboards.jsx index 88421b57786d..83686893aec3 100644 --- a/app/javascript/packs/admin/billboards.jsx +++ b/app/javascript/packs/admin/billboards.jsx @@ -23,7 +23,7 @@ function saveTags(selectionString) { /** * Shows and Renders a Tags preact component for the Targeted Tag(s) field */ -function showTagsField() { +function showPrecisionFields() { const billboardsTargetedTags = document.getElementById( 'billboard-targeted-tags', ); @@ -35,17 +35,30 @@ function showTagsField() { billboardsTargetedTags, ); } + + const billboardPrecisionElements = document.querySelectorAll('.billboard-requires-precision-targeting'); + + + billboardPrecisionElements.forEach((element) => { + element?.classList.remove('hidden'); + }); } /** * Hides the Targeted Tag(s) field */ -function hideTagsField() { +function hidePrecisionFields() { const billboardsTargetedTags = document.getElementById( 'billboard-targeted-tags', ); billboardsTargetedTags?.classList.add('hidden'); + + const billboardPrecisionElements = document.querySelectorAll('.billboard-requires-precision-targeting'); + + billboardPrecisionElements.forEach((element) => { + element?.classList.add('hidden'); + }); } /** @@ -154,14 +167,14 @@ document.ready.then(() => { ]; if (targetedTagPlacements.includes(select.value)) { - showTagsField(); + showPrecisionFields(); } select.addEventListener('change', (event) => { if (targetedTagPlacements.includes(event.target.value)) { - showTagsField(); + showPrecisionFields(); } else { - hideTagsField(); + hidePrecisionFields(); clearTagList(); } }); diff --git a/app/models/billboard.rb b/app/models/billboard.rb index d5556b4a075d..a0b82ed33399 100644 --- a/app/models/billboard.rb +++ b/app/models/billboard.rb @@ -5,7 +5,6 @@ class Billboard < ApplicationRecord belongs_to :creator, class_name: "User", optional: true belongs_to :audience_segment, optional: true - # rubocop:disable Layout/LineLength ALLOWED_PLACEMENT_AREAS = %w[sidebar_left sidebar_left_2 sidebar_right @@ -17,7 +16,6 @@ class Billboard < ApplicationRecord post_fixed_bottom post_sidebar post_comments].freeze - # rubocop:enable Layout/LineLength ALLOWED_PLACEMENT_AREAS_HUMAN_READABLE = ["Sidebar Left (First Position)", "Sidebar Left (Second Position)", "Sidebar Right (Home first position)", @@ -46,6 +44,7 @@ class Billboard < ApplicationRecord enum render_mode: { forem_markdown: 0, raw: 1 } enum template: { authorship_box: 0, plain: 1 } enum :special_behavior, { nothing: 0, delayed: 1 } + enum :browser_context, { all_browsers: 0, desktop: 1, mobile_web: 2, mobile_in_app: 3 } belongs_to :organization, optional: true has_many :billboard_events, foreign_key: :display_ad_id, inverse_of: :billboard, dependent: :destroy @@ -79,7 +78,14 @@ class Billboard < ApplicationRecord self.table_name = "display_ads" - def self.for_display(area:, user_signed_in:, user_id: nil, article: nil, user_tags: nil, location: nil, cookies_allowed: false) + def self.for_display(area:, + user_signed_in:, + user_id: nil, + article: nil, + user_tags: nil, + location: nil, + cookies_allowed: false, + user_agent: nil) permit_adjacent = article ? article.permit_adjacent_sponsors? : true billboards_for_display = Billboards::FilteredAdsQuery.call( @@ -94,6 +100,7 @@ def self.for_display(area:, user_signed_in:, user_id: nil, article: nil, user_ta user_tags: user_tags, location: location, cookies_allowed: cookies_allowed, + user_agent: user_agent, ) case rand(99) # output integer from 0-99 diff --git a/app/queries/billboards/filtered_ads_query.rb b/app/queries/billboards/filtered_ads_query.rb index df9d4ff1c556..29ae6cb61860 100644 --- a/app/queries/billboards/filtered_ads_query.rb +++ b/app/queries/billboards/filtered_ads_query.rb @@ -11,7 +11,7 @@ def self.call(...) # @param location [Geolocation|String] the visitor's geographic location def initialize(area:, user_signed_in:, organization_id: nil, article_tags: [], permit_adjacent_sponsors: true, article_id: nil, billboards: Billboard, - user_id: nil, user_tags: nil, location: nil, cookies_allowed: false) + user_id: nil, user_tags: nil, location: nil, cookies_allowed: false, user_agent: nil) @filtered_billboards = billboards.includes([:organization]) @area = area @user_signed_in = user_signed_in @@ -21,6 +21,7 @@ def initialize(area:, user_signed_in:, organization_id: nil, article_tags: [], @article_id = article_id @permit_adjacent_sponsors = permit_adjacent_sponsors @user_tags = user_tags + @user_agent = user_agent @location = Geolocation.from_iso3166(location) @cookies_allowed = cookies_allowed end @@ -28,6 +29,7 @@ def initialize(area:, user_signed_in:, organization_id: nil, article_tags: [], def call @filtered_billboards = approved_and_published_ads @filtered_billboards = placement_area_ads + @filtered_billboards = browser_context_ads if @user_agent @filtered_billboards = cookies_allowed_ads unless @cookies_allowed if @article_id.present? @@ -121,6 +123,19 @@ def location_targeted_ads @filtered_billboards.where(geo_query) end + def browser_context_ads + case @user_agent + when /DEV-Native-ios|DEV-Native-android|ForemWebView/ + @filtered_billboards.where(browser_context: %i[all_browsers mobile_in_app]) + when /Mobile|iPhone|Android/ + @filtered_billboards.where(browser_context: %i[all_browsers mobile_web]) + when /Windows|Macintosh|Mac OS X|Linux/ + @filtered_billboards.where(browser_context: %i[all_browsers desktop]) + else + @filtered_billboards + end + end + def type_of_ads # If this is an organization article and community-type ads exist, show them if @organization_id.present? diff --git a/app/views/admin/billboards/_form.html.erb b/app/views/admin/billboards/_form.html.erb index 26796ef41aec..77879def656e 100644 --- a/app/views/admin/billboards/_form.html.erb +++ b/app/views/admin/billboards/_form.html.erb @@ -42,12 +42,17 @@ <%= select_tag :placement_area, options_for_select(billboards_placement_area_options_array, selected: @billboard.placement_area), include_blank: "Select...", class: "crayons-select js-placement-area" %> + +
<% if FeatureFlag.enabled?(Geolocation::FEATURE_FLAG) %> -
+ diff --git a/db/migrate/20240226173118_add_browser_targeting_to_billboards.rb b/db/migrate/20240226173118_add_browser_targeting_to_billboards.rb new file mode 100644 index 000000000000..91c984f1c5da --- /dev/null +++ b/db/migrate/20240226173118_add_browser_targeting_to_billboards.rb @@ -0,0 +1,5 @@ +class AddBrowserTargetingToBillboards < ActiveRecord::Migration[7.0] + def change + add_column :display_ads, :browser_context, :integer, default: 0, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 9983464bd465..7855ca46f1af 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_02_12_142953) do +ActiveRecord::Schema[7.0].define(version: 2024_02_26_173118) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "ltree" @@ -473,6 +473,7 @@ t.boolean "approved", default: false t.integer "audience_segment_id" t.text "body_markdown" + t.integer "browser_context", default: 0, null: false t.string "cached_tag_list" t.integer "clicks_count", default: 0 t.datetime "created_at", precision: nil, null: false @@ -1388,6 +1389,7 @@ create_table "users_settings", force: :cascade do |t| t.string "brand_color1", default: "#000000" + t.integer "config_feed_style", default: 0, null: false t.integer "config_font", default: 0, null: false t.integer "config_homepage_feed", default: 0, null: false t.integer "config_navbar", default: 0, null: false diff --git a/spec/queries/billboards/filtered_ads_query_spec.rb b/spec/queries/billboards/filtered_ads_query_spec.rb index 52580a4f5858..a63e79abbbb2 100644 --- a/spec/queries/billboards/filtered_ads_query_spec.rb +++ b/spec/queries/billboards/filtered_ads_query_spec.rb @@ -339,4 +339,38 @@ def filter_billboards(**options) end end end + + context "when considering browser context" do + let!(:all_browsers_ad) { create_billboard browser_context: :all_browsers } + let!(:mobile_in_app_ad) { create_billboard browser_context: :mobile_in_app } + let!(:mobile_web_ad) { create_billboard browser_context: :mobile_web } + let!(:desktop_ad) { create_billboard browser_context: :desktop } + + it "filters ads based on user_agent string for mobile in-app context" do + filtered = filter_billboards(user_agent: "DEV-Native-ios") + expect(filtered).to include(all_browsers_ad, mobile_in_app_ad) + expect(filtered).not_to include(mobile_web_ad, desktop_ad) + + filtered = filter_billboards(user_agent: "DEV-Native-android") + expect(filtered).to include(all_browsers_ad, mobile_in_app_ad) + expect(filtered).not_to include(mobile_web_ad, desktop_ad) + end + + it "filters ads based on user_agent string for mobile web context" do + filtered = filter_billboards(user_agent: "Mobile Safari") + expect(filtered).to include(all_browsers_ad, mobile_web_ad) + expect(filtered).not_to include(mobile_in_app_ad, desktop_ad) + end + + it "filters ads based on user_agent string for desktop context" do + filtered = filter_billboards(user_agent: "Windows NT 10.0; Win64; x64") + expect(filtered).to include(all_browsers_ad, desktop_ad) + expect(filtered).not_to include(mobile_in_app_ad, mobile_web_ad) + end + + it "includes all ads for unknown user_agent contexts" do + filtered = filter_billboards(user_agent: "SomeUnknownBrowser/1.0") + expect(filtered).to include(all_browsers_ad, mobile_in_app_ad, mobile_web_ad, desktop_ad) + end + end end diff --git a/spec/requests/api/v1/billboards_spec.rb b/spec/requests/api/v1/billboards_spec.rb index 3d05aa5fb4e8..e86f006866c4 100644 --- a/spec/requests/api/v1/billboards_spec.rb +++ b/spec/requests/api/v1/billboards_spec.rb @@ -14,6 +14,7 @@ type_of: "community", published: true, approved: true, + browser_context: "all_browsers", requires_cookies: false, target_geolocations: "US-WA, CA-BC" } @@ -51,7 +52,7 @@ "impressions_count", "name", "organization_id", "placement_area", "processed_html", "published", "success_rate", "tag_list", "type_of", "updated_at", - "creator_id", "exclude_article_ids", + "creator_id", "exclude_article_ids", "browser_context", "audience_segment_type", "audience_segment_id", "custom_display_label", "template", "render_mode", "preferred_article_ids", "priority", "weight", "target_geolocations", "requires_cookies", "special_behavior") @@ -71,7 +72,7 @@ "impressions_count", "name", "organization_id", "placement_area", "processed_html", "published", "success_rate", "tag_list", "type_of", "updated_at", - "creator_id", "exclude_article_ids", + "creator_id", "exclude_article_ids", "browser_context", "audience_segment_type", "audience_segment_id", "custom_display_label", "template", "render_mode", "preferred_article_ids", "priority", "weight", "target_geolocations", "requires_cookies", "special_behavior") @@ -135,7 +136,7 @@ contain_exactly("approved", "body_markdown", "cached_tag_list", "clicks_count", "created_at", "display_to", "id", "impressions_count", "name", "organization_id", - "placement_area", "processed_html", "published", + "placement_area", "processed_html", "published", "browser_context", "success_rate", "tag_list", "type_of", "updated_at", "creator_id", "exclude_article_ids", "requires_cookies", "audience_segment_type", "audience_segment_id", "special_behavior",