- <%= column_chart scoped_visits.group_by_hour_of_day(:started_at, format: "%l %P").count,
- title: "Average visits by Hour" %>
-
+ <%= chart_card("Average visits by Hour") do %>
+ <%= column_chart scoped_visits.group_by_hour_of_day(:started_at, format: "%l %P").count %>
+ <% end %>
-
- <%= column_chart scoped_visits.group_by_day_of_week(:started_at, format: "%A").count,
- title: "Average visits by Weekday" %>
-
+ <%= chart_card("Average visits by Weekday") do %>
+ <%= column_chart scoped_visits.group_by_day_of_week(:started_at, format: "%A").count %>
+ <% end %>
-
+ <%= chart_card("Top Cities") do %>
<%= bar_chart scoped_visits.where.not(city: nil)
.group(:city)
- .count.sort_by { |_c, v| -v }.first(10).to_h,
- title: "Top Cities" %>
-
+ .count.sort_by { |_c, v| -v }.first(10).to_h %>
+ <% end %>
-
- <%= column_chart @session_duration_chart, title: "Session Duration" %>
-
+ <%= chart_card("Session Duration") do %>
+ <%= column_chart @session_duration_chart %>
+ <% end %>
<%= @avg_events_per_visit %>
@@ -63,12 +60,11 @@
-
+ <%= chart_card("Visits by Date") do %>
<%= line_chart scoped_visits.group_by_day(:started_at).count,
xtitle: "Date", ytitle: "Visits",
- height: "300px", width: "100%",
- title: "Visits by Date" %>
-
+ height: "300px", width: "100%" %>
+ <% end %>
@@ -114,41 +110,15 @@
Workshop analytics
-
- <%= bar_chart(@ws_category_types, title: "Workshop Search: Category Types") %>
-
-
-
- <%= bar_chart(@ws_category_names, title: "Workshop Search: Categories") %>
-
-
-
- <%= bar_chart(@ws_sectors, title: "Workshop Search: Sectors") %>
-
-
-
- <%= bar_chart(@ws_search_titles, title: "Workshop Search: Titles") %>
-
-
-
- <%= bar_chart(@ws_search_authors, title: "Workshop Search: Authors") %>
-
-
-
- <%= bar_chart(@ws_search_full_text, title: "Workshop Search: Full-Text") %>
-
-
-
- <%= bar_chart(@ws_windows_types, title: "Workshop search: Windows audiences") %>
-
-
-
- <%= bar_chart(@ws_zero_results, title: "Workshop Search: No Results") %>
-
-
-
- <%= column_chart(@ws_funnel, title: "Workshop Discovery Funnel") %>
-
+ <%= chart_card("Workshop Search: Category Types") { bar_chart(@ws_category_types) } %>
+ <%= chart_card("Workshop Search: Categories") { bar_chart(@ws_category_names) } %>
+ <%= chart_card("Workshop Search: Sectors") { bar_chart(@ws_sectors) } %>
+ <%= chart_card("Workshop Search: Titles") { bar_chart(@ws_search_titles) } %>
+ <%= chart_card("Workshop Search: Authors") { bar_chart(@ws_search_authors) } %>
+ <%= chart_card("Workshop Search: Full-Text") { bar_chart(@ws_search_full_text) } %>
+ <%= chart_card("Workshop search: Windows audiences") { bar_chart(@ws_windows_types) } %>
+ <%= chart_card("Workshop Search: No Results") { bar_chart(@ws_zero_results) } %>
+ <%= chart_card("Workshop Discovery Funnel") { column_chart(@ws_funnel) } %>
@@ -157,17 +127,9 @@
Resource analytics
-
- <%= bar_chart(@rs_keywords, title: "Resource Search: Keywords") %>
-
-
-
- <%= bar_chart(@rs_kinds, title: "Resource Search: Kinds") %>
-
-
-
- <%= column_chart(@rs_funnel, title: "Resource Discovery Funnel") %>
-
+ <%= chart_card("Resource Search: Keywords") { bar_chart(@rs_keywords) } %>
+ <%= chart_card("Resource Search: Kinds") { bar_chart(@rs_kinds) } %>
+ <%= chart_card("Resource Discovery Funnel") { column_chart(@rs_funnel) } %>
@@ -176,51 +138,32 @@
Content discovery
-
- <%= bar_chart(@tagging_sectors, title: "Tagging views: Sectors") %>
-
-
-
- <%= bar_chart(@tagging_categories, title: "Tagging views: Categories") %>
-
-
-
- <%= column_chart(@discovery_funnel, title: "User Discovery Funnel") %>
-
+ <%= chart_card("Tagging views: Sectors") { bar_chart(@tagging_sectors) } %>
+ <%= chart_card("Tagging views: Categories") { bar_chart(@tagging_categories) } %>
+ <%= chart_card("User Discovery Funnel") { column_chart(@discovery_funnel) } %>
-
+ <%= chart_card("Content Types People View Most") do %>
<%= pie_chart(
scoped_events
.where("name LIKE 'view.%'")
.group("SUBSTRING_INDEX(name, '.', -1)")
- .count,
- title: "Content Types People View Most"
- ) %>
-
+ .count) %>
+ <% end %>
-
+ <%= chart_card("Content Types Printed Most") do %>
<%= pie_chart(
scoped_events
.where("name LIKE 'print.%'")
.group("SUBSTRING_INDEX(name, '.', -1)")
- .count,
- title: "Content Types Printed Most"
- ) %>
-
-
-
- <%= pie_chart(@content_discovery, title: "How Users Discover Content") %>
-
+ .count) %>
+ <% end %>
-
- <%= pie_chart(@search_conversion, title: "Search-to-View Conversion") %>
-
+ <%= chart_card("How Users Discover Content") { pie_chart(@content_discovery) } %>
+ <%= chart_card("Search-to-View Conversion") { pie_chart(@search_conversion) } %>
-
- <%= line_chart @user_signup_trend,
- title: "User Signup Trend",
- points: true %>
-
+ <%= chart_card("User Signup Trend") do %>
+ <%= line_chart @user_signup_trend, points: true %>
+ <% end %>
@@ -229,42 +172,33 @@
Referrals & engagement
-
+ <%= chart_card("Top Referrer → Landing Pages") do %>
<%= bar_chart scoped_visits.group([:referring_domain, :landing_page])
- .count.sort_by { |_k, v| -v }.first(10).to_h,
- title: "Top Referrer → Landing Pages" %>
-
+ .count.sort_by { |_k, v| -v }.first(10).to_h %>
+ <% end %>
-
+ <%= chart_card("Top Landing Pages") do %>
<%= bar_chart scoped_visits.group(:landing_page)
.order("count_id DESC")
.limit(10)
.count(:id),
- title: "Top Landing Pages", height: "350px" %>
-
+ height: "350px" %>
+ <% end %>
-
- <%= bar_chart @top_engaged_users,
- title: "Top Engaged Users",
- library: { indexAxis: "y" } %>
-
+ <%= chart_card("Top Engaged Users") do %>
+ <%= bar_chart @top_engaged_users, library: { indexAxis: "y" } %>
+ <% end %>
-
- <%= pie_chart @bounce_rate, title: "Bounce Rate" %>
-
+ <%= chart_card("Bounce Rate") { pie_chart @bounce_rate } %>
-
- <%= pie_chart scoped_visits.group(:country).count,
- title: "Visits by Country" %>
-
+ <%= chart_card("Visits by Country") do %>
+ <%= pie_chart scoped_visits.group(:country).count %>
+ <% end %>
-
- <%= bar_chart @top_exit_events, title: "Top Exit Events" %>
-
+ <%= chart_card("Top Exit Events") { bar_chart @top_exit_events } %>
-
+ <%= chart_card("Content Creation Velocity by Type") do %>
<%= line_chart @creation_velocity_data,
- title: "Content Creation Velocity by Type",
height: "400px",
points: true,
curve: false,
@@ -278,7 +212,7 @@
legend: { position: "top" },
yAxis: { title: { text: "Items Created" } }
} %>
-
+ <% end %>
@@ -287,21 +221,20 @@
Geographic & technical
-
- <%= pie_chart ({"Logged In" => scoped_visits.where.not(user_id: nil).count,
- "Anonymous" => scoped_visits.where(user_id: nil).count }),
- title: "Login status" %>
-
-
<%= pie_chart scoped_visits.group(:device_type).count, title: "Device Type" %>
-
<%= pie_chart scoped_visits.group(:utm_source).count, title: "UTM Sources" %>
-
<%= pie_chart scoped_visits.group(:referring_domain).count, title: "Referring Domains" %>
-
<%= pie_chart scoped_visits.group(:browser).count, title: "Browsers" %>
-
+ <%= chart_card("Login status") do %>
+ <%= pie_chart({ "Logged In" => scoped_visits.where.not(user_id: nil).count,
+ "Anonymous" => scoped_visits.where(user_id: nil).count }) %>
+ <% end %>
+ <%= chart_card("Device Type") { pie_chart scoped_visits.group(:device_type).count } %>
+ <%= chart_card("UTM Sources") { pie_chart scoped_visits.group(:utm_source).count } %>
+ <%= chart_card("Referring Domains") { pie_chart scoped_visits.group(:referring_domain).count } %>
+ <%= chart_card("Browsers") { pie_chart scoped_visits.group(:browser).count } %>
+ <%= chart_card("New vs Returning Visitors") do %>
<%= pie_chart({
"New" => scoped_visits.group(:visitor_token).having("count(*) = 1").count.size,
"Returning" => scoped_visits.group(:visitor_token).having("count(*) > 1").count.size
- }, title: "New vs Returning Visitors") %>
-
+ }) %>
+ <% end %>
diff --git a/db/seeds/dummy_dev_seeds.rb b/db/seeds/dummy_dev_seeds.rb
index ab88126c7..e9c9c3445 100644
--- a/db/seeds/dummy_dev_seeds.rb
+++ b/db/seeds/dummy_dev_seeds.rb
@@ -1475,3 +1475,253 @@
priya.bookmarks.pluck(:bookmarkable_type, :bookmarkable_id)
puts " #{shared.size} bookmarks shared between both users"
end
+
+# ─── Ahoy visits & events (analytics charts) ───────────────────────────
+puts "Creating Ahoy visits and events for analytics charts…"
+
+ahoy_users = [
+ User.find_by(email: "amy.user@example.com"),
+ User.find_by(email: "priya.user@example.com"),
+ nil # anonymous visitor
+].compact
+
+cities = [ "Los Angeles", "San Diego", "Portland", "Seattle", "Denver" ]
+browsers = %w[Chrome Safari Firefox Edge]
+devices = %w[Desktop Mobile Tablet]
+
+# Create visits spread over the past month
+ahoy_visits = []
+30.times do |day_offset|
+ rand(2..4).times do
+ user = ahoy_users.sample
+ visit = Ahoy::Visit.create!(
+ visit_token: SecureRandom.uuid,
+ visitor_token: SecureRandom.uuid,
+ user: user,
+ started_at: (30 - day_offset).days.ago + rand(0..23).hours,
+ browser: browsers.sample,
+ device_type: devices.sample,
+ city: cities.sample,
+ country: "US",
+ landing_page: %w[/workshops /resources /stories /].sample
+ )
+ ahoy_visits << visit
+ end
+end
+
+# Gather real records for view/print events
+view_targets = {
+ "workshop" => Workshop.published.limit(6).to_a,
+ "resource" => Resource.published.limit(6).to_a,
+ "person" => Person.limit(4).to_a,
+ "story" => Story.published.limit(4).to_a,
+ "event" => Event.limit(3).to_a,
+ "community_news" => CommunityNews.published.limit(3).to_a,
+ "workshop_variation" => WorkshopVariation.published.limit(3).to_a,
+ "tutorial" => Tutorial.limit(3).to_a
+}.reject { |_, v| v.empty? }
+
+# ── view.* events (populates "Content Types People View Most" pie chart) ──
+view_targets.each do |resource_name, records|
+ weight = resource_name == "workshop" ? 8 : (resource_name == "resource" ? 5 : 3)
+ weight.times do
+ record = records.sample
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "view.#{resource_name}",
+ resource_type: record.class.name,
+ resource_id: record.id,
+ properties: { resource_type: record.class.name, resource_id: record.id, resource_title: record.try(:title) || record.try(:name) },
+ time: visit.started_at + rand(1..300).seconds
+ )
+ end
+end
+
+# ── print.* events ──
+%w[workshop resource story community_news].each do |resource_name|
+ next unless view_targets[resource_name]&.any?
+
+ rand(2..4).times do
+ record = view_targets[resource_name].sample
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "print.#{resource_name}",
+ resource_type: record.class.name,
+ resource_id: record.id,
+ properties: { resource_type: record.class.name, resource_id: record.id, resource_title: record.try(:title) || record.try(:name) },
+ time: visit.started_at + rand(60..600).seconds
+ )
+ end
+end
+
+# ── download.resource events ──
+if view_targets["resource"]&.any?
+ 3.times do
+ record = view_targets["resource"].sample
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "download.resource",
+ resource_type: "Resource",
+ resource_id: record.id,
+ properties: { resource_type: "Resource", resource_id: record.id, resource_title: record.try(:title) },
+ time: visit.started_at + rand(60..600).seconds
+ )
+ end
+end
+
+# ── filter.workshops / search.workshops events (populates category/sector/windows type charts) ──
+seed_categories = Category.joins(:category_type).where(published: true).limit(15).to_a
+seed_sectors = Sector.where(published: true).limit(10).to_a
+seed_windows_types = WindowsType.all.to_a
+
+search_titles = [ "self care", "container of feelings", "self-care", "resilience", "touchstones",
+ "domestic violence", "grief", "personal needs flower", "butterfly", "may you be" ]
+search_authors = [ "fabian", "aaron", "power and control wheel", "aaron mason", "Janet Hughes" ]
+search_full_texts = [ "anxiety", "we rise", "luck", "collage", "mental wellness",
+ "mental well-being", "friendship", "transforming", "north star" ]
+
+# Filter events with categories, sectors, and windows types
+12.times do
+ visit = ahoy_visits.sample
+ cats = seed_categories.sample(rand(1..3)).map { |c| { id: c.id, name: c.name, type: c.category_type&.name } }
+ secs = seed_sectors.sample(rand(1..2)).map { |s| { id: s.id, name: s.name } }
+ wts = seed_windows_types.sample(rand(1..2)).map(&:id)
+
+ props = {
+ resource_type: "Workshop",
+ result_count: rand(3..40),
+ filters: { categories: cats, sectors: secs, windows_types: wts }
+ }
+
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "filter.workshops",
+ properties: props,
+ time: visit.started_at + rand(10..120).seconds
+ )
+end
+
+# Search events with keywords AND filters
+10.times do
+ visit = ahoy_visits.sample
+ cats = seed_categories.sample(rand(1..2)).map { |c| { id: c.id, name: c.name, type: c.category_type&.name } }
+ secs = seed_sectors.sample(rand(1..2)).map { |s| { id: s.id, name: s.name } }
+ wts = seed_windows_types.sample(rand(1..2)).map(&:id)
+
+ props = {
+ resource_type: "Workshop",
+ result_count: rand(1..20),
+ keywords: {
+ title: search_titles.sample,
+ author: search_authors.sample,
+ full_text: search_full_texts.sample
+ },
+ filters: { categories: cats, sectors: secs, windows_types: wts }
+ }
+
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "search.workshops",
+ properties: props,
+ time: visit.started_at + rand(10..120).seconds
+ )
+end
+
+# ── search_zero.workshops events (populates "No Results" chart) ──
+zero_queries = [ "watercolor techniques for teens", "music therapy", "yoga breathing",
+ "sand tray", "outdoor art", "digital collage", "puppet making 101",
+ "grief journaling advanced" ]
+zero_queries.each do |query|
+ visit = ahoy_visits.sample
+ rand(1..3).times do
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "search_zero.workshops",
+ properties: {
+ resource_type: "Workshop",
+ result_count: 0,
+ query: query,
+ keywords: { full_text: query }
+ },
+ time: visit.started_at + rand(10..300).seconds
+ )
+ end
+end
+
+# ── browse.taggings events (supplements tagging charts) ──
+5.times do
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "browse.taggings",
+ properties: {
+ sectors: seed_sectors.sample(rand(1..3)).map(&:name),
+ categories: seed_categories.sample(rand(1..3)).map(&:name),
+ page_result_count: rand(5..30)
+ },
+ time: visit.started_at + rand(10..300).seconds
+ )
+end
+
+# ── filter.resources / search.resources events ──
+resource_keywords = [ "art supplies guide", "facilitator handbook", "trauma informed", "group activity",
+ "healing through art", "coloring pages", "workshop template" ]
+resource_kinds = %w[pdf video link document]
+
+5.times do
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "filter.resources",
+ properties: { resource_type: "Resource", result_count: rand(2..15), filters: { kind: resource_kinds.sample } },
+ time: visit.started_at + rand(10..120).seconds
+ )
+end
+
+5.times do
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "search.resources",
+ properties: { resource_type: "Resource", result_count: rand(1..10), keywords: { full_text: resource_keywords.sample } },
+ time: visit.started_at + rand(10..120).seconds
+ )
+end
+
+# ── create.* events (populates "Content Creation Velocity" chart) ──
+if view_targets["workshop"]&.any?
+ %w[workshop_idea story_idea workshop_log quote bookmark].each do |model_name|
+ klass = model_name.classify.safe_constantize
+ next unless klass
+
+ records = klass.limit(3).to_a
+ next if records.empty?
+
+ records.each do |record|
+ visit = ahoy_visits.sample
+ Ahoy::Event.create!(
+ visit: visit,
+ user: visit.user,
+ name: "create.#{model_name}",
+ resource_type: record.class.name,
+ resource_id: record.id,
+ properties: { resource_type: record.class.name, resource_id: record.id, resource_title: record.try(:title) || record.try(:name) },
+ time: visit.started_at + rand(60..600).seconds
+ )
+ end
+ end
+end
+
+puts " Created #{Ahoy::Visit.count} visits, #{Ahoy::Event.count} events"
diff --git a/spec/requests/admin/ahoy_activities_spec.rb b/spec/requests/admin/ahoy_activities_spec.rb
index 67db19eee..5b1252b2f 100644
--- a/spec/requests/admin/ahoy_activities_spec.rb
+++ b/spec/requests/admin/ahoy_activities_spec.rb
@@ -213,6 +213,197 @@
get charts_path
expect(response).to have_http_status(:ok)
end
+
+ it "always renders chart titles even with no data" do
+ get charts_path, params: { time_period: "all_time" }
+ body = response.body
+
+ [
+ "Workshop Search: Category Types",
+ "Workshop Search: Categories",
+ "Workshop Search: Sectors",
+ "Workshop Search: Titles",
+ "Workshop Search: Authors",
+ "Workshop Search: Full-Text",
+ "Workshop search: Windows audiences",
+ "Workshop Search: No Results",
+ "Workshop Discovery Funnel",
+ "Content Types People View Most",
+ "Content Types Printed Most",
+ "How Users Discover Content",
+ "Search-to-View Conversion",
+ "User Signup Trend"
+ ].each do |title|
+ expect(body).to include(title), "Expected chart title '#{title}' to be present"
+ end
+ end
+ end
+
+ # ==============================================================
+ # Charts with populated data
+ # ==============================================================
+
+ describe "GET /admin/activities/charts with event data" do
+ let(:visit) { create(:ahoy_visit, user: user, started_at: 2.days.ago) }
+
+ let(:category_type) { create(:category_type, :published, name: "ArtType") }
+ let(:category) { create(:category, :published, name: "Drawing", category_type: category_type) }
+ let(:sector) { create(:sector, :published, name: "Domestic Violence") }
+ let(:windows_type) { create(:windows_type, :adult) }
+
+ let!(:filter_event) do
+ create(
+ :ahoy_event,
+ name: "filter.workshops",
+ user: user,
+ visit: visit,
+ time: 2.days.ago,
+ properties: {
+ "resource_type" => "Workshop",
+ "result_count" => 5,
+ "filters" => {
+ "categories" => [ { "id" => category.id, "name" => category.name, "type" => category_type.name } ],
+ "sectors" => [ { "id" => sector.id, "name" => sector.name } ],
+ "windows_types" => [ { "id" => windows_type.id, "name" => windows_type.name } ]
+ }
+ }
+ )
+ end
+
+ let!(:search_event) do
+ create(
+ :ahoy_event,
+ name: "search.workshops",
+ user: user,
+ visit: visit,
+ time: 2.days.ago,
+ properties: {
+ "resource_type" => "Workshop",
+ "result_count" => 3,
+ "keywords" => { "title" => "self care", "author" => "fabian", "full_text" => "anxiety" },
+ "filters" => {
+ "categories" => [ { "id" => category.id, "name" => category.name, "type" => category_type.name } ],
+ "sectors" => [ { "id" => sector.id, "name" => sector.name } ],
+ "windows_types" => [ { "id" => windows_type.id, "name" => windows_type.name } ]
+ }
+ }
+ )
+ end
+
+ let!(:zero_result_event) do
+ create(
+ :ahoy_event,
+ name: "search_zero.workshops",
+ user: user,
+ visit: visit,
+ time: 2.days.ago,
+ properties: {
+ "resource_type" => "Workshop",
+ "result_count" => 0,
+ "query" => "music therapy",
+ "keywords" => { "full_text" => "music therapy" }
+ }
+ )
+ end
+
+ let!(:view_workshop_event) do
+ create(
+ :ahoy_event,
+ name: "view.workshop",
+ user: user,
+ visit: visit,
+ resource_type: "Workshop",
+ resource_id: 1,
+ time: 2.days.ago,
+ properties: { "resource_type" => "Workshop", "resource_id" => 1 }
+ )
+ end
+
+ let!(:view_resource_event) do
+ create(
+ :ahoy_event,
+ name: "view.resource",
+ user: user,
+ visit: visit,
+ resource_type: "Resource",
+ resource_id: 1,
+ time: 2.days.ago,
+ properties: { "resource_type" => "Resource", "resource_id" => 1 }
+ )
+ end
+
+ let!(:print_event) do
+ create(
+ :ahoy_event,
+ name: "print.workshop",
+ user: user,
+ visit: visit,
+ resource_type: "Workshop",
+ resource_id: 1,
+ time: 2.days.ago,
+ properties: { "resource_type" => "Workshop", "resource_id" => 1 }
+ )
+ end
+
+ let!(:tagging_event) do
+ create(
+ :ahoy_event,
+ name: "browse.taggings",
+ user: user,
+ visit: visit,
+ time: 2.days.ago,
+ properties: {
+ "sectors" => [ sector.name ],
+ "categories" => [ category.name ],
+ "page_result_count" => 10
+ }
+ )
+ end
+
+ it "renders charts page successfully" do
+ get charts_path, params: { time_period: "all_time" }
+ expect(response).to have_http_status(:ok)
+ end
+
+ it "populates workshop filter charts with category, sector, and windows type data" do
+ get charts_path, params: { time_period: "all_time" }
+ body = response.body
+
+ expect(body).to include(category_type.name)
+ expect(body).to include(category.name)
+ expect(body).to include(sector.name)
+ expect(body).to include(windows_type.short_name)
+ end
+
+ it "populates workshop keyword search charts" do
+ get charts_path, params: { time_period: "all_time" }
+ body = response.body
+
+ expect(body).to include("self care")
+ expect(body).to include("fabian")
+ expect(body).to include("anxiety")
+ end
+
+ it "populates zero-result search chart" do
+ get charts_path, params: { time_period: "all_time" }
+ expect(response.body).to include("music therapy")
+ end
+
+ it "populates content type view pie chart with multiple types" do
+ get charts_path, params: { time_period: "all_time" }
+ body = response.body
+
+ expect(body).to include("workshop")
+ expect(body).to include("resource")
+ end
+
+ it "populates tagging charts with sector and category names" do
+ get charts_path, params: { time_period: "all_time" }
+ body = response.body
+
+ expect(body).to include(sector.name)
+ expect(body).to include(category.name)
+ end
end
end
end
diff --git a/spec/services/analytics/ahoy_tracker_spec.rb b/spec/services/analytics/ahoy_tracker_spec.rb
new file mode 100644
index 000000000..00387eb46
--- /dev/null
+++ b/spec/services/analytics/ahoy_tracker_spec.rb
@@ -0,0 +1,110 @@
+require "rails_helper"
+
+RSpec.describe Analytics::AhoyTracker do
+ describe ".extract_search_params (via track_index_intent)" do
+ let(:user) { create(:user) }
+ let(:category_type) { create(:category_type, :published, name: "ArtType") }
+ let(:category) { create(:category, :published, name: "Drawing", category_type: category_type) }
+ let(:sector) { create(:sector, :published, name: "Domestic Violence") }
+ let(:windows_type) { create(:windows_type, :adult) }
+
+ let(:controller) do
+ instance_double(WorkshopsController).tap do |ctrl|
+ ahoy = instance_double(Ahoy::Tracker, visit_token: "abc-123")
+ allow(ctrl).to receive(:ahoy).and_return(ahoy)
+ allow(ctrl).to receive(:current_user).and_return(user)
+ allow(ahoy).to receive(:track)
+ allow(ahoy).to receive(:visit).and_return(nil)
+ end
+ end
+
+ it "captures categories from filter form param name" do
+ params = ActionController::Parameters.new(
+ "categories" => { category.id.to_s => category.id.to_s }
+ )
+
+ described_class.track_index_intent(
+ controller, Workshop, params: params, result_count: 5
+ )
+
+ expect(controller.ahoy).to have_received(:track).with(
+ "filter.workshops",
+ hash_including(filters: hash_including(:categories))
+ )
+ end
+
+ it "captures sectors from filter form param name" do
+ params = ActionController::Parameters.new(
+ "sectors" => { sector.id.to_s => sector.id.to_s }
+ )
+
+ described_class.track_index_intent(
+ controller, Workshop, params: params, result_count: 5
+ )
+
+ expect(controller.ahoy).to have_received(:track).with(
+ "filter.workshops",
+ hash_including(filters: hash_including(:sectors))
+ )
+ end
+
+ it "captures windows_types from filter form param name" do
+ params = ActionController::Parameters.new(
+ "windows_types" => { windows_type.id.to_s => windows_type.id.to_s }
+ )
+
+ described_class.track_index_intent(
+ controller, Workshop, params: params, result_count: 5
+ )
+
+ expect(controller.ahoy).to have_received(:track).with(
+ "filter.workshops",
+ hash_including(filters: hash_including(:windows_types))
+ )
+ end
+
+ it "still captures category_ids from edit form param name" do
+ params = ActionController::Parameters.new(
+ "category_ids" => [ category.id.to_s ]
+ )
+
+ described_class.track_index_intent(
+ controller, Workshop, params: params, result_count: 5
+ )
+
+ expect(controller.ahoy).to have_received(:track).with(
+ "filter.workshops",
+ hash_including(filters: hash_including(:categories))
+ )
+ end
+
+ it "includes query in search_zero events" do
+ params = ActionController::Parameters.new(
+ "query" => "music therapy"
+ )
+
+ described_class.track_index_intent(
+ controller, Workshop, params: params, result_count: 0
+ )
+
+ expect(controller.ahoy).to have_received(:track).with(
+ "search_zero.workshops",
+ hash_including(query: "music therapy")
+ )
+ end
+
+ it "does not fire search_zero when results exist" do
+ params = ActionController::Parameters.new(
+ "query" => "self care"
+ )
+
+ described_class.track_index_intent(
+ controller, Workshop, params: params, result_count: 5
+ )
+
+ expect(controller.ahoy).not_to have_received(:track).with(
+ "search_zero.workshops", anything
+ )
+ end
+ end
+end