# Generates realistic analytics demo data spanning 12 months. # # mix run priv/repo/seeds/analytics.exs # # Clears existing analytics events first, then creates ~50k events with # realistic traffic patterns, referrers, device mix, and e-commerce funnel. alias Berrypod.Repo alias Berrypod.Analytics.Event # ── Config ── # How many unique "visitors" to simulate per day (base — actual varies by day) base_daily_visitors = 40 # Date range: 12 months back from today end_date = Date.utc_today() start_date = Date.add(end_date, -364) # ── Reference data ── # Shop pages and their relative popularity (weights) pages = [ {"/", 30}, {"/collections/t-shirts", 15}, {"/collections/hoodies", 12}, {"/collections/mugs", 10}, {"/collections/stickers", 8}, {"/products/1", 8}, {"/products/2", 7}, {"/products/3", 6}, {"/products/4", 5}, {"/products/5", 5}, {"/products/6", 4}, {"/products/7", 3}, {"/products/8", 3}, {"/products/9", 2}, {"/products/10", 2}, {"/about", 4}, {"/delivery", 3}, {"/contact", 2}, {"/privacy", 1}, {"/terms", 1}, {"/cart", 6} ] # Referrer sources with weights (nil = direct traffic) sources = [ {nil, nil, 35}, {"google.com", "Google", 25}, {"instagram.com", "Instagram", 10}, {"facebook.com", "Facebook", 8}, {"tiktok.com", "TikTok", 6}, {"twitter.com", "Twitter", 4}, {"pinterest.com", "Pinterest", 4}, {"reddit.com", "Reddit", 3}, {"youtube.com", "YouTube", 2}, {"bing.com", "Bing", 1}, {"etsy.com", nil, 1}, {"blogpost.example.com", nil, 1} ] # UTM campaigns (only applied to social/search traffic, not direct) campaigns = [ {nil, nil, nil, 60}, {"instagram", "social", "summer_sale", 10}, {"facebook", "cpc", "retarget_q4", 8}, {"google", "cpc", "brand_terms", 7}, {"tiktok", "social", "viral_hoodie", 5}, {"newsletter", "email", "weekly_digest", 5}, {"pinterest", "social", "pin_collection", 3}, {"twitter", "social", "launch_promo", 2} ] # Countries with weights (UK-focused shop) countries = [ {"GB", 40}, {"US", 15}, {"DE", 7}, {"FR", 5}, {"CA", 4}, {"AU", 4}, {"NL", 3}, {"IE", 3}, {"SE", 2}, {"IT", 2}, {"ES", 2}, {"BE", 2}, {"NO", 1}, {"DK", 1}, {"PL", 1}, {"CH", 1}, {"NZ", 1}, {"JP", 1}, {"IN", 2}, {"BR", 2}, {"PT", 1} ] # Browsers (matches UAParser output) browsers = [ {"Chrome", 55}, {"Safari", 25}, {"Firefox", 10}, {"Edge", 8}, {"Opera", 2} ] # Operating systems (matches UAParser output) oses = [ {"iOS", 30}, {"Android", 25}, {"Windows", 22}, {"macOS", 15}, {"Linux", 5}, {"ChromeOS", 3} ] # Screen sizes (matches classify_screen/1 in AnalyticsHook) screen_sizes = [ {"mobile", 55}, {"tablet", 12}, {"desktop", 33} ] # ── Helpers ── # Weighted random pick from a list of {item, weight} or {a, b, weight} tuples weighted_pick = fn items -> total = items |> Enum.map(&elem(&1, tuple_size(&1) - 1)) |> Enum.sum() roll = :rand.uniform() * total Enum.reduce_while(items, 0.0, fn item, acc -> weight = elem(item, tuple_size(item) - 1) acc = acc + weight if acc >= roll, do: {:halt, item}, else: {:cont, acc} end) end # Day-of-week multiplier (weekends get more traffic for a consumer shop) day_multiplier = fn date -> case Date.day_of_week(date) do 6 -> 1.3 7 -> 1.2 1 -> 0.85 _ -> 1.0 end end # Monthly growth curve — traffic grows over time (new shop ramping up) month_multiplier = fn date -> months_ago = Date.diff(end_date, date) / 30.0 # Start at 0.3x and grow to 1.0x over the year max(0.3, 1.0 - months_ago * 0.06) end # Seasonal bumps (Nov-Dec holiday shopping, Jan sale) seasonal_multiplier = fn date -> case date.month do 11 -> 1.4 12 -> 1.8 1 -> 1.3 6 -> 1.1 7 -> 1.1 _ -> 1.0 end end # Random time of day (weighted toward daytime UK hours) random_time = fn -> # Bell curve centered around 14:00 UTC (afternoon UK time) hour = min(23, max(0, round(:rand.normal(14.0, 4.0)))) minute = :rand.uniform(60) - 1 second = :rand.uniform(60) - 1 Time.new!(hour, minute, second) end # ── Clear existing data ── IO.puts("Clearing existing analytics events...") {deleted, _} = Repo.delete_all(Event) IO.puts(" Deleted #{deleted} existing events") # ── Generate events ── IO.puts("Generating analytics data from #{start_date} to #{end_date}...") dates = Date.range(start_date, end_date) all_events = Enum.flat_map(dates, fn date -> # Calculate visitor count for this day base = base_daily_visitors multiplied = base * day_multiplier.(date) * month_multiplier.(date) * seasonal_multiplier.(date) # Add some randomness (+/- 20%) jitter = 0.8 + :rand.uniform() * 0.4 visitor_count = round(multiplied * jitter) # Generate visitors for this day Enum.flat_map(1..visitor_count, fn _v -> visitor_hash = :crypto.strong_rand_bytes(8) session_hash = :crypto.strong_rand_bytes(8) # Pick visitor attributes (consistent within a visit) {referrer, referrer_source, _} = weighted_pick.(sources) {country_code, _} = weighted_pick.(countries) {browser, _} = weighted_pick.(browsers) {os, _} = weighted_pick.(oses) {screen_size, _} = weighted_pick.(screen_sizes) # Maybe assign UTM params (only if has a referrer source) {utm_source, utm_medium, utm_campaign} = if referrer_source do {src, med, camp, _} = weighted_pick.(campaigns) {src, med, camp} else {nil, nil, nil} end # How many pages does this visitor view? (1-6, weighted toward fewer) page_count = min(6, max(1, round(:rand.normal(2.0, 1.2)))) # Pick pages for this session session_pages = Enum.map(1..page_count, fn _ -> {page, _} = weighted_pick.(pages) page end) # Generate pageview events with increasing timestamps base_time = random_time.() base_dt = DateTime.new!(date, base_time, "Etc/UTC") pageview_events = session_pages |> Enum.with_index() |> Enum.map(fn {pathname, i} -> # Each subsequent page is 15-120 seconds later offset = if i == 0, do: 0, else: Enum.sum(for _ <- 1..i, do: 15 + :rand.uniform(105)) ts = DateTime.add(base_dt, offset, :second) [ id: Ecto.UUID.generate(), name: "pageview", pathname: pathname, visitor_hash: visitor_hash, session_hash: session_hash, referrer: referrer, referrer_source: referrer_source, utm_source: utm_source, utm_medium: utm_medium, utm_campaign: utm_campaign, country_code: country_code, browser: browser, os: os, screen_size: screen_size, revenue: nil, inserted_at: DateTime.truncate(ts, :second) ] end) # E-commerce funnel events (progressive drop-off) # Only trigger if visitor viewed a product page viewed_product = Enum.any?(session_pages, &String.starts_with?(&1, "/products/")) ecommerce_events = if viewed_product do product_path = Enum.find(session_pages, &String.starts_with?(&1, "/products/")) last_ts = pageview_events |> List.last() |> Keyword.get(:inserted_at) base_attrs = [ visitor_hash: visitor_hash, session_hash: session_hash, referrer: referrer, referrer_source: referrer_source, utm_source: utm_source, utm_medium: utm_medium, utm_campaign: utm_campaign, country_code: country_code, browser: browser, os: os, screen_size: screen_size ] # product_view: 100% of product page viewers product_view = [ [ id: Ecto.UUID.generate(), name: "product_view", pathname: product_path, revenue: nil, inserted_at: DateTime.add(last_ts, 5, :second) ] ++ base_attrs ] # add_to_cart: ~35% of product viewers add_to_cart = if :rand.uniform() < 0.35 do [ [ id: Ecto.UUID.generate(), name: "add_to_cart", pathname: product_path, revenue: nil, inserted_at: DateTime.add(last_ts, 30, :second) ] ++ base_attrs ] else [] end # checkout_start: ~60% of add-to-carters checkout_start = if add_to_cart != [] and :rand.uniform() < 0.60 do [ [ id: Ecto.UUID.generate(), name: "checkout_start", pathname: "/cart", revenue: nil, inserted_at: DateTime.add(last_ts, 60, :second) ] ++ base_attrs ] else [] end # purchase: ~70% of checkout starters purchase = if checkout_start != [] and :rand.uniform() < 0.70 do # Revenue between 1500 (GBP 15.00) and 8500 (GBP 85.00) revenue = 1500 + :rand.uniform(7000) [ [ id: Ecto.UUID.generate(), name: "purchase", pathname: "/checkout/success", revenue: revenue, inserted_at: DateTime.add(last_ts, 120, :second) ] ++ base_attrs ] else [] end product_view ++ add_to_cart ++ checkout_start ++ purchase else [] end pageview_events ++ ecommerce_events end) end) # ── Batch insert ── total = length(all_events) IO.puts("Inserting #{total} events in batches...") all_events |> Enum.chunk_every(1000) |> Enum.with_index(1) |> Enum.each(fn {batch, i} -> Repo.insert_all(Event, batch) progress = min(i * 1000, total) IO.write("\r #{progress}/#{total}") end) IO.puts("\n Done!") # ── Summary stats ── pageviews = Enum.count(all_events, fn e -> Keyword.get(e, :name) == "pageview" end) product_views = Enum.count(all_events, fn e -> Keyword.get(e, :name) == "product_view" end) add_to_carts = Enum.count(all_events, fn e -> Keyword.get(e, :name) == "add_to_cart" end) checkouts = Enum.count(all_events, fn e -> Keyword.get(e, :name) == "checkout_start" end) purchases = Enum.count(all_events, fn e -> Keyword.get(e, :name) == "purchase" end) total_revenue = all_events |> Enum.filter(fn e -> Keyword.get(e, :name) == "purchase" end) |> Enum.map(fn e -> Keyword.get(e, :revenue) end) |> Enum.sum() IO.puts(""" Summary: Pageviews: #{pageviews} Product views: #{product_views} Add to cart: #{add_to_carts} Checkouts: #{checkouts} Purchases: #{purchases} Revenue: GBP #{Float.round(total_revenue / 100, 2)} """)