From 8d03e8a04acad8a67a7b97073257cf169c837425 Mon Sep 17 00:00:00 2001 From: Jacek Kubiak Date: Thu, 8 Jan 2026 12:06:49 +0100 Subject: [PATCH] Remove big chunk of unused code --- .gitlab/build-scripts/docker-entrypoint.sh | 14 - .gitlab/build-scripts/docker.gitlab.sh | 37 - .pre-commit-config.yaml | 46 - .tool-versions | 2 +- Dockerfile | 76 - Makefile | 35 - config/config.exs | 4 - config/runtime.exs | 294 +-- config/test.exs | 8 - docker_push | 4 - lib/mix/tasks/analyze_plans.ex | 60 - lib/mix/tasks/create_free_subscription.ex | 24 - lib/mix/tasks/download_country_database.ex | 55 - lib/mix/tasks/generate_referrer_favicons.ex | 30 - lib/plausible/auth/api_key.ex | 65 - lib/plausible/auth/api_key_admin.ex | 46 - lib/plausible/auth/auth.ex | 101 - lib/plausible/auth/invitation.ex | 23 - lib/plausible/auth/token.ex | 23 - lib/plausible/auth/user.ex | 106 - lib/plausible/auth/user_admin.ex | 85 - lib/plausible/billing/billing.ex | 284 --- lib/plausible/billing/enterprise_plan.ex | 32 - .../billing/enterprise_plan_admin.ex | 39 - lib/plausible/billing/paddle_api.ex | 131 -- lib/plausible/billing/plans.ex | 157 -- lib/plausible/billing/site_locker.ex | 55 - lib/plausible/billing/subscription.ex | 55 - lib/plausible/event/write_buffer.ex | 5 +- lib/plausible/goal/schema.ex | 54 - lib/plausible/goals.ex | 43 - lib/plausible/google/api.ex | 382 ---- lib/plausible/imported/site.ex | 249 --- lib/plausible/mailer.ex | 29 - lib/plausible/session/write_buffer.ex | 5 +- lib/plausible/site/admin.ex | 46 - lib/plausible/site/custom_domain.ex | 11 - lib/plausible/site/google_auth.ex | 29 - lib/plausible/site/monthly_report.ex | 28 - lib/plausible/site/schema.ex | 110 - lib/plausible/site/shared_link.ex | 34 - lib/plausible/site/site.ex | 18 + lib/plausible/site/spike_notification.ex | 35 - lib/plausible/site/weekly_report.ex | 28 - lib/plausible/sites.ex | 102 +- lib/plausible/slack.ex | 25 - lib/plausible/stats/aggregate.ex | 4 +- lib/plausible/stats/breakdown.ex | 48 +- lib/plausible/stats/filter_suggestions.ex | 11 +- lib/plausible/stats/imported.ex | 434 ---- lib/plausible/stats/query.ex | 30 +- lib/plausible/stats/timeseries.ex | 2 - lib/plausible_release.ex | 146 -- lib/plausible_web/captcha.ex | 29 - .../controllers/api/external_controller.ex | 11 +- .../api/external_sites_controller.ex | 140 -- .../api/external_stats_controller.ex | 231 --- .../controllers/api/internal_controller.ex | 24 - .../controllers/api/paddle_controller.ex | 76 - .../controllers/api/stats_controller.ex | 4 +- .../controllers/auth_controller.ex | 561 ------ .../controllers/billing_controller.ex | 179 -- .../controllers/invitation_controller.ex | 125 -- .../controllers/page_controller.ex | 11 +- .../controllers/site/membership_controller.ex | 149 -- .../controllers/site_controller.ex | 701 ------- .../controllers/stats_controller.ex | 202 +- .../controllers/unsubscribe_controller.ex | 43 - lib/plausible_web/email.ex | 315 --- lib/plausible_web/endpoint.ex | 7 - lib/plausible_web/plugs/auth_plug.ex | 24 +- .../plugs/authorize_site_access.ex | 44 +- .../plugs/authorize_sites_api.ex | 57 - .../plugs/authorize_stats_api.ex | 82 - lib/plausible_web/plugs/auto_auth_plug.ex | 25 - lib/plausible_web/plugs/crm_auth_plug.ex | 24 - lib/plausible_web/plugs/firewall.ex | 17 - lib/plausible_web/plugs/last_seen_plug.ex | 37 - .../plugs/logger_metadata_plug.ex | 62 - lib/plausible_web/plugs/require_account.ex | 26 - lib/plausible_web/plugs/require_logged_out.ex | 23 - .../plugs/session_timeout_plug.ex | 37 - lib/plausible_web/plugs/tracker.ex | 74 - lib/plausible_web/router.ex | 189 +- .../templates/auth/_onboarding_steps.html.eex | 39 - .../templates/auth/activate.html.eex | 59 - .../auth/invitation_expired.html.eex | 12 - .../templates/auth/login_form.html.eex | 24 - .../templates/auth/new_api_key.html.eex | 21 - .../templates/auth/password_form.html.eex | 14 - .../auth/password_reset_form.html.eex | 15 - .../auth/password_reset_request_form.html.eex | 22 - .../password_reset_request_success.html.eex | 17 - .../templates/auth/register_form.html.eex | 65 - .../register_from_invitation_form.html.eex | 61 - .../templates/auth/register_success.html.eex | 34 - .../templates/auth/user_settings.html.eex | 281 --- .../templates/billing/_paddle_script.html.eex | 7 - .../templates/billing/_plan_option.html.eex | 7 - .../billing/change_enterprise_plan.html.eex | 68 - ...change_enterprise_plan_contact_us.html.eex | 16 - .../templates/billing/change_plan.html.eex | 122 -- .../billing/change_plan_preview.html.eex | 84 - .../templates/billing/upgrade.html.eex | 128 -- .../billing/upgrade_enterprise_plan.html.eex | 43 - .../billing/upgrade_success.html.eex | 18 - .../billing/upgrade_to_plan.html.eex | 43 - .../templates/email/activation_email.html.eex | 3 - .../email/cancellation_email.html.eex | 10 - .../email/check_stats_email.html.eex | 15 - .../email/create_site_email.html.eex | 13 - .../templates/email/dashboard_locked.html.eex | 23 - .../email/enterprise_over_limit.html.eex | 9 - .../email/existing_user_invitation.html.eex | 10 - .../email/google_analytics_import.html.eex | 13 - .../email/invitation_accepted.html.eex | 9 - .../email/invitation_rejected.html.eex | 9 - .../email/new_user_invitation.html.eex | 11 - .../templates/email/over_limit.html.eex | 28 - .../ownership_transfer_accepted.html.eex | 10 - .../ownership_transfer_rejected.html.eex | 9 - .../email/ownership_transfer_request.html.eex | 15 - .../email/password_reset_email.html.eex | 2 - .../email/site_member_removed.html.eex | 10 - .../email/site_setup_help_email.html.eex | 17 - .../email/site_setup_success_email.html.eex | 19 - .../email/spike_notification.html.eex | 18 - .../email/trial_one_week_reminder.html.eex | 15 - .../templates/email/trial_over_email.html.eex | 15 - .../email/trial_upgrade_email.html.eex | 20 - .../templates/email/weekly_report.html.eex | 697 ------- .../templates/email/welcome_email.html.eex | 25 - .../yearly_expiration_notification.html.eex | 14 - .../yearly_renewal_notification.html.eex | 14 - .../templates/error/error.html.eex | 1 - .../templates/layout/_flash.html.eex | 65 - .../templates/layout/_footer.html.eex | 156 -- .../templates/layout/_header.html.eex | 66 +- .../templates/layout/_notice.html.eex | 96 - .../templates/layout/_settings_tab.html.eex | 5 - .../templates/layout/_tracking.html.eex | 17 - .../templates/layout/app.html.eex | 12 +- .../templates/layout/embedded.html.eex | 0 .../templates/layout/focus.html.eex | 34 - .../templates/layout/site_settings.html.eex | 26 - .../templates/page/index.html.eex | 3 - .../templates/site/edit_shared_link.html.eex | 12 - .../templates/site/index.html.eex | 188 -- .../membership/invite_member_form.html.eex | 63 - .../transfer_ownership_form.html.eex | 30 - lib/plausible_web/templates/site/new.html.eex | 79 - .../templates/site/new_goal.html.eex | 40 - .../templates/site/new_shared_link.html.eex | 24 - .../site/settings_custom_domain.html.eex | 39 - .../site/settings_danger_zone.html.eex | 32 - .../site/settings_email_reports.html.eex | 191 -- .../templates/site/settings_general.html.eex | 118 -- .../templates/site/settings_goals.html.eex | 26 - .../templates/site/settings_people.html.eex | 148 -- .../site/settings_search_console.html.eex | 56 - .../site/settings_visibility.html.eex | 118 -- .../templates/site/snippet.html.eex | 22 - .../stats/shared_link_password.html.eex | 17 - .../templates/stats/site_locked.html.eex | 49 - .../templates/stats/stats.html.eex | 38 +- .../stats/waiting_first_pageview.html.eex | 5 - .../templates/unsubscribe/success.html.eex | 4 - lib/plausible_web/views/auth_view.ex | 63 - lib/plausible_web/views/billing_view.ex | 40 - lib/plausible_web/views/email_view.ex | 27 - lib/plausible_web/views/layout_view.ex | 46 +- lib/plausible_web/views/page_view.ex | 15 - .../views/site/membership_view.ex | 3 - lib/plausible_web/views/site_view.ex | 61 - lib/plausible_web/views/unsubscribe_view.ex | 3 - lib/workers/check_usage.ex | 142 -- lib/workers/clean_email_verification_codes.ex | 17 - lib/workers/clean_invitations.ex | 14 - lib/workers/import_google_analytics.ex | 46 - lib/workers/lock_sites.ex | 16 - lib/workers/notify_annual_renewal.ex | 63 - lib/workers/schedule_email_reports.ex | 97 - lib/workers/send_check_stats_emails.ex | 41 - lib/workers/send_email_report.ex | 84 - lib/workers/send_site_setup_emails.ex | 112 - lib/workers/send_trial_notifications.ex | 74 - lib/workers/spike_notifier.ex | 63 - mix.exs | 10 - priv/paddle.pem | 14 - priv/plans_v1.json | 72 - priv/plans_v2.json | 72 - priv/plans_v3.json | 59 - test/plausible/auth/auth_test.exs | 39 - test/plausible/billing/billing_test.exs | 551 ----- test/plausible/billing/plans_test.exs | 85 - test/plausible/billing/site_locker_test.exs | 194 -- test/plausible/imported/imported_test.exs | 618 ------ .../CSVs/30d-filter-goal/browsers.csv | 2 - .../CSVs/30d-filter-goal/cities.csv | 1 - .../CSVs/30d-filter-goal/conversions.csv | 2 - .../CSVs/30d-filter-goal/countries.csv | 1 - .../CSVs/30d-filter-goal/devices.csv | 2 - .../CSVs/30d-filter-goal/entry_pages.csv | 2 - .../CSVs/30d-filter-goal/exit_pages.csv | 2 - .../30d-filter-goal/operating_systems.csv | 2 - .../CSVs/30d-filter-goal/pages.csv | 2 - .../CSVs/30d-filter-goal/prop_breakdown.csv | 2 - .../CSVs/30d-filter-goal/regions.csv | 1 - .../CSVs/30d-filter-goal/sources.csv | 1 - .../CSVs/30d-filter-goal/utm_campaigns.csv | 1 - .../CSVs/30d-filter-goal/utm_contents.csv | 1 - .../CSVs/30d-filter-goal/utm_mediums.csv | 1 - .../CSVs/30d-filter-goal/utm_sources.csv | 1 - .../CSVs/30d-filter-goal/utm_terms.csv | 1 - .../CSVs/30d-filter-goal/visitors.csv | 32 - .../CSVs/30d-filter-path/browsers.csv | 2 - .../CSVs/30d-filter-path/cities.csv | 2 - .../CSVs/30d-filter-path/conversions.csv | 1 - .../CSVs/30d-filter-path/countries.csv | 2 - .../CSVs/30d-filter-path/devices.csv | 2 - .../CSVs/30d-filter-path/entry_pages.csv | 2 - .../CSVs/30d-filter-path/exit_pages.csv | 2 - .../30d-filter-path/operating_systems.csv | 2 - .../CSVs/30d-filter-path/pages.csv | 2 - .../CSVs/30d-filter-path/prop_breakdown.csv | 1 - .../CSVs/30d-filter-path/regions.csv | 2 - .../CSVs/30d-filter-path/sources.csv | 1 - .../CSVs/30d-filter-path/utm_campaigns.csv | 1 - .../CSVs/30d-filter-path/utm_contents.csv | 1 - .../CSVs/30d-filter-path/utm_mediums.csv | 1 - .../CSVs/30d-filter-path/utm_sources.csv | 1 - .../CSVs/30d-filter-path/utm_terms.csv | 1 - .../CSVs/30d-filter-path/visitors.csv | 32 - .../controllers/CSVs/30d/browsers.csv | 3 - .../controllers/CSVs/30d/cities.csv | 2 - .../controllers/CSVs/30d/conversions.csv | 2 - .../controllers/CSVs/30d/countries.csv | 2 - .../controllers/CSVs/30d/devices.csv | 2 - .../controllers/CSVs/30d/entry_pages.csv | 2 - .../controllers/CSVs/30d/exit_pages.csv | 3 - .../CSVs/30d/operating_systems.csv | 2 - .../controllers/CSVs/30d/pages.csv | 3 - .../controllers/CSVs/30d/prop_breakdown.csv | 1 - .../controllers/CSVs/30d/regions.csv | 2 - .../controllers/CSVs/30d/sources.csv | 2 - .../controllers/CSVs/30d/utm_campaigns.csv | 2 - .../controllers/CSVs/30d/utm_contents.csv | 2 - .../controllers/CSVs/30d/utm_mediums.csv | 2 - .../controllers/CSVs/30d/utm_sources.csv | 2 - .../controllers/CSVs/30d/utm_terms.csv | 2 - .../controllers/CSVs/30d/visitors.csv | 32 - .../controllers/CSVs/6m/browsers.csv | 3 - .../controllers/CSVs/6m/cities.csv | 2 - .../controllers/CSVs/6m/conversions.csv | 2 - .../controllers/CSVs/6m/countries.csv | 2 - .../controllers/CSVs/6m/devices.csv | 2 - .../controllers/CSVs/6m/entry_pages.csv | 2 - .../controllers/CSVs/6m/exit_pages.csv | 3 - .../controllers/CSVs/6m/operating_systems.csv | 2 - .../controllers/CSVs/6m/pages.csv | 3 - .../controllers/CSVs/6m/prop_breakdown.csv | 1 - .../controllers/CSVs/6m/regions.csv | 2 - .../controllers/CSVs/6m/sources.csv | 2 - .../controllers/CSVs/6m/utm_campaigns.csv | 2 - .../controllers/CSVs/6m/utm_contents.csv | 2 - .../controllers/CSVs/6m/utm_mediums.csv | 2 - .../controllers/CSVs/6m/utm_sources.csv | 2 - .../controllers/CSVs/6m/utm_terms.csv | 2 - .../controllers/CSVs/6m/visitors.csv | 7 - .../admin_auth_controller_test.exs | 53 - .../api/external_sites_controller_test.exs | 414 ---- .../aggregate_test.exs | 813 -------- .../external_stats_controller/auth_test.exs | 113 -- .../breakdown_test.exs | 1794 ----------------- .../timeseries_test.exs | 824 -------- .../api/paddle_controller_test.exs | 45 - .../controllers/auth_controller_test.exs | 632 ------ .../controllers/billing_controller_test.exs | 138 -- .../invitation_controller_test.exs | 209 -- .../site/membership_controller_test.exs | 217 -- .../controllers/site_controller_test.exs | 809 -------- .../controllers/stats_controller_test.exs | 6 +- .../unsubscribe_controller_test.exs | 52 - test/plausible_web/plugs/auth_plug_test.exs | 44 - test/plausible_web/plugs/firewall_test.exs | 32 - .../plugs/session_timeout_plug_test.exs | 35 - test/plausible_web/views/email_view_test.exs | 16 - test/support/factory.ex | 191 -- test/support/google_api_mock.ex | 9 - test/support/paddle_api_mock.ex | 53 - test/workers/check_usage_test.exs | 279 --- .../clean_email_verification_codes_test.exs | 35 - test/workers/clean_invitations_test.exs | 30 - test/workers/import_google_analytics_test.exs | 86 - test/workers/lock_sites_test.exs | 115 -- test/workers/notify_annual_renewal_test.exs | 173 -- test/workers/schedule_email_reports_test.exs | 101 - test/workers/send_check_stats_emails_test.exs | 52 - test/workers/send_email_report_test.exs | 138 -- test/workers/send_site_setup_emails_test.exs | 100 - .../workers/send_trial_notifications_test.exs | 201 -- test/workers/spike_notifier_test.exs | 88 - tracker/LICENSE.md | 7 - tracker/compile.js | 29 - tracker/package-lock.json | 137 -- tracker/package.json | 11 - tracker/src/p.js | 123 -- tracker/src/plausible.js | 164 -- 308 files changed, 84 insertions(+), 22612 deletions(-) delete mode 100755 .gitlab/build-scripts/docker-entrypoint.sh delete mode 100644 .gitlab/build-scripts/docker.gitlab.sh delete mode 100644 .pre-commit-config.yaml delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100755 docker_push delete mode 100644 lib/mix/tasks/analyze_plans.ex delete mode 100644 lib/mix/tasks/create_free_subscription.ex delete mode 100644 lib/mix/tasks/download_country_database.ex delete mode 100644 lib/mix/tasks/generate_referrer_favicons.ex delete mode 100644 lib/plausible/auth/api_key.ex delete mode 100644 lib/plausible/auth/api_key_admin.ex delete mode 100644 lib/plausible/auth/auth.ex delete mode 100644 lib/plausible/auth/invitation.ex delete mode 100644 lib/plausible/auth/token.ex delete mode 100644 lib/plausible/auth/user_admin.ex delete mode 100644 lib/plausible/billing/billing.ex delete mode 100644 lib/plausible/billing/enterprise_plan.ex delete mode 100644 lib/plausible/billing/enterprise_plan_admin.ex delete mode 100644 lib/plausible/billing/paddle_api.ex delete mode 100644 lib/plausible/billing/plans.ex delete mode 100644 lib/plausible/billing/site_locker.ex delete mode 100644 lib/plausible/billing/subscription.ex delete mode 100644 lib/plausible/goal/schema.ex delete mode 100644 lib/plausible/goals.ex delete mode 100644 lib/plausible/google/api.ex delete mode 100644 lib/plausible/imported/site.ex delete mode 100644 lib/plausible/mailer.ex delete mode 100644 lib/plausible/site/admin.ex delete mode 100644 lib/plausible/site/custom_domain.ex delete mode 100644 lib/plausible/site/google_auth.ex delete mode 100644 lib/plausible/site/monthly_report.ex delete mode 100644 lib/plausible/site/schema.ex delete mode 100644 lib/plausible/site/shared_link.ex create mode 100644 lib/plausible/site/site.ex delete mode 100644 lib/plausible/site/spike_notification.ex delete mode 100644 lib/plausible/site/weekly_report.ex delete mode 100644 lib/plausible/slack.ex delete mode 100644 lib/plausible/stats/imported.ex delete mode 100644 lib/plausible_web/captcha.ex delete mode 100644 lib/plausible_web/controllers/api/external_sites_controller.ex delete mode 100644 lib/plausible_web/controllers/api/external_stats_controller.ex delete mode 100644 lib/plausible_web/controllers/api/internal_controller.ex delete mode 100644 lib/plausible_web/controllers/api/paddle_controller.ex delete mode 100644 lib/plausible_web/controllers/auth_controller.ex delete mode 100644 lib/plausible_web/controllers/billing_controller.ex delete mode 100644 lib/plausible_web/controllers/invitation_controller.ex delete mode 100644 lib/plausible_web/controllers/site/membership_controller.ex delete mode 100644 lib/plausible_web/controllers/site_controller.ex delete mode 100644 lib/plausible_web/controllers/unsubscribe_controller.ex delete mode 100644 lib/plausible_web/email.ex delete mode 100644 lib/plausible_web/plugs/authorize_sites_api.ex delete mode 100644 lib/plausible_web/plugs/authorize_stats_api.ex delete mode 100644 lib/plausible_web/plugs/auto_auth_plug.ex delete mode 100644 lib/plausible_web/plugs/crm_auth_plug.ex delete mode 100644 lib/plausible_web/plugs/firewall.ex delete mode 100644 lib/plausible_web/plugs/last_seen_plug.ex delete mode 100644 lib/plausible_web/plugs/logger_metadata_plug.ex delete mode 100644 lib/plausible_web/plugs/require_account.ex delete mode 100644 lib/plausible_web/plugs/require_logged_out.ex delete mode 100644 lib/plausible_web/plugs/session_timeout_plug.ex delete mode 100644 lib/plausible_web/plugs/tracker.ex delete mode 100644 lib/plausible_web/templates/auth/_onboarding_steps.html.eex delete mode 100644 lib/plausible_web/templates/auth/activate.html.eex delete mode 100644 lib/plausible_web/templates/auth/invitation_expired.html.eex delete mode 100644 lib/plausible_web/templates/auth/login_form.html.eex delete mode 100644 lib/plausible_web/templates/auth/new_api_key.html.eex delete mode 100644 lib/plausible_web/templates/auth/password_form.html.eex delete mode 100644 lib/plausible_web/templates/auth/password_reset_form.html.eex delete mode 100644 lib/plausible_web/templates/auth/password_reset_request_form.html.eex delete mode 100644 lib/plausible_web/templates/auth/password_reset_request_success.html.eex delete mode 100644 lib/plausible_web/templates/auth/register_form.html.eex delete mode 100644 lib/plausible_web/templates/auth/register_from_invitation_form.html.eex delete mode 100644 lib/plausible_web/templates/auth/register_success.html.eex delete mode 100644 lib/plausible_web/templates/auth/user_settings.html.eex delete mode 100644 lib/plausible_web/templates/billing/_paddle_script.html.eex delete mode 100644 lib/plausible_web/templates/billing/_plan_option.html.eex delete mode 100644 lib/plausible_web/templates/billing/change_enterprise_plan.html.eex delete mode 100644 lib/plausible_web/templates/billing/change_enterprise_plan_contact_us.html.eex delete mode 100644 lib/plausible_web/templates/billing/change_plan.html.eex delete mode 100644 lib/plausible_web/templates/billing/change_plan_preview.html.eex delete mode 100644 lib/plausible_web/templates/billing/upgrade.html.eex delete mode 100644 lib/plausible_web/templates/billing/upgrade_enterprise_plan.html.eex delete mode 100644 lib/plausible_web/templates/billing/upgrade_success.html.eex delete mode 100644 lib/plausible_web/templates/billing/upgrade_to_plan.html.eex delete mode 100644 lib/plausible_web/templates/email/activation_email.html.eex delete mode 100644 lib/plausible_web/templates/email/cancellation_email.html.eex delete mode 100644 lib/plausible_web/templates/email/check_stats_email.html.eex delete mode 100644 lib/plausible_web/templates/email/create_site_email.html.eex delete mode 100644 lib/plausible_web/templates/email/dashboard_locked.html.eex delete mode 100644 lib/plausible_web/templates/email/enterprise_over_limit.html.eex delete mode 100644 lib/plausible_web/templates/email/existing_user_invitation.html.eex delete mode 100644 lib/plausible_web/templates/email/google_analytics_import.html.eex delete mode 100644 lib/plausible_web/templates/email/invitation_accepted.html.eex delete mode 100644 lib/plausible_web/templates/email/invitation_rejected.html.eex delete mode 100644 lib/plausible_web/templates/email/new_user_invitation.html.eex delete mode 100644 lib/plausible_web/templates/email/over_limit.html.eex delete mode 100644 lib/plausible_web/templates/email/ownership_transfer_accepted.html.eex delete mode 100644 lib/plausible_web/templates/email/ownership_transfer_rejected.html.eex delete mode 100644 lib/plausible_web/templates/email/ownership_transfer_request.html.eex delete mode 100644 lib/plausible_web/templates/email/password_reset_email.html.eex delete mode 100644 lib/plausible_web/templates/email/site_member_removed.html.eex delete mode 100644 lib/plausible_web/templates/email/site_setup_help_email.html.eex delete mode 100644 lib/plausible_web/templates/email/site_setup_success_email.html.eex delete mode 100644 lib/plausible_web/templates/email/spike_notification.html.eex delete mode 100644 lib/plausible_web/templates/email/trial_one_week_reminder.html.eex delete mode 100644 lib/plausible_web/templates/email/trial_over_email.html.eex delete mode 100644 lib/plausible_web/templates/email/trial_upgrade_email.html.eex delete mode 100644 lib/plausible_web/templates/email/weekly_report.html.eex delete mode 100644 lib/plausible_web/templates/email/welcome_email.html.eex delete mode 100644 lib/plausible_web/templates/email/yearly_expiration_notification.html.eex delete mode 100644 lib/plausible_web/templates/email/yearly_renewal_notification.html.eex delete mode 100644 lib/plausible_web/templates/layout/_flash.html.eex delete mode 100644 lib/plausible_web/templates/layout/_footer.html.eex delete mode 100644 lib/plausible_web/templates/layout/_notice.html.eex delete mode 100644 lib/plausible_web/templates/layout/_settings_tab.html.eex delete mode 100644 lib/plausible_web/templates/layout/_tracking.html.eex delete mode 100644 lib/plausible_web/templates/layout/embedded.html.eex delete mode 100644 lib/plausible_web/templates/layout/focus.html.eex delete mode 100644 lib/plausible_web/templates/layout/site_settings.html.eex delete mode 100644 lib/plausible_web/templates/page/index.html.eex delete mode 100644 lib/plausible_web/templates/site/edit_shared_link.html.eex delete mode 100644 lib/plausible_web/templates/site/index.html.eex delete mode 100644 lib/plausible_web/templates/site/membership/invite_member_form.html.eex delete mode 100644 lib/plausible_web/templates/site/membership/transfer_ownership_form.html.eex delete mode 100644 lib/plausible_web/templates/site/new.html.eex delete mode 100644 lib/plausible_web/templates/site/new_goal.html.eex delete mode 100644 lib/plausible_web/templates/site/new_shared_link.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_custom_domain.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_danger_zone.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_email_reports.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_general.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_goals.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_people.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_search_console.html.eex delete mode 100644 lib/plausible_web/templates/site/settings_visibility.html.eex delete mode 100644 lib/plausible_web/templates/site/snippet.html.eex delete mode 100644 lib/plausible_web/templates/stats/shared_link_password.html.eex delete mode 100644 lib/plausible_web/templates/stats/site_locked.html.eex delete mode 100644 lib/plausible_web/templates/unsubscribe/success.html.eex delete mode 100644 lib/plausible_web/views/auth_view.ex delete mode 100644 lib/plausible_web/views/billing_view.ex delete mode 100644 lib/plausible_web/views/email_view.ex delete mode 100644 lib/plausible_web/views/page_view.ex delete mode 100644 lib/plausible_web/views/site/membership_view.ex delete mode 100644 lib/plausible_web/views/site_view.ex delete mode 100644 lib/plausible_web/views/unsubscribe_view.ex delete mode 100644 lib/workers/check_usage.ex delete mode 100644 lib/workers/clean_email_verification_codes.ex delete mode 100644 lib/workers/clean_invitations.ex delete mode 100644 lib/workers/import_google_analytics.ex delete mode 100644 lib/workers/lock_sites.ex delete mode 100644 lib/workers/notify_annual_renewal.ex delete mode 100644 lib/workers/schedule_email_reports.ex delete mode 100644 lib/workers/send_check_stats_emails.ex delete mode 100644 lib/workers/send_email_report.ex delete mode 100644 lib/workers/send_site_setup_emails.ex delete mode 100644 lib/workers/send_trial_notifications.ex delete mode 100644 lib/workers/spike_notifier.ex delete mode 100644 priv/paddle.pem delete mode 100644 priv/plans_v1.json delete mode 100644 priv/plans_v2.json delete mode 100644 priv/plans_v3.json delete mode 100644 test/plausible/auth/auth_test.exs delete mode 100644 test/plausible/billing/billing_test.exs delete mode 100644 test/plausible/billing/plans_test.exs delete mode 100644 test/plausible/billing/site_locker_test.exs delete mode 100644 test/plausible/imported/imported_test.exs delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/browsers.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/cities.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/conversions.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/countries.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/devices.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/entry_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/exit_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/operating_systems.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/prop_breakdown.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/regions.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/utm_campaigns.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/utm_contents.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/utm_mediums.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/utm_sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/utm_terms.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-goal/visitors.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/browsers.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/cities.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/conversions.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/countries.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/devices.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/entry_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/exit_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/operating_systems.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/prop_breakdown.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/regions.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/utm_campaigns.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/utm_contents.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/utm_mediums.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/utm_sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/utm_terms.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d-filter-path/visitors.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/browsers.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/cities.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/conversions.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/countries.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/devices.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/entry_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/exit_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/operating_systems.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/prop_breakdown.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/regions.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/utm_campaigns.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/utm_contents.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/utm_mediums.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/utm_sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/utm_terms.csv delete mode 100644 test/plausible_web/controllers/CSVs/30d/visitors.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/browsers.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/cities.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/conversions.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/countries.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/devices.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/entry_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/exit_pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/operating_systems.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/pages.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/prop_breakdown.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/regions.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/utm_campaigns.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/utm_contents.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/utm_mediums.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/utm_sources.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/utm_terms.csv delete mode 100644 test/plausible_web/controllers/CSVs/6m/visitors.csv delete mode 100644 test/plausible_web/controllers/admin_auth_controller_test.exs delete mode 100644 test/plausible_web/controllers/api/external_sites_controller_test.exs delete mode 100644 test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs delete mode 100644 test/plausible_web/controllers/api/external_stats_controller/auth_test.exs delete mode 100644 test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs delete mode 100644 test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs delete mode 100644 test/plausible_web/controllers/api/paddle_controller_test.exs delete mode 100644 test/plausible_web/controllers/auth_controller_test.exs delete mode 100644 test/plausible_web/controllers/billing_controller_test.exs delete mode 100644 test/plausible_web/controllers/invitation_controller_test.exs delete mode 100644 test/plausible_web/controllers/site/membership_controller_test.exs delete mode 100644 test/plausible_web/controllers/site_controller_test.exs delete mode 100644 test/plausible_web/controllers/unsubscribe_controller_test.exs delete mode 100644 test/plausible_web/plugs/auth_plug_test.exs delete mode 100644 test/plausible_web/plugs/firewall_test.exs delete mode 100644 test/plausible_web/plugs/session_timeout_plug_test.exs delete mode 100644 test/plausible_web/views/email_view_test.exs delete mode 100644 test/support/google_api_mock.ex delete mode 100644 test/support/paddle_api_mock.ex delete mode 100644 test/workers/check_usage_test.exs delete mode 100644 test/workers/clean_email_verification_codes_test.exs delete mode 100644 test/workers/clean_invitations_test.exs delete mode 100644 test/workers/import_google_analytics_test.exs delete mode 100644 test/workers/lock_sites_test.exs delete mode 100644 test/workers/notify_annual_renewal_test.exs delete mode 100644 test/workers/schedule_email_reports_test.exs delete mode 100644 test/workers/send_check_stats_emails_test.exs delete mode 100644 test/workers/send_email_report_test.exs delete mode 100644 test/workers/send_site_setup_emails_test.exs delete mode 100644 test/workers/send_trial_notifications_test.exs delete mode 100644 test/workers/spike_notifier_test.exs delete mode 100644 tracker/LICENSE.md delete mode 100644 tracker/compile.js delete mode 100644 tracker/package-lock.json delete mode 100644 tracker/package.json delete mode 100644 tracker/src/p.js delete mode 100644 tracker/src/plausible.js diff --git a/.gitlab/build-scripts/docker-entrypoint.sh b/.gitlab/build-scripts/docker-entrypoint.sh deleted file mode 100755 index 5f32276ab433..000000000000 --- a/.gitlab/build-scripts/docker-entrypoint.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -set -e - -if [ "$1" = 'run' ]; then - exec /app/bin/plausible start - -elif [ "$1" = 'db' ]; then - exec /app/"$2".sh - else - exec "$@" - -fi - -exec "$@" diff --git a/.gitlab/build-scripts/docker.gitlab.sh b/.gitlab/build-scripts/docker.gitlab.sh deleted file mode 100644 index 51395c8b18ab..000000000000 --- a/.gitlab/build-scripts/docker.gitlab.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -############################ -function docker_create_config() { -############################ - mkdir -p /kaniko/.docker/ - echo "###############" - echo "Logging into GitLab Container Registry with CI credentials for kaniko..." - echo "###############" - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - echo "" - -} - - -############################ -function docker_build_image() { -############################ - if [[ -f Dockerfile ]]; then - echo "###############" - echo "Building Dockerfile-based application..." - echo "###############" - - /kaniko/executor \ - --cache=true \ - --context "${CI_PROJECT_DIR}" \ - --dockerfile "${CI_PROJECT_DIR}"/Dockerfile \ - --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}-${CI_COMMIT_SHORT_SHA}" \ - --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_SLUG}-latest" \ - \ - "$@" - - else - echo "No Dockerfile found." - return 1 - fi -} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 7f9d9aac5eb5..000000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v2.4.1" - hooks: - - id: prettier - files: "assets/js|assets/css" - args: [--config, assets/.prettierrc.json] - - - repo: https://github.com/awebdeveloper/pre-commit-stylelint - rev: '0.0.2' - hooks: - - id: stylelint - additional_dependencies: - - stylelint@13.2.1 - - stylelint-config-standard@20.0.0 - - - repo: https://github.com/pre-commit/mirrors-eslint - rev: 'v8.3.0' - hooks: - - id: eslint - files: "assets/js" - additional_dependencies: - - eslint@7.2.0 - - eslint-plugin-import@2.22.1 - - eslint-plugin-jsx-a11y@6.4.1 - - eslint-plugin-react@7.21.5 - - eslint-plugin-react-hooks@4.2.0 - - - repo: https://gitlab.com/jvenom/elixir-pre-commit-hooks - rev: v1.0.0 - hooks: - - id: mix-format - - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 - hooks: - - id: check-case-conflict - - id: check-symlinks - - id: check-yaml - - id: destroyed-symlinks - - id: end-of-file-fixer - exclude: priv/tracker/js - - id: mixed-line-ending - - id: trailing-whitespace diff --git a/.tool-versions b/.tool-versions index 47b346048e77..66909d80888b 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ elixir 1.14.4-otp-25 -erlang 25.2.2 +erlang 25.3.2.21 nodejs 16.3.0 python 3.9.4 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3815c9f9a99f..000000000000 --- a/Dockerfile +++ /dev/null @@ -1,76 +0,0 @@ -# we can not use the pre-built tar because the distribution is -# platform specific, it makes sense to build it in the docker - -#### Builder -FROM hexpm/elixir:1.12.2-erlang-24.0-alpine-3.13.3 as buildcontainer - -# preparation -ARG APP_VER=0.0.1 -ENV MIX_ENV=prod -ENV NODE_ENV=production -ENV APP_VERSION=$APP_VER - -RUN mkdir /app -WORKDIR /app - -# install build dependencies -RUN apk add --no-cache git nodejs yarn python3 npm ca-certificates wget gnupg make erlang gcc libc-dev && \ - npm install npm@latest -g && \ - npm install -g webpack - -RUN wget https://s3.eu-central-1.wasabisys.com/plausible-application/geonames.csv -q - -COPY mix.exs ./ -COPY mix.lock ./ -RUN mix local.hex --force && \ - mix local.rebar --force && \ - mix deps.get --only prod && \ - mix deps.compile - -COPY assets/package.json assets/package-lock.json ./assets/ -COPY tracker/package.json tracker/package-lock.json ./tracker/ - -RUN npm install --prefix ./assets && \ - npm install --prefix ./tracker - -COPY assets ./assets -COPY tracker ./tracker -COPY config ./config -COPY priv ./priv -COPY lib ./lib - -RUN npm run deploy --prefix ./assets && \ - npm run deploy --prefix ./tracker && \ - mix phx.digest priv/static && \ - mix download_country_database && \ -# https://hexdocs.pm/sentry/Sentry.Sources.html#module-source-code-storage - mix sentry_recompile && \ - mv geonames.csv ./priv/geonames.csv - -WORKDIR /app -COPY rel rel -RUN mix release plausible - -# Main Docker Image -FROM alpine:3.13.3 -LABEL maintainer="tckb " -ENV LANG=C.UTF-8 - -RUN apk upgrade --no-cache - -RUN apk add --no-cache openssl ncurses libstdc++ libgcc - -COPY .gitlab/build-scripts/docker-entrypoint.sh /entrypoint.sh - -RUN chmod a+x /entrypoint.sh && \ - adduser -h /app -u 1000 -s /bin/sh -D plausibleuser - -COPY --from=buildcontainer /app/_build/prod/rel/plausible /app -RUN chown -R plausibleuser:plausibleuser /app -USER plausibleuser -WORKDIR /app -ENV GEONAMES_SOURCE_FILE=/app/lib/plausible-0.0.1/priv/geonames.csv -ENV LISTEN_IP=0.0.0.0 -ENTRYPOINT ["/entrypoint.sh"] -EXPOSE 8000 -CMD ["run"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 7f1098a707a1..000000000000 --- a/Makefile +++ /dev/null @@ -1,35 +0,0 @@ -install: - mix deps.get - mix ecto.create - mix ecto.migrate - npm install --prefix assets - -server: - mix phx.server - -clickhouse: - docker run --detach -p 8123:8123 --ulimit nofile=262144:262144 --volume=$$PWD/.clickhouse_db_vol:/var/lib/clickhouse --name plausible_clickhouse yandex/clickhouse-server:21.3.2.5 - -clickhouse-stop: - docker stop plausible_clickhouse && docker rm plausible_clickhouse - -postgres: - docker run --detach -e POSTGRES_PASSWORD="postgres" -p 5432:5432 --volume=plausible_db:/var/lib/postgresql/data --name plausible_db postgres:12 - -postgres-stop: - docker stop plausible_db && docker rm plausible_db - -dummy_event: - curl 'http://localhost:8000/api/event' \ - -H 'authority: localhost:8000' \ - -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36 OPR/71.0.3770.284' \ - -H 'content-type: text/plain' \ - -H 'accept: */*' \ - -H 'origin: http://dummy.site' \ - -H 'sec-fetch-site: cross-site' \ - -H 'sec-fetch-mode: cors' \ - -H 'sec-fetch-dest: empty' \ - -H 'referer: http://dummy.site' \ - -H 'accept-language: en-US,en;q=0.9' \ - --data-binary '{"n":"pageview","u":"http://dummy.site","d":"dummy.site","r":null,"w":1666}' \ - --compressed diff --git a/config/config.exs b/config/config.exs index 62c3134b800e..1fbb64454f66 100644 --- a/config/config.exs +++ b/config/config.exs @@ -19,10 +19,6 @@ config :ua_inspector, config :ref_inspector, database_path: "priv/ref_inspector" -config :plausible, - paddle_api: Plausible.Billing.PaddleApi, - google_api: Plausible.Google.Api - config :plausible, # 30 minutes session_timeout: 1000 * 60 * 30, diff --git a/config/runtime.exs b/config/runtime.exs index 84cf4884fe2f..1949ed438620 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -23,32 +23,11 @@ listen_ip = # System.get_env does not accept a non string default port = get_var_from_path_or_env(config_dir, "PORT") || 8000 - base_url = get_var_from_path_or_env(config_dir, "BASE_URL") - -if !base_url do - raise "BASE_URL configuration option is required. See https://plausible.io/docs/self-hosting-configuration#server" -end - base_url = URI.parse(base_url) -if base_url.scheme not in ["http", "https"] do - raise "BASE_URL must start with `http` or `https`. Currently configured as `#{System.get_env("BASE_URL")}`" -end - secret_key_base = get_var_from_path_or_env(config_dir, "SECRET_KEY_BASE", nil) -case secret_key_base do - nil -> - raise "SECRET_KEY_BASE configuration option is required. See https://plausible.io/docs/self-hosting-configuration#server" - - key when byte_size(key) < 32 -> - raise "SECRET_KEY_BASE must be at least 32 bytes long. See https://plausible.io/docs/self-hosting-configuration#server" - - _ -> - nil -end - db_url = get_var_from_path_or_env( config_dir, @@ -58,23 +37,7 @@ db_url = db_socket_dir = get_var_from_path_or_env(config_dir, "DATABASE_SOCKET_DIR") -admin_user = get_var_from_path_or_env(config_dir, "ADMIN_USER_NAME") -admin_email = get_var_from_path_or_env(config_dir, "ADMIN_USER_EMAIL") - -super_admin_user_ids = - get_var_from_path_or_env(config_dir, "ADMIN_USER_IDS", "") - |> String.split(",") - |> Enum.map(fn id -> Integer.parse(id) end) - |> Enum.map(fn - {int, ""} -> int - _ -> nil - end) - |> Enum.filter(& &1) - -admin_pwd = get_var_from_path_or_env(config_dir, "ADMIN_USER_PWD") env = get_var_from_path_or_env(config_dir, "ENVIRONMENT", "prod") -mailer_adapter = get_var_from_path_or_env(config_dir, "MAILER_ADAPTER", "Bamboo.SMTPAdapter") -mailer_email = get_var_from_path_or_env(config_dir, "MAILER_EMAIL", "hello@plausible.local") app_version = get_var_from_path_or_env(config_dir, "APP_VERSION", "0.0.1") ch_db_url = @@ -84,32 +47,11 @@ ch_db_url = "http://plausible_events_db:8123/plausible_events_db" ) -{ch_flush_interval_ms, ""} = - config_dir - |> get_var_from_path_or_env("CLICKHOUSE_FLUSH_INTERVAL_MS", "5000") - |> Integer.parse() -{ch_max_buffer_size, ""} = - config_dir - |> get_var_from_path_or_env("CLICKHOUSE_MAX_BUFFER_SIZE", "10000") - |> Integer.parse() ### Mandatory params End sentry_dsn = get_var_from_path_or_env(config_dir, "SENTRY_DSN") -honeycomb_api_key = get_var_from_path_or_env(config_dir, "HONEYCOMB_API_KEY") -honeycomb_dataset = get_var_from_path_or_env(config_dir, "HONEYCOMB_DATASET") -paddle_auth_code = get_var_from_path_or_env(config_dir, "PADDLE_VENDOR_AUTH_CODE") -paddle_vendor_id = get_var_from_path_or_env(config_dir, "PADDLE_VENDOR_ID") -google_cid = get_var_from_path_or_env(config_dir, "GOOGLE_CLIENT_ID") -google_secret = get_var_from_path_or_env(config_dir, "GOOGLE_CLIENT_SECRET") -slack_hook_url = get_var_from_path_or_env(config_dir, "SLACK_WEBHOOK") -postmark_api_key = get_var_from_path_or_env(config_dir, "POSTMARK_API_KEY") - -cron_enabled = - config_dir - |> get_var_from_path_or_env("CRON_ENABLED", "false") - |> String.to_existing_atom() geolite2_country_db = get_var_from_path_or_env( @@ -121,59 +63,6 @@ geolite2_country_db = ip_geolocation_db = get_var_from_path_or_env(config_dir, "IP_GEOLOCATION_DB", geolite2_country_db) geonames_source_file = get_var_from_path_or_env(config_dir, "GEONAMES_SOURCE_FILE") -disable_auth = - config_dir - |> get_var_from_path_or_env("DISABLE_AUTH", "false") - |> String.to_existing_atom() - -enable_email_verification = - config_dir - |> get_var_from_path_or_env("ENABLE_EMAIL_VERIFICATION", "false") - |> String.to_existing_atom() - -disable_registration = - config_dir - |> get_var_from_path_or_env("DISABLE_REGISTRATION", "false") - |> String.to_existing_atom() - -hcaptcha_sitekey = get_var_from_path_or_env(config_dir, "HCAPTCHA_SITEKEY") -hcaptcha_secret = get_var_from_path_or_env(config_dir, "HCAPTCHA_SECRET") - -log_level = - config_dir - |> get_var_from_path_or_env("LOG_LEVEL", "warn") - |> String.to_existing_atom() - -domain_blacklist = - config_dir - |> get_var_from_path_or_env("DOMAIN_BLACKLIST", "") - |> String.split(",") - -is_selfhost = - config_dir - |> get_var_from_path_or_env("SELFHOST", "true") - |> String.to_existing_atom() - -custom_script_name = - config_dir - |> get_var_from_path_or_env("CUSTOM_SCRIPT_NAME", "script") - -{site_limit, ""} = - config_dir - |> get_var_from_path_or_env("SITE_LIMIT", "50") - |> Integer.parse() - -site_limit_exempt = - config_dir - |> get_var_from_path_or_env("SITE_LIMIT_EXEMPT", "") - |> String.split(",") - |> Enum.map(&String.trim/1) - -disable_cron = - config_dir - |> get_var_from_path_or_env("DISABLE_CRON", "false") - |> String.to_existing_atom() - {user_agent_cache_limit, ""} = config_dir |> get_var_from_path_or_env("USER_AGENT_CACHE_LIMIT", "1000") @@ -185,23 +74,8 @@ user_agent_cache_stats = |> String.to_existing_atom() config :plausible, - admin_user: admin_user, - admin_email: admin_email, - admin_pwd: admin_pwd, environment: env, - system_environment: env, - mailer_email: mailer_email, - super_admin_user_ids: super_admin_user_ids, - site_limit: site_limit, - site_limit_exempt: site_limit_exempt, - is_selfhost: is_selfhost, - custom_script_name: custom_script_name, - domain_blacklist: domain_blacklist - -config :plausible, :selfhost, - disable_authentication: disable_auth, - enable_email_verification: enable_email_verification, - disable_registration: if(!disable_auth, do: disable_registration, else: false) + system_environment: env config :plausible, PlausibleWeb.Endpoint, url: [scheme: base_url.scheme, host: base_url.host, path: base_url.path, port: base_url.port], @@ -220,126 +94,31 @@ else database: get_var_from_path_or_env(config_dir, "DATABASE_NAME", "plausible") end -config :sentry, - dsn: sentry_dsn, - environment_name: env, - included_environments: ["prod", "staging"], - release: app_version, - tags: %{app_version: app_version}, - enable_source_code_context: true, - root_source_code_path: [File.cwd!()] - -config :plausible, :paddle, - vendor_auth_code: paddle_auth_code, - vendor_id: paddle_vendor_id - -config :plausible, :google, - client_id: google_cid, - client_secret: google_secret - -config :plausible, :slack, webhook: slack_hook_url - config :plausible, Plausible.ClickhouseRepo, loggers: [Ecto.LogEntry], queue_target: 500, queue_interval: 2000, url: ch_db_url, - flush_interval_ms: ch_flush_interval_ms, - max_buffer_size: ch_max_buffer_size - -case mailer_adapter do - "Bamboo.PostmarkAdapter" -> - config :plausible, Plausible.Mailer, - adapter: :"Elixir.#{mailer_adapter}", - request_options: [recv_timeout: 10_000], - api_key: get_var_from_path_or_env(config_dir, "POSTMARK_API_KEY") - - "Bamboo.SMTPAdapter" -> - config :plausible, Plausible.Mailer, - adapter: :"Elixir.#{mailer_adapter}", - server: get_var_from_path_or_env(config_dir, "SMTP_HOST_ADDR", "mail"), - hostname: base_url.host, - port: get_var_from_path_or_env(config_dir, "SMTP_HOST_PORT", "25"), - username: get_var_from_path_or_env(config_dir, "SMTP_USER_NAME"), - password: get_var_from_path_or_env(config_dir, "SMTP_USER_PWD"), - tls: :if_available, - allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"], - ssl: get_var_from_path_or_env(config_dir, "SMTP_HOST_SSL_ENABLED") || false, - retries: get_var_from_path_or_env(config_dir, "SMTP_RETRIES") || 2, - no_mx_lookups: get_var_from_path_or_env(config_dir, "SMTP_MX_LOOKUPS_ENABLED") || true - - "Bamboo.LocalAdapter" -> - config :plausible, Plausible.Mailer, adapter: Bamboo.LocalAdapter - - "Bamboo.TestAdapter" -> - config :plausible, Plausible.Mailer, adapter: Bamboo.TestAdapter - - _ -> - raise "Unknown mailer_adapter; expected SMTPAdapter or PostmarkAdapter" -end - -config :plausible, PlausibleWeb.Firewall, - blocklist: - get_var_from_path_or_env(config_dir, "IP_BLOCKLIST", "") - |> String.split(",") - |> Enum.map(&String.trim/1) + flush_interval_ms: 5000, + max_buffer_size: 10000 cond do - config_env() == :prod && !disable_cron -> + config_env() == :prod -> base_cron = [ # Daily at midnight - {"0 0 * * *", Plausible.Workers.RotateSalts}, - #  hourly - {"0 * * * *", Plausible.Workers.ScheduleEmailReports}, - # hourly - {"0 * * * *", Plausible.Workers.SendSiteSetupEmails}, - # Daily at midday - {"0 12 * * *", Plausible.Workers.SendCheckStatsEmails}, - # Every 15 minutes - {"*/15 * * * *", Plausible.Workers.SpikeNotifier}, - # Every day at midnight - {"0 0 * * *", Plausible.Workers.CleanEmailVerificationCodes}, - # Every day at 1am - {"0 1 * * *", Plausible.Workers.CleanInvitations} - ] - - extra_cron = [ - # Daily at midday - {"0 12 * * *", Plausible.Workers.SendTrialNotifications}, - # Daily at 14 - {"0 14 * * *", Plausible.Workers.CheckUsage}, - # Daily at 15 - {"0 15 * * *", Plausible.Workers.NotifyAnnualRenewal}, - # Every midnight - {"0 0 * * *", Plausible.Workers.LockSites} + {"0 0 * * *", Plausible.Workers.RotateSalts} ] base_queues = [ - rotate_salts: 1, - schedule_email_reports: 1, - send_email_reports: 1, - spike_notifications: 1, - check_stats_emails: 1, - site_setup_emails: 1, - clean_email_verification_codes: 1, - clean_invitations: 1, - google_analytics_imports: 1 - ] - - extra_queues = [ - provision_ssl_certificates: 1, - trial_notification_emails: 1, - check_usage: 1, - notify_annual_renewal: 1, - lock_sites: 1 + rotate_salts: 1 ] # Keep 30 days history config :plausible, Oban, repo: Plausible.Repo, plugins: [{Oban.Plugins.Pruner, max_age: 2_592_000}], - queues: if(is_selfhost, do: base_queues, else: base_queues ++ extra_queues), - crontab: if(is_selfhost, do: base_cron, else: base_cron ++ extra_cron) + queues: base_queues, + crontab: base_cron config_env() == :test -> config :plausible, Oban, @@ -353,50 +132,10 @@ cond do plugins: [] end -config :plausible, :hcaptcha, - sitekey: hcaptcha_sitekey, - secret: hcaptcha_secret - -config :ref_inspector, - init: {Plausible.Release, :configure_ref_inspector} - -config :ua_inspector, - init: {Plausible.Release, :configure_ua_inspector} - config :plausible, :user_agent_cache, limit: user_agent_cache_limit, stats: user_agent_cache_stats -config :hammer, - backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} - -config :kaffy, - otp_app: :plausible, - ecto_repo: Plausible.Repo, - router: PlausibleWeb.Router, - admin_title: "Plausible Admin", - resources: [ - auth: [ - resources: [ - user: [schema: Plausible.Auth.User, admin: Plausible.Auth.UserAdmin], - api_key: [schema: Plausible.Auth.ApiKey, admin: Plausible.Auth.ApiKeyAdmin] - ] - ], - sites: [ - resources: [ - site: [schema: Plausible.Site, admin: Plausible.SiteAdmin] - ] - ], - billing: [ - resources: [ - enterprise_plan: [ - schema: Plausible.Billing.EnterprisePlan, - admin: Plausible.Billing.EnterprisePlanAdmin - ] - ] - ] - ] - if config_env() != :test do config :geolix, databases: [ @@ -414,7 +153,7 @@ if geonames_source_file do end config :logger, - level: log_level, + level: :info, backends: [:console] config :logger, Sentry.LoggerBackend, @@ -422,21 +161,6 @@ config :logger, Sentry.LoggerBackend, level: :error, excluded_domains: [] -if honeycomb_api_key && honeycomb_dataset do - config :opentelemetry, :processors, - otel_batch_processor: %{ - exporter: - {:opentelemetry_exporter, - %{ - endpoints: ['https://api.honeycomb.io:443'], - headers: [ - {"x-honeycomb-team", honeycomb_api_key}, - {"x-honeycomb-dataset", honeycomb_dataset} - ] - }} - } -end - config :tzdata, :data_dir, get_var_from_path_or_env(config_dir, "STORAGE_DIR", Application.app_dir(:tzdata, "priv")) diff --git a/config/test.exs b/config/test.exs index 10d7164df351..0e5289e3f19a 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,14 +12,6 @@ config :plausible, Plausible.ClickhouseRepo, loggers: [Ecto.LogEntry], pool_size: 5 -config :plausible, Plausible.Mailer, adapter: Bamboo.TestAdapter - -config :plausible, - paddle_api: Plausible.PaddleApi.Mock, - google_api: Plausible.Google.Api.Mock - -config :bamboo, :refute_timeout, 10 - config :geolix, databases: [ %{ diff --git a/docker_push b/docker_push deleted file mode 100755 index 2d01a2967737..000000000000 --- a/docker_push +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin -docker build -t plausible/analytics:dev . -docker push plausible/analytics:dev diff --git a/lib/mix/tasks/analyze_plans.ex b/lib/mix/tasks/analyze_plans.ex deleted file mode 100644 index 748709607829..000000000000 --- a/lib/mix/tasks/analyze_plans.ex +++ /dev/null @@ -1,60 +0,0 @@ -defmodule Mix.Tasks.AnalyzePlans do - use Mix.Task - use Plausible.Repo - - # coveralls-ignore-start - - def run(_) do - Mix.Task.run("app.start") - - res = - Repo.all( - from s in Plausible.Billing.Subscription, - where: s.status == "active", - group_by: s.paddle_plan_id, - select: {s.paddle_plan_id, count(s)} - ) - - res = - Enum.map(res, fn {plan_id, count} -> - plan = Plausible.Billing.Plans.for_product_id(plan_id) - - if plan do - is_monthly = plan_id == plan[:monthly_product_id] - - monthly_revenue = - if is_monthly do - price(plan[:monthly_cost]) - else - price(plan[:yearly_cost]) / 12 - end - - {PlausibleWeb.StatsView.large_number_format(plan[:limit]), monthly_revenue, count} - end - end) - |> Enum.filter(& &1) - - res = - Enum.reduce(res, %{}, fn {limit, revenue, count}, acc -> - total_revenue = revenue * count - - Map.update(acc, limit, {total_revenue, count}, fn {ex_rev, ex_count} -> - {ex_rev + total_revenue, ex_count + count} - end) - end) - - total_revenue = round(Enum.reduce(res, 0, fn {_, {revenue, _}}, sum -> sum + revenue end)) - - for {limit, {rev, _}} <- res do - percentage = round(rev / total_revenue * 100) - - IO.puts( - "The #{limit} plan makes up #{percentage}% of total revenue ($#{round(rev)} / $#{total_revenue})" - ) - end - end - - defp price("$" <> nr) do - String.to_integer(nr) - end -end diff --git a/lib/mix/tasks/create_free_subscription.ex b/lib/mix/tasks/create_free_subscription.ex deleted file mode 100644 index be0c7a993bff..000000000000 --- a/lib/mix/tasks/create_free_subscription.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Mix.Tasks.CreateFreeSubscription do - use Mix.Task - use Plausible.Repo - require Logger - alias Plausible.Billing.Subscription - - # coveralls-ignore-start - - def run([user_id]) do - Application.ensure_all_started(:plausible) - execute(user_id) - end - - def run(_), do: IO.puts("Usage - mix create_free_subscription ") - - def execute(user_id) do - user = Repo.get(Plausible.Auth.User, user_id) - - Subscription.free(%{user_id: user_id}) - |> Repo.insert!() - - IO.puts("Created a free subscription for user: #{user.name}") - end -end diff --git a/lib/mix/tasks/download_country_database.ex b/lib/mix/tasks/download_country_database.ex deleted file mode 100644 index e566b0448885..000000000000 --- a/lib/mix/tasks/download_country_database.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Mix.Tasks.DownloadCountryDatabase do - use Mix.Task - use Plausible.Repo - require Logger - - # coveralls-ignore-start - - def run(_) do - Application.ensure_all_started(:httpoison) - Application.ensure_all_started(:timex) - this_month = Timex.today() - last_month = Timex.shift(this_month, months: -1) - this_month = this_month |> Date.to_iso8601() |> binary_part(0, 7) - last_month = last_month |> Date.to_iso8601() |> binary_part(0, 7) - this_month_url = "https://download.db-ip.com/free/dbip-country-lite-#{this_month}.mmdb.gz" - last_month_url = "https://download.db-ip.com/free/dbip-country-lite-#{last_month}.mmdb.gz" - Logger.info("Downloading #{this_month_url}") - res = HTTPoison.get!(this_month_url) - - res = - case res.status_code do - 404 -> - Logger.info("Got 404 for #{this_month_url}, trying #{last_month_url}") - HTTPoison.get!(last_month_url) - - _ -> - res - end - - if res.status_code == 200 do - File.mkdir(geodb_dir_path()) - File.write!(geodb_file_path(), res.body) - Logger.info("Downloaded and saved the database successfully") - else - Logger.error("Unable to download and save the database. Response: #{inspect(res)}") - end - end - - defp geodb_dir_path do - geodb_file_path() - |> String.split("/") - |> List.pop_at(-1) - |> then(fn {_, elements} -> Enum.join(elements, "/") end) - end - - defp geodb_file_path do - default_file_path = "geodb/dbip-country.mmdb" - - with [%{source: path} | _] <- Application.get_env(:geolix, :databases) do - path - else - _ -> default_file_path - end - end -end diff --git a/lib/mix/tasks/generate_referrer_favicons.ex b/lib/mix/tasks/generate_referrer_favicons.ex deleted file mode 100644 index ac1376cc6605..000000000000 --- a/lib/mix/tasks/generate_referrer_favicons.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Mix.Tasks.GenerateReferrerFavicons do - use Mix.Task - use Plausible.Repo - require Logger - - @dialyzer {:nowarn_function, run: 1} - # coveralls-ignore-start - - def run(_) do - entries = - :yamerl_constr.file(Application.app_dir(:plausible, "priv/ref_inspector/referers.yml")) - |> List.first() - |> Enum.map(fn {_key, val} -> val end) - |> Enum.concat() - - domains = - Enum.reduce(entries, %{}, fn {key, val}, domains -> - domain = - Enum.into(val, %{})['domains'] - |> List.first() - - Map.put_new(domains, List.to_string(key), List.to_string(domain)) - end) - - File.write!( - Application.app_dir(:plausible, "priv/referer_favicon_domains.json"), - Jason.encode!(domains) - ) - end -end diff --git a/lib/plausible/auth/api_key.ex b/lib/plausible/auth/api_key.ex deleted file mode 100644 index edff9ad5a0cd..000000000000 --- a/lib/plausible/auth/api_key.ex +++ /dev/null @@ -1,65 +0,0 @@ -defmodule Plausible.Auth.ApiKey do - use Ecto.Schema - import Ecto.Changeset - - @required [:user_id, :name] - @optional [:key, :scopes, :hourly_request_limit] - schema "api_keys" do - field :name, :string - field :scopes, {:array, :string}, default: ["stats:read:*"] - field :hourly_request_limit, :integer, default: 600 - - field :key, :string, virtual: true - field :key_hash, :string - field :key_prefix, :string - - belongs_to :user, Plausible.Auth.User - - timestamps() - end - - def changeset(schema, attrs \\ %{}) do - schema - |> cast(attrs, @required ++ @optional) - |> validate_required(@required) - |> generate_key() - |> process_key() - end - - def update(schema, attrs \\ %{}) do - schema - |> cast(attrs, [:name, :user_id, :scopes, :hourly_request_limit]) - |> validate_required([:user_id, :name]) - end - - def do_hash(key) do - :crypto.hash(:sha256, [secret_key_base(), key]) - |> Base.encode16() - |> String.downcase() - end - - def process_key(%{errors: [], changes: changes} = changeset) do - prefix = binary_part(changes[:key], 0, 6) - - change(changeset, - key_hash: do_hash(changes[:key]), - key_prefix: prefix - ) - end - - def process_key(changeset), do: changeset - - defp generate_key(changeset) do - if !changeset.changes[:key] do - key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) - change(changeset, key: key) - else - changeset - end - end - - defp secret_key_base() do - Application.get_env(:plausible, PlausibleWeb.Endpoint) - |> Keyword.fetch!(:secret_key_base) - end -end diff --git a/lib/plausible/auth/api_key_admin.ex b/lib/plausible/auth/api_key_admin.ex deleted file mode 100644 index aa9dc7be3f25..000000000000 --- a/lib/plausible/auth/api_key_admin.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Plausible.Auth.ApiKeyAdmin do - use Plausible.Repo - - def search_fields(_schema) do - [ - :name, - user: [:name, :email] - ] - end - - def custom_index_query(_conn, _schema, query) do - from(r in query, preload: [:user]) - end - - def create_changeset(schema, attrs) do - scopes = [attrs["scope"]] - Plausible.Auth.ApiKey.changeset(schema, Map.merge(%{"scopes" => scopes}, attrs)) - end - - def update_changeset(schema, attrs) do - Plausible.Auth.ApiKey.update(schema, attrs) - end - - @plaintext_key_help """ - The value of the API key is sensitive data like a password. Once created, the value of they will never be revealed again. Make sure to copy/paste this into a secure place before hitting 'save'. When sending the key to a customer, use a secure E2EE system that destructs the message after a certain period like https://bitwarden.com/products/send - """ - def form_fields(_) do - [ - name: nil, - key: %{create: :readonly, update: :hidden, help_text: @plaintext_key_help}, - key_prefix: %{create: :hidden, update: :readonly}, - hourly_request_limit: %{default: 1000}, - scope: %{choices: [{"Stats API", ["stats:read:*"]}, {"Sites API", ["sites:provision:*"]}]}, - user_id: nil - ] - end - - def index(_) do - [ - key_prefix: nil, - name: nil, - scopes: nil, - owner: %{value: & &1.user.email} - ] - end -end diff --git a/lib/plausible/auth/auth.ex b/lib/plausible/auth/auth.ex deleted file mode 100644 index cb12997ca2ba..000000000000 --- a/lib/plausible/auth/auth.ex +++ /dev/null @@ -1,101 +0,0 @@ -defmodule Plausible.Auth do - use Plausible.Repo - alias Plausible.Auth - - def issue_email_verification(user) do - Repo.update_all(from(c in "email_verification_codes", where: c.user_id == ^user.id), - set: [user_id: nil] - ) - - code = - Repo.one( - from(c in "email_verification_codes", where: is_nil(c.user_id), select: c.code, limit: 1) - ) - - Repo.update_all(from(c in "email_verification_codes", where: c.code == ^code), - set: [user_id: user.id, issued_at: Timex.now()] - ) - - code - end - - defp is_expired?(activation_code_issued) do - Timex.before?(activation_code_issued, Timex.shift(Timex.now(), hours: -4)) - end - - def verify_email(user, code) do - found_code = - Repo.one( - from c in "email_verification_codes", - where: c.user_id == ^user.id, - where: c.code == ^code, - select: %{code: c.code, issued: c.issued_at} - ) - - cond do - is_nil(found_code) -> - {:error, :incorrect} - - is_expired?(found_code[:issued]) -> - {:error, :expired} - - true -> - {:ok, _} = - Ecto.Multi.new() - |> Ecto.Multi.update( - :user, - Plausible.Auth.User.changeset(user, %{email_verified: true}) - ) - |> Ecto.Multi.update_all( - :codes, - from(c in "email_verification_codes", where: c.user_id == ^user.id), - set: [user_id: nil] - ) - |> Repo.transaction() - - :ok - end - end - - def create_user(name, email, pwd) do - Auth.User.new(%{name: name, email: email, password: pwd, password_confirmation: pwd}) - |> Repo.insert() - end - - def find_user_by(opts) do - Repo.get_by(Auth.User, opts) - end - - def has_active_sites?(user, roles \\ [:owner, :admin, :viewer]) do - sites = - Repo.all( - from u in Plausible.Auth.User, - where: u.id == ^user.id, - join: sm in Plausible.Site.Membership, - on: sm.user_id == u.id, - where: sm.role in ^roles, - join: s in Plausible.Site, - on: s.id == sm.site_id, - select: s - ) - - Enum.any?(sites, &Plausible.Sites.has_stats?/1) - end - - def user_owns_sites?(user) do - Repo.exists?( - from(s in Plausible.Site, - join: sm in Plausible.Site.Membership, - on: sm.site_id == s.id, - where: sm.user_id == ^user.id, - where: sm.role == :owner - ) - ) - end - - def is_super_admin?(nil), do: false - - def is_super_admin?(user_id) do - user_id in Application.get_env(:plausible, :super_admin_user_ids) - end -end diff --git a/lib/plausible/auth/invitation.ex b/lib/plausible/auth/invitation.ex deleted file mode 100644 index 189255732da9..000000000000 --- a/lib/plausible/auth/invitation.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Plausible.Auth.Invitation do - use Ecto.Schema - import Ecto.Changeset - - @derive {Jason.Encoder, only: [:invitation_id, :role, :site]} - @required [:email, :role, :site_id, :inviter_id] - schema "invitations" do - field :invitation_id, :string - field :email, :string - field :role, Ecto.Enum, values: [:owner, :admin, :viewer] - - belongs_to :inviter, Plausible.Auth.User - belongs_to :site, Plausible.Site - - timestamps() - end - - def new(attrs \\ %{}) do - %__MODULE__{invitation_id: Nanoid.generate()} - |> cast(attrs, @required) - |> validate_required(@required) - end -end diff --git a/lib/plausible/auth/token.ex b/lib/plausible/auth/token.ex deleted file mode 100644 index 01b818e4626d..000000000000 --- a/lib/plausible/auth/token.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Plausible.Auth.Token do - @one_hour_in_seconds 30 * 60 - - def sign_shared_link(slug) do - Phoenix.Token.sign(PlausibleWeb.Endpoint, "shared-link", %{slug: slug}) - end - - def verify_shared_link(token) do - Phoenix.Token.verify(PlausibleWeb.Endpoint, "shared-link", token, - max_age: @one_hour_in_seconds * 24 - ) - end - - def sign_password_reset(email) do - Phoenix.Token.sign(PlausibleWeb.Endpoint, "password-reset", %{email: email}) - end - - def verify_password_reset(token) do - Phoenix.Token.verify(PlausibleWeb.Endpoint, "password-reset", token, - max_age: @one_hour_in_seconds - ) - end -end diff --git a/lib/plausible/auth/user.ex b/lib/plausible/auth/user.ex index 52a21dc5a100..d3518dafe056 100644 --- a/lib/plausible/auth/user.ex +++ b/lib/plausible/auth/user.ex @@ -1,24 +1,6 @@ -defimpl Bamboo.Formatter, for: Plausible.Auth.User do - def format_email_address(user, _opts) do - {user.name, user.email} - end -end - -defmodule Plausible.Auth.GracePeriod do - use Ecto.Schema - - embedded_schema do - field :end_date, :date - field :allowance_required, :integer - field :is_over, :boolean - end -end - defmodule Plausible.Auth.User do use Ecto.Schema - import Ecto.Changeset - @required [:email, :name, :password, :password_confirmation] schema "users" do field :email, :string field :password_hash @@ -29,98 +11,10 @@ defmodule Plausible.Auth.User do field :trial_expiry_date, :date field :theme, :string field :email_verified, :boolean - embeds_one :grace_period, Plausible.Auth.GracePeriod, on_replace: :update has_many :site_memberships, Plausible.Site.Membership has_many :sites, through: [:site_memberships, :site] - has_many :api_keys, Plausible.Auth.ApiKey - has_one :google_auth, Plausible.Site.GoogleAuth - has_one :subscription, Plausible.Billing.Subscription - has_one :enterprise_plan, Plausible.Billing.EnterprisePlan timestamps() end - - def new(attrs \\ %{}) do - %Plausible.Auth.User{} - |> cast(attrs, @required) - |> validate_required(@required) - |> validate_length(:password, min: 6, message: "has to be at least 6 characters") - |> validate_length(:password, max: 64, message: "cannot be longer than 64 characters") - |> validate_confirmation(:password) - |> hash_password() - |> start_trial - |> set_email_verified - |> unique_constraint(:email) - end - - def changeset(user, attrs \\ %{}) do - user - |> cast(attrs, [:email, :name, :email_verified, :theme, :trial_expiry_date]) - |> validate_required([:email, :name, :email_verified]) - |> unique_constraint(:email) - end - - def set_password(user, password) do - hash = Plausible.Auth.Password.hash(password) - - user - |> cast(%{password: password}, [:password]) - |> validate_required(:password) - |> validate_length(:password, min: 6, message: "has to be at least 6 characters") - |> cast(%{password_hash: hash}, [:password_hash]) - end - - def hash_password(%{errors: [], changes: changes} = changeset) do - hash = Plausible.Auth.Password.hash(changes[:password]) - change(changeset, password_hash: hash) - end - - def hash_password(changeset), do: changeset - - def remove_trial_expiry(user) do - change(user, trial_expiry_date: nil) - end - - def start_trial(user) do - change(user, trial_expiry_date: trial_expiry()) - end - - def end_trial(user) do - change(user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - end - - def start_grace_period(user, allowance_required) do - grace_period = %Plausible.Auth.GracePeriod{ - end_date: Timex.today() |> Timex.shift(days: 7), - allowance_required: allowance_required, - is_over: false - } - - change(user, grace_period: grace_period) - end - - def end_grace_period(user) do - change(user, grace_period: %{is_over: true}) - end - - def remove_grace_period(user) do - change(user, grace_period: nil) - end - - defp trial_expiry() do - if Application.get_env(:plausible, :is_selfhost) do - Timex.today() |> Timex.shift(years: 100) - else - Timex.today() |> Timex.shift(days: 30) - end - end - - defp set_email_verified(user) do - if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :enable_email_verification) do - change(user, email_verified: false) - else - change(user, email_verified: true) - end - end end diff --git a/lib/plausible/auth/user_admin.ex b/lib/plausible/auth/user_admin.ex deleted file mode 100644 index 0e5ac45e9402..000000000000 --- a/lib/plausible/auth/user_admin.ex +++ /dev/null @@ -1,85 +0,0 @@ -defmodule Plausible.Auth.UserAdmin do - use Plausible.Repo - - def custom_index_query(_conn, _schema, query) do - subscripton_q = from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at]) - from(r in query, preload: [subscription: ^subscripton_q]) - end - - def form_fields(_) do - [ - name: nil, - email: nil, - trial_expiry_date: nil - ] - end - - def index(_) do - [ - name: nil, - email: nil, - inserted_at: %{name: "Created at", value: &format_date(&1.inserted_at)}, - trial_expiry_date: %{name: "Trial expiry", value: &format_date(&1.trial_expiry_date)}, - subscription_tier: %{value: &subscription_tier/1}, - subscription_status: %{value: &subscription_status/1}, - grace_period: %{value: &grace_period_status/1} - ] - end - - def resource_actions(_) do - [ - remove_grace_period: %{ - name: "Remove grace period", - action: fn _, user -> remove_grace_period(user) end - } - ] - end - - defp remove_grace_period(user) do - if user.grace_period do - Plausible.Auth.User.remove_grace_period(user) |> Repo.update() - else - {:error, user, "No active grace period on this user"} - end - end - - defp grace_period_status(%{grace_period: nil}), do: "--" - - defp grace_period_status(user) do - if user.grace_period.is_over do - "ended" - else - days_left = Timex.diff(user.grace_period.end_date, Timex.now(), :days) - "#{days_left} days left" - end - end - - defp subscription_tier(user) do - if user.subscription && user.subscription.status == "active" do - quota = PlausibleWeb.AuthView.subscription_quota(user.subscription) - interval = PlausibleWeb.AuthView.subscription_interval(user.subscription) - "#{quota} (#{interval})" - else - "--" - end - end - - defp subscription_status(user) do - cond do - user.subscription -> - PlausibleWeb.AuthView.present_subscription_status(user.subscription.status) - - Plausible.Billing.on_trial?(user) -> - "On trial" - - true -> - "Trial expired" - end - end - - defp format_date(nil), do: "--" - - defp format_date(date) do - Timex.format!(date, "{Mshort} {D}, {YYYY}") - end -end diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex deleted file mode 100644 index 6d43d703272a..000000000000 --- a/lib/plausible/billing/billing.ex +++ /dev/null @@ -1,284 +0,0 @@ -defmodule Plausible.Billing do - use Plausible.Repo - alias Plausible.Billing.{Subscription, PaddleApi} - - def active_subscription_for(user_id) do - Repo.get_by(Subscription, user_id: user_id, status: "active") - end - - def subscription_created(params) do - params = - if present?(params["passthrough"]) do - params - else - user = Repo.get_by(Plausible.Auth.User, email: params["email"]) - Map.put(params, "passthrough", user && user.id) - end - - changeset = Subscription.changeset(%Subscription{}, format_subscription(params)) - - Repo.insert(changeset) |> after_subscription_update - end - - def subscription_updated(params) do - subscription = Repo.get_by!(Subscription, paddle_subscription_id: params["subscription_id"]) - changeset = Subscription.changeset(subscription, format_subscription(params)) - - Repo.update(changeset) |> after_subscription_update - end - - defp after_subscription_update({:ok, subscription}) do - user = - Repo.get(Plausible.Auth.User, subscription.user_id) - |> Map.put(:subscription, subscription) - - {:ok, user} - |> maybe_remove_grace_period - |> check_lock_status - |> maybe_adjust_api_key_limits - end - - defp after_subscription_update(err), do: err - - def subscription_cancelled(params) do - subscription = - Repo.get_by(Subscription, paddle_subscription_id: params["subscription_id"]) - |> Repo.preload(:user) - - if subscription do - changeset = - Subscription.changeset(subscription, %{ - status: params["status"] - }) - - case Repo.update(changeset) do - {:ok, updated} -> - PlausibleWeb.Email.cancellation_email(subscription.user) - |> Plausible.Mailer.send_email_safe() - - {:ok, updated} - - err -> - err - end - else - {:ok, nil} - end - end - - def subscription_payment_succeeded(params) do - subscription = Repo.get_by(Subscription, paddle_subscription_id: params["subscription_id"]) - - if subscription do - {:ok, api_subscription} = paddle_api().get_subscription(subscription.paddle_subscription_id) - - amount = - :erlang.float_to_binary(api_subscription["next_payment"]["amount"] / 1, decimals: 2) - - changeset = - Subscription.changeset(subscription, %{ - next_bill_amount: amount, - next_bill_date: api_subscription["next_payment"]["date"], - last_bill_date: api_subscription["last_payment"]["date"] - }) - - Repo.update(changeset) - else - {:ok, nil} - end - end - - def change_plan(user, new_plan_id) do - subscription = active_subscription_for(user.id) - - res = - paddle_api().update_subscription(subscription.paddle_subscription_id, %{ - plan_id: new_plan_id - }) - - case res do - {:ok, response} -> - amount = :erlang.float_to_binary(response["next_payment"]["amount"] / 1, decimals: 2) - - Subscription.changeset(subscription, %{ - paddle_plan_id: Integer.to_string(response["plan_id"]), - next_bill_amount: amount, - next_bill_date: response["next_payment"]["date"] - }) - |> Repo.update() - - e -> - e - end - end - - def change_plan_preview(subscription, new_plan_id) do - PaddleApi.update_subscription_preview(subscription.paddle_subscription_id, new_plan_id) - end - - def needs_to_upgrade?(%Plausible.Auth.User{trial_expiry_date: nil}), do: {true, :no_trial} - - def needs_to_upgrade?(user) do - trial_is_over = Timex.before?(user.trial_expiry_date, Timex.today()) - subscription_active = subscription_is_active?(user.subscription) - - grace_period_ended = - user.grace_period && Timex.before?(user.grace_period.end_date, Timex.today()) - - cond do - trial_is_over && !subscription_active -> {true, :no_active_subscription} - grace_period_ended -> {true, :grace_period_ended} - true -> false - end - end - - defp subscription_is_active?(%Subscription{status: "active"}), do: true - defp subscription_is_active?(%Subscription{status: "past_due"}), do: true - - defp subscription_is_active?(%Subscription{status: "deleted"} = subscription) do - subscription.next_bill_date && !Timex.before?(subscription.next_bill_date, Timex.today()) - end - - defp subscription_is_active?(%Subscription{}), do: false - defp subscription_is_active?(nil), do: false - - def on_trial?(%Plausible.Auth.User{trial_expiry_date: nil}), do: false - - def on_trial?(user) do - !subscription_is_active?(user.subscription) && trial_days_left(user) >= 0 - end - - def trial_days_left(user) do - Timex.diff(user.trial_expiry_date, Timex.today(), :days) - end - - def usage(user) do - {pageviews, custom_events} = usage_breakdown(user) - pageviews + custom_events - end - - def last_two_billing_months_usage(user, today \\ Timex.today()) do - {first, second} = last_two_billing_cycles(user, today) - sites = Plausible.Sites.owned_by(user) - - usage_for_sites = fn sites, date_range -> - domains = Enum.map(sites, & &1.domain) - {pageviews, custom_events} = Plausible.Stats.Clickhouse.usage_breakdown(domains, date_range) - pageviews + custom_events - end - - { - usage_for_sites.(sites, first), - usage_for_sites.(sites, second) - } - end - - def last_two_billing_cycles(user, today \\ Timex.today()) do - last_bill_date = user.subscription.last_bill_date - - normalized_last_bill_date = - Timex.shift(last_bill_date, - months: Timex.diff(today, last_bill_date, :months) - ) - - { - Date.range( - Timex.shift(normalized_last_bill_date, months: -2), - Timex.shift(normalized_last_bill_date, days: -1, months: -1) - ), - Date.range( - Timex.shift(normalized_last_bill_date, months: -1), - Timex.shift(normalized_last_bill_date, days: -1) - ) - } - end - - def usage_breakdown(user) do - domains = Plausible.Sites.owned_by(user) |> Enum.map(& &1.domain) - Plausible.Stats.Clickhouse.usage_breakdown(domains) - end - - @doc """ - Returns the number of sites that an account is allowed to have. Accounts for - grandfathering old accounts to unlimited websites and ignores site limit on self-hosted - installations. - """ - @limit_accounts_since ~D[2021-05-05] - def sites_limit(user) do - user = Plausible.Repo.preload(user, :enterprise_plan) - - cond do - Timex.before?(user.inserted_at, @limit_accounts_since) -> nil - Application.get_env(:plausible, :is_selfhost) -> nil - user.email in Application.get_env(:plausible, :site_limit_exempt) -> nil - user.enterprise_plan -> nil - true -> Application.get_env(:plausible, :site_limit) - end - end - - defp format_subscription(params) do - %{ - paddle_subscription_id: params["subscription_id"], - paddle_plan_id: params["subscription_plan_id"], - cancel_url: params["cancel_url"], - update_url: params["update_url"], - user_id: params["passthrough"], - status: params["status"], - next_bill_date: params["next_bill_date"], - next_bill_amount: params["unit_price"] || params["new_unit_price"], - currency_code: params["currency"] - } - end - - defp present?(""), do: false - defp present?(nil), do: false - defp present?(_), do: true - - defp maybe_remove_grace_period({:ok, user}) do - alias Plausible.Auth.GracePeriod - - case user.grace_period do - %GracePeriod{allowance_required: allowance_required} -> - new_allowance = Plausible.Billing.Plans.allowance(user.subscription) - - if new_allowance > allowance_required do - Plausible.Auth.User.remove_grace_period(user) - |> Repo.update() - else - {:ok, user} - end - - _ -> - {:ok, user} - end - end - - defp maybe_remove_grace_period(err), do: err - - defp check_lock_status({:ok, user}) do - Plausible.Billing.SiteLocker.check_sites_for(user) - {:ok, user} - end - - defp check_lock_status(err), do: err - - defp maybe_adjust_api_key_limits({:ok, user}) do - plan = - Repo.get_by(Plausible.Billing.EnterprisePlan, - user_id: user.id, - paddle_plan_id: user.subscription.paddle_plan_id - ) - - if plan do - user_id = user.id - api_keys = from(key in Plausible.Auth.ApiKey, where: key.user_id == ^user_id) - Repo.update_all(api_keys, set: [hourly_request_limit: plan.hourly_api_request_limit]) - end - - {:ok, user} - end - - defp maybe_adjust_api_key_limits(err), do: err - - def paddle_api(), do: Application.fetch_env!(:plausible, :paddle_api) -end diff --git a/lib/plausible/billing/enterprise_plan.ex b/lib/plausible/billing/enterprise_plan.ex deleted file mode 100644 index cfae0d4ff7f2..000000000000 --- a/lib/plausible/billing/enterprise_plan.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Plausible.Billing.EnterprisePlan do - use Ecto.Schema - import Ecto.Changeset - - @required_fields [ - :user_id, - :paddle_plan_id, - :billing_interval, - :monthly_pageview_limit, - :hourly_api_request_limit, - :site_limit - ] - - schema "enterprise_plans" do - field :paddle_plan_id, :string - field :billing_interval, Ecto.Enum, values: [:monthly, :yearly] - field :monthly_pageview_limit, :integer - field :hourly_api_request_limit, :integer - field :site_limit, :integer - - belongs_to :user, Plausible.Auth.User - - timestamps() - end - - def changeset(model, attrs \\ %{}) do - model - |> cast(attrs, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:user_id) - end -end diff --git a/lib/plausible/billing/enterprise_plan_admin.ex b/lib/plausible/billing/enterprise_plan_admin.ex deleted file mode 100644 index 08c396732863..000000000000 --- a/lib/plausible/billing/enterprise_plan_admin.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Plausible.Billing.EnterprisePlanAdmin do - use Plausible.Repo - - def search_fields(_schema) do - [ - :paddle_plan_id, - user: [:name, :email] - ] - end - - def form_fields(_) do - [ - user_id: nil, - paddle_plan_id: nil, - billing_interval: %{choices: [{"Yearly", "yearly"}, {"Monthly", "monthly"}]}, - monthly_pageview_limit: nil, - hourly_api_request_limit: nil, - site_limit: nil - ] - end - - def custom_index_query(_conn, _schema, query) do - from(r in query, preload: :user) - end - - def index(_) do - [ - id: nil, - user_email: %{value: &get_user_email/1}, - paddle_plan_id: nil, - billing_interval: nil, - monthly_pageview_limit: nil, - hourly_api_request_limit: nil, - site_limit: nil - ] - end - - defp get_user_email(plan), do: plan.user.email -end diff --git a/lib/plausible/billing/paddle_api.ex b/lib/plausible/billing/paddle_api.ex deleted file mode 100644 index 42a2742faad1..000000000000 --- a/lib/plausible/billing/paddle_api.ex +++ /dev/null @@ -1,131 +0,0 @@ -defmodule Plausible.Billing.PaddleApi do - @update_preview_endpoint "https://vendors.paddle.com/api/2.0/subscription/preview_update" - @update_endpoint "https://vendors.paddle.com/api/2.0/subscription/users/update" - @get_endpoint "https://vendors.paddle.com/api/2.0/subscription/users" - @headers [ - {"Content-type", "application/json"}, - {"Accept", "application/json"} - ] - - def update_subscription_preview(paddle_subscription_id, new_plan_id) do - config = get_config() - - params = %{ - vendor_id: config[:vendor_id], - vendor_auth_code: config[:vendor_auth_code], - subscription_id: paddle_subscription_id, - plan_id: new_plan_id, - prorate: true, - keep_modifiers: true, - bill_immediately: true, - quantity: 1 - } - - {:ok, response} = HTTPoison.post(@update_preview_endpoint, Jason.encode!(params), @headers) - body = Jason.decode!(response.body) - - if body["success"] do - {:ok, body["response"]} - else - {:error, body["error"]} - end - end - - def update_subscription(paddle_subscription_id, params) do - config = get_config() - - params = - Map.merge(params, %{ - vendor_id: config[:vendor_id], - vendor_auth_code: config[:vendor_auth_code], - subscription_id: paddle_subscription_id, - prorate: true, - keep_modifiers: true, - bill_immediately: true, - quantity: 1 - }) - - {:ok, response} = HTTPoison.post(@update_endpoint, Jason.encode!(params), @headers) - body = Jason.decode!(response.body) - - if body["success"] do - {:ok, body["response"]} - else - {:error, body["error"]} - end - end - - def get_subscription(paddle_subscription_id) do - config = get_config() - - params = %{ - vendor_id: config[:vendor_id], - vendor_auth_code: config[:vendor_auth_code], - subscription_id: paddle_subscription_id - } - - {:ok, response} = HTTPoison.post(@get_endpoint, Jason.encode!(params), @headers) - body = Jason.decode!(response.body) - - if body["success"] do - [subscription] = body["response"] - {:ok, subscription} - else - {:error, body["error"]} - end - end - - def get_invoices(nil), do: {:error, :no_subscription} - - def get_invoices(subscription) do - config = get_config() - - params = %{ - vendor_id: config[:vendor_id], - vendor_auth_code: config[:vendor_auth_code], - subscription_id: subscription.paddle_subscription_id, - is_paid: 1, - from: Timex.shift(Timex.today(), years: -5) |> Timex.format!("{YYYY}-{0M}-{0D}"), - to: Timex.shift(Timex.today(), days: 1) |> Timex.format!("{YYYY}-{0M}-{0D}") - } - - case HTTPoison.post(invoices_endpoint(), Jason.encode!(params), @headers) do - {:ok, response} -> - body = Jason.decode!(response.body) - - if body["success"] && body["response"] != [] do - body["response"] |> last_12_invoices() - else - {:error, :request_failed} - end - - {:error, _reason} -> - {:error, :request_failed} - end - end - - defp invoices_endpoint() do - case Application.get_env(:plausible, :environment) do - "dev" -> "https://sandbox-vendors.paddle.com/api/2.0/subscription/payments" - _ -> "https://vendors.paddle.com/api/2.0/subscription/payments" - end - end - - defp last_12_invoices(invoice_list) do - Enum.sort(invoice_list, fn %{"payout_date" => d1}, %{"payout_date" => d2} -> - Date.compare(Date.from_iso8601!(d1), Date.from_iso8601!(d2)) == :gt - end) - |> Enum.take(12) - end - - def checkout_domain() do - case Application.get_env(:plausible, :environment) do - "dev" -> "https://sandbox-checkout.paddle.com" - _ -> "https://checkout.paddle.com" - end - end - - defp get_config() do - Application.get_env(:plausible, :paddle) - end -end diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex deleted file mode 100644 index e156697f7c25..000000000000 --- a/lib/plausible/billing/plans.ex +++ /dev/null @@ -1,157 +0,0 @@ -defmodule Plausible.Billing.Plans do - use Plausible.Repo - - @unlisted_plans_v1 [ - %{limit: 150_000_000, yearly_product_id: "648089", yearly_cost: "$4800"} - ] - - @unlisted_plans_v2 [ - %{limit: 10_000_000, monthly_product_id: "655350", monthly_cost: "$250"} - ] - - @sandbox_plans [ - %{ - limit: 10_000, - monthly_product_id: "19878", - yearly_product_id: "20127", - monthly_cost: "$6", - yearly_cost: "$60" - }, - %{ - limit: 100_000, - monthly_product_id: "20657", - yearly_product_id: "20658", - monthly_cost: "$12.34", - yearly_cost: "$120.34" - } - ] - - def plans_for(user) do - user = Repo.preload(user, :subscription) - sandbox_plans = plans_sandbox() - v1_plans = plans_v1() - v2_plans = plans_v2() - v3_plans = plans_v3() - - raw_plans = - cond do - contains?(v1_plans, user.subscription) -> - v1_plans - - contains?(v2_plans, user.subscription) -> - v2_plans - - contains?(v3_plans, user.subscription) -> - v3_plans - - contains?(sandbox_plans, user.subscription) -> - sandbox_plans - - true -> - cond do - Application.get_env(:plausible, :environment) == "dev" -> sandbox_plans - Timex.before?(user.inserted_at, ~D[2022-01-01]) -> v2_plans - true -> v3_plans - end - end - - Enum.map(raw_plans, fn plan -> Map.put(plan, :volume, number_format(plan[:limit])) end) - end - - def all_yearly_plan_ids do - Enum.map(all_plans(), fn plan -> plan[:yearly_product_id] end) - end - - def for_product_id(product_id) do - Enum.find(all_plans(), fn plan -> - product_id in [plan[:monthly_product_id], plan[:yearly_product_id]] - end) - end - - def subscription_interval(%Plausible.Billing.Subscription{paddle_plan_id: "free_10k"}), - do: "N/A" - - def subscription_interval(subscription) do - case for_product_id(subscription.paddle_plan_id) do - nil -> - enterprise_plan = - Repo.get_by(Plausible.Billing.EnterprisePlan, user_id: subscription.user_id) - - enterprise_plan && enterprise_plan.billing_interval - - plan -> - if subscription.paddle_plan_id == plan[:monthly_product_id] do - "monthly" - else - "yearly" - end - end - end - - def allowance(%Plausible.Billing.Subscription{paddle_plan_id: "free_10k"}), do: 10_000 - - def allowance(subscription) do - found = for_product_id(subscription.paddle_plan_id) - - if found do - Map.fetch!(found, :limit) - else - enterprise_plan = - Repo.get_by(Plausible.Billing.EnterprisePlan, user_id: subscription.user_id) - - if enterprise_plan do - enterprise_plan.monthly_pageview_limit - else - Sentry.capture_message("Unknown allowance for plan", - extra: %{ - paddle_plan_id: subscription.paddle_plan_id - } - ) - end - end - end - - def suggested_plan(user, usage) do - Enum.find(plans_for(user), fn plan -> usage < plan[:limit] end) - end - - defp contains?(_plans, nil), do: false - - defp contains?(plans, subscription) do - Enum.any?(plans, fn plan -> - plan[:monthly_product_id] == subscription.paddle_plan_id || - plan[:yearly_product_id] == subscription.paddle_plan_id - end) - end - - defp number_format(num) do - PlausibleWeb.StatsView.large_number_format(num) - end - - defp all_plans() do - plans_v1() ++ - @unlisted_plans_v1 ++ plans_v2() ++ @unlisted_plans_v2 ++ plans_v3() ++ plans_sandbox() - end - - defp plans_v1() do - File.read!(Application.app_dir(:plausible) <> "/priv/plans_v1.json") - |> Jason.decode!(keys: :atoms) - end - - defp plans_v2() do - File.read!(Application.app_dir(:plausible) <> "/priv/plans_v2.json") - |> Jason.decode!(keys: :atoms) - end - - defp plans_v3() do - File.read!(Application.app_dir(:plausible) <> "/priv/plans_v3.json") - |> Jason.decode!(keys: :atoms) - end - - defp plans_sandbox() do - case Application.get_env(:plausible, :environment) do - "dev" -> @sandbox_plans - _ -> [] - end - end -end diff --git a/lib/plausible/billing/site_locker.ex b/lib/plausible/billing/site_locker.ex deleted file mode 100644 index fe858c6fc86b..000000000000 --- a/lib/plausible/billing/site_locker.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Plausible.Billing.SiteLocker do - use Plausible.Repo - - def check_sites_for(user) do - case Plausible.Billing.needs_to_upgrade?(user) do - {true, :grace_period_ended} -> - set_lock_status_for(user, true) - - if !user.grace_period.is_over do - send_grace_period_end_email(user) - Plausible.Auth.User.end_grace_period(user) |> Repo.update() - end - - {true, _} -> - set_lock_status_for(user, true) - - _ -> - set_lock_status_for(user, false) - end - end - - defp set_lock_status_for(user, status) do - site_ids = - Repo.all( - from s in Plausible.Site.Membership, - where: s.user_id == ^user.id, - where: s.role == :owner, - select: s.site_id - ) - - site_q = - from( - s in Plausible.Site, - where: s.id in ^site_ids - ) - - Repo.update_all(site_q, set: [locked: status]) - end - - defp send_grace_period_end_email(user) do - {_, last_cycle} = Plausible.Billing.last_two_billing_cycles(user) - {_, last_cycle_usage} = Plausible.Billing.last_two_billing_months_usage(user) - suggested_plan = Plausible.Billing.Plans.suggested_plan(user, last_cycle_usage) - - template = - PlausibleWeb.Email.dashboard_locked( - user, - last_cycle_usage, - last_cycle, - suggested_plan - ) - - Plausible.Mailer.send_email_safe(template) - end -end diff --git a/lib/plausible/billing/subscription.ex b/lib/plausible/billing/subscription.ex deleted file mode 100644 index 806b217e456f..000000000000 --- a/lib/plausible/billing/subscription.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Plausible.Billing.Subscription do - use Ecto.Schema - import Ecto.Changeset - - @required_fields [ - :paddle_subscription_id, - :paddle_plan_id, - :update_url, - :cancel_url, - :status, - :next_bill_amount, - :next_bill_date, - :user_id, - :currency_code - ] - - @optional_fields [:last_bill_date] - @valid_statuses ["active", "past_due", "deleted", "paused"] - - schema "subscriptions" do - field :paddle_subscription_id, :string - field :paddle_plan_id, :string - field :update_url, :string - field :cancel_url, :string - field :status, :string - field :next_bill_amount, :string - field :next_bill_date, :date - field :last_bill_date, :date - field :currency_code, :string - - belongs_to :user, Plausible.Auth.User - - timestamps() - end - - def changeset(model, attrs \\ %{}) do - model - |> cast(attrs, @required_fields ++ @optional_fields) - |> validate_required(@required_fields) - |> validate_inclusion(:status, @valid_statuses) - |> unique_constraint(:paddle_subscription_id) - end - - def free(attrs \\ %{}) do - %__MODULE__{ - paddle_plan_id: "free_10k", - status: "active", - next_bill_amount: "0" - } - |> cast(attrs, @required_fields) - |> validate_required([:user_id]) - |> validate_inclusion(:status, @valid_statuses) - |> unique_constraint(:paddle_subscription_id) - end -end diff --git a/lib/plausible/event/write_buffer.ex b/lib/plausible/event/write_buffer.ex index b265cbc3617e..70dac53fef85 100644 --- a/lib/plausible/event/write_buffer.ex +++ b/lib/plausible/event/write_buffer.ex @@ -1,7 +1,8 @@ defmodule Plausible.Event.WriteBuffer do use GenServer require Logger - use OpenTelemetryDecorator + # telemetry will be brought back once we integrate the plausible codebase + # use OpenTelemetryDecorator def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -55,7 +56,7 @@ defmodule Plausible.Event.WriteBuffer do do_flush(buffer) end - @decorate trace("ingest.flush_events") + # @decorate trace("ingest.flush_events") defp do_flush(buffer) do case buffer do [] -> diff --git a/lib/plausible/goal/schema.ex b/lib/plausible/goal/schema.ex deleted file mode 100644 index 31986a18479a..000000000000 --- a/lib/plausible/goal/schema.ex +++ /dev/null @@ -1,54 +0,0 @@ -defimpl Jason.Encoder, for: Plausible.Goal do - def encode(value, opts) do - goal_type = - cond do - value.event_name -> :event - value.page_path -> :page - end - - value - |> Map.put(:goal_type, goal_type) - |> Map.take([:id, :domain, :goal_type, :event_name, :page_path]) - |> Jason.Encode.map(opts) - end -end - -defmodule Plausible.Goal do - use Ecto.Schema - import Ecto.Changeset - - schema "goals" do - field :domain, :string - field :event_name, :string - field :page_path, :string - - timestamps() - end - - def changeset(goal, attrs \\ %{}) do - goal - |> cast(attrs, [:domain, :event_name, :page_path]) - |> validate_required([:domain]) - |> validate_event_name_and_page_path() - end - - defp validate_event_name_and_page_path(changeset) do - if validate_page_path(changeset) || validate_event_name(changeset) do - changeset - else - changeset - |> add_error(:event_name, "this field is required and cannot be blank") - |> add_error(:page_path, "this field is required and must start with a /") - end - end - - defp validate_page_path(changeset) do - value = get_field(changeset, :page_path) - value && String.match?(value, ~r/^\/.*/) - end - - defp validate_event_name(changeset) do - value = get_field(changeset, :event_name) - value && String.match?(value, ~r/^.+/) - end -end diff --git a/lib/plausible/goals.ex b/lib/plausible/goals.ex deleted file mode 100644 index af3a7d5cf099..000000000000 --- a/lib/plausible/goals.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule Plausible.Goals do - use Plausible.Repo - alias Plausible.Goal - - def create(site, params) do - params = Map.merge(params, %{"domain" => site.domain}) - - Goal.changeset(%Goal{}, params) |> Repo.insert() - end - - def find_or_create(site, %{"goal_type" => "event", "event_name" => event_name}) do - goal = Repo.get_by(Plausible.Goal, domain: site.domain, event_name: event_name) - - case goal do - nil -> create(site, %{"event_name" => event_name}) - goal -> {:ok, goal} - end - end - - def find_or_create(_, %{"goal_type" => "event"}), do: {:missing, "event_name"} - - def find_or_create(site, %{"goal_type" => "page", "page_path" => page_path}) do - goal = Repo.get_by(Plausible.Goal, domain: site.domain, page_path: page_path) - - case goal do - nil -> create(site, %{"page_path" => page_path}) - goal -> {:ok, goal} - end - end - - def find_or_create(_, %{"goal_type" => "page"}), do: {:missing, "page_path"} - - def for_site(domain) do - Repo.all( - from g in Goal, - where: g.domain == ^domain - ) - end - - def delete(id) do - Repo.one(from g in Goal, where: g.id == ^id) |> Repo.delete!() - end -end diff --git a/lib/plausible/google/api.ex b/lib/plausible/google/api.ex deleted file mode 100644 index 44372da25048..000000000000 --- a/lib/plausible/google/api.ex +++ /dev/null @@ -1,382 +0,0 @@ -defmodule Plausible.Google.Api do - alias Plausible.Imported - use Timex - - @scope URI.encode_www_form( - "https://www.googleapis.com/auth/webmasters.readonly email https://www.googleapis.com/auth/analytics.readonly" - ) - @verified_permission_levels ["siteOwner", "siteFullUser", "siteRestrictedUser"] - - def authorize_url(site_id, redirect_to) do - if Application.get_env(:plausible, :environment) == "test" do - "" - else - "https://accounts.google.com/o/oauth2/v2/auth?client_id=#{client_id()}&redirect_uri=#{redirect_uri()}&prompt=consent&response_type=code&access_type=offline&scope=#{@scope}&state=" <> - Jason.encode!([site_id, redirect_to]) - end - end - - def fetch_access_token(code) do - res = - HTTPoison.post!( - "https://www.googleapis.com/oauth2/v4/token", - "client_id=#{client_id()}&client_secret=#{client_secret()}&code=#{code}&grant_type=authorization_code&redirect_uri=#{redirect_uri()}", - "Content-Type": "application/x-www-form-urlencoded" - ) - - Jason.decode!(res.body) - end - - def fetch_verified_properties(auth) do - with {:ok, auth} <- refresh_if_needed(auth) do - res = - HTTPoison.get!("https://www.googleapis.com/webmasters/v3/sites", - "Content-Type": "application/json", - Authorization: "Bearer #{auth.access_token}" - ) - - domains = - Jason.decode!(res.body) - |> Map.get("siteEntry", []) - |> Enum.filter(fn site -> site["permissionLevel"] in @verified_permission_levels end) - |> Enum.map(fn site -> site["siteUrl"] end) - |> Enum.map(fn url -> String.trim_trailing(url, "/") end) - - {:ok, domains} - else - err -> err - end - end - - defp property_base_url(property) do - case property do - "sc-domain:" <> domain -> "https://" <> domain - url -> url - end - end - - def fetch_stats(site, query, limit) do - with {:ok, auth} <- refresh_if_needed(site.google_auth) do - do_fetch_stats(auth, query, limit) - else - err -> err - end - end - - defp do_fetch_stats(auth, query, limit) do - property = URI.encode_www_form(auth.property) - base_url = property_base_url(auth.property) - - filter_groups = - if query.filters["page"] do - [ - %{ - filters: [ - %{ - dimension: "page", - expression: "https://#{base_url}#{query.filters["page"]}" - } - ] - } - ] - end - - res = - HTTPoison.post!( - "https://www.googleapis.com/webmasters/v3/sites/#{property}/searchAnalytics/query", - Jason.encode!(%{ - startDate: Date.to_iso8601(query.date_range.first), - endDate: Date.to_iso8601(query.date_range.last), - dimensions: ["query"], - rowLimit: limit, - dimensionFilterGroups: filter_groups || %{} - }), - "Content-Type": "application/json", - Authorization: "Bearer #{auth.access_token}" - ) - - case res.status_code do - 200 -> - terms = - (Jason.decode!(res.body)["rows"] || []) - |> Enum.filter(fn row -> row["clicks"] > 0 end) - |> Enum.map(fn row -> %{name: row["keys"], visitors: round(row["clicks"])} end) - - {:ok, terms} - - 401 -> - Sentry.capture_message("Error fetching Google queries", extra: Jason.decode!(res.body)) - {:error, :invalid_credentials} - - 403 -> - Sentry.capture_message("Error fetching Google queries", extra: Jason.decode!(res.body)) - msg = Jason.decode!(res.body)["error"]["message"] - {:error, msg} - - _ -> - Sentry.capture_message("Error fetching Google queries", extra: Jason.decode!(res.body)) - {:error, :unknown} - end - end - - def get_analytics_view_ids(site) do - with {:ok, auth} <- refresh_if_needed(site.google_auth) do - do_get_analytics_view_ids(auth) - end - end - - def do_get_analytics_view_ids(auth) do - res = - HTTPoison.get!( - "https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles", - Authorization: "Bearer #{auth.access_token}" - ) - - case res.status_code do - 200 -> - profiles = - Jason.decode!(res.body) - |> Map.get("items") - |> Enum.map(fn item -> - uri = URI.parse(Map.get(item, "websiteUrl")) - name = Map.get(item, "name") - {"#{uri.host} - #{name}", Map.get(item, "id")} - end) - |> Map.new() - - {:ok, profiles} - - _ -> - Sentry.capture_message("Error fetching Google view ID", extra: Jason.decode!(res.body)) - {:error, res.body} - end - end - - def import_analytics(site, profile) do - with {:ok, auth} <- refresh_if_needed(site.google_auth) do - do_import_analytics(site, auth, profile) - end - end - - @doc """ - API reference: - https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet#ReportRequest - - Dimensions reference: https://ga-dev-tools.web.app/dimensions-metrics-explorer - """ - def do_import_analytics(site, auth, profile) do - end_date = - Plausible.Stats.Clickhouse.pageviews_begin(site) - |> NaiveDateTime.to_date() - - end_date = - if end_date == ~D[1970-01-01] do - Timex.today() - else - end_date - end - - request = %{ - auth: auth, - profile: profile, - end_date: Date.to_iso8601(end_date) - } - - # Each element is: {dataset, dimensions, metrics} - request_data = [ - { - "imported_visitors", - ["ga:date"], - [ - "ga:users", - "ga:pageviews", - "ga:bounces", - "ga:sessions", - "ga:sessionDuration" - ] - }, - { - "imported_sources", - ["ga:date", "ga:source", "ga:medium", "ga:campaign", "ga:adContent", "ga:keyword"], - ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"] - }, - { - "imported_pages", - ["ga:date", "ga:hostname", "ga:pagePath"], - ["ga:users", "ga:pageviews", "ga:exits", "ga:timeOnPage"] - }, - { - "imported_entry_pages", - ["ga:date", "ga:landingPagePath"], - ["ga:users", "ga:entrances", "ga:sessionDuration", "ga:bounces"] - }, - { - "imported_exit_pages", - ["ga:date", "ga:exitPagePath"], - ["ga:users", "ga:exits"] - }, - { - "imported_locations", - ["ga:date", "ga:countryIsoCode", "ga:regionIsoCode"], - ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"] - }, - { - "imported_devices", - ["ga:date", "ga:deviceCategory"], - ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"] - }, - { - "imported_browsers", - ["ga:date", "ga:browser"], - ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"] - }, - { - "imported_operating_systems", - ["ga:date", "ga:operatingSystem"], - ["ga:users", "ga:sessions", "ga:bounces", "ga:sessionDuration"] - } - ] - - responses = - Enum.map( - request_data, - fn {dataset, dimensions, metrics} -> - fetch_analytic_reports(dataset, dimensions, metrics, request) - end - ) - - case Keyword.get(responses, :error) do - nil -> - results = - responses - |> Enum.map(fn {:ok, resp} -> resp end) - |> Enum.concat() - - if Enum.any?(results, fn {_, val} -> val end) do - maybe_error = - results - |> Enum.map(fn {dataset, data} -> - Imported.from_google_analytics(data, site.id, dataset) - end) - |> Keyword.get(:error) - - case maybe_error do - nil -> - {:ok, nil} - - {:error, error} -> - Plausible.ClickhouseRepo.clear_imported_stats_for(site.domain) - - Sentry.capture_message("Error saving Google analytics data", extra: error) - {:error, error["error"]["message"]} - end - else - {:error, "No Google Analytics data found."} - end - - error -> - Sentry.capture_message("Error fetching Google analytics data", extra: error) - {:error, error} - end - end - - defp fetch_analytic_reports(dataset, dimensions, metrics, request, page_token \\ "") do - report = %{ - viewId: request.profile, - dateRanges: [ - %{ - # The earliest valid date - startDate: "2005-01-01", - endDate: request.end_date - } - ], - dimensions: Enum.map(dimensions, &%{name: &1, histogramBuckets: []}), - metrics: Enum.map(metrics, &%{expression: &1}), - hideTotals: true, - hideValueRanges: true, - orderBys: [ - %{ - fieldName: "ga:date", - sortOrder: "DESCENDING" - } - ], - pageSize: 100_00, - pageToken: page_token - } - - res = - HTTPoison.post!( - "https://analyticsreporting.googleapis.com/v4/reports:batchGet", - Jason.encode!(%{reportRequests: [report]}), - Authorization: "Bearer #{request.auth.access_token}" - ) - - if res.status_code == 200 do - report = List.first(Jason.decode!(res.body)["reports"]) - data = report["data"]["rows"] - next_page_token = report["nextPageToken"] - - if next_page_token do - # Recursively make more requests until we run out of next page tokens - case fetch_analytic_reports( - dataset, - dimensions, - metrics, - request, - next_page_token - ) do - {:ok, %{^dataset => remainder}} -> - {:ok, %{dataset => data ++ remainder}} - - error -> - error - end - else - {:ok, %{dataset => data}} - end - else - {:error, Jason.decode!(res.body)["error"]["message"]} - end - end - - defp refresh_if_needed(auth) do - if Timex.before?(auth.expires, Timex.now() |> Timex.shift(seconds: 30)) do - refresh_token(auth) - else - {:ok, auth} - end - end - - defp refresh_token(auth) do - res = - HTTPoison.post!( - "https://www.googleapis.com/oauth2/v4/token", - "client_id=#{client_id()}&client_secret=#{client_secret()}&refresh_token=#{auth.refresh_token}&grant_type=refresh_token&redirect_uri=#{redirect_uri()}", - "Content-Type": "application/x-www-form-urlencoded" - ) - - body = Jason.decode!(res.body) - - if res.status_code == 200 do - Plausible.Site.GoogleAuth.changeset(auth, %{ - access_token: body["access_token"], - expires: NaiveDateTime.utc_now() |> NaiveDateTime.add(body["expires_in"]) - }) - |> Plausible.Repo.update() - else - {:error, body["error"]} - end - end - - defp client_id() do - Keyword.fetch!(Application.get_env(:plausible, :google), :client_id) - end - - defp client_secret() do - Keyword.fetch!(Application.get_env(:plausible, :google), :client_secret) - end - - defp redirect_uri() do - PlausibleWeb.Endpoint.url() <> "/auth/google/callback" - end -end diff --git a/lib/plausible/imported/site.ex b/lib/plausible/imported/site.ex deleted file mode 100644 index d137fb667656..000000000000 --- a/lib/plausible/imported/site.ex +++ /dev/null @@ -1,249 +0,0 @@ -defmodule Plausible.Imported do - use Plausible.ClickhouseRepo - use Timex - - def forget(site) do - Plausible.ClickhouseRepo.clear_imported_stats_for(site.id) - end - - def from_google_analytics(nil, _site_id, _metric, _timezone), do: {:ok, nil} - - def from_google_analytics(data, site_id, table) do - data = - Enum.map(data, fn row -> - new_from_google_analytics(site_id, table, row) - end) - - case ClickhouseRepo.insert_all(table, data) do - {n_rows, _} when n_rows > 0 -> :ok - error -> error - end - end - - defp new_from_google_analytics(site_id, "imported_visitors", %{ - "dimensions" => [date], - "metrics" => [%{"values" => values}] - }) do - [visitors, pageviews, bounces, visits, visit_duration] = - values - |> Enum.map(&Integer.parse/1) - |> Enum.map(&elem(&1, 0)) - - %{ - site_id: site_id, - date: format_date(date), - visitors: visitors, - pageviews: pageviews, - bounces: bounces, - visits: visits, - visit_duration: visit_duration - } - end - - # Credit: https://github.com/kvesteri/validators - @domain ~r/^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$/ - - defp new_from_google_analytics(site_id, "imported_sources", %{ - "dimensions" => [date, source, medium, campaign, content, term], - "metrics" => [%{"values" => [visitors, visits, bounces, visit_duration]}] - }) do - {visitors, ""} = Integer.parse(visitors) - {visits, ""} = Integer.parse(visits) - {bounces, ""} = Integer.parse(bounces) - {visit_duration, _} = Integer.parse(visit_duration) - - source = if source == "(direct)", do: nil, else: source - source = if source && String.match?(source, @domain), do: parse_referrer(source), else: source - - %{ - site_id: site_id, - date: format_date(date), - source: parse_referrer(source), - utm_medium: nil_if_missing(medium), - utm_campaign: nil_if_missing(campaign), - utm_content: nil_if_missing(content), - utm_term: nil_if_missing(term), - visitors: visitors, - visits: visits, - bounces: bounces, - visit_duration: visit_duration - } - end - - defp new_from_google_analytics(site_id, "imported_pages", %{ - "dimensions" => [date, hostname, page], - "metrics" => [%{"values" => [visitors, pageviews, exits, time_on_page]}] - }) do - page = URI.parse(page).path - {visitors, ""} = Integer.parse(visitors) - {pageviews, ""} = Integer.parse(pageviews) - {exits, ""} = Integer.parse(exits) - {time_on_page, _} = Integer.parse(time_on_page) - - %{ - site_id: site_id, - date: format_date(date), - hostname: String.replace_prefix(hostname, "www.", ""), - page: page, - visitors: visitors, - pageviews: pageviews, - exits: exits, - time_on_page: time_on_page - } - end - - defp new_from_google_analytics(site_id, "imported_entry_pages", %{ - "dimensions" => [date, entry_page], - "metrics" => [%{"values" => [visitors, entrances, visit_duration, bounces]}] - }) do - {visitors, ""} = Integer.parse(visitors) - {entrances, ""} = Integer.parse(entrances) - {bounces, ""} = Integer.parse(bounces) - {visit_duration, _} = Integer.parse(visit_duration) - - %{ - site_id: site_id, - date: format_date(date), - entry_page: entry_page, - visitors: visitors, - entrances: entrances, - visit_duration: visit_duration, - bounces: bounces - } - end - - defp new_from_google_analytics(site_id, "imported_exit_pages", %{ - "dimensions" => [date, exit_page], - "metrics" => [%{"values" => [visitors, exits]}] - }) do - {visitors, ""} = Integer.parse(visitors) - {exits, ""} = Integer.parse(exits) - - %{ - site_id: site_id, - date: format_date(date), - exit_page: exit_page, - visitors: visitors, - exits: exits - } - end - - defp new_from_google_analytics(site_id, "imported_locations", %{ - "dimensions" => [date, country, region], - "metrics" => [%{"values" => [visitors, visits, bounces, visit_duration]}] - }) do - country = if country == "(not set)", do: "", else: country - region = if region == "(not set)", do: "", else: region - {visitors, ""} = Integer.parse(visitors) - {visits, ""} = Integer.parse(visits) - {bounces, ""} = Integer.parse(bounces) - {visit_duration, _} = Integer.parse(visit_duration) - - %{ - site_id: site_id, - date: format_date(date), - country: country, - region: region, - city: 0, - visitors: visitors, - visits: visits, - bounces: bounces, - visit_duration: visit_duration - } - end - - defp new_from_google_analytics(site_id, "imported_devices", %{ - "dimensions" => [date, device], - "metrics" => [%{"values" => [visitors, visits, bounces, visit_duration]}] - }) do - {visitors, ""} = Integer.parse(visitors) - {visits, ""} = Integer.parse(visits) - {bounces, ""} = Integer.parse(bounces) - {visit_duration, _} = Integer.parse(visit_duration) - - %{ - site_id: site_id, - date: format_date(date), - device: String.capitalize(device), - visitors: visitors, - visits: visits, - bounces: bounces, - visit_duration: visit_duration - } - end - - @browser_google_to_plausible %{ - "User-Agent:Opera" => "Opera", - "Mozilla Compatible Agent" => "Mobile App", - "Android Webview" => "Mobile App", - "Android Browser" => "Mobile App", - "Safari (in-app)" => "Mobile App", - "User-Agent: Mozilla" => "Firefox", - "(not set)" => "" - } - - defp new_from_google_analytics(site_id, "imported_browsers", %{ - "dimensions" => [date, browser], - "metrics" => [%{"values" => [visitors, visits, bounces, visit_duration]}] - }) do - {visitors, ""} = Integer.parse(visitors) - {visits, ""} = Integer.parse(visits) - {bounces, ""} = Integer.parse(bounces) - {visit_duration, _} = Integer.parse(visit_duration) - - %{ - site_id: site_id, - date: format_date(date), - browser: Map.get(@browser_google_to_plausible, browser, browser), - visitors: visitors, - visits: visits, - bounces: bounces, - visit_duration: visit_duration - } - end - - @os_google_to_plausible %{ - "Macintosh" => "Mac", - "Linux" => "GNU/Linux", - "(not set)" => "" - } - - defp new_from_google_analytics(site_id, "imported_operating_systems", %{ - "dimensions" => [date, operating_system], - "metrics" => [%{"values" => [visitors, visits, bounces, visit_duration]}] - }) do - {visitors, ""} = Integer.parse(visitors) - {visits, ""} = Integer.parse(visits) - {bounces, ""} = Integer.parse(bounces) - {visit_duration, _} = Integer.parse(visit_duration) - - %{ - site_id: site_id, - date: format_date(date), - operating_system: Map.get(@os_google_to_plausible, operating_system, operating_system), - visitors: visitors, - visits: visits, - bounces: bounces, - visit_duration: visit_duration - } - end - - defp format_date(date) do - Timex.parse!("#{date}", "%Y%m%d", :strftime) - |> NaiveDateTime.to_date() - end - - @missing_values ["(none)", "(not set)", "(not provided)"] - def nil_if_missing(value) when value in @missing_values, do: nil - def nil_if_missing(value), do: value - - def parse_referrer(nil), do: nil - def parse_referrer("google"), do: "Google" - def parse_referrer("bing"), do: "Bing" - def parse_referrer("duckduckgo"), do: "DuckDuckGo" - - def parse_referrer(ref) do - RefInspector.parse("https://" <> ref) - |> PlausibleWeb.RefInspector.parse() - end -end diff --git a/lib/plausible/mailer.ex b/lib/plausible/mailer.ex deleted file mode 100644 index 8768e542a3a7..000000000000 --- a/lib/plausible/mailer.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Plausible.Mailer do - use Bamboo.Mailer, otp_app: :plausible - - def send_email(email) do - try do - Plausible.Mailer.deliver_now!(email) - rescue - error -> - Sentry.capture_exception(error, - stacktrace: __STACKTRACE__, - extra: %{extra: "Error while sending email"} - ) - - reraise error, __STACKTRACE__ - end - end - - def send_email_safe(email) do - try do - Plausible.Mailer.deliver_now!(email) - rescue - error -> - Sentry.capture_exception(error, - stacktrace: __STACKTRACE__, - extra: %{extra: "Error while sending email"} - ) - end - end -end diff --git a/lib/plausible/session/write_buffer.ex b/lib/plausible/session/write_buffer.ex index 18f6ed037206..509ccc3606d1 100644 --- a/lib/plausible/session/write_buffer.ex +++ b/lib/plausible/session/write_buffer.ex @@ -1,7 +1,8 @@ defmodule Plausible.Session.WriteBuffer do use GenServer require Logger - use OpenTelemetryDecorator + # telemetry will be brought back once we integrate the plausible codebase + # use OpenTelemetryDecorator def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -43,7 +44,7 @@ defmodule Plausible.Session.WriteBuffer do flush(buffer) end - @decorate trace("ingest.flush_sessions") + # @decorate trace("ingest.flush_sessions") defp flush(buffer) do case buffer do [] -> diff --git a/lib/plausible/site/admin.ex b/lib/plausible/site/admin.ex deleted file mode 100644 index f2d2938d5b8b..000000000000 --- a/lib/plausible/site/admin.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Plausible.SiteAdmin do - use Plausible.Repo - - def search_fields(_schema) do - [ - :domain, - members: [:name, :email] - ] - end - - def custom_index_query(_conn, _schema, query) do - from(r in query, preload: [memberships: :user]) - end - - def form_fields(_) do - [ - domain: nil, - timezone: nil, - public: nil - ] - end - - def index(_) do - [ - domain: nil, - inserted_at: %{name: "Created at", value: &format_date(&1.inserted_at)}, - timezone: nil, - public: nil, - owner: %{value: &get_owner_email/1}, - other_members: %{value: &get_other_members_emails/1} - ] - end - - defp get_owner_email(site) do - Enum.find(site.memberships, fn m -> m.role == :owner end).user.email - end - - defp get_other_members_emails(site) do - memberships = Enum.reject(site.memberships, fn m -> m.role == :owner end) - Enum.map(memberships, fn m -> m.user.email end) |> Enum.join(", ") - end - - defp format_date(date) do - Timex.format!(date, "{Mshort} {D}, {YYYY}") - end -end diff --git a/lib/plausible/site/custom_domain.ex b/lib/plausible/site/custom_domain.ex deleted file mode 100644 index 325b72bb0470..000000000000 --- a/lib/plausible/site/custom_domain.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Plausible.Site.CustomDomain do - use Ecto.Schema - - schema "custom_domains" do - field :domain, :string - field :has_ssl_certificate, :boolean - belongs_to :site, Plausible.Site - - timestamps() - end -end diff --git a/lib/plausible/site/google_auth.ex b/lib/plausible/site/google_auth.ex deleted file mode 100644 index d19b9d5a9589..000000000000 --- a/lib/plausible/site/google_auth.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Plausible.Site.GoogleAuth do - use Ecto.Schema - import Ecto.Changeset - - schema "google_auth" do - field :email, :string - field :property, :string - field :refresh_token, :string - field :access_token, :string - field :expires, :naive_datetime - - belongs_to :user, Plausible.Auth.User - belongs_to :site, Plausible.Site - - timestamps() - end - - def changeset(auth, attrs \\ %{}) do - auth - |> cast(attrs, [:refresh_token, :access_token, :expires, :email, :user_id, :site_id]) - |> validate_required([:refresh_token, :access_token, :expires, :email, :user_id, :site_id]) - |> unique_constraint(:site) - end - - def set_property(auth, attrs \\ %{}) do - auth - |> cast(attrs, [:property]) - end -end diff --git a/lib/plausible/site/monthly_report.ex b/lib/plausible/site/monthly_report.ex deleted file mode 100644 index e6069ff13968..000000000000 --- a/lib/plausible/site/monthly_report.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Plausible.Site.MonthlyReport do - use Ecto.Schema - import Ecto.Changeset - - schema "monthly_reports" do - field :recipients, {:array, :string} - belongs_to :site, Plausible.Site - - timestamps() - end - - def changeset(settings, attrs \\ %{}) do - settings - |> cast(attrs, [:site_id, :recipients]) - |> validate_required([:site_id, :recipients]) - |> unique_constraint(:site) - end - - def add_recipient(report, recipient) do - report - |> change(recipients: report.recipients ++ [recipient]) - end - - def remove_recipient(report, recipient) do - report - |> change(recipients: List.delete(report.recipients, recipient)) - end -end diff --git a/lib/plausible/site/schema.ex b/lib/plausible/site/schema.ex deleted file mode 100644 index 8dee788c0b5c..000000000000 --- a/lib/plausible/site/schema.ex +++ /dev/null @@ -1,110 +0,0 @@ -defmodule Plausible.Site.ImportedData do - use Ecto.Schema - - embedded_schema do - field :end_date, :date - field :source, :string - field :status, :string - end -end - -defmodule Plausible.Site do - use Ecto.Schema - import Ecto.Changeset - alias Plausible.Auth.User - alias Plausible.Site.GoogleAuth - - @derive {Jason.Encoder, only: [:domain, :timezone]} - schema "sites" do - field :domain, :string - field :timezone, :string, default: "Etc/UTC" - field :public, :boolean - field :locked, :boolean - field :has_stats, :boolean - - embeds_one :imported_data, Plausible.Site.ImportedData, on_replace: :update - - many_to_many :members, User, join_through: Plausible.Site.Membership - has_many :memberships, Plausible.Site.Membership - has_many :invitations, Plausible.Auth.Invitation - has_one :google_auth, GoogleAuth - has_one :weekly_report, Plausible.Site.WeeklyReport - has_one :monthly_report, Plausible.Site.MonthlyReport - has_one :custom_domain, Plausible.Site.CustomDomain - has_one :spike_notification, Plausible.Site.SpikeNotification - - timestamps() - end - - def changeset(site, attrs \\ %{}) do - site - |> cast(attrs, [:domain, :timezone]) - |> validate_required([:domain, :timezone]) - |> validate_format(:domain, ~r/^[a-zA-Z0-9\-\.\/\:]*$/, - message: "only letters, numbers, slashes and period allowed" - ) - |> unique_constraint(:domain, - message: - "This domain has already been taken. Perhaps one of your team members registered it? If that's not the case, please contact support@plausible.io" - ) - |> clean_domain - end - - def make_public(site) do - change(site, public: true) - end - - def make_private(site) do - change(site, public: false) - end - - def set_has_stats(site, has_stats_val) do - change(site, has_stats: has_stats_val) - end - - def start_import(site, imported_source, status \\ "importing") do - change(site, - imported_data: %Plausible.Site.ImportedData{ - end_date: Timex.today(), - source: imported_source, - status: status - } - ) - end - - def import_success(site) do - change(site, imported_data: %{status: "ok"}) - end - - def import_failure(site) do - change(site, imported_data: %{status: "error"}) - end - - def set_imported_source(site, imported_source) do - change(site, - imported_data: %Plausible.Site.ImportedData{ - end_date: Timex.today(), - source: imported_source - } - ) - end - - def remove_imported_data(site) do - change(site, imported_data: nil) - end - - defp clean_domain(changeset) do - clean_domain = - (get_field(changeset, :domain) || "") - |> String.trim() - |> String.replace_leading("http://", "") - |> String.replace_leading("https://", "") - |> String.replace_leading("www.", "") - |> String.replace_trailing("/", "") - |> String.downcase() - - change(changeset, %{ - domain: clean_domain - }) - end -end diff --git a/lib/plausible/site/shared_link.ex b/lib/plausible/site/shared_link.ex deleted file mode 100644 index e01c582f8b50..000000000000 --- a/lib/plausible/site/shared_link.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Plausible.Site.SharedLink do - use Ecto.Schema - import Ecto.Changeset - - schema "shared_links" do - belongs_to :site, Plausible.Site - field :name, :string - field :slug, :string - field :password_hash, :string - field :password, :string, virtual: true - - timestamps() - end - - def changeset(link, attrs \\ %{}) do - link - |> cast(attrs, [:slug, :password, :name]) - |> validate_required([:slug, :name]) - |> unique_constraint(:slug) - |> unique_constraint(:name, name: :shared_links_site_id_name_index) - |> hash_password() - end - - defp hash_password(link) do - case link.changes[:password] do - nil -> - link - - password -> - hash = Plausible.Auth.Password.hash(password) - change(link, password_hash: hash) - end - end -end diff --git a/lib/plausible/site/site.ex b/lib/plausible/site/site.ex new file mode 100644 index 000000000000..99ae26ef46b1 --- /dev/null +++ b/lib/plausible/site/site.ex @@ -0,0 +1,18 @@ +defmodule Plausible.Site do + use Ecto.Schema + alias Plausible.Auth.User + + @derive {Jason.Encoder, only: [:domain, :timezone]} + schema "sites" do + field :domain, :string + field :timezone, :string, default: "Etc/UTC" + field :public, :boolean + field :locked, :boolean + field :has_stats, :boolean + + many_to_many :members, User, join_through: Plausible.Site.Membership + has_many :memberships, Plausible.Site.Membership + + timestamps() + end +end diff --git a/lib/plausible/site/spike_notification.ex b/lib/plausible/site/spike_notification.ex deleted file mode 100644 index c37692da23ac..000000000000 --- a/lib/plausible/site/spike_notification.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule Plausible.Site.SpikeNotification do - use Ecto.Schema - import Ecto.Changeset - - schema "spike_notifications" do - field :recipients, {:array, :string} - field :threshold, :integer - field :last_sent, :naive_datetime - belongs_to :site, Plausible.Site - - timestamps() - end - - def changeset(schema, attrs) do - schema - |> cast(attrs, [:site_id, :recipients, :threshold]) - |> validate_required([:site_id, :recipients, :threshold]) - |> unique_constraint(:site_id) - end - - def add_recipient(schema, recipient) do - schema - |> change(recipients: schema.recipients ++ [recipient]) - end - - def remove_recipient(schema, recipient) do - schema - |> change(recipients: List.delete(schema.recipients, recipient)) - end - - def was_sent(schema) do - schema - |> change(last_sent: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)) - end -end diff --git a/lib/plausible/site/weekly_report.ex b/lib/plausible/site/weekly_report.ex deleted file mode 100644 index 80f533cfe914..000000000000 --- a/lib/plausible/site/weekly_report.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Plausible.Site.WeeklyReport do - use Ecto.Schema - import Ecto.Changeset - - schema "weekly_reports" do - field :recipients, {:array, :string} - belongs_to :site, Plausible.Site - - timestamps() - end - - def changeset(settings, attrs \\ %{}) do - settings - |> cast(attrs, [:site_id, :recipients]) - |> validate_required([:site_id, :recipients]) - |> unique_constraint(:site) - end - - def add_recipient(report, recipient) do - report - |> change(recipients: report.recipients ++ [recipient]) - end - - def remove_recipient(report, recipient) do - report - |> change(recipients: List.delete(report.recipients, recipient)) - end -end diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index 357ad4df806a..1ddea35893c3 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -1,95 +1,7 @@ defmodule Plausible.Sites do use Plausible.Repo - alias Plausible.Site.SharedLink - def create(user, params) do - count = Enum.count(owned_by(user)) - limit = Plausible.Billing.sites_limit(user) - - if count >= limit do - {:error, :limit, limit} - else - site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params) - - Ecto.Multi.new() - |> Ecto.Multi.insert(:site, site_changeset) - |> Ecto.Multi.run(:site_membership, fn repo, %{site: site} -> - membership_changeset = - Plausible.Site.Membership.changeset(%Plausible.Site.Membership{}, %{ - site_id: site.id, - user_id: user.id - }) - - repo.insert(membership_changeset) - end) - |> maybe_start_trial(user) - |> Repo.transaction() - end - end - - def create(user, params, force: true) do - site_changeset = Plausible.Site.changeset(%Plausible.Site{}, params) - - Ecto.Multi.new() - |> Ecto.Multi.insert(:site, site_changeset) - |> Ecto.Multi.run(:site_membership, fn repo, %{site: site} -> - membership_changeset = - Plausible.Site.Membership.changeset(%Plausible.Site.Membership{}, %{ - site_id: site.id, - user_id: user.id - }) - - repo.insert(membership_changeset) - end) - |> Repo.transaction() - end - - defp maybe_start_trial(multi, user) do - case user.trial_expiry_date do - nil -> - changeset = Plausible.Auth.User.start_trial(user) - Ecto.Multi.update(multi, :user, changeset) - - _ -> - multi - end - end - - def has_stats?(site) do - if site.has_stats do - true - else - has_stats = Plausible.Stats.Clickhouse.has_pageviews?(site) - - if has_stats do - Plausible.Site.set_has_stats(site, true) - |> Repo.update() - - true - else - false - end - end - end - - def create_shared_link(site, name, password \\ nil) do - changes = - SharedLink.changeset( - %SharedLink{ - site_id: site.id, - slug: Nanoid.generate() - }, - %{name: name, password: password} - ) - - Repo.insert(changes) - end - - def shared_link_url(site, link) do - base = PlausibleWeb.Endpoint.url() - domain = "/share/#{URI.encode_www_form(site.domain)}" - base <> domain <> "?auth=" <> link.slug - end + def has_stats?(_site), do: true def get_for_user!(user_id, domain, roles \\ [:owner, :admin, :viewer]), do: Repo.one!(get_for_user_q(user_id, domain, roles)) @@ -108,13 +20,6 @@ defmodule Plausible.Sites do ) end - def has_goals?(site) do - Repo.exists?( - from g in Plausible.Goal, - where: g.domain == ^site.domain - ) - end - def is_member?(user_id, site) do role(user_id, site) !== nil end @@ -161,9 +66,4 @@ defmodule Plausible.Sites do where: sm.role == :owner ) end - - def delete!(site) do - Repo.delete!(site) - Plausible.ClickhouseRepo.clear_stats_for(site.domain) - end end diff --git a/lib/plausible/slack.ex b/lib/plausible/slack.ex deleted file mode 100644 index 5c648aa3d119..000000000000 --- a/lib/plausible/slack.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Plausible.Slack do - require Logger - - def notify(text) do - Task.start(fn -> - if env() == "prod" && !self_hosted() do - HTTPoison.post!(webhook_url(), Jason.encode!(%{text: text})) - else - Logger.debug(text) - end - end) - end - - defp webhook_url() do - Keyword.fetch!(Application.get_env(:plausible, :slack), :webhook) - end - - defp env() do - Application.get_env(:plausible, :environment) - end - - defp self_hosted() do - Application.get_env(:plausible, :is_selfhost) - end -end diff --git a/lib/plausible/stats/aggregate.ex b/lib/plausible/stats/aggregate.ex index d99aca36a13c..b79fc521c601 100644 --- a/lib/plausible/stats/aggregate.ex +++ b/lib/plausible/stats/aggregate.ex @@ -1,7 +1,7 @@ defmodule Plausible.Stats.Aggregate do alias Plausible.Stats.Query use Plausible.ClickhouseRepo - import Plausible.Stats.{Base, Imported} + import Plausible.Stats.Base @event_metrics [:visitors, :pageviews, :events, :sample_percent] @session_metrics [:visits, :bounce_rate, :visit_duration, :sample_percent] @@ -53,7 +53,6 @@ defmodule Plausible.Stats.Aggregate do defp aggregate_events(site, query, metrics) do from(e in base_event_query(site, query), select: %{}) |> select_event_metrics(metrics) - |> merge_imported(site, query, :aggregate, metrics) |> ClickhouseRepo.one() end @@ -73,7 +72,6 @@ defmodule Plausible.Stats.Aggregate do from(e in query_sessions(site, query), select: %{}) |> filter_converted_sessions(site, query) |> select_session_metrics(metrics) - |> merge_imported(site, query, :aggregate, metrics) |> ClickhouseRepo.one() end diff --git a/lib/plausible/stats/breakdown.ex b/lib/plausible/stats/breakdown.ex index ccef6a49a72d..cdf85595a194 100644 --- a/lib/plausible/stats/breakdown.ex +++ b/lib/plausible/stats/breakdown.ex @@ -1,6 +1,6 @@ defmodule Plausible.Stats.Breakdown do use Plausible.ClickhouseRepo - import Plausible.Stats.{Base, Imported} + import Plausible.Stats.Base alias Plausible.Stats.Query @no_ref "Direct / None" @@ -9,9 +9,7 @@ defmodule Plausible.Stats.Breakdown do @event_props ["event:page", "event:page_match", "event:name"] def breakdown(site, query, "event:goal", metrics, pagination) do - {event_goals, pageview_goals} = - Plausible.Repo.all(from g in Plausible.Goal, where: g.domain == ^site.domain) - |> Enum.split_with(fn goal -> goal.event_name end) + {event_goals, pageview_goals} = {[], []} events = Enum.map(event_goals, & &1.event_name) event_query = %Query{query | filters: Map.put(query.filters, "event:name", {:member, events})} @@ -202,7 +200,6 @@ defmodule Plausible.Stats.Breakdown do |> filter_converted_sessions(site, query) |> do_group_by(property) |> select_session_metrics(metrics) - |> merge_imported(site, query, property, metrics) |> apply_pagination(pagination) |> ClickhouseRepo.all() |> transform_keys(%{operating_system: :os}) @@ -217,7 +214,6 @@ defmodule Plausible.Stats.Breakdown do ) |> do_group_by(property) |> select_event_metrics(metrics) - |> merge_imported(site, query, property, metrics) |> apply_pagination(pagination) |> ClickhouseRepo.all() |> transform_keys(%{operating_system: :os}) @@ -244,12 +240,7 @@ defmodule Plausible.Stats.Breakdown do {base_query_raw, base_query_raw_params} = ClickhouseRepo.to_sql(:all, q) - select = - if query.include_imported do - "sum(td), count(case when p2 != p then 1 end)" - else - "round(sum(td)/count(case when p2 != p then 1 end))" - end + select = "round(sum(td)/count(case when p2 != p then 1 end))" time_query = " SELECT @@ -273,38 +264,7 @@ defmodule Plausible.Stats.Breakdown do {:ok, res} = ClickhouseRepo.query(time_query, base_query_raw_params ++ [pages]) - if query.include_imported do - # Imported page views have pre-calculated values - res = - res.rows - |> Enum.map(fn [page, time, visits] -> {page, {time, visits}} end) - |> Enum.into(%{}) - - from( - i in "imported_pages", - group_by: i.page, - where: i.site_id == ^site.id, - where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last, - where: i.page in ^pages, - select: %{ - page: i.page, - pageviews: fragment("sum(?) - sum(?)", i.pageviews, i.exits), - time_on_page: sum(i.time_on_page) - } - ) - |> ClickhouseRepo.all() - |> Enum.reduce(res, fn %{page: page, pageviews: pageviews, time_on_page: time}, res -> - {restime, resviews} = Map.get(res, page, {0, 0}) - Map.put(res, page, {restime + time, resviews + pageviews}) - end) - |> Enum.map(fn - {page, {_, 0}} -> {page, nil} - {page, {time, pageviews}} -> {page, time / pageviews} - end) - |> Enum.into(%{}) - else - res.rows |> Enum.map(fn [page, time] -> {page, time} end) |> Enum.into(%{}) - end + res.rows |> Enum.map(fn [page, time] -> {page, time} end) |> Enum.into(%{}) end defp do_group_by( diff --git a/lib/plausible/stats/filter_suggestions.ex b/lib/plausible/stats/filter_suggestions.ex index 569a3ceeee5f..a214dd6e67c8 100644 --- a/lib/plausible/stats/filter_suggestions.ex +++ b/lib/plausible/stats/filter_suggestions.ex @@ -116,16 +116,7 @@ defmodule Plausible.Stats.FilterSuggestions do end) end - def filter_suggestions(site, _query, "goal", filter_search) do - Repo.all(from g in Plausible.Goal, where: g.domain == ^site.domain) - |> Enum.map(fn x -> if x.event_name, do: x.event_name, else: "Visit #{x.page_path}" end) - |> Enum.filter(fn goal -> - String.contains?( - String.downcase(goal), - String.downcase(filter_search) - ) - end) - end + def filter_suggestions(_site, _query, "goal", _filter_search), do: [] def filter_suggestions(site, query, filter_name, filter_search) do filter_search = if filter_search == nil, do: "", else: filter_search diff --git a/lib/plausible/stats/imported.ex b/lib/plausible/stats/imported.ex deleted file mode 100644 index f39e8a27be57..000000000000 --- a/lib/plausible/stats/imported.ex +++ /dev/null @@ -1,434 +0,0 @@ -defmodule Plausible.Stats.Imported do - use Plausible.ClickhouseRepo - alias Plausible.Stats.Query - import Ecto.Query - - @no_ref "Direct / None" - - def merge_imported_timeseries(native_q, _, %Plausible.Stats.Query{include_imported: false}, _), - do: native_q - - def merge_imported_timeseries( - native_q, - site, - query, - metrics - ) do - imported_q = - from(v in "imported_visitors", - where: v.site_id == ^site.id, - where: v.date >= ^query.date_range.first and v.date <= ^query.date_range.last, - select: %{visitors: sum(v.visitors)} - ) - |> apply_interval(query) - - from(s in Ecto.Query.subquery(native_q), - full_join: i in subquery(imported_q), - on: field(s, :date) == field(i, :date) - ) - |> select_joined_metrics(metrics) - end - - defp apply_interval(imported_q, %Plausible.Stats.Query{interval: "month"}) do - imported_q - |> group_by([i], fragment("toStartOfMonth(?)", i.date)) - |> select_merge([i], %{date: fragment("toStartOfMonth(?)", i.date)}) - end - - defp apply_interval(imported_q, _query) do - imported_q - |> group_by([i], i.date) - |> select_merge([i], %{date: i.date}) - end - - def merge_imported(q, _, %Query{include_imported: false}, _, _), do: q - def merge_imported(q, _, _, _, [:events | _]), do: q - # GA only has 'source' - def merge_imported(q, _, _, "utm_source", _), do: q - - def merge_imported(q, site, query, property, metrics) - when property in [ - "visit:source", - "visit:utm_medium", - "visit:utm_campaign", - "visit:utm_term", - "visit:utm_content", - "visit:entry_page", - "visit:exit_page", - "visit:country", - "visit:region", - "visit:city", - "visit:device", - "visit:browser", - "visit:os", - "event:page" - ] do - {table, dim} = - case property do - "visit:country" -> - {"imported_locations", :country} - - "visit:region" -> - {"imported_locations", :region} - - "visit:city" -> - {"imported_locations", :city} - - "visit:utm_medium" -> - {"imported_sources", :utm_medium} - - "visit:utm_campaign" -> - {"imported_sources", :utm_campaign} - - "visit:utm_term" -> - {"imported_sources", :utm_term} - - "visit:utm_content" -> - {"imported_sources", :utm_content} - - "visit:os" -> - {"imported_operating_systems", :operating_system} - - "event:page" -> - {"imported_pages", :page} - - _ -> - dim = String.trim_leading(property, "visit:") - {"imported_#{dim}s", String.to_existing_atom(dim)} - end - - imported_q = - from( - i in table, - group_by: field(i, ^dim), - where: i.site_id == ^site.id, - where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last, - select: %{} - ) - |> select_imported_metrics(metrics) - - imported_q = - case query.filters[property] do - {:is_not, value} -> - value = if value == @no_ref, do: "", else: value - where(imported_q, [i], field(i, ^dim) != ^value) - - {:member, list} -> - where(imported_q, [i], field(i, ^dim) in ^list) - - _ -> - imported_q - end - - imported_q = - case dim do - :source -> - imported_q - |> select_merge([i], %{ - source: fragment("if(empty(?), ?, ?)", i.source, @no_ref, i.source) - }) - - :utm_medium -> - imported_q - |> select_merge([i], %{ - utm_medium: fragment("if(empty(?), ?, ?)", i.utm_medium, @no_ref, i.utm_medium) - }) - - :utm_source -> - imported_q - |> select_merge([i], %{ - utm_source: fragment("if(empty(?), ?, ?)", i.utm_source, @no_ref, i.utm_source) - }) - - :utm_campaign -> - imported_q - |> select_merge([i], %{ - utm_campaign: fragment("if(empty(?), ?, ?)", i.utm_campaign, @no_ref, i.utm_campaign) - }) - - :utm_term -> - imported_q - |> select_merge([i], %{ - utm_term: fragment("if(empty(?), ?, ?)", i.utm_term, @no_ref, i.utm_term) - }) - - :utm_content -> - imported_q - |> select_merge([i], %{ - utm_content: fragment("if(empty(?), ?, ?)", i.utm_content, @no_ref, i.utm_content) - }) - - :page -> - imported_q - |> select_merge([i], %{ - page: i.page, - time_on_page: sum(i.time_on_page) - }) - - :entry_page -> - imported_q - |> select_merge([i], %{ - entry_page: i.entry_page, - visits: sum(i.entrances) - }) - - :exit_page -> - imported_q - |> select_merge([i], %{exit_page: i.exit_page, visits: sum(i.exits)}) - - :country -> - imported_q |> select_merge([i], %{country: i.country}) - - :region -> - imported_q |> select_merge([i], %{region: i.region}) - - :city -> - imported_q |> select_merge([i], %{city: i.city}) - - :device -> - imported_q |> select_merge([i], %{device: i.device}) - - :browser -> - imported_q |> select_merge([i], %{browser: i.browser}) - - :operating_system -> - imported_q |> select_merge([i], %{operating_system: i.operating_system}) - end - - q = - from(s in Ecto.Query.subquery(q), - full_join: i in subquery(imported_q), - on: field(s, ^dim) == field(i, ^dim) - ) - |> select_joined_metrics(metrics) - |> apply_order_by(metrics) - - case dim do - :source -> - q - |> select_merge([s, i], %{ - source: fragment("if(empty(?), ?, ?)", s.source, i.source, s.source) - }) - - :utm_medium -> - q - |> select_merge([s, i], %{ - utm_medium: fragment("if(empty(?), ?, ?)", s.utm_medium, i.utm_medium, s.utm_medium) - }) - - :utm_source -> - q - |> select_merge([s, i], %{ - utm_source: fragment("if(empty(?), ?, ?)", s.utm_source, i.utm_source, s.utm_source) - }) - - :utm_campaign -> - q - |> select_merge([s, i], %{ - utm_campaign: - fragment("if(empty(?), ?, ?)", s.utm_campaign, i.utm_campaign, s.utm_campaign) - }) - - :utm_term -> - q - |> select_merge([s, i], %{ - utm_term: fragment("if(empty(?), ?, ?)", s.utm_term, i.utm_term, s.utm_term) - }) - - :utm_content -> - q - |> select_merge([s, i], %{ - utm_content: fragment("if(empty(?), ?, ?)", s.utm_content, i.utm_content, s.utm_content) - }) - - :page -> - q - |> select_merge([s, i], %{ - page: fragment("if(empty(?), ?, ?)", i.page, s.page, i.page) - }) - - :entry_page -> - q - |> select_merge([s, i], %{ - entry_page: fragment("if(empty(?), ?, ?)", i.entry_page, s.entry_page, i.entry_page), - visits: fragment("? + ?", s.visits, i.visits) - }) - - :exit_page -> - q - |> select_merge([s, i], %{ - exit_page: fragment("if(empty(?), ?, ?)", i.exit_page, s.exit_page, i.exit_page), - visits: fragment("coalesce(?, 0) + coalesce(?, 0)", s.visits, i.visits) - }) - - :country -> - q - |> select_merge([i, s], %{ - country: fragment("if(empty(?), ?, ?)", s.country, i.country, s.country) - }) - - :region -> - q - |> select_merge([i, s], %{ - region: fragment("if(empty(?), ?, ?)", s.region, i.region, s.region) - }) - - :city -> - q - |> select_merge([i, s], %{ - city: fragment("coalesce(?, ?)", s.city, i.city) - }) - - :device -> - q - |> select_merge([i, s], %{ - device: fragment("if(empty(?), ?, ?)", s.device, i.device, s.device) - }) - - :browser -> - q - |> select_merge([i, s], %{ - browser: fragment("if(empty(?), ?, ?)", s.browser, i.browser, s.browser) - }) - - :operating_system -> - q - |> select_merge([i, s], %{ - operating_system: - fragment( - "if(empty(?), ?, ?)", - s.operating_system, - i.operating_system, - s.operating_system - ) - }) - end - end - - def merge_imported(q, site, query, :aggregate, metrics) do - imported_q = - from( - i in "imported_visitors", - where: i.site_id == ^site.id, - where: i.date >= ^query.date_range.first and i.date <= ^query.date_range.last, - select: %{} - ) - |> select_imported_metrics(metrics) - - from( - s in subquery(q), - cross_join: i in subquery(imported_q), - select: %{} - ) - |> select_joined_metrics(metrics) - end - - def merge_imported(q, _, _, _, _), do: q - - defp select_imported_metrics(q, []), do: q - - defp select_imported_metrics(q, [:visitors | rest]) do - q - |> select_merge([i], %{visitors: sum(i.visitors)}) - |> select_imported_metrics(rest) - end - - defp select_imported_metrics(q, [:pageviews | rest]) do - q - |> select_merge([i], %{pageviews: sum(i.pageviews)}) - |> select_imported_metrics(rest) - end - - defp select_imported_metrics(q, [:bounce_rate | rest]) do - q - |> select_merge([i], %{ - bounces: sum(i.bounces), - visits: sum(i.visits) - }) - |> select_imported_metrics(rest) - end - - defp select_imported_metrics(q, [:visit_duration | rest]) do - q - |> select_merge([i], %{visit_duration: sum(i.visit_duration)}) - |> select_imported_metrics(rest) - end - - defp select_imported_metrics(q, [_ | rest]) do - q - |> select_imported_metrics(rest) - end - - defp select_joined_metrics(q, []), do: q - # TODO: Reverse-engineering the native data bounces and total visit - # durations to combine with imported data is inefficient. Instead both - # queries should fetch bounces/total_visit_duration and visits and be - # used as subqueries to a main query that then find the bounce rate/avg - # visit_duration. - - defp select_joined_metrics(q, [:visitors | rest]) do - q - |> select_merge([s, i], %{ - :visitors => fragment("coalesce(?, 0) + coalesce(?, 0)", s.visitors, i.visitors) - }) - |> select_joined_metrics(rest) - end - - defp select_joined_metrics(q, [:pageviews | rest]) do - q - |> select_merge([s, i], %{ - pageviews: fragment("coalesce(?, 0) + coalesce(?, 0)", s.pageviews, i.pageviews) - }) - |> select_joined_metrics(rest) - end - - defp select_joined_metrics(q, [:bounce_rate | rest]) do - q - |> select_merge([s, i], %{ - bounce_rate: - fragment( - "round(100 * (coalesce(?, 0) + coalesce((? * ? / 100), 0)) / (coalesce(?, 0) + coalesce(?, 0)))", - i.bounces, - s.bounce_rate, - s.visits, - i.visits, - s.visits - ) - }) - |> select_joined_metrics(rest) - end - - defp select_joined_metrics(q, [:visit_duration | rest]) do - q - |> select_merge([s, i], %{ - visit_duration: - fragment( - "(? + ? * ?) / (? + ?)", - i.visit_duration, - s.visit_duration, - s.visits, - s.visits, - i.visits - ) - }) - |> select_joined_metrics(rest) - end - - defp select_joined_metrics(q, [:sample_percent | rest]) do - q - |> select_merge([s, i], %{sample_percent: s.sample_percent}) - |> select_joined_metrics(rest) - end - - defp select_joined_metrics(q, [_ | rest]) do - q - |> select_joined_metrics(rest) - end - - defp apply_order_by(q, [:visitors | rest]) do - order_by(q, [s, i], desc: fragment("coalesce(?, 0) + coalesce(?, 0)", s.visitors, i.visitors)) - |> apply_order_by(rest) - end - - defp apply_order_by(q, _), do: q -end diff --git a/lib/plausible/stats/query.ex b/lib/plausible/stats/query.ex index 1e742d46943b..a29269ad560e 100644 --- a/lib/plausible/stats/query.ex +++ b/lib/plausible/stats/query.ex @@ -3,8 +3,7 @@ defmodule Plausible.Stats.Query do interval: nil, period: nil, filters: %{}, - sample_threshold: 20_000_000, - include_imported: false + sample_threshold: 20_000_000 @default_sample_threshold 20_000_000 @@ -46,8 +45,7 @@ defmodule Plausible.Stats.Query do interval: "minute", date_range: Date.range(date, date), filters: parse_filters(params), - sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold), - include_imported: false + sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } end @@ -73,7 +71,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(%{domain: "all"} = site, %{"period" => "7d"} = params) do @@ -100,7 +97,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(%{domain: "all"} = site, %{"period" => "30d"} = params) do @@ -127,7 +123,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(%{domain: "all"} = site, %{"period" => "month"} = params) do @@ -158,7 +153,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(%{domain: "all"} = site, %{"period" => "6mo"} = params) do @@ -195,7 +189,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(%{domain: "all"} = site, %{"period" => "12mo"} = params) do @@ -232,7 +225,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(site, %{"period" => "custom", "from" => from, "to" => to} = params) do @@ -259,7 +251,7 @@ defmodule Plausible.Stats.Query do } end - def from(site, %{"period" => "custom", "date" => date} = params) do + def from(_site, %{"period" => "custom", "date" => date} = params) do [from, to] = String.split(date, ",") from_date = Date.from_iso8601!(String.trim(from)) to_date = Date.from_iso8601!(String.trim(to)) @@ -271,7 +263,6 @@ defmodule Plausible.Stats.Query do filters: parse_filters(params), sample_threshold: Map.get(params, "sample_threshold", @default_sample_threshold) } - |> maybe_include_imported(site, params) end def from(tz, params) do @@ -359,19 +350,4 @@ defmodule Plausible.Stats.Query do defp parse_goal_filter("Visit " <> page), do: {:is, :page, page} defp parse_goal_filter(event), do: {:is, :event, event} - - defp maybe_include_imported(query, site, params) do - imported_data_requested = params["with_imported"] == "true" - has_imported_data = site.imported_data && site.imported_data.status == "ok" - - date_range_overlaps = - has_imported_data && !Timex.after?(query.date_range.first, site.imported_data.end_date) - - no_filters_applied = Enum.empty?(query.filters) - - include_imported = - imported_data_requested && has_imported_data && date_range_overlaps && no_filters_applied - - %{query | include_imported: !!include_imported} - end end diff --git a/lib/plausible/stats/timeseries.ex b/lib/plausible/stats/timeseries.ex index 694a7dd21ace..a8f0bf837639 100644 --- a/lib/plausible/stats/timeseries.ex +++ b/lib/plausible/stats/timeseries.ex @@ -34,7 +34,6 @@ defmodule Plausible.Stats.Timeseries do from(e in base_event_query(site, query), select: %{}) |> select_bucket(site, query) |> select_event_metrics(metrics) - |> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics) |> ClickhouseRepo.all() end @@ -46,7 +45,6 @@ defmodule Plausible.Stats.Timeseries do from(e in query_sessions(site, query), select: %{}) |> select_bucket(site, query) |> select_session_metrics(metrics) - |> Plausible.Stats.Imported.merge_imported_timeseries(site, query, metrics) |> ClickhouseRepo.all() end diff --git a/lib/plausible_release.ex b/lib/plausible_release.ex index 69fde22d4d57..ed8b31474bc3 100644 --- a/lib/plausible_release.ex +++ b/lib/plausible_release.ex @@ -1,74 +1,4 @@ defmodule Plausible.Release do - use Plausible.Repo - @app :plausible - @start_apps [ - :postgrex, - :clickhousex, - :ecto - ] - - def init_admin do - prepare() - - {admin_email, admin_user, admin_pwd} = - validate_admin( - {Application.get_env(:plausible, :admin_email), - Application.get_env(:plausible, :admin_user), - Application.get_env(:plausible, :admin_pwd)} - ) - - case Plausible.Auth.find_user_by(email: admin_email) do - nil -> - {:ok, _} = Plausible.Auth.create_user(admin_user, admin_email, admin_pwd) - IO.puts("Admin user created successful!") - - _ -> - IO.puts("Admin user already exists. I won't override, bailing") - end - end - - def migrate do - prepare() - Enum.each(repos(), &run_migrations_for/1) - IO.puts("Migrations successful!") - end - - def seed do - prepare() - # Run seed script - Enum.each(repos(), &run_seeds_for/1) - # Signal shutdown - IO.puts("Success!") - end - - def createdb do - prepare() - - for repo <- repos() do - :ok = ensure_repo_created(repo) - end - - IO.puts("Creation of Db successful!") - end - - def rollback do - prepare() - - get_step = - IO.gets("Enter the number of steps: ") - |> String.trim() - |> Integer.parse() - - case get_step do - {int, _trailing} -> - Enum.each(repos(), fn repo -> run_rollbacks_for(repo, int) end) - IO.puts("Rollback successful!") - - :error -> - IO.puts("Invalid integer") - end - end - def configure_ref_inspector() do priv_dir = Application.app_dir(:plausible, "priv/ref_inspector") Application.put_env(:ref_inspector, :database_path, priv_dir) @@ -78,80 +8,4 @@ defmodule Plausible.Release do priv_dir = Application.app_dir(:plausible, "priv/ua_inspector") Application.put_env(:ua_inspector, :database_path, priv_dir) end - - ############################## - - defp validate_admin({nil, nil, nil}) do - random_user = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8) - random_pwd = :crypto.strong_rand_bytes(20) |> Base.encode64() |> binary_part(0, 20) - random_email = "#{random_user}@#{System.get_env("HOST")}" - IO.puts("generated admin user/password: #{random_email} / #{random_pwd}") - {random_email, random_user, random_pwd} - end - - defp validate_admin({admin_email, admin_user, admin_password}) do - {admin_email, admin_user, admin_password} - end - - defp repos do - Application.fetch_env!(@app, :ecto_repos) - end - - defp run_seeds_for(repo) do - # Run the seed script if it exists - seed_script = seeds_path(repo) - - if File.exists?(seed_script) do - IO.puts("Running seed script..") - Code.eval_file(seed_script) - end - end - - defp run_migrations_for(repo) do - IO.puts("Running migrations for #{repo}") - {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) - end - - defp ensure_repo_created(repo) do - IO.puts("create #{inspect(repo)} database if it doesn't exist") - - case repo.__adapter__.storage_up(repo.config) do - :ok -> :ok - {:error, :already_up} -> :ok - {:error, term} -> {:error, term} - end - end - - defp run_rollbacks_for(repo, step) do - app = Keyword.get(repo.config, :otp_app) - IO.puts("Running rollbacks for #{app} (STEP=#{step})") - - {:ok, _, _} = - Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, all: false, step: step)) - end - - defp prepare do - IO.puts("Loading #{@app}..") - # Load the code for myapp, but don't start it - :ok = Application.load(@app) - - IO.puts("Starting dependencies..") - # Start apps necessary for executing migrations - Enum.each(@start_apps, &Application.ensure_all_started/1) - - # Start the Repo(s) for myapp - IO.puts("Starting repos..") - Enum.each(repos(), & &1.start_link(pool_size: 2)) - end - - defp seeds_path(repo), do: priv_path_for(repo, "seeds.exs") - - defp priv_path_for(repo, filename) do - app = Keyword.get(repo.config, :otp_app) - IO.puts("App: #{app}") - repo_underscore = repo |> Module.split() |> List.last() |> Macro.underscore() - Path.join([priv_dir(app), repo_underscore, filename]) - end - - defp priv_dir(app), do: "#{:code.priv_dir(app)}" end diff --git a/lib/plausible_web/captcha.ex b/lib/plausible_web/captcha.ex deleted file mode 100644 index 397687f575ee..000000000000 --- a/lib/plausible_web/captcha.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule PlausibleWeb.Captcha do - @verify_endpoint "https://hcaptcha.com/siteverify" - - def enabled? do - !!sitekey() - end - - def sitekey() do - Application.get_env(:plausible, :hcaptcha, []) - |> Keyword.fetch!(:sitekey) - end - - def verify(token) do - if enabled?() do - res = - HTTPoison.post!(@verify_endpoint, {:form, [{"response", token}, {"secret", secret()}]}) - - json = Jason.decode!(res.body) - json["success"] - else - true - end - end - - defp secret() do - Application.get_env(:plausible, :hcaptcha, []) - |> Keyword.fetch!(:secret) - end -end diff --git a/lib/plausible_web/controllers/api/external_controller.ex b/lib/plausible_web/controllers/api/external_controller.ex index c19c6317a5a0..868f7b3fb2dd 100644 --- a/lib/plausible_web/controllers/api/external_controller.ex +++ b/lib/plausible_web/controllers/api/external_controller.ex @@ -1,6 +1,7 @@ defmodule PlausibleWeb.Api.ExternalController do use PlausibleWeb, :controller - use OpenTelemetryDecorator + # telemetry will be brought back once we integrate the plausible codebase + # use OpenTelemetryDecorator require Logger def event(conn, _params) do @@ -46,7 +47,7 @@ defmodule PlausibleWeb.Api.ExternalController do }) end - @decorate trace("ingest.parse_user_agent") + # @decorate trace("ingest.parse_user_agent") defp parse_user_agent(conn) do user_agent = Plug.Conn.get_req_header(conn, "user-agent") |> List.first() @@ -81,7 +82,7 @@ defmodule PlausibleWeb.Api.ExternalController do ua = parse_user_agent(conn) - blacklist_domain = params["domain"] in Application.get_env(:plausible, :domain_blacklist) + blacklist_domain = params["domain"] in [] referrer_spam = is_spammer?(params["referrer"]) if is_bot?(ua) || blacklist_domain || referrer_spam do @@ -448,7 +449,7 @@ defmodule PlausibleWeb.Api.ExternalController do 2_647_694 => 2_643_743 } - @decorate trace("ingest.geolocation") + # @decorate trace("ingest.geolocation") defp visitor_location_details(conn) do ip = PlausibleWeb.RemoteIp.get(conn) @@ -505,7 +506,7 @@ defmodule PlausibleWeb.Api.ExternalController do defp ignore_unknown_country("ZZ"), do: "" defp ignore_unknown_country(country), do: country - @decorate trace("ingest.parse_referrer") + # @decorate trace("ingest.parse_referrer") defp parse_referrer(_, nil), do: nil defp parse_referrer(uri, referrer_str) do diff --git a/lib/plausible_web/controllers/api/external_sites_controller.ex b/lib/plausible_web/controllers/api/external_sites_controller.ex deleted file mode 100644 index 299195ca86ae..000000000000 --- a/lib/plausible_web/controllers/api/external_sites_controller.ex +++ /dev/null @@ -1,140 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalSitesController do - use PlausibleWeb, :controller - use Plausible.Repo - use Plug.ErrorHandler - alias Plausible.Sites - alias Plausible.Goals - alias PlausibleWeb.Api.Helpers, as: H - - def create_site(conn, params) do - user = conn.assigns[:current_user] - - case Sites.create(user, params) do - {:ok, %{site: site}} -> - json(conn, site) - - {:error, :site, changeset, _} -> - conn - |> put_status(400) - |> json(serialize_errors(changeset)) - - {:error, :limit, limit} -> - conn - |> put_status(403) - |> json(%{ - error: - "Your account has reached the limit of #{limit} sites per account. Please contact hello@plausible.io to unlock more sites." - }) - end - end - - def delete_site(conn, %{"site_id" => site_id}) do - site = Sites.get_for_user(conn.assigns[:current_user].id, site_id, [:owner]) - - if site do - Sites.delete!(site) - json(conn, %{"deleted" => true}) - else - H.not_found(conn, "Site could not be found") - end - end - - defp expect_param_key(params, key) do - case Map.fetch(params, key) do - :error -> {:missing, key} - res -> res - end - end - - def find_or_create_shared_link(conn, params) do - with {:ok, site_id} <- expect_param_key(params, "site_id"), - {:ok, link_name} <- expect_param_key(params, "name"), - site when not is_nil(site) <- - Sites.get_for_user(conn.assigns[:current_user].id, site_id, [:owner, :admin]) do - shared_link = Repo.get_by(Plausible.Site.SharedLink, site_id: site.id, name: link_name) - - shared_link = - case shared_link do - nil -> Sites.create_shared_link(site, link_name) - link -> {:ok, link} - end - - case shared_link do - {:ok, link} -> - json(conn, %{ - name: link.name, - url: Sites.shared_link_url(site, link) - }) - end - else - nil -> - H.not_found(conn, "Site could not be found") - - {:missing, "site_id"} -> - H.bad_request(conn, "Parameter `site_id` is required to create a shared link") - - {:missing, "name"} -> - H.bad_request(conn, "Parameter `name` is required to create a shared link") - - e -> - H.bad_request(conn, "Something went wrong: #{inspect(e)}") - end - end - - def find_or_create_goal(conn, params) do - with {:ok, site_id} <- expect_param_key(params, "site_id"), - {:ok, _} <- expect_param_key(params, "goal_type"), - site when not is_nil(site) <- - Sites.get_for_user(conn.assigns[:current_user].id, site_id, [:owner, :admin]), - {:ok, goal} <- Goals.find_or_create(site, params) do - json(conn, goal) - else - nil -> - H.not_found(conn, "Site could not be found") - - {:missing, param} -> - H.bad_request(conn, "Parameter `#{param}` is required to create a goal") - - e -> - H.bad_request(conn, "Something went wrong: #{inspect(e)}") - end - end - - def delete_goal(conn, params) do - with {:ok, site_id} <- expect_param_key(params, "site_id"), - {:ok, goal_id} <- expect_param_key(params, "goal_id"), - site when not is_nil(site) <- - Sites.get_for_user(conn.assigns[:current_user].id, site_id, [:owner, :admin]) do - goal = Repo.get_by(Plausible.Goal, id: goal_id) - - if goal do - Goals.delete(goal_id) - json(conn, %{"deleted" => true}) - else - H.not_found(conn, "Goal could not be found") - end - else - nil -> - H.not_found(conn, "Site could not be found") - - {:missing, "site_id"} -> - H.bad_request(conn, "Parameter `site_id` is required to delete a goal") - - {:missing, "goal_id"} -> - H.bad_request(conn, "Parameter `goal_id` is required to delete a goal") - - e -> - H.bad_request(conn, "Something went wrong: #{inspect(e)}") - end - end - - defp serialize_errors(changeset) do - {field, {msg, _opts}} = List.first(changeset.errors) - error_msg = Atom.to_string(field) <> " " <> msg - %{"error" => error_msg} - end - - def handle_errors(conn, %{kind: kind, reason: reason}) do - json(conn, %{error: Exception.format_banner(kind, reason)}) - end -end diff --git a/lib/plausible_web/controllers/api/external_stats_controller.ex b/lib/plausible_web/controllers/api/external_stats_controller.ex deleted file mode 100644 index 554a1e50919e..000000000000 --- a/lib/plausible_web/controllers/api/external_stats_controller.ex +++ /dev/null @@ -1,231 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalStatsController do - use PlausibleWeb, :controller - use Plausible.Repo - use Plug.ErrorHandler - alias Plausible.Stats.{Query, Props} - - def realtime_visitors(conn, _params) do - site = conn.assigns[:site] - query = Query.from(site, %{"period" => "realtime"}) - json(conn, Plausible.Stats.Clickhouse.current_visitors(site, query)) - end - - def aggregate(conn, params) do - site = conn.assigns[:site] - params = Map.put(params, "sample_threshold", "infinite") - - with :ok <- validate_period(params), - :ok <- validate_date(params), - query <- Query.from(site, params), - {:ok, metrics} <- parse_metrics(params, nil, query) do - results = - if params["compare"] == "previous_period" do - prev_query = Query.shift_back(query, site) - - [prev_result, curr_result] = - Task.await_many( - [ - Task.async(fn -> Plausible.Stats.aggregate(site, prev_query, metrics) end), - Task.async(fn -> Plausible.Stats.aggregate(site, query, metrics) end) - ], - 10_000 - ) - - Enum.map(curr_result, fn {metric, %{value: current_val}} -> - %{value: prev_val} = prev_result[metric] - - {metric, - %{ - value: current_val, - change: percent_change(prev_val, current_val) - }} - end) - |> Enum.into(%{}) - else - Plausible.Stats.aggregate(site, query, metrics) - end - - json(conn, %{results: Map.take(results, metrics)}) - else - {:error, msg} -> - conn - |> put_status(400) - |> json(%{error: msg}) - end - end - - def breakdown(conn, params) do - site = conn.assigns[:site] - params = Map.put(params, "sample_threshold", "infinite") - - with :ok <- validate_period(params), - :ok <- validate_date(params), - {:ok, property} <- validate_property(params), - query <- Query.from(site, params), - {:ok, metrics} <- parse_metrics(params, property, query) do - limit = String.to_integer(Map.get(params, "limit", "100")) - page = String.to_integer(Map.get(params, "page", "1")) - results = Plausible.Stats.breakdown(site, query, property, metrics, {limit, page}) - - results = - if property == "event:goal" do - prop_names = Props.props(site, query) - - Enum.map(results, fn row -> - Map.put(row, "props", prop_names[row[:goal]] || []) - end) - else - results - end - - json(conn, %{results: results}) - else - {:error, msg} -> - conn - |> put_status(400) - |> json(%{error: msg}) - end - end - - defp validate_property(%{"property" => property}) do - {:ok, property} - end - - defp validate_property(_) do - {:error, - "The `property` parameter is required. Please provide at least one property to show a breakdown by."} - end - - defp event_only_property?("event:name"), do: true - defp event_only_property?("event:props:" <> _), do: true - defp event_only_property?(_), do: false - - @event_metrics ["visitors", "pageviews", "events"] - @session_metrics ["visits", "bounce_rate", "visit_duration"] - defp parse_metrics(params, property, query) do - metrics = - Map.get(params, "metrics", "visitors") - |> String.split(",") - - event_only_filter = Map.keys(query.filters) |> Enum.find(&event_only_property?/1) - - valid_metrics = - if event_only_property?(property) || event_only_filter do - @event_metrics - else - @event_metrics ++ @session_metrics - end - - invalid_metric = Enum.find(metrics, fn metric -> metric not in valid_metrics end) - - if invalid_metric do - cond do - event_only_property?(property) && invalid_metric in @session_metrics -> - {:error, - "Session metric `#{invalid_metric}` cannot be queried for breakdown by `#{property}`."} - - event_only_filter && invalid_metric in @session_metrics -> - {:error, - "Session metric `#{invalid_metric}` cannot be queried when using a filter on `#{event_only_filter}`."} - - true -> - {:error, - "The metric `#{invalid_metric}` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#get-apiv1statsbreakdown"} - end - else - {:ok, Enum.map(metrics, &String.to_atom/1)} - end - end - - def timeseries(conn, params) do - site = conn.assigns[:site] - params = Map.put(params, "sample_threshold", "infinite") - - with :ok <- validate_period(params), - :ok <- validate_date(params), - :ok <- validate_interval(params), - query <- Query.from(site, params), - {:ok, metrics} <- parse_metrics(params, nil, query) do - graph = Plausible.Stats.timeseries(site, query, metrics) - metrics = metrics ++ [:date] - json(conn, %{results: Enum.map(graph, &Map.take(&1, metrics))}) - else - {:error, msg} -> - conn - |> put_status(400) - |> json(%{error: msg}) - end - end - - def handle_errors(conn, %{kind: kind, reason: reason}) do - json(conn, %{error: Exception.format_banner(kind, reason)}) - end - - defp percent_change(old_count, new_count) do - cond do - old_count == 0 and new_count > 0 -> - 100 - - old_count == 0 and new_count == 0 -> - 0 - - true -> - round((new_count - old_count) / old_count * 100) - end - end - - defp validate_date(%{"period" => "custom"} = params) do - with {:ok, date} <- Map.fetch(params, "date"), - [from, to] <- String.split(date, ","), - {:ok, _from} <- Date.from_iso8601(String.trim(from)), - {:ok, _to} <- Date.from_iso8601(String.trim(to)) do - :ok - else - :error -> - {:error, - "The `date` parameter is required when using a custom period. See https://plausible.io/docs/stats-api#time-periods"} - - _ -> - {:error, - "Invalid format for `date` parameter. When using a custom period, please include two ISO-8601 formatted dates joined by a comma. See https://plausible.io/docs/stats-api#time-periods"} - end - end - - defp validate_date(%{"date" => date}) do - case Date.from_iso8601(date) do - {:ok, _date} -> - :ok - - {:error, msg} -> - {:error, - "Error parsing `date` parameter: #{msg}. Please specify a valid date in ISO-8601 format."} - end - end - - defp validate_date(_), do: :ok - - defp validate_period(%{"period" => period}) do - if period in ["day", "7d", "30d", "month", "6mo", "12mo", "custom"] do - :ok - else - {:error, - "Error parsing `period` parameter: invalid period `#{period}`. Please find accepted values in our docs: https://plausible.io/docs/stats-api#time-periods"} - end - end - - defp validate_period(_), do: :ok - - @valid_intervals ["date", "month"] - @valid_intervals_str Enum.map(@valid_intervals, &("`" <> &1 <> "`")) |> Enum.join(", ") - - defp validate_interval(%{"interval" => interval}) do - if interval in @valid_intervals do - :ok - else - {:error, - "Error parsing `interval` parameter: invalid interval `#{interval}`. Valid intervals are #{@valid_intervals_str}"} - end - end - - defp validate_interval(_), do: :ok -end diff --git a/lib/plausible_web/controllers/api/internal_controller.ex b/lib/plausible_web/controllers/api/internal_controller.ex deleted file mode 100644 index 6bd858ba8e43..000000000000 --- a/lib/plausible_web/controllers/api/internal_controller.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule PlausibleWeb.Api.InternalController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Plausible.Stats.Clickhouse, as: Stats - - def domain_status(conn, %{"domain" => domain}) do - if Stats.has_pageviews?(%Plausible.Site{domain: domain}) do - json(conn, "READY") - else - json(conn, "WAITING") - end - end - - def sites(conn, _) do - if conn.assigns[:current_user] do - user = Repo.preload(conn.assigns[:current_user], :sites) - json(conn, Enum.map(user.sites, & &1.domain)) - else - conn - |> put_status(401) - |> json(%{error: "You need to be logged in to request a list of sites"}) - end - end -end diff --git a/lib/plausible_web/controllers/api/paddle_controller.ex b/lib/plausible_web/controllers/api/paddle_controller.ex deleted file mode 100644 index 5d1798f287df..000000000000 --- a/lib/plausible_web/controllers/api/paddle_controller.ex +++ /dev/null @@ -1,76 +0,0 @@ -defmodule PlausibleWeb.Api.PaddleController do - use PlausibleWeb, :controller - use Plausible.Repo - require Logger - - plug :verify_signature - - def webhook(conn, %{"alert_name" => "subscription_created"} = params) do - Plausible.Billing.subscription_created(params) - |> webhook_response(conn, params) - end - - def webhook(conn, %{"alert_name" => "subscription_updated"} = params) do - Plausible.Billing.subscription_updated(params) - |> webhook_response(conn, params) - end - - def webhook(conn, %{"alert_name" => "subscription_cancelled"} = params) do - Plausible.Billing.subscription_cancelled(params) - |> webhook_response(conn, params) - end - - def webhook(conn, %{"alert_name" => "subscription_payment_succeeded"} = params) do - Plausible.Billing.subscription_payment_succeeded(params) - |> webhook_response(conn, params) - end - - def webhook(conn, _params) do - send_resp(conn, 404, "") |> halt - end - - @paddle_key File.read!("priv/paddle.pem") - - def verify_signature(conn, _opts) do - signature = Base.decode64!(conn.params["p_signature"]) - - msg = - Map.delete(conn.params, "p_signature") - |> Enum.map(fn {key, val} -> {key, "#{val}"} end) - |> List.keysort(0) - |> PhpSerializer.serialize() - - [key_entry] = :public_key.pem_decode(@paddle_key) - public_key = :public_key.pem_entry_decode(key_entry) - - if :public_key.verify(msg, :sha, signature, public_key) do - conn - else - send_resp(conn, 400, "") |> halt - end - end - - def verified_signature?(params) do - signature = Base.decode64!(params["p_signature"]) - - msg = - Map.delete(params, "p_signature") - |> Enum.map(fn {key, val} -> {key, "#{val}"} end) - |> List.keysort(0) - |> PhpSerializer.serialize() - - [key_entry] = :public_key.pem_decode(@paddle_key) - public_key = :public_key.pem_entry_decode(key_entry) - :public_key.verify(msg, :sha, signature, public_key) - end - - defp webhook_response({:ok, _}, conn, _params) do - json(conn, "") - end - - defp webhook_response({:error, changeset}, conn, _params) do - Logger.error("Error processing Paddle webhook: #{inspect(changeset)}") - - conn |> send_resp(400, "") |> halt - end -end diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 943aa98e331b..94fa9705bca0 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -30,9 +30,7 @@ defmodule PlausibleWeb.Api.StatsController do present_index: present_index, top_stats: top_stats, interval: query.interval, - sample_percent: sample_percent, - with_imported: query.include_imported, - imported_source: site.imported_data && site.imported_data.source + sample_percent: sample_percent }) end diff --git a/lib/plausible_web/controllers/auth_controller.ex b/lib/plausible_web/controllers/auth_controller.ex deleted file mode 100644 index 917e0201d40a..000000000000 --- a/lib/plausible_web/controllers/auth_controller.ex +++ /dev/null @@ -1,561 +0,0 @@ -defmodule PlausibleWeb.AuthController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Plausible.Auth - require Logger - - plug PlausibleWeb.RequireLoggedOutPlug - when action in [ - :register_form, - :register, - :register_from_invitation_form, - :register_from_invitation, - :login_form, - :login - ] - - plug PlausibleWeb.RequireAccountPlug - when action in [ - :user_settings, - :save_settings, - :delete_me, - :password_form, - :set_password, - :activate_form - ] - - def register_form(conn, _params) do - if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_registration) do - redirect(conn, to: Routes.auth_path(conn, :login_form)) - else - changeset = Plausible.Auth.User.changeset(%Plausible.Auth.User{}) - - render(conn, "register_form.html", - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def register(conn, params) do - if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_registration) do - redirect(conn, to: Routes.auth_path(conn, :login_form)) - else - user = Plausible.Auth.User.new(params["user"]) - - if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do - case Repo.insert(user) do - {:ok, user} -> - conn = set_user_session(conn, user) - - case user.email_verified do - false -> - send_email_verification(user) - redirect(conn, to: Routes.auth_path(conn, :activate_form)) - - true -> - redirect(conn, to: Routes.site_path(conn, :new)) - end - - {:error, changeset} -> - render(conn, "register_form.html", - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - else - render(conn, "register_form.html", - changeset: user, - captcha_error: "Please complete the captcha to register", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - end - - def register_from_invitation_form(conn, %{"invitation_id" => invitation_id}) do - if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_registration) do - redirect(conn, to: Routes.auth_path(conn, :login_form)) - else - invitation = Repo.get_by(Plausible.Auth.Invitation, invitation_id: invitation_id) - changeset = Plausible.Auth.User.changeset(%Plausible.Auth.User{}) - - if invitation do - render(conn, "register_from_invitation_form.html", - changeset: changeset, - invitation: invitation, - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - else - render(conn, "invitation_expired.html", - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - end - end - end - - def register_from_invitation(conn, %{"invitation_id" => invitation_id} = params) do - if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_registration) do - redirect(conn, to: Routes.auth_path(conn, :login_form)) - else - invitation = Repo.get_by(Plausible.Auth.Invitation, invitation_id: invitation_id) - user = Plausible.Auth.User.new(params["user"]) - - user = - case invitation.role do - :owner -> user - _ -> Plausible.Auth.User.remove_trial_expiry(user) - end - - if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do - case Repo.insert(user) do - {:ok, user} -> - conn = set_user_session(conn, user) - - case user.email_verified do - false -> - send_email_verification(user) - redirect(conn, to: Routes.auth_path(conn, :activate_form)) - - true -> - redirect(conn, to: Routes.site_path(conn, :index)) - end - - {:error, changeset} -> - render(conn, "register_from_invitation_form.html", - invitation: invitation, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - end - else - render(conn, "register_from_invitation_form.html", - invitation: invitation, - changeset: user, - captcha_error: "Please complete the captcha to register", - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - end - end - end - - defp send_email_verification(user) do - code = Auth.issue_email_verification(user) - Logger.info("VERIFICATION CODE: #{code}") - email_template = PlausibleWeb.Email.activation_email(user, code) - Plausible.Mailer.send_email(email_template) - end - - defp set_user_session(conn, user) do - conn - |> put_session(:current_user_id, user.id) - |> put_resp_cookie("logged_in", "true", - http_only: false, - max_age: 60 * 60 * 24 * 365 * 5000 - ) - end - - def activate_form(conn, _params) do - user = conn.assigns[:current_user] - - has_invitation = - Repo.exists?( - from i in Plausible.Auth.Invitation, - where: i.email == ^user.email - ) - - has_code = - Repo.exists?( - from c in "email_verification_codes", - where: c.user_id == ^user.id - ) - - render(conn, "activate.html", - has_pin: has_code, - has_invitation: has_invitation, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def activate(conn, %{"code" => code}) do - user = conn.assigns[:current_user] - - has_invitation = - Repo.exists?( - from i in Plausible.Auth.Invitation, - where: i.email == ^user.email - ) - - {code, ""} = Integer.parse(code) - - case Auth.verify_email(user, code) do - :ok -> - if has_invitation do - redirect(conn, to: Routes.site_path(conn, :index)) - else - redirect(conn, to: Routes.site_path(conn, :new)) - end - - {:error, :incorrect} -> - render(conn, "activate.html", - error: "Incorrect activation code", - has_pin: true, - has_invitation: has_invitation, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - {:error, :expired} -> - render(conn, "activate.html", - error: "Code is expired, please request another one", - has_pin: false, - has_invitation: has_invitation, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def request_activation_code(conn, _params) do - user = conn.assigns[:current_user] - code = Auth.issue_email_verification(user) - - email_template = PlausibleWeb.Email.activation_email(user, code) - Plausible.Mailer.send_email(email_template) - - conn - |> put_flash(:success, "Activation code was sent to #{user.email}") - |> redirect(to: Routes.auth_path(conn, :activate_form)) - end - - def password_reset_request_form(conn, _) do - render(conn, "password_reset_request_form.html", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def password_reset_request(conn, %{"email" => ""}) do - render(conn, "password_reset_request_form.html", - error: "Please enter an email address", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def password_reset_request(conn, %{"email" => email} = params) do - if PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do - user = Repo.get_by(Plausible.Auth.User, email: email) - - if user do - token = Auth.Token.sign_password_reset(email) - url = PlausibleWeb.Endpoint.url() <> "/password/reset?token=#{token}" - Logger.debug("PASSWORD RESET LINK: " <> url) - email_template = PlausibleWeb.Email.password_reset_email(email, url) - Plausible.Mailer.deliver_later(email_template) - - render(conn, "password_reset_request_success.html", - email: email, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - else - render(conn, "password_reset_request_success.html", - email: email, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - else - render(conn, "password_reset_request_form.html", - error: "Please complete the captcha to reset your password", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def password_reset_form(conn, %{"token" => token}) do - case Auth.Token.verify_password_reset(token) do - {:ok, _} -> - render(conn, "password_reset_form.html", - token: token, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - {:error, :expired} -> - render_error( - conn, - 401, - "Your token has expired. Please request another password reset link." - ) - - {:error, _} -> - render_error( - conn, - 401, - "Your token is invalid. Please request another password reset link." - ) - end - end - - def password_reset(conn, %{"token" => token, "password" => pw}) do - case Auth.Token.verify_password_reset(token) do - {:ok, %{email: email}} -> - user = Repo.get_by(Auth.User, email: email) - changeset = Auth.User.set_password(user, pw) - - case Repo.update(changeset) do - {:ok, _updated} -> - conn - |> put_flash(:login_title, "Password updated successfully") - |> put_flash(:login_instructions, "Please log in with your new credentials") - |> put_session(:current_user_id, nil) - |> delete_resp_cookie("logged_in") - |> redirect(to: Routes.auth_path(conn, :login_form)) - - {:error, changeset} -> - render(conn, "password_reset_form.html", - changeset: changeset, - token: token, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - {:error, :expired} -> - render_error( - conn, - 401, - "Your token has expired. Please request another password reset link." - ) - - {:error, _} -> - render_error( - conn, - 401, - "Your token is invalid. Please request another password reset link." - ) - end - end - - def login(conn, %{"email" => email, "password" => password}) do - with :ok <- check_ip_rate_limit(conn), - {:ok, user} <- find_user(email), - :ok <- check_user_rate_limit(user), - :ok <- check_password(user, password) do - login_dest = get_session(conn, :login_dest) || Routes.site_path(conn, :index) - - conn - |> put_session(:current_user_id, user.id) - |> put_resp_cookie("logged_in", "true", - http_only: false, - max_age: 60 * 60 * 24 * 365 * 5000 - ) - |> put_session(:login_dest, nil) - |> redirect(to: login_dest) - else - :wrong_password -> - render(conn, "login_form.html", - error: "Wrong email or password. Please try again.", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - :user_not_found -> - Plausible.Auth.Password.dummy_calculation() - - render(conn, "login_form.html", - error: "Wrong email or password. Please try again.", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - {:rate_limit, _} -> - render_error( - conn, - 429, - "Too many login attempts. Wait a minute before trying again." - ) - end - end - - @login_interval 60_000 - @login_limit 5 - defp check_ip_rate_limit(conn) do - ip_address = PlausibleWeb.RemoteIp.get(conn) - - case Hammer.check_rate("login:ip:#{ip_address}", @login_interval, @login_limit) do - {:allow, _} -> :ok - {:deny, _} -> {:rate_limit, :ip_address} - end - end - - defp find_user(email) do - user = - Repo.one( - from u in Plausible.Auth.User, - where: u.email == ^email - ) - - if user, do: {:ok, user}, else: :user_not_found - end - - defp check_user_rate_limit(user) do - case Hammer.check_rate("login:user:#{user.id}", @login_interval, @login_limit) do - {:allow, _} -> :ok - {:deny, _} -> {:rate_limit, :user} - end - end - - defp check_password(user, password) do - if Plausible.Auth.Password.match?(password, user.password_hash || "") do - :ok - else - :wrong_password - end - end - - def login_form(conn, _params) do - render(conn, "login_form.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) - end - - def password_form(conn, _params) do - render(conn, "password_form.html", - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - end - - def set_password(conn, %{"password" => pw}) do - changeset = Auth.User.set_password(conn.assigns[:current_user], pw) - - case Repo.update(changeset) do - {:ok, _user} -> - redirect(conn, to: "/sites/new") - - {:error, changeset} -> - render(conn, "password_form.html", - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def user_settings(conn, _params) do - user = conn.assigns[:current_user] - changeset = Auth.User.changeset(user) - - {usage_pageviews, usage_custom_events} = Plausible.Billing.usage_breakdown(user) - - render(conn, "user_settings.html", - user: user |> Repo.preload(:api_keys), - changeset: changeset, - subscription: user.subscription, - invoices: Plausible.Billing.paddle_api().get_invoices(user.subscription), - theme: user.theme || "system", - usage_pageviews: usage_pageviews, - usage_custom_events: usage_custom_events - ) - end - - def save_settings(conn, %{"user" => user_params}) do - changes = Auth.User.changeset(conn.assigns[:current_user], user_params) - - case Repo.update(changes) do - {:ok, _user} -> - conn - |> put_flash(:success, "Account settings saved successfully") - |> redirect(to: Routes.auth_path(conn, :user_settings)) - - {:error, changeset} -> - render(conn, "user_settings.html", - changeset: changeset, - subscription: conn.assigns[:current_user].subscription - ) - end - end - - def new_api_key(conn, _params) do - key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) - changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, %{key: key}) - - render(conn, "new_api_key.html", - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def create_api_key(conn, %{"api_key" => key_params}) do - api_key = %Auth.ApiKey{user_id: conn.assigns[:current_user].id} - changeset = Auth.ApiKey.changeset(api_key, key_params) - - case Repo.insert(changeset) do - {:ok, _api_key} -> - conn - |> put_flash(:success, "API key created successfully") - |> redirect(to: "/settings#api-keys") - - {:error, changeset} -> - render(conn, "new_api_key.html", - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def delete_api_key(conn, %{"id" => id}) do - Repo.get_by(Auth.ApiKey, id: id) - |> Repo.delete!() - - conn - |> put_flash(:success, "API key revoked successfully") - |> redirect(to: "/settings#api-keys") - end - - def delete_me(conn, params) do - user = - conn.assigns[:current_user] - |> Repo.preload(site_memberships: :site) - |> Repo.preload(:subscription) - - for membership <- user.site_memberships do - Repo.delete!(membership) - - if membership.role == :owner do - Plausible.Sites.delete!(membership.site) - end - end - - if user.subscription, do: Repo.delete!(user.subscription) - Repo.delete!(user) - - logout(conn, params) - end - - def logout(conn, params) do - redirect_to = Map.get(params, "redirect", "/") - - conn - |> configure_session(drop: true) - |> delete_resp_cookie("logged_in") - |> redirect(to: redirect_to) - end - - def google_auth_callback(conn, %{"code" => code, "state" => state}) do - res = Plausible.Google.Api.fetch_access_token(code) - id_token = res["id_token"] - [_, body, _] = String.split(id_token, ".") - id = body |> Base.decode64!(padding: false) |> Jason.decode!() - - [site_id, redirect_to] = Jason.decode!(state) - - Plausible.Site.GoogleAuth.changeset(%Plausible.Site.GoogleAuth{}, %{ - email: id["email"], - refresh_token: res["refresh_token"], - access_token: res["access_token"], - expires: NaiveDateTime.utc_now() |> NaiveDateTime.add(res["expires_in"]), - user_id: conn.assigns[:current_user].id, - site_id: site_id - }) - |> Repo.insert!() - - site = Repo.get(Plausible.Site, site_id) - - redirect(conn, to: "/#{URI.encode_www_form(site.domain)}/settings/#{redirect_to}") - end -end diff --git a/lib/plausible_web/controllers/billing_controller.ex b/lib/plausible_web/controllers/billing_controller.ex deleted file mode 100644 index a057678b18f0..000000000000 --- a/lib/plausible_web/controllers/billing_controller.ex +++ /dev/null @@ -1,179 +0,0 @@ -defmodule PlausibleWeb.BillingController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Plausible.Billing - require Logger - - plug PlausibleWeb.RequireAccountPlug - - def upgrade(conn, _params) do - user = - conn.assigns[:current_user] - |> Repo.preload(:enterprise_plan) - - cond do - user.subscription && user.subscription.status == "active" -> - redirect(conn, to: Routes.billing_path(conn, :change_plan_form)) - - user.enterprise_plan -> - redirect(conn, - to: Routes.billing_path(conn, :upgrade_enterprise_plan, user.enterprise_plan.id) - ) - - true -> - render(conn, "upgrade.html", - usage: Plausible.Billing.usage(user), - user: user, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def upgrade_enterprise_plan(conn, %{"plan_id" => plan_id}) do - user = - conn.assigns[:current_user] - |> Repo.preload(:enterprise_plan) - - if user.enterprise_plan && user.enterprise_plan.id == String.to_integer(plan_id) do - usage = Plausible.Billing.usage(conn.assigns[:current_user]) - - render(conn, "upgrade_to_plan.html", - usage: usage, - user: user, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - else - render_error(conn, 404) - end - end - - def upgrade_to_plan(conn, %{"plan_id" => plan_id}) do - plan = Plausible.Billing.Plans.for_product_id(plan_id) - - if plan do - cycle = if plan[:monthly_product_id] == plan_id, do: "monthly", else: "yearly" - plan = Map.merge(plan, %{cycle: cycle, product_id: plan_id}) - usage = Plausible.Billing.usage(conn.assigns[:current_user]) - - render(conn, "upgrade_to_plan.html", - usage: usage, - plan: plan, - user: conn.assigns[:current_user], - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - else - render_error(conn, 404) - end - end - - def upgrade_success(conn, _params) do - render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) - end - - def change_plan_form(conn, _params) do - user = - conn.assigns[:current_user] - |> Repo.preload(:enterprise_plan) - - subscription = Billing.active_subscription_for(user.id) - - cond do - subscription && user.enterprise_plan -> - redirect(conn, - to: Routes.billing_path(conn, :change_enterprise_plan, user.enterprise_plan.id) - ) - - subscription -> - render(conn, "change_plan.html", - subscription: subscription, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - true -> - redirect(conn, to: Routes.billing_path(conn, :upgrade)) - end - end - - def change_enterprise_plan(conn, %{"plan_id" => plan_id}) do - user = - conn.assigns[:current_user] - |> Repo.preload(:enterprise_plan) - - cond do - is_nil(user.subscription) -> - redirect(conn, to: "/billing/upgrade") - - is_nil(user.enterprise_plan) -> - render_error(conn, 404) - - user.enterprise_plan.id !== String.to_integer(plan_id) -> - render_error(conn, 404) - - user.enterprise_plan.paddle_plan_id == user.subscription.paddle_plan_id -> - render(conn, "change_enterprise_plan_contact_us.html", - user: user, - plan: user.enterprise_plan, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - true -> - render(conn, "change_enterprise_plan.html", - user: user, - plan: user.enterprise_plan, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do - subscription = Billing.active_subscription_for(conn.assigns[:current_user].id) - - if subscription do - {:ok, preview_info} = Billing.change_plan_preview(subscription, new_plan_id) - - render(conn, "change_plan_preview.html", - subscription: subscription, - preview_info: preview_info, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - else - redirect(conn, to: "/billing/upgrade") - end - end - - def change_plan(conn, %{"new_plan_id" => new_plan_id}) do - case Billing.change_plan(conn.assigns[:current_user], new_plan_id) do - {:ok, _subscription} -> - conn - |> put_flash(:success, "Plan changed successfully") - |> redirect(to: "/settings") - - {:error, e} -> - # https://developer.paddle.com/api-reference/intro/api-error-codes - msg = - case e do - %{"code" => 147} -> - "We were unable to charge your card. Click 'update billing info' to update your payment details and try again." - - %{"message" => msg} when not is_nil(msg) -> - msg - - _ -> - "Something went wrong. Please try again or contact support at support@plausible.io" - end - - Sentry.capture_message("Error changing plans", - extra: %{ - errors: inspect(e), - message: msg, - new_plan_id: new_plan_id, - user_id: conn.assigns[:current_user].id - } - ) - - conn - |> put_flash(:error, msg) - |> redirect(to: "/settings") - end - end -end diff --git a/lib/plausible_web/controllers/invitation_controller.ex b/lib/plausible_web/controllers/invitation_controller.ex deleted file mode 100644 index 9e0d2c3740b8..000000000000 --- a/lib/plausible_web/controllers/invitation_controller.ex +++ /dev/null @@ -1,125 +0,0 @@ -defmodule PlausibleWeb.InvitationController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Ecto.Multi - alias Plausible.Auth.Invitation - alias Plausible.Site.Membership - - plug PlausibleWeb.RequireAccountPlug - - def accept_invitation(conn, %{"invitation_id" => invitation_id}) do - invitation = - Repo.get_by!(Invitation, invitation_id: invitation_id) - |> Repo.preload([:site, :inviter]) - - user = conn.assigns[:current_user] - existing_membership = Repo.get_by(Membership, user_id: user.id, site_id: invitation.site.id) - - multi = - if invitation.role == :owner do - Multi.new() - |> downgrade_previous_owner(invitation.site) - |> maybe_end_trial_of_new_owner(user) - else - Multi.new() - end - - membership_changeset = - Membership.changeset(existing_membership || %Membership{}, %{ - user_id: user.id, - site_id: invitation.site.id, - role: invitation.role - }) - - multi = - multi - |> Multi.insert_or_update(:membership, membership_changeset) - |> Multi.delete(:invitation, invitation) - - case Repo.transaction(multi) do - {:ok, changes} -> - updated_user = Map.get(changes, :user, user) - notify_invitation_accepted(invitation) - Plausible.Billing.SiteLocker.check_sites_for(updated_user) - - conn - |> put_flash(:success, "You now have access to #{invitation.site.domain}") - |> redirect(to: "/#{URI.encode_www_form(invitation.site.domain)}") - - {:error, _} -> - conn - |> put_flash(:error, "Something went wrong, please try again") - |> redirect(to: "/sites") - end - end - - defp downgrade_previous_owner(multi, site) do - prev_owner = - from( - sm in Plausible.Site.Membership, - where: sm.site_id == ^site.id, - where: sm.role == :owner - ) - - Multi.update_all(multi, :prev_owner, prev_owner, set: [role: :admin]) - end - - defp maybe_end_trial_of_new_owner(multi, new_owner) do - if !Application.get_env(:plausible, :is_selfhost) do - end_trial_of_new_owner(multi, new_owner) - end - end - - defp end_trial_of_new_owner(multi, new_owner) do - if Plausible.Billing.on_trial?(new_owner) || is_nil(new_owner.trial_expiry_date) do - Ecto.Multi.update(multi, :user, Plausible.Auth.User.end_trial(new_owner)) - else - multi - end - end - - def reject_invitation(conn, %{"invitation_id" => invitation_id}) do - invitation = - Repo.get_by!(Invitation, invitation_id: invitation_id) - |> Repo.preload([:site, :inviter]) - - Repo.delete!(invitation) - notify_invitation_rejected(invitation) - - conn - |> put_flash(:success, "You have rejected the invitation to #{invitation.site.domain}") - |> redirect(to: "/sites") - end - - defp notify_invitation_accepted(%Invitation{role: :owner} = invitation) do - PlausibleWeb.Email.ownership_transfer_accepted(invitation) - |> Plausible.Mailer.send_email_safe() - end - - defp notify_invitation_accepted(invitation) do - PlausibleWeb.Email.invitation_accepted(invitation) - |> Plausible.Mailer.send_email_safe() - end - - defp notify_invitation_rejected(%Invitation{role: :owner} = invitation) do - PlausibleWeb.Email.ownership_transfer_rejected(invitation) - |> Plausible.Mailer.send_email_safe() - end - - defp notify_invitation_rejected(invitation) do - PlausibleWeb.Email.invitation_rejected(invitation) - |> Plausible.Mailer.send_email_safe() - end - - def remove_invitation(conn, %{"invitation_id" => invitation_id}) do - invitation = - Repo.get_by!(Invitation, invitation_id: invitation_id) - |> Repo.preload(:site) - - Repo.delete!(invitation) - - conn - |> put_flash(:success, "You have removed the invitation for #{invitation.email}") - |> redirect(to: Routes.site_path(conn, :settings_general, invitation.site.domain)) - end -end diff --git a/lib/plausible_web/controllers/page_controller.ex b/lib/plausible_web/controllers/page_controller.ex index 64e217d08e5b..f94eec7dd75a 100644 --- a/lib/plausible_web/controllers/page_controller.ex +++ b/lib/plausible_web/controllers/page_controller.ex @@ -1,14 +1,9 @@ defmodule PlausibleWeb.PageController do use PlausibleWeb, :controller - use Plausible.Repo - plug PlausibleWeb.AutoAuthPlug def index(conn, _params) do - if conn.assigns[:current_user] do - user = conn.assigns[:current_user] |> Repo.preload(:sites) - render(conn, "sites.html", sites: user.sites) - else - render(conn, "index.html") - end + conn + |> redirect(to: "/all") + |> halt() end end diff --git a/lib/plausible_web/controllers/site/membership_controller.ex b/lib/plausible_web/controllers/site/membership_controller.ex deleted file mode 100644 index c05a5920629e..000000000000 --- a/lib/plausible_web/controllers/site/membership_controller.ex +++ /dev/null @@ -1,149 +0,0 @@ -defmodule PlausibleWeb.Site.MembershipController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Plausible.Sites - alias Plausible.Site.Membership - alias Plausible.Auth.Invitation - - @only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership] - - plug PlausibleWeb.RequireAccountPlug - plug PlausibleWeb.AuthorizeSiteAccess, [:owner] when action in @only_owner_is_allowed_to - - plug PlausibleWeb.AuthorizeSiteAccess, - [:owner, :admin] when action not in @only_owner_is_allowed_to - - def invite_member_form(conn, %{"website" => site_domain}) do - site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain) - - render( - conn, - "invite_member_form.html", - site: site, - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - end - - def invite_member(conn, %{"website" => site_domain, "email" => email, "role" => role}) do - site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain) - user = Plausible.Auth.find_user_by(email: email) - - if user && Sites.is_member?(user.id, site) do - msg = "Cannot send invite because #{user.email} is already a member of #{site.domain}" - - render(conn, "invite_member_form.html", - error: msg, - site: site, - layout: {PlausibleWeb.LayoutView, "focus.html"}, - skip_plausible_tracking: true - ) - else - invitation = - Invitation.new(%{ - email: email, - role: role, - site_id: site.id, - inviter_id: conn.assigns[:current_user].id - }) - |> Repo.insert!() - |> Repo.preload([:site, :inviter]) - - email_template = - if user do - PlausibleWeb.Email.existing_user_invitation(invitation) - else - PlausibleWeb.Email.new_user_invitation(invitation) - end - - Plausible.Mailer.send_email(email_template) - - conn - |> put_flash( - :success, - "#{email} has been invited to #{site_domain} as #{PlausibleWeb.SiteView.with_indefinite_article(role)}" - ) - |> redirect(to: Routes.site_path(conn, :settings_people, site.domain)) - end - end - - def transfer_ownership_form(conn, %{"website" => site_domain}) do - site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain) - - render( - conn, - "transfer_ownership_form.html", - site: site, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def transfer_ownership(conn, %{"website" => site_domain, "email" => email}) do - site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain) - user = Plausible.Auth.find_user_by(email: email) - - invitation = - Invitation.new(%{ - email: email, - role: :owner, - site_id: site.id, - inviter_id: conn.assigns[:current_user].id - }) - |> Repo.insert!() - |> Repo.preload([:site, :inviter]) - - PlausibleWeb.Email.ownership_transfer_request(invitation, user) - |> Plausible.Mailer.send_email_safe() - - conn - |> put_flash(:success, "Site transfer request has been sent to #{email}") - |> redirect(to: Routes.site_path(conn, :settings_people, site.domain)) - end - - def update_role(conn, %{"id" => id, "new_role" => new_role}) do - membership = - Repo.get!(Membership, id) - |> Repo.preload([:site, :user]) - |> Membership.changeset(%{role: new_role}) - |> Repo.update!() - - redirect_target = - if membership.user.id == conn.assigns[:current_user].id && new_role == "viewer" do - "/#{URI.encode_www_form(membership.site.domain)}" - else - Routes.site_path(conn, :settings_people, membership.site.domain) - end - - conn - |> put_flash( - :success, - "#{membership.user.name} is now #{PlausibleWeb.SiteView.with_indefinite_article(new_role)}" - ) - |> redirect(to: redirect_target) - end - - def remove_member(conn, %{"id" => id}) do - membership = - Repo.get!(Membership, id) - |> Repo.preload([:user, :site]) - - Repo.delete!(membership) - - PlausibleWeb.Email.site_member_removed(membership) - |> Plausible.Mailer.send_email() - - redirect_target = - if membership.user.id == conn.assigns[:current_user].id do - "/#{URI.encode_www_form(membership.site.domain)}" - else - Routes.site_path(conn, :settings_people, membership.site.domain) - end - - conn - |> put_flash( - :success, - "#{membership.user.name} has been removed from #{membership.site.domain}" - ) - |> redirect(to: redirect_target) - end -end diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex deleted file mode 100644 index 7ec776fddd68..000000000000 --- a/lib/plausible_web/controllers/site_controller.ex +++ /dev/null @@ -1,701 +0,0 @@ -defmodule PlausibleWeb.SiteController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Plausible.{Sites, Goals} - - plug PlausibleWeb.RequireAccountPlug - - plug PlausibleWeb.AuthorizeSiteAccess, - [:owner, :admin, :super_admin] when action not in [:index, :new, :create_site] - - def index(conn, params) do - user = conn.assigns[:current_user] - - invitations = - Repo.all( - from i in Plausible.Auth.Invitation, - where: i.email == ^user.email - ) - |> Repo.preload(:site) - - invitation_site_ids = Enum.map(invitations, & &1.site.id) - - {sites, pagination} = - Repo.paginate( - from(s in Plausible.Site, - join: sm in Plausible.Site.Membership, - on: sm.site_id == s.id, - where: sm.user_id == ^user.id, - where: s.id not in ^invitation_site_ids, - order_by: s.domain, - preload: [memberships: sm] - ), - params - ) - - user_owns_sites = - Enum.any?(sites, fn site -> List.first(site.memberships).role == :owner end) || - Plausible.Auth.user_owns_sites?(user) - - visitors = - Plausible.Stats.Clickhouse.last_24h_visitors(sites ++ Enum.map(invitations, & &1.site)) - - render(conn, "index.html", - invitations: invitations, - sites: sites, - visitors: visitors, - pagination: pagination, - needs_to_upgrade: user_owns_sites && Plausible.Billing.needs_to_upgrade?(user) - ) - end - - def new(conn, _params) do - current_user = conn.assigns[:current_user] |> Repo.preload(site_memberships: :site) - - owned_site_count = - current_user.site_memberships |> Enum.filter(fn m -> m.role == :owner end) |> Enum.count() - - site_limit = Plausible.Billing.sites_limit(current_user) - is_at_limit = site_limit && owned_site_count >= site_limit - is_first_site = Enum.empty?(current_user.site_memberships) - - changeset = Plausible.Site.changeset(%Plausible.Site{}) - - render(conn, "new.html", - changeset: changeset, - is_first_site: is_first_site, - is_at_limit: is_at_limit, - site_limit: site_limit, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def create_site(conn, %{"site" => site_params}) do - user = conn.assigns[:current_user] - site_count = Enum.count(Plausible.Sites.owned_by(user)) - is_first_site = site_count == 0 - - case Sites.create(user, site_params) do - {:ok, %{site: site}} -> - Plausible.Slack.notify("#{user.name} created #{site.domain} [email=#{user.email}]") - - if is_first_site do - PlausibleWeb.Email.welcome_email(user) - |> Plausible.Mailer.send_email() - end - - conn - |> put_session(site.domain <> "_offer_email_report", true) - |> redirect(to: Routes.site_path(conn, :add_snippet, site.domain)) - - {:error, :site, changeset, _} -> - render(conn, "new.html", - changeset: changeset, - is_first_site: is_first_site, - is_at_limit: false, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - - {:error, :limit, _limit} -> - send_resp(conn, 400, "Site limit reached") - end - end - - def add_snippet(conn, _params) do - user = conn.assigns[:current_user] - site = conn.assigns[:site] |> Repo.preload(:custom_domain) - - is_first_site = - !Repo.exists?( - from sm in Plausible.Site.Membership, - where: - sm.user_id == ^user.id and - sm.site_id != ^site.id - ) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("snippet.html", - site: site, - is_first_site: is_first_site, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def new_goal(conn, _params) do - site = conn.assigns[:site] - changeset = Plausible.Goal.changeset(%Plausible.Goal{}) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("new_goal.html", - site: site, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def create_goal(conn, %{"goal" => goal}) do - site = conn.assigns[:site] - - case Plausible.Goals.create(site, goal) do - {:ok, _} -> - conn - |> put_flash(:success, "Goal created successfully") - |> redirect(to: Routes.site_path(conn, :settings_goals, site.domain)) - - {:error, changeset} -> - conn - |> assign(:skip_plausible_tracking, true) - |> render("new_goal.html", - site: site, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def delete_goal(conn, %{"website" => website, "id" => goal_id}) do - Plausible.Goals.delete(goal_id) - - conn - |> put_flash(:success, "Goal deleted successfully") - |> redirect(to: Routes.site_path(conn, :settings_goals, website)) - end - - def settings(conn, %{"website" => website}) do - redirect(conn, to: Routes.site_path(conn, :settings_general, website)) - end - - defp can_trigger_import(site) do - no_import = is_nil(site.imported_data) || site.imported_data.status == "error" - - no_import && site.google_auth - end - - def settings_general(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload([:custom_domain, :google_auth]) - - google_profiles = - if can_trigger_import(site) do - Plausible.Google.Api.get_analytics_view_ids(site) - end - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_general.html", - site: site, - google_profiles: google_profiles, - imported_data: site.imported_data, - changeset: Plausible.Site.changeset(site, %{}), - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_people(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload(memberships: :user, invitations: [], custom_domain: []) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_people.html", - site: site, - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_visibility(conn, _params) do - site = conn.assigns[:site] |> Repo.preload(:custom_domain) - shared_links = Repo.all(from l in Plausible.Site.SharedLink, where: l.site_id == ^site.id) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_visibility.html", - site: site, - shared_links: shared_links, - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_goals(conn, _params) do - site = conn.assigns[:site] |> Repo.preload(:custom_domain) - goals = Goals.for_site(site.domain) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_goals.html", - site: site, - goals: goals, - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_search_console(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload([:google_auth, :custom_domain]) - - search_console_domains = - if site.google_auth do - Plausible.Google.Api.fetch_verified_properties(site.google_auth) - end - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_search_console.html", - site: site, - search_console_domains: search_console_domains, - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_email_reports(conn, _params) do - site = conn.assigns[:site] |> Repo.preload(:custom_domain) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_email_reports.html", - site: site, - weekly_report: Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id), - monthly_report: Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id), - spike_notification: Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id), - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_custom_domain(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload(:custom_domain) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_custom_domain.html", - site: site, - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def settings_danger_zone(conn, _params) do - site = conn.assigns[:site] |> Repo.preload(:custom_domain) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("settings_danger_zone.html", - site: site, - layout: {PlausibleWeb.LayoutView, "site_settings.html"} - ) - end - - def update_google_auth(conn, %{"google_auth" => attrs}) do - site = conn.assigns[:site] |> Repo.preload(:google_auth) - - Plausible.Site.GoogleAuth.set_property(site.google_auth, attrs) - |> Repo.update!() - - conn - |> put_flash(:success, "Google integration saved successfully") - |> redirect(to: Routes.site_path(conn, :settings_search_console, site.domain)) - end - - def delete_google_auth(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload(:google_auth) - - Repo.delete!(site.google_auth) - - conn = put_flash(conn, :success, "Google account unlinked from Plausible") - - panel = - conn.path_info - |> List.last() - |> String.split("-") - |> List.last() - - case panel do - "search" -> - redirect(conn, to: Routes.site_path(conn, :settings_search_console, site.domain)) - - "import" -> - redirect(conn, to: Routes.site_path(conn, :settings_general, site.domain)) - end - end - - def update_settings(conn, %{"site" => site_params}) do - site = conn.assigns[:site] - changeset = site |> Plausible.Site.changeset(site_params) - res = changeset |> Repo.update() - - case res do - {:ok, site} -> - site_session_key = "authorized_site__" <> site.domain - - conn - |> put_session(site_session_key, nil) - |> put_flash(:success, "Your site settings have been saved") - |> redirect(to: Routes.site_path(conn, :settings_general, site.domain)) - - {:error, changeset} -> - render(conn, "settings_general.html", site: site, changeset: changeset) - end - end - - def reset_stats(conn, _params) do - site = conn.assigns[:site] - Plausible.ClickhouseRepo.clear_stats_for(site.domain) - - conn - |> put_flash(:success, "#{site.domain} stats will be reset in a few minutes") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/danger-zone") - end - - def delete_site(conn, _params) do - site = conn.assigns[:site] - - Plausible.Sites.delete!(site) - - conn - |> put_flash(:success, "Site deleted successfully along with all pageviews") - |> redirect(to: "/sites") - end - - def make_public(conn, _params) do - site = - conn.assigns[:site] - |> Plausible.Site.make_public() - |> Repo.update!() - - conn - |> put_flash(:success, "Stats for #{site.domain} are now public.") - |> redirect(to: Routes.site_path(conn, :settings_visibility, site.domain)) - end - - def make_private(conn, _params) do - site = - conn.assigns[:site] - |> Plausible.Site.make_private() - |> Repo.update!() - - conn - |> put_flash(:success, "Stats for #{site.domain} are now private.") - |> redirect(to: Routes.site_path(conn, :settings_visibility, site.domain)) - end - - def enable_weekly_report(conn, _params) do - site = conn.assigns[:site] - - Plausible.Site.WeeklyReport.changeset(%Plausible.Site.WeeklyReport{}, %{ - site_id: site.id, - recipients: [conn.assigns[:current_user].email] - }) - |> Repo.insert!() - - conn - |> put_flash(:success, "You will receive an email report every Monday going forward") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def disable_weekly_report(conn, _params) do - site = conn.assigns[:site] - Repo.delete_all(from wr in Plausible.Site.WeeklyReport, where: wr.site_id == ^site.id) - - conn - |> put_flash(:success, "You will not receive weekly email reports going forward") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def add_weekly_report_recipient(conn, %{"recipient" => recipient}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - |> Plausible.Site.WeeklyReport.add_recipient(recipient) - |> Repo.update!() - - conn - |> put_flash(:success, "Added #{recipient} as a recipient for the weekly report") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def remove_weekly_report_recipient(conn, %{"recipient" => recipient}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - |> Plausible.Site.WeeklyReport.remove_recipient(recipient) - |> Repo.update!() - - conn - |> put_flash( - :success, - "Removed #{recipient} as a recipient for the weekly report" - ) - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def enable_monthly_report(conn, _params) do - site = conn.assigns[:site] - - Plausible.Site.MonthlyReport.changeset(%Plausible.Site.MonthlyReport{}, %{ - site_id: site.id, - recipients: [conn.assigns[:current_user].email] - }) - |> Repo.insert!() - - conn - |> put_flash(:success, "You will receive an email report every month going forward") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def disable_monthly_report(conn, _params) do - site = conn.assigns[:site] - Repo.delete_all(from mr in Plausible.Site.MonthlyReport, where: mr.site_id == ^site.id) - - conn - |> put_flash(:success, "You will not receive monthly email reports going forward") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def add_monthly_report_recipient(conn, %{"recipient" => recipient}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - |> Plausible.Site.MonthlyReport.add_recipient(recipient) - |> Repo.update!() - - conn - |> put_flash(:success, "Added #{recipient} as a recipient for the monthly report") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def remove_monthly_report_recipient(conn, %{"recipient" => recipient}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - |> Plausible.Site.MonthlyReport.remove_recipient(recipient) - |> Repo.update!() - - conn - |> put_flash( - :success, - "Removed #{recipient} as a recipient for the monthly report" - ) - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def enable_spike_notification(conn, _params) do - site = conn.assigns[:site] - - res = - Plausible.Site.SpikeNotification.changeset(%Plausible.Site.SpikeNotification{}, %{ - site_id: site.id, - threshold: 10, - recipients: [conn.assigns[:current_user].email] - }) - |> Repo.insert() - - case res do - {:ok, _} -> - conn - |> put_flash(:success, "You will a notification with traffic spikes going forward") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - - {:error, _} -> - conn - |> put_flash(:error, "Unable to create a spike notification") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - end - - def disable_spike_notification(conn, _params) do - site = conn.assigns[:site] - Repo.delete_all(from mr in Plausible.Site.SpikeNotification, where: mr.site_id == ^site.id) - - conn - |> put_flash(:success, "Spike notification disabled") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def update_spike_notification(conn, %{"spike_notification" => params}) do - site = conn.assigns[:site] - notification = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - - Plausible.Site.SpikeNotification.changeset(notification, params) - |> Repo.update!() - - conn - |> put_flash(:success, "Notification settings updated") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def add_spike_notification_recipient(conn, %{"recipient" => recipient}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - |> Plausible.Site.SpikeNotification.add_recipient(recipient) - |> Repo.update!() - - conn - |> put_flash(:success, "Added #{recipient} as a recipient for the traffic spike notification") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def remove_spike_notification_recipient(conn, %{"recipient" => recipient}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - |> Plausible.Site.SpikeNotification.remove_recipient(recipient) - |> Repo.update!() - - conn - |> put_flash( - :success, - "Removed #{recipient} as a recipient for the monthly report" - ) - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/email-reports") - end - - def new_shared_link(conn, _params) do - site = conn.assigns[:site] - changeset = Plausible.Site.SharedLink.changeset(%Plausible.Site.SharedLink{}, %{}) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("new_shared_link.html", - site: site, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def create_shared_link(conn, %{"shared_link" => link}) do - site = conn.assigns[:site] - - case Sites.create_shared_link(site, link["name"], link["password"]) do - {:ok, _created} -> - redirect(conn, to: "/#{URI.encode_www_form(site.domain)}/settings/visibility") - - {:error, changeset} -> - conn - |> assign(:skip_plausible_tracking, true) - |> render("new_shared_link.html", - site: site, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def edit_shared_link(conn, %{"slug" => slug}) do - site = conn.assigns[:site] - shared_link = Repo.get_by(Plausible.Site.SharedLink, slug: slug) - changeset = Plausible.Site.SharedLink.changeset(shared_link, %{}) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("edit_shared_link.html", - site: site, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def update_shared_link(conn, %{"slug" => slug, "shared_link" => params}) do - site = conn.assigns[:site] - shared_link = Repo.get_by(Plausible.Site.SharedLink, slug: slug) - changeset = Plausible.Site.SharedLink.changeset(shared_link, params) - - case Repo.update(changeset) do - {:ok, _created} -> - redirect(conn, to: "/#{URI.encode_www_form(site.domain)}/settings/visibility") - - {:error, changeset} -> - conn - |> assign(:skip_plausible_tracking, true) - |> render("edit_shared_link.html", - site: site, - changeset: changeset, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - end - - def delete_shared_link(conn, %{"slug" => slug}) do - site = conn.assigns[:site] - - Repo.get_by(Plausible.Site.SharedLink, slug: slug) - |> Repo.delete!() - - redirect(conn, to: "/#{URI.encode_www_form(site.domain)}/settings/visibility") - end - - def delete_custom_domain(conn, _params) do - site = - conn.assigns[:site] - |> Repo.preload(:custom_domain) - - Repo.delete!(site.custom_domain) - - conn - |> put_flash(:success, "Custom domain deleted successfully") - |> redirect(to: "/#{URI.encode_www_form(site.domain)}/settings/general") - end - - def import_from_google(conn, %{"profile" => profile}) do - site = - conn.assigns[:site] - |> Repo.preload(:google_auth) - - cond do - site.imported_data -> - conn - |> put_flash(:error, "Data already imported from: #{site.imported_data.source}") - |> redirect(to: Routes.site_path(conn, :settings_general, site.domain)) - - profile == "" -> - conn - |> put_flash(:error, "A Google Analytics profile must be selected") - |> redirect(to: Routes.site_path(conn, :settings_general, site.domain)) - - true -> - job = - Plausible.Workers.ImportGoogleAnalytics.new(%{ - "site_id" => site.id, - "profile" => profile - }) - - Ecto.Multi.new() - |> Ecto.Multi.update(:update_site, Plausible.Site.start_import(site, "Google Analytics")) - |> Oban.insert(:oban_job, job) - |> Repo.transaction() - - conn - |> put_flash(:success, "Import scheduled. An email will be sent when it completes.") - |> redirect(to: Routes.site_path(conn, :settings_general, site.domain)) - end - end - - def forget_imported(conn, _params) do - site = conn.assigns[:site] - - cond do - site.imported_data -> - Plausible.Imported.forget(site) - - site - |> Plausible.Site.remove_imported_data() - |> Repo.update!() - - conn - |> put_flash(:success, "Imported data has been forgotten") - |> redirect(to: Routes.site_path(conn, :settings_general, site.domain)) - - true -> - conn - |> put_flash(:error, "No data has been imported") - |> redirect(to: Routes.site_path(conn, :settings_general, site.domain)) - end - end -end diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 29178516edc7..27fffc22f15c 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -1,29 +1,18 @@ defmodule PlausibleWeb.StatsController do use PlausibleWeb, :controller - use Plausible.Repo - alias PlausibleWeb.Api - alias Plausible.Stats.{Query, Filters} - plug PlausibleWeb.AuthorizeSiteAccess when action in [:stats, :csv_export] + plug PlausibleWeb.AuthorizeSiteAccess when action == :stats def stats(%{assigns: %{site: %{domain: "all"} = site}} = conn, _params) do - can_see_stats = conn.assigns[:current_user_role] == :owner - - if can_see_stats do - conn - |> assign(:skip_plausible_tracking, true) - |> remove_email_report_banner(site) - |> put_resp_header("x-robots-tag", "noindex") - |> render("stats.html", - site: site, - has_goals: false, - title: "Careers Analtyics", - offer_email_report: false, - demo: false - ) - else - render_error(conn, 404) - end + conn + |> put_resp_header("x-robots-tag", "noindex") + |> render("stats.html", + site: site, + has_goals: false, + title: "Careers Analytics", + offer_email_report: false, + demo: false + ) end def stats(%{assigns: %{site: site}} = conn, _params) do @@ -36,12 +25,10 @@ defmodule PlausibleWeb.StatsController do offer_email_report = get_session(conn, site.domain <> "_offer_email_report") conn - |> assign(:skip_plausible_tracking, !demo) - |> remove_email_report_banner(site) |> put_resp_header("x-robots-tag", "noindex") |> render("stats.html", site: site, - has_goals: Plausible.Sites.has_goals?(site), + has_goals: false, title: "Plausible · " <> site.domain, offer_email_report: offer_email_report, demo: demo @@ -49,180 +36,13 @@ defmodule PlausibleWeb.StatsController do !has_stats && can_see_stats -> conn - |> assign(:skip_plausible_tracking, true) |> render("waiting_first_pageview.html", site: site) site.locked -> owner = Plausible.Sites.owner_for(site) conn - |> assign(:skip_plausible_tracking, true) |> render("site_locked.html", owner: owner, site: site) end end - - @doc """ - The export is limited to 300 entries for other reports and 100 entries for pages because bigger result sets - start causing failures. Since we request data like time on page or bounce_rate for pages in a separate query - using the IN filter, it causes the requests to balloon in payload size. - """ - def csv_export(conn, params) do - site = conn.assigns[:site] - query = Query.from(site, params) |> Filters.add_prefix() - - metrics = [:visitors, :pageviews, :bounce_rate, :visit_duration] - graph = Plausible.Stats.timeseries(site, query, metrics) - headers = [:date | metrics] - - visitors = - Enum.map(graph, fn row -> Enum.map(headers, &row[&1]) end) - |> (fn data -> [headers | data] end).() - |> CSV.encode() - |> Enum.join() - - filename = - "Plausible export #{params["domain"]} #{Timex.format!(query.date_range.first, "{ISOdate} ")} to #{Timex.format!(query.date_range.last, "{ISOdate} ")}.zip" - - params = Map.merge(params, %{"limit" => "300", "csv" => "True", "detailed" => "True"}) - limited_params = Map.merge(params, %{"limit" => "100"}) - - csvs = [ - {'sources.csv', fn -> Api.StatsController.sources(conn, params) end}, - {'utm_mediums.csv', fn -> Api.StatsController.utm_mediums(conn, params) end}, - {'utm_sources.csv', fn -> Api.StatsController.utm_sources(conn, params) end}, - {'utm_campaigns.csv', fn -> Api.StatsController.utm_campaigns(conn, params) end}, - {'utm_contents.csv', fn -> Api.StatsController.utm_contents(conn, params) end}, - {'utm_terms.csv', fn -> Api.StatsController.utm_terms(conn, params) end}, - {'pages.csv', fn -> Api.StatsController.pages(conn, limited_params) end}, - {'entry_pages.csv', fn -> Api.StatsController.entry_pages(conn, params) end}, - {'exit_pages.csv', fn -> Api.StatsController.exit_pages(conn, limited_params) end}, - {'countries.csv', fn -> Api.StatsController.countries(conn, params) end}, - {'regions.csv', fn -> Api.StatsController.regions(conn, params) end}, - {'cities.csv', fn -> Api.StatsController.cities(conn, params) end}, - {'browsers.csv', fn -> Api.StatsController.browsers(conn, params) end}, - {'operating_systems.csv', fn -> Api.StatsController.operating_systems(conn, params) end}, - {'devices.csv', fn -> Api.StatsController.screen_sizes(conn, params) end}, - {'conversions.csv', fn -> Api.StatsController.conversions(conn, params) end}, - {'prop_breakdown.csv', fn -> Api.StatsController.all_props_breakdown(conn, params) end} - ] - - csvs = - csvs - |> Enum.map(fn {file, task} -> {file, Task.async(task)} end) - |> Enum.map(fn {file, task} -> {file, Task.await(task)} end) - - csvs = [{'visitors.csv', visitors} | csvs] - - {:ok, {_, zip_content}} = :zip.create(filename, csvs, [:memory]) - - conn - |> put_resp_content_type("application/zip") - |> put_resp_header("content-disposition", "attachment; filename=\"#{filename}\"") - |> delete_resp_cookie("exporting") - |> send_resp(200, zip_content) - end - - def shared_link(conn, %{"domain" => domain, "auth" => auth}) do - shared_link = - Repo.get_by(Plausible.Site.SharedLink, slug: auth) - |> Repo.preload(:site) - - if shared_link && shared_link.site.domain == domain do - if shared_link.password_hash do - with conn <- Plug.Conn.fetch_cookies(conn), - {:ok, token} <- Map.fetch(conn.req_cookies, shared_link_cookie_name(auth)), - {:ok, %{slug: token_slug}} <- Plausible.Auth.Token.verify_shared_link(token), - true <- token_slug == shared_link.slug do - render_shared_link(conn, shared_link) - else - _e -> - conn - |> assign(:skip_plausible_tracking, true) - |> render("shared_link_password.html", - link: shared_link, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - else - render_shared_link(conn, shared_link) - end - end - end - - def shared_link(conn, %{"slug" => slug}) do - shared_link = - Repo.get_by(Plausible.Site.SharedLink, slug: slug) - |> Repo.preload(:site) - - if shared_link do - redirect(conn, to: "/share/#{URI.encode_www_form(shared_link.site.domain)}?auth=#{slug}") - else - render_error(conn, 404) - end - end - - def authenticate_shared_link(conn, %{"slug" => slug, "password" => password}) do - shared_link = - Repo.get_by(Plausible.Site.SharedLink, slug: slug) - |> Repo.preload(:site) - - if shared_link do - if Plausible.Auth.Password.match?(password, shared_link.password_hash) do - token = Plausible.Auth.Token.sign_shared_link(slug) - - conn - |> put_resp_cookie(shared_link_cookie_name(slug), token) - |> redirect(to: "/share/#{URI.encode_www_form(shared_link.site.domain)}?auth=#{slug}") - else - conn - |> assign(:skip_plausible_tracking, true) - |> render("shared_link_password.html", - link: shared_link, - error: "Incorrect password. Please try again.", - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - else - render_error(conn, 404) - end - end - - defp render_shared_link(conn, shared_link) do - cond do - !shared_link.site.locked -> - conn - |> assign(:skip_plausible_tracking, true) - |> put_resp_header("x-robots-tag", "noindex") - |> delete_resp_header("x-frame-options") - |> render("stats.html", - site: shared_link.site, - has_goals: Plausible.Sites.has_goals?(shared_link.site), - title: "Plausible · " <> shared_link.site.domain, - offer_email_report: false, - demo: false, - skip_plausible_tracking: true, - shared_link_auth: shared_link.slug, - embedded: conn.params["embed"] == "true", - background: conn.params["background"], - theme: conn.params["theme"] - ) - - shared_link.site.locked -> - owner = Plausible.Sites.owner_for(shared_link.site) - - conn - |> assign(:skip_plausible_tracking, true) - |> render("site_locked.html", owner: owner, site: shared_link.site) - end - end - - defp remove_email_report_banner(conn, site) do - if conn.assigns[:current_user] do - delete_session(conn, site.domain <> "_offer_email_report") - else - conn - end - end - - defp shared_link_cookie_name(slug), do: "shared-link-" <> slug end diff --git a/lib/plausible_web/controllers/unsubscribe_controller.ex b/lib/plausible_web/controllers/unsubscribe_controller.ex deleted file mode 100644 index eb38813b8bcd..000000000000 --- a/lib/plausible_web/controllers/unsubscribe_controller.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule PlausibleWeb.UnsubscribeController do - use PlausibleWeb, :controller - use Plausible.Repo - alias Plausible.Site.{WeeklyReport, MonthlyReport} - - def weekly_report(conn, %{"website" => website, "email" => email}) do - site = Repo.get_by(Plausible.Site, domain: website) - weekly_report = site && Repo.get_by(WeeklyReport, site_id: site.id) - - if weekly_report do - weekly_report - |> WeeklyReport.remove_recipient(email) - |> Repo.update!() - end - - conn - |> assign(:skip_plausible_tracking, true) - |> render("success.html", - interval: "weekly", - site: website, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end - - def monthly_report(conn, %{"website" => website, "email" => email}) do - site = Repo.get_by(Plausible.Site, domain: website) - monthly_report = site && Repo.get_by(MonthlyReport, site_id: site.id) - - if monthly_report do - monthly_report - |> MonthlyReport.remove_recipient(email) - |> Repo.update!() - end - - conn - |> assign(:skip_plausible_tracking, true) - |> render("success.html", - interval: "monthly", - site: website, - layout: {PlausibleWeb.LayoutView, "focus.html"} - ) - end -end diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex deleted file mode 100644 index 2508babdbbf4..000000000000 --- a/lib/plausible_web/email.ex +++ /dev/null @@ -1,315 +0,0 @@ -defmodule PlausibleWeb.Email do - use Bamboo.Phoenix, view: PlausibleWeb.EmailView - import Bamboo.PostmarkHelper - - def mailer_email_from do - Application.get_env(:plausible, :mailer_email) - end - - def activation_email(user, code) do - base_email() - |> to(user) - |> tag("activation-email") - |> subject("#{code} is your Plausible email verification code") - |> render("activation_email.html", user: user, code: code) - end - - def welcome_email(user) do - base_email() - |> to(user) - |> tag("welcome-email") - |> subject("Welcome to Plausible") - |> render("welcome_email.html", user: user) - end - - def create_site_email(user) do - base_email() - |> to(user) - |> tag("create-site-email") - |> subject("Your Plausible setup: Add your website details") - |> render("create_site_email.html", user: user) - end - - def site_setup_help(user, site) do - base_email() - |> to(user) - |> tag("help-email") - |> subject("Your Plausible setup: Waiting for the first page views") - |> render("site_setup_help_email.html", user: user, site: site) - end - - def site_setup_success(user, site) do - base_email() - |> to(user) - |> tag("setup-success-email") - |> subject("Plausible is now tracking your website stats") - |> render("site_setup_success_email.html", user: user, site: site) - end - - def check_stats_email(user) do - base_email() - |> to(user) - |> tag("check-stats-email") - |> subject("Check your Plausible website stats") - |> render("check_stats_email.html", user: user) - end - - def password_reset_email(email, reset_link) do - base_email() - |> to(email) - |> tag("password-reset-email") - |> subject("Plausible password reset") - |> render("password_reset_email.html", reset_link: reset_link) - end - - def trial_one_week_reminder(user) do - base_email() - |> to(user) - |> tag("trial-one-week-reminder") - |> subject("Your Plausible trial expires next week") - |> render("trial_one_week_reminder.html", user: user) - end - - def trial_upgrade_email(user, day, {pageviews, custom_events}) do - suggested_plan = Plausible.Billing.Plans.suggested_plan(user, pageviews + custom_events) - - base_email() - |> to(user) - |> tag("trial-upgrade-email") - |> subject("Your Plausible trial ends #{day}") - |> render("trial_upgrade_email.html", - user: user, - day: day, - custom_events: custom_events, - usage: pageviews + custom_events, - suggested_plan: suggested_plan - ) - end - - def trial_over_email(user) do - base_email() - |> to(user) - |> tag("trial-over-email") - |> subject("Your Plausible trial has ended") - |> render("trial_over_email.html", user: user) - end - - def weekly_report(email, site, assigns) do - base_email() - |> to(email) - |> tag("weekly-report") - |> subject("#{assigns[:name]} report for #{site.domain}") - |> render("weekly_report.html", Keyword.put(assigns, :site, site)) - end - - def spike_notification(email, site, current_visitors, sources, dashboard_link) do - base_email() - |> to(email) - |> tag("spike-notification") - |> subject("Traffic spike on #{site.domain}") - |> render("spike_notification.html", %{ - site: site, - current_visitors: current_visitors, - sources: sources, - link: dashboard_link - }) - end - - def over_limit_email(user, usage, last_cycle, suggested_plan) do - base_email() - |> to(user) - |> tag("over-limit") - |> subject("[Action required] You have outgrown your Plausible subscription tier") - |> render("over_limit.html", %{ - user: user, - usage: usage, - last_cycle: last_cycle, - suggested_plan: suggested_plan - }) - end - - def enterprise_over_limit_email(user, usage, last_cycle, site_usage, site_allowance) do - base_email() - |> to("enterprise@plausible.io") - |> tag("enterprise-over-limit") - |> subject("#{user.email} has outgrown their enterprise plan") - |> render("enterprise_over_limit.html", %{ - user: user, - usage: usage, - last_cycle: last_cycle, - site_usage: site_usage, - site_allowance: site_allowance - }) - end - - def dashboard_locked(user, usage, last_cycle, suggested_plan) do - base_email() - |> to(user) - |> tag("dashboard-locked") - |> subject("[Action required] Your Plausible dashboard is now locked") - |> render("dashboard_locked.html", %{ - user: user, - usage: usage, - last_cycle: last_cycle, - suggested_plan: suggested_plan - }) - end - - def yearly_renewal_notification(user) do - date = Timex.format!(user.subscription.next_bill_date, "{Mfull} {D}, {YYYY}") - - base_email() - |> to(user) - |> tag("yearly-renewal") - |> subject("Your Plausible subscription is up for renewal") - |> render("yearly_renewal_notification.html", %{ - user: user, - date: date, - next_bill_amount: user.subscription.next_bill_amount, - currency: user.subscription.currency_code - }) - end - - def yearly_expiration_notification(user) do - date = Timex.format!(user.subscription.next_bill_date, "{Mfull} {D}, {YYYY}") - - base_email() - |> to(user) - |> tag("yearly-expiration") - |> subject("Your Plausible subscription is about to expire") - |> render("yearly_expiration_notification.html", %{ - user: user, - date: date - }) - end - - def cancellation_email(user) do - base_email() - |> to(user.email) - |> tag("cancelled-email") - |> subject("Your Plausible Analytics subscription has been canceled") - |> render("cancellation_email.html", name: user.name) - end - - def new_user_invitation(invitation) do - base_email() - |> to(invitation.email) - |> tag("new-user-invitation") - |> subject("[Plausible Analytics] You've been invited to #{invitation.site.domain}") - |> render("new_user_invitation.html", - invitation: invitation - ) - end - - def existing_user_invitation(invitation) do - base_email() - |> to(invitation.email) - |> tag("existing-user-invitation") - |> subject("[Plausible Analytics] You've been invited to #{invitation.site.domain}") - |> render("existing_user_invitation.html", - invitation: invitation - ) - end - - def ownership_transfer_request(invitation, new_owner_account) do - base_email() - |> to(invitation.email) - |> tag("ownership-transfer-request") - |> subject("[Plausible Analytics] Request to transfer ownership of #{invitation.site.domain}") - |> render("ownership_transfer_request.html", - invitation: invitation, - new_owner_account: new_owner_account - ) - end - - def invitation_accepted(invitation) do - base_email() - |> to(invitation.inviter.email) - |> tag("invitation-accepted") - |> subject( - "[Plausible Analytics] #{invitation.email} accepted your invitation to #{invitation.site.domain}" - ) - |> render("invitation_accepted.html", - invitation: invitation - ) - end - - def invitation_rejected(invitation) do - base_email() - |> to(invitation.inviter.email) - |> tag("invitation-rejected") - |> subject( - "[Plausible Analytics] #{invitation.email} rejected your invitation to #{invitation.site.domain}" - ) - |> render("invitation_rejected.html", - invitation: invitation - ) - end - - def ownership_transfer_accepted(invitation) do - base_email() - |> to(invitation.inviter.email) - |> tag("ownership-transfer-accepted") - |> subject( - "[Plausible Analytics] #{invitation.email} accepted the ownership transfer of #{invitation.site.domain}" - ) - |> render("ownership_transfer_accepted.html", - invitation: invitation - ) - end - - def ownership_transfer_rejected(invitation) do - base_email() - |> to(invitation.inviter.email) - |> tag("ownership-transfer-rejected") - |> subject( - "[Plausible Analytics] #{invitation.email} rejected the ownership transfer of #{invitation.site.domain}" - ) - |> render("ownership_transfer_rejected.html", - invitation: invitation - ) - end - - def site_member_removed(membership) do - base_email() - |> to(membership.user.email) - |> tag("site-member-removed") - |> subject("[Plausible Analytics] Your access to #{membership.site.domain} has been revoked") - |> render("site_member_removed.html", - membership: membership - ) - end - - def import_success(user, site) do - base_email() - |> to(user) - |> tag("import-success-email") - |> subject("Google Analytics data imported for #{site.domain}") - |> render("google_analytics_import.html", %{ - site: site, - link: PlausibleWeb.Endpoint.url() <> "/" <> URI.encode_www_form(site.domain), - user: user, - success: true - }) - end - - def import_failure(user, site) do - base_email() - |> to(user) - |> tag("import-failure-email") - |> subject("Google Analytics import failed for #{site.domain}") - |> render("google_analytics_import.html", %{ - user: user, - site: site, - success: false - }) - end - - defp base_email() do - mailer_from = Application.get_env(:plausible, :mailer_email) - - new_email() - |> put_param("TrackOpens", false) - |> from(mailer_from) - end -end diff --git a/lib/plausible_web/endpoint.ex b/lib/plausible_web/endpoint.ex index 2bafa948b037..84f65f185045 100644 --- a/lib/plausible_web/endpoint.ex +++ b/lib/plausible_web/endpoint.ex @@ -6,7 +6,6 @@ defmodule PlausibleWeb.Endpoint do # # You should set gzip to true if you are running phx.digest # when deploying your static files in production. - plug PlausibleWeb.Tracker plug PlausibleWeb.Favicon plug Plug.Static, @@ -15,12 +14,6 @@ defmodule PlausibleWeb.Endpoint do gzip: false, only: ~w(css fonts images js favicon.ico robots.txt) - plug Plug.Static, - at: "/kaffy", - from: :kaffy, - gzip: false, - only: ~w(assets) - # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/plausible_web/plugs/auth_plug.ex b/lib/plausible_web/plugs/auth_plug.ex index ee116a8f1258..64cd35b7a489 100644 --- a/lib/plausible_web/plugs/auth_plug.ex +++ b/lib/plausible_web/plugs/auth_plug.ex @@ -1,30 +1,16 @@ defmodule PlausibleWeb.AuthPlug do import Plug.Conn - use Plausible.Repo + import Ecto.Query + + alias Plausible.Repo def init(options) do options end def call(conn, _opts) do - case get_session(conn, :current_user_id) do - nil -> - conn - - id -> - user = - Repo.get_by(Plausible.Auth.User, id: id) - |> Repo.preload( - subscription: - from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at]) - ) + user = Plausible.Auth.User |> limit(1) |> Repo.one() - if user do - Sentry.Context.set_user_context(%{id: user.id, name: user.name, email: user.email}) - assign(conn, :current_user, user) - else - conn - end - end + assign(conn, :current_user, user) end end diff --git a/lib/plausible_web/plugs/authorize_site_access.ex b/lib/plausible_web/plugs/authorize_site_access.ex index 9d5987e862f4..80cca81b7fa5 100644 --- a/lib/plausible_web/plugs/authorize_site_access.ex +++ b/lib/plausible_web/plugs/authorize_site_access.ex @@ -1,46 +1,14 @@ defmodule PlausibleWeb.AuthorizeSiteAccess do import Plug.Conn - use Plausible.Repo + import Ecto.Query - def init([]), do: [:public, :viewer, :admin, :super_admin, :owner] - def init(allowed_roles), do: allowed_roles + alias Plausible.Repo - def call(conn, allowed_roles) do - site = Repo.get_by(Plausible.Site, domain: conn.params["domain"] || conn.params["website"]) - shared_link_auth = conn.params["auth"] + def init(opts), do: opts - shared_link_record = - shared_link_auth && Repo.get_by(Plausible.Site.SharedLink, slug: shared_link_auth) + def call(conn, _opts) do + site = Plausible.Site |> limit(1) |> Repo.one() - if !site do - PlausibleWeb.ControllerHelpers.render_error(conn, 404) |> halt - else - user_id = get_session(conn, :current_user_id) - membership_role = user_id && Plausible.Sites.role(user_id, site) - - role = - cond do - user_id && membership_role -> - membership_role - - Plausible.Auth.is_super_admin?(user_id) -> - :super_admin - - site.public -> - :public - - shared_link_record && shared_link_record.site_id == site.id -> - :public - - true -> - nil - end - - if role in allowed_roles do - merge_assigns(conn, site: site, current_user_role: role) - else - PlausibleWeb.ControllerHelpers.render_error(conn, 404) |> halt - end - end + merge_assigns(conn, site: site, current_user_role: :owner) end end diff --git a/lib/plausible_web/plugs/authorize_sites_api.ex b/lib/plausible_web/plugs/authorize_sites_api.ex deleted file mode 100644 index b55796e3d67d..000000000000 --- a/lib/plausible_web/plugs/authorize_sites_api.ex +++ /dev/null @@ -1,57 +0,0 @@ -defmodule PlausibleWeb.AuthorizeSitesApiPlug do - import Plug.Conn - use Plausible.Repo - alias Plausible.Auth.ApiKey - alias PlausibleWeb.Api.Helpers, as: H - - def init(options) do - options - end - - def call(conn, _opts) do - with {:ok, raw_api_key} <- get_bearer_token(conn), - {:ok, api_key} <- verify_access(raw_api_key) do - user = Repo.get_by(Plausible.Auth.User, id: api_key.user_id) - assign(conn, :current_user, user) - else - {:error, :missing_api_key} -> - H.unauthorized( - conn, - "Missing API key. Please use a valid Plausible API key as a Bearer Token." - ) - - {:error, :invalid_api_key} -> - H.unauthorized( - conn, - "Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested." - ) - end - end - - defp verify_access(api_key) do - hashed_key = ApiKey.do_hash(api_key) - - found_key = - Repo.one( - from a in ApiKey, - where: a.key_hash == ^hashed_key, - where: fragment("? @> ?", a.scopes, ["sites:provision:*"]) - ) - - cond do - found_key -> {:ok, found_key} - true -> {:error, :invalid_api_key} - end - end - - defp get_bearer_token(conn) do - authorization_header = - Plug.Conn.get_req_header(conn, "authorization") - |> List.first() - - case authorization_header do - "Bearer " <> token -> {:ok, String.trim(token)} - _ -> {:error, :missing_api_key} - end - end -end diff --git a/lib/plausible_web/plugs/authorize_stats_api.ex b/lib/plausible_web/plugs/authorize_stats_api.ex deleted file mode 100644 index b7515cc1480d..000000000000 --- a/lib/plausible_web/plugs/authorize_stats_api.ex +++ /dev/null @@ -1,82 +0,0 @@ -defmodule PlausibleWeb.AuthorizeStatsApiPlug do - import Plug.Conn - use Plausible.Repo - alias Plausible.Auth.ApiKey - alias PlausibleWeb.Api.Helpers, as: H - - def init(options) do - options - end - - def call(conn, _opts) do - with {:ok, token} <- get_bearer_token(conn), - {:ok, api_key} <- find_api_key(token), - :ok <- check_api_key_rate_limit(api_key), - {:ok, site} <- verify_access(api_key, conn.params["site_id"]) do - assign(conn, :site, site) - else - {:error, :missing_api_key} -> - H.unauthorized( - conn, - "Missing API key. Please use a valid Plausible API key as a Bearer Token." - ) - - {:error, :missing_site_id} -> - H.bad_request( - conn, - "Missing site ID. Please provide the required site_id parameter with your request." - ) - - {:error, :rate_limit, limit} -> - H.too_many_requests( - conn, - "Too many API requests. Your API key is limited to #{limit} requests per hour." - ) - - {:error, :invalid_api_key} -> - H.unauthorized( - conn, - "Invalid API key or site ID. Please make sure you're using a valid API key with access to the site you've requested." - ) - end - end - - defp verify_access(_api_key, nil), do: {:error, :missing_site_id} - - defp verify_access(api_key, site_id) do - site = Repo.get_by(Plausible.Site, domain: site_id) - is_member = site && Plausible.Sites.is_member?(api_key.user_id, site) - is_super_admin = Plausible.Auth.is_super_admin?(api_key.user_id) - - cond do - site && is_member -> {:ok, site} - site && is_super_admin -> {:ok, site} - true -> {:error, :invalid_api_key} - end - end - - defp get_bearer_token(conn) do - authorization_header = - Plug.Conn.get_req_header(conn, "authorization") - |> List.first() - - case authorization_header do - "Bearer " <> token -> {:ok, String.trim(token)} - _ -> {:error, :missing_api_key} - end - end - - defp find_api_key(token) do - hashed_key = ApiKey.do_hash(token) - found_key = Repo.get_by(ApiKey, key_hash: hashed_key) - if found_key, do: {:ok, found_key}, else: {:error, :invalid_api_key} - end - - @one_hour 60 * 60 * 1000 - defp check_api_key_rate_limit(api_key) do - case Hammer.check_rate("api_request:#{api_key.id}", @one_hour, api_key.hourly_request_limit) do - {:allow, _} -> :ok - {:deny, _} -> {:error, :rate_limit, api_key.hourly_request_limit} - end - end -end diff --git a/lib/plausible_web/plugs/auto_auth_plug.ex b/lib/plausible_web/plugs/auto_auth_plug.ex deleted file mode 100644 index b56c4fc8f705..000000000000 --- a/lib/plausible_web/plugs/auto_auth_plug.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule PlausibleWeb.AutoAuthPlug do - import Plug.Conn - alias PlausibleWeb.AuthController - - def init(options) do - options - end - - def call(conn, _opts) do - cond do - Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_authentication) -> - conn - |> AuthController.login(%{ - "email" => Application.fetch_env!(:plausible, :admin_email), - "password" => Application.fetch_env!(:plausible, :admin_pwd) - }) - |> halt - - true -> - Plug.Conn.put_session(conn, :login_dest, conn.request_path) - |> Phoenix.Controller.redirect(to: "/login") - |> halt - end - end -end diff --git a/lib/plausible_web/plugs/crm_auth_plug.ex b/lib/plausible_web/plugs/crm_auth_plug.ex deleted file mode 100644 index fa68864d8e77..000000000000 --- a/lib/plausible_web/plugs/crm_auth_plug.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule PlausibleWeb.CRMAuthPlug do - import Plug.Conn - use Plausible.Repo - - def init(options) do - options - end - - def call(conn, _opts) do - case get_session(conn, :current_user_id) do - nil -> - conn |> send_resp(403, "Not allowed") |> halt - - id -> - user = Repo.get_by(Plausible.Auth.User, id: id) - - if user && Plausible.Auth.is_super_admin?(user.id) do - assign(conn, :current_user, user) - else - conn |> send_resp(403, "Not allowed") |> halt - end - end - end -end diff --git a/lib/plausible_web/plugs/firewall.ex b/lib/plausible_web/plugs/firewall.ex deleted file mode 100644 index 266a109283f0..000000000000 --- a/lib/plausible_web/plugs/firewall.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule PlausibleWeb.Firewall do - import Plug.Conn - - def init(opts) do - opts - end - - def call(conn, _opts) do - blocklist = Keyword.fetch!(Application.get_env(:plausible, __MODULE__), :blocklist) - - if PlausibleWeb.RemoteIp.get(conn) in blocklist do - send_resp(conn, 404, "Not found") |> halt - else - conn - end - end -end diff --git a/lib/plausible_web/plugs/last_seen_plug.ex b/lib/plausible_web/plugs/last_seen_plug.ex deleted file mode 100644 index 5a935d62f0c7..000000000000 --- a/lib/plausible_web/plugs/last_seen_plug.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule PlausibleWeb.LastSeenPlug do - import Plug.Conn - use Plausible.Repo - - @one_hour 60 * 60 - - def init(opts) do - opts - end - - def call(conn, _opts) do - last_seen = get_session(conn, :last_seen) - user = conn.assigns[:current_user] - - cond do - user && last_seen && last_seen < unix_now() - @one_hour -> - persist_last_seen(user) - put_session(conn, :last_seen, unix_now()) - - user && !last_seen -> - put_session(conn, :last_seen, unix_now()) - - true -> - conn - end - end - - defp persist_last_seen(user) do - q = from(u in Plausible.Auth.User, where: u.id == ^user.id) - - Repo.update_all(q, set: [last_seen: DateTime.utc_now()]) - end - - defp unix_now do - DateTime.utc_now() |> DateTime.to_unix() - end -end diff --git a/lib/plausible_web/plugs/logger_metadata_plug.ex b/lib/plausible_web/plugs/logger_metadata_plug.ex deleted file mode 100644 index 9cdca092e7d3..000000000000 --- a/lib/plausible_web/plugs/logger_metadata_plug.ex +++ /dev/null @@ -1,62 +0,0 @@ -defmodule PlausibleWeb.LoggerMetadataPlug do - require Logger - - @behaviour Plug - - def init(opts), do: opts - - def call(conn, _opts) do - start = System.monotonic_time() - - Plug.Conn.register_before_send(conn, fn conn -> - pipelines = conn.private[:phoenix_pipelines] - controller = conn.private[:phoenix_controller] |> encode_controller() - action = conn.private[:phoenix_action] - method = conn.method - path = conn.request_path - url = "#{conn.scheme}://#{conn.host}:#{conn.port}#{path}" - source = to_string(:inet.ntoa(conn.remote_ip)) - status = conn.status - target = "plausible" - params = filter_values(conn.params) - - stop = System.monotonic_time() - diff = System.convert_time_unit(stop - start, :native, :microsecond) - - meta = [ - action: action, - controller: controller, - method: method, - params: params, - path: path, - pipelines: pipelines, - source: source, - status: status, - target: target, - time_us: diff, - url: url - ] - - Logger.metadata(meta) - - conn - end) - end - - defp encode_controller(nil), do: nil - - defp encode_controller(atom) when is_atom(atom), - do: atom |> Atom.to_string() |> encode_controller() - - defp encode_controller("Elixir." <> rest), do: rest - defp encode_controller(controller), do: controller - - @filtered_keys ~w(email password password_confirmation) - - defp filter_values(%Plug.Conn.Unfetched{}), do: "[UNFETCHED]" - defp filter_values(map) when is_map(map), do: Enum.into(map, %{}, &filter_values/1) - defp filter_values(list) when is_list(list), do: Enum.map(list, &filter_values/1) - - defp filter_values({key, _value}) when key in @filtered_keys, do: {key, "[FILTERED]"} - defp filter_values({key, value}), do: {key, value} -end diff --git a/lib/plausible_web/plugs/require_account.ex b/lib/plausible_web/plugs/require_account.ex deleted file mode 100644 index 4295a4801719..000000000000 --- a/lib/plausible_web/plugs/require_account.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule PlausibleWeb.RequireAccountPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _opts) do - user = conn.assigns[:current_user] - - cond do - is_nil(user) -> - Plug.Conn.put_session(conn, :login_dest, conn.request_path) - |> Phoenix.Controller.redirect(to: "/login") - |> halt - - not user.email_verified and conn.path_info not in [["activate"], ["me"]] -> - conn - |> Phoenix.Controller.redirect(to: "/activate") - |> halt - - true -> - conn - end - end -end diff --git a/lib/plausible_web/plugs/require_logged_out.ex b/lib/plausible_web/plugs/require_logged_out.ex deleted file mode 100644 index ac35a4e7fb28..000000000000 --- a/lib/plausible_web/plugs/require_logged_out.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule PlausibleWeb.RequireLoggedOutPlug do - import Plug.Conn - - def init(options) do - options - end - - def call(conn, _opts) do - cond do - conn.assigns[:current_user] -> - conn - |> put_resp_cookie("logged_in", "true", - http_only: false, - max_age: 60 * 60 * 24 * 365 * 5000 - ) - |> Phoenix.Controller.redirect(to: "/sites") - |> halt - - :else -> - conn - end - end -end diff --git a/lib/plausible_web/plugs/session_timeout_plug.ex b/lib/plausible_web/plugs/session_timeout_plug.ex deleted file mode 100644 index efa9fa054137..000000000000 --- a/lib/plausible_web/plugs/session_timeout_plug.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule PlausibleWeb.SessionTimeoutPlug do - import Plug.Conn - - def init(opts \\ []) do - opts - end - - def call(conn, opts) do - timeout_at = get_session(conn, :session_timeout_at) - user_id = get_session(conn, :current_user_id) - - cond do - user_id && timeout_at && now() > timeout_at -> - conn - |> configure_session(drop: true) - |> delete_resp_cookie("logged_in") - - user_id -> - put_session( - conn, - :session_timeout_at, - new_session_timeout_at(opts[:timeout_after_seconds]) - ) - - true -> - conn - end - end - - defp now do - DateTime.utc_now() |> DateTime.to_unix() - end - - defp new_session_timeout_at(timeout_after_seconds) do - now() + timeout_after_seconds - end -end diff --git a/lib/plausible_web/plugs/tracker.ex b/lib/plausible_web/plugs/tracker.ex deleted file mode 100644 index 71d60327414a..000000000000 --- a/lib/plausible_web/plugs/tracker.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule PlausibleWeb.Tracker do - import Plug.Conn - use Agent - - base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual"] - base_filenames = ["plausible", "script"] - - # Generates Power Set of all variants - variants = - 1..Enum.count(base_variants) - |> Enum.map(fn x -> - Combination.combine(base_variants, x) - |> Enum.map(fn y -> Enum.sort(y) |> Enum.join(".") end) - end) - |> List.flatten() - - # Formats power set into filenames - files_available = - ["plausible.js", "p.js"] ++ Enum.map(variants, fn v -> "plausible.#{v}.js" end) - - # Computes permutations for every power set elements, formats them as alias filenames - aliases_available = - Enum.map(variants, fn x -> - variants = - String.split(x, ".") - |> Combination.permutate() - |> Enum.map(fn p -> Enum.join(p, ".") end) - |> Enum.map(fn v -> Enum.map(base_filenames, fn filename -> "#{filename}.#{v}.js" end) end) - |> List.flatten() - - if Enum.count(variants) > 0 do - {"plausible.#{x}.js", variants} - end - end) - |> Enum.reject(fn x -> x == nil end) - |> Enum.into(%{}) - |> Map.put("plausible.js", ["analytics.js", "script.js"]) - - @templates files_available - @aliases aliases_available - - def init(_) do - all_files = - Enum.reduce(@templates, %{}, fn template_filename, all_files -> - aliases = Map.get(@aliases, template_filename, []) - - [template_filename | aliases] - |> Enum.map(fn filename -> {"/js/" <> filename, template_filename} end) - |> Enum.into(%{}) - |> Map.merge(all_files) - end) - - [files: all_files] - end - - def call(conn, files: files) do - case files[conn.request_path] do - nil -> - conn - - found -> - location = Application.app_dir(:plausible, "priv/tracker/js/" <> found) - - conn - |> put_resp_header("content-type", "application/javascript") - |> put_resp_header("x-content-type-options", "nosniff") - |> put_resp_header("cross-origin-resource-policy", "cross-origin") - |> put_resp_header("access-control-allow-origin", "*") - |> put_resp_header("cache-control", "public, max-age=86400, must-revalidate") - |> send_file(200, location) - |> halt() - end - end -end diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 19b84a0d9ff6..884f6b5abbd4 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -1,54 +1,26 @@ defmodule PlausibleWeb.Router do use PlausibleWeb, :router - @two_weeks_in_seconds 60 * 60 * 24 * 14 - pipeline :browser do plug :accepts, ["html"] - plug PlausibleWeb.Firewall plug :fetch_session plug :fetch_flash plug :put_secure_browser_headers - plug PlausibleWeb.SessionTimeoutPlug, timeout_after_seconds: @two_weeks_in_seconds - plug PlausibleWeb.AuthPlug - plug PlausibleWeb.LastSeenPlug - end - - # pipeline :shared_link do - # plug :accepts, ["html"] - # plug :put_secure_browser_headers - # end - - pipeline :csrf do plug :protect_from_forgery + plug PlausibleWeb.AuthPlug end pipeline :api do plug :accepts, ["json"] - plug PlausibleWeb.Firewall plug :fetch_session - plug PlausibleWeb.AuthPlug - plug PlausibleWeb.LoggerMetadataPlug end pipeline :internal_stats_api do plug :accepts, ["json"] - plug PlausibleWeb.Firewall plug :fetch_session plug PlausibleWeb.AuthorizeSiteAccess end - # pipeline :public_api do - # plug :accepts, ["json"] - # plug PlausibleWeb.Firewall - # end - - # if Mix.env() == :dev do - # forward "/sent-emails", Bamboo.SentEmailViewerPlug - # end - - # use Kaffy.Routes, scope: "/crm", pipe_through: [PlausibleWeb.CRMAuthPlug] - scope "/api/stats", PlausibleWeb.Api do pipe_through :internal_stats_api @@ -77,173 +49,16 @@ defmodule PlausibleWeb.Router do get "/:domain/suggestions/:filter_name", StatsController, :filter_suggestions end - # scope "/api/v1/stats", PlausibleWeb.Api do - # pipe_through [:public_api, PlausibleWeb.AuthorizeStatsApiPlug] - - # get "/realtime/visitors", ExternalStatsController, :realtime_visitors - # get "/aggregate", ExternalStatsController, :aggregate - # get "/breakdown", ExternalStatsController, :breakdown - # get "/timeseries", ExternalStatsController, :timeseries - # end - - # scope "/api/v1/sites", PlausibleWeb.Api do - # pipe_through [:public_api, PlausibleWeb.AuthorizeSitesApiPlug] - - # post "/", ExternalSitesController, :create_site - # delete "/:site_id", ExternalSitesController, :delete_site - # put "/shared-links", ExternalSitesController, :find_or_create_shared_link - # put "/goals", ExternalSitesController, :find_or_create_goal - # delete "/goals/:goal_id", ExternalSitesController, :delete_goal - # end - scope "/api", PlausibleWeb do pipe_through :api post "/event", Api.ExternalController, :event - # get "/error", Api.ExternalController, :error - # get "/health", Api.ExternalController, :health - - # post "/paddle/webhook", Api.PaddleController, :webhook - - # get "/:domain/status", Api.InternalController, :domain_status - # get "/sites", Api.InternalController, :sites end scope "/", PlausibleWeb do - pipe_through [:browser, :csrf] - - # get "/register", AuthController, :register_form - # post "/register", AuthController, :register - # get "/register/invitation/:invitation_id", AuthController, :register_from_invitation_form - # post "/register/invitation/:invitation_id", AuthController, :register_from_invitation - # get "/activate", AuthController, :activate_form - # post "/activate/request-code", AuthController, :request_activation_code - # post "/activate", AuthController, :activate - get "/login", AuthController, :login_form - post "/login", AuthController, :login - # get "/password/request-reset", AuthController, :password_reset_request_form - # post "/password/request-reset", AuthController, :password_reset_request - # get "/password/reset", AuthController, :password_reset_form - # post "/password/reset", AuthController, :password_reset - end - - # scope "/", PlausibleWeb do - # pipe_through [:shared_link] - - # get "/share/:domain", StatsController, :shared_link - # post "/share/:slug/authenticate", StatsController, :authenticate_shared_link - # end - - scope "/", PlausibleWeb do - pipe_through [:browser, :csrf] - - # get "/password", AuthController, :password_form - # post "/password", AuthController, :set_password - get "/logout", AuthController, :logout - # get "/settings", AuthController, :user_settings - # put "/settings", AuthController, :save_settings - # delete "/me", AuthController, :delete_me - # get "/settings/api-keys/new", AuthController, :new_api_key - # post "/settings/api-keys", AuthController, :create_api_key - # delete "/settings/api-keys/:id", AuthController, :delete_api_key - - # get "/auth/google/callback", AuthController, :google_auth_callback + pipe_through :browser get "/", PageController, :index - - # get "/billing/change-plan", BillingController, :change_plan_form - # get "/billing/change-plan/preview/:plan_id", BillingController, :change_plan_preview - # post "/billing/change-plan/:new_plan_id", BillingController, :change_plan - # get "/billing/upgrade", BillingController, :upgrade - # get "/billing/upgrade/:plan_id", BillingController, :upgrade_to_plan - # get "/billing/upgrade/enterprise/:plan_id", BillingController, :upgrade_enterprise_plan - # get "/billing/change-plan/enterprise/:plan_id", BillingController, :change_enterprise_plan - # get "/billing/upgrade-success", BillingController, :upgrade_success - - get "/sites", SiteController, :index - # get "/sites/new", SiteController, :new - # post "/sites", SiteController, :create_site - # post "/sites/:website/make-public", SiteController, :make_public - # post "/sites/:website/make-private", SiteController, :make_private - # post "/sites/:website/weekly-report/enable", SiteController, :enable_weekly_report - # post "/sites/:website/weekly-report/disable", SiteController, :disable_weekly_report - # post "/sites/:website/weekly-report/recipients", SiteController, :add_weekly_report_recipient - - # delete "/sites/:website/weekly-report/recipients/:recipient", - # SiteController, - # :remove_weekly_report_recipient - - # post "/sites/:website/monthly-report/enable", SiteController, :enable_monthly_report - # post "/sites/:website/monthly-report/disable", SiteController, :disable_monthly_report - - # post "/sites/:website/monthly-report/recipients", - # SiteController, - # :add_monthly_report_recipient - - # delete "/sites/:website/monthly-report/recipients/:recipient", - # SiteController, - # :remove_monthly_report_recipient - - # post "/sites/:website/spike-notification/enable", SiteController, :enable_spike_notification - # post "/sites/:website/spike-notification/disable", SiteController, :disable_spike_notification - # put "/sites/:website/spike-notification", SiteController, :update_spike_notification - - # post "/sites/:website/spike-notification/recipients", - # SiteController, - # :add_spike_notification_recipient - - # delete "/sites/:website/spike-notification/recipients/:recipient", - # SiteController, - # :remove_spike_notification_recipient - - # get "/sites/:website/shared-links/new", SiteController, :new_shared_link - # post "/sites/:website/shared-links", SiteController, :create_shared_link - # get "/sites/:website/shared-links/:slug/edit", SiteController, :edit_shared_link - # put "/sites/:website/shared-links/:slug", SiteController, :update_shared_link - # delete "/sites/:website/shared-links/:slug", SiteController, :delete_shared_link - - # delete "/sites/:website/custom-domains/:id", SiteController, :delete_custom_domain - - # get "/sites/:website/memberships/invite", Site.MembershipController, :invite_member_form - # post "/sites/:website/memberships/invite", Site.MembershipController, :invite_member - - # post "/sites//invitations/:invitation_id/accept", InvitationController, :accept_invitation - # post "/sites//invitations/:invitation_id/reject", InvitationController, :reject_invitation - # delete "/sites//invitations/:invitation_id", InvitationController, :remove_invitation - - # get "/sites/:website/transfer-ownership", Site.MembershipController, :transfer_ownership_form - # post "/sites/:website/transfer-ownership", Site.MembershipController, :transfer_ownership - - # put "/sites/:website/memberships/:id/role/:new_role", Site.MembershipController, :update_role - # delete "/sites/:website/memberships/:id", Site.MembershipController, :remove_member - - # get "/sites/:website/weekly-report/unsubscribe", UnsubscribeController, :weekly_report - # get "/sites/:website/monthly-report/unsubscribe", UnsubscribeController, :monthly_report - - # get "/:website/snippet", SiteController, :add_snippet - # get "/:website/settings", SiteController, :settings - # get "/:website/settings/general", SiteController, :settings_general - # get "/:website/settings/people", SiteController, :settings_people - # get "/:website/settings/visibility", SiteController, :settings_visibility - # get "/:website/settings/goals", SiteController, :settings_goals - # get "/:website/settings/search-console", SiteController, :settings_search_console - # get "/:website/settings/email-reports", SiteController, :settings_email_reports - # get "/:website/settings/custom-domain", SiteController, :settings_custom_domain - # get "/:website/settings/danger-zone", SiteController, :settings_danger_zone - # get "/:website/goals/new", SiteController, :new_goal - # post "/:website/goals", SiteController, :create_goal - # delete "/:website/goals/:id", SiteController, :delete_goal - # put "/:website/settings", SiteController, :update_settings - # put "/:website/settings/google", SiteController, :update_google_auth - # delete "/:website/settings/google-search", SiteController, :delete_google_auth - # delete "/:website/settings/google-import", SiteController, :delete_google_auth - # delete "/:website", SiteController, :delete_site - # delete "/:website/stats", SiteController, :reset_stats - - # get "/:domain/export", StatsController, :csv_export get "/:domain/*path", StatsController, :stats - - # post "/:website/settings/google-import", SiteController, :import_from_google - # delete "/:website/settings/forget-imported", SiteController, :forget_imported end end diff --git a/lib/plausible_web/templates/auth/_onboarding_steps.html.eex b/lib/plausible_web/templates/auth/_onboarding_steps.html.eex deleted file mode 100644 index b888b0fd89f7..000000000000 --- a/lib/plausible_web/templates/auth/_onboarding_steps.html.eex +++ /dev/null @@ -1,39 +0,0 @@ -
- -
diff --git a/lib/plausible_web/templates/auth/activate.html.eex b/lib/plausible_web/templates/auth/activate.html.eex deleted file mode 100644 index c32140c5d080..000000000000 --- a/lib/plausible_web/templates/auth/activate.html.eex +++ /dev/null @@ -1,59 +0,0 @@ -
- <%= if @has_pin do %> - <%= form_for @conn, "/activate", [class: "w-full max-w-lg mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8"], fn f -> %> -

Activate your account

- -
- Please enter the 4-digit code we sent to <%= @conn.assigns[:current_user].email %> -
- -
-
- <%= text_input f, :code, class: "tracking-widest font-medium shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-36 px-8 border-gray-300 dark:border-gray-500 rounded-l-md dark:text-gray-200 dark:bg-gray-900", oninput: "this.value=this.value.replace(/[^0-9]/g, ''); if (this.value.length >= 4) document.getElementById('submit').focus()", onclick: "this.select();", maxlength: "4", placeholder: "••••", style: "letter-spacing: 10px;", required: "required" %> -
- -
- <%= error_tag(assigns, :error) %> - -
- Didn't receive an email? -
-
    -
  1. Check your spam folder
  2. -
  3. <%= link("Send a new code", class: "underline text-indigo-600", to: "/activate/request-code", method: :post) %> to <%= @conn.assigns[:current_user].email %>
  4. - <%= if Application.get_env(:plausible, :is_selfhost) do %> -
  5. Ask on our <%= link("community-supported forum", to: "https://github.com/plausible/analytics/discussions", class: "text-indigo-600 underline" ) %>
  6. - <% else %> -
  7. Contact support@plausible.io if the problem persists
  8. - <% end %> -
-
- Entered the wrong email address? -
-
    -
  • - <%= link("Delete this account", class: "underline text-indigo-600", to: "/me?redirect=/register", method: "delete", data: [confirm: "Deleting your account cannot be reversed. Are you sure?"]) %> and start over -
  • -
- <% end %> - <% else %> -
-

Activate your account

- -
- A 4-digit activation code will be sent to <%= @conn.assigns[:current_user].email %> -
- - <%= error_tag(assigns, :error) %> - - <%= button("Request activation code", to: "/activate/request-code", method: :post, class: "button mt-12") %> - -
- <% end %> - - <%= if !@has_invitation do %> - - <% end %> -
diff --git a/lib/plausible_web/templates/auth/invitation_expired.html.eex b/lib/plausible_web/templates/auth/invitation_expired.html.eex deleted file mode 100644 index b3a7b11cb5ff..000000000000 --- a/lib/plausible_web/templates/auth/invitation_expired.html.eex +++ /dev/null @@ -1,12 +0,0 @@ -
-

Plausible Analytics

-
Lightweight and privacy-friendly web analytics
-
- -
-

Invitation expired

- -

- Your invitation has expired or been revoked. Please request fresh one or you can <%= link("sign up", class: "text-indigo-600 hover:text-indigo-900", to: Routes.auth_path(@conn, :register)) %> for a 30-day unlimited free trial without an invitation. -

-
diff --git a/lib/plausible_web/templates/auth/login_form.html.eex b/lib/plausible_web/templates/auth/login_form.html.eex deleted file mode 100644 index fbb9eb9fcd9e..000000000000 --- a/lib/plausible_web/templates/auth/login_form.html.eex +++ /dev/null @@ -1,24 +0,0 @@ -<%= form_for @conn, "/login", [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"], fn f -> %> -

<%= get_flash(@conn, :login_title) || "Enter your email and password" %>

- <%= if get_flash(@conn, :login_instructions) do %> -

<%= get_flash(@conn, :login_instructions) %>

- <% end %> - <%= if @conn.assigns[:error] do %> -
<%= @conn.assigns[:error] %>
- <% end %> -
- <%= label f, :email, class: "block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2" %> - <%= email_input f, :email, class: "bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500", placeholder: "user@example.com" %> -
-
- <%= label f, :password, class: "block text-gray-700 dark:text-gray-300 text-sm font-bold mb-2" %> - <%= password_input f, :password, class: "transition bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500" %> -

Forgot password? Click here to reset it.

-
- <%= submit "Login →", class: "button mt-4 w-full" %> - <%= if !Keyword.fetch!(Application.get_env(:plausible, :selfhost),:disable_registration) do %> -

- Don't have an account? <%= link("Register", to: "/register", class: "text-gray-800 dark:text-gray-50 underline") %> instead. -

- <% end %> -<% end %> diff --git a/lib/plausible_web/templates/auth/new_api_key.html.eex b/lib/plausible_web/templates/auth/new_api_key.html.eex deleted file mode 100644 index f42c0dde1524..000000000000 --- a/lib/plausible_web/templates/auth/new_api_key.html.eex +++ /dev/null @@ -1,21 +0,0 @@ -<%= form_for @changeset, "/settings/api-keys", [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"], fn f -> %> -

Create new API key

-
- <%= label f, :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= text_input f, :name, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 dark:text-gray-300 dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500 rounded-md", placeholder: "Development" %> -
- <%= error_tag f, :name %> -
-
- <%= label f, :key, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= text_input f, :key, id: "key-input", class: "dark:text-gray-300 shadow-sm bg-gray-50 dark:bg-gray-850 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md pr-16", readonly: "readonly" %> - - COPY - -

Make sure to store the key in a secure place. Once created, we will not be able to show it again.

-
-
- <%= submit "Continue", class: "button mt-4 w-full" %> -<% end %> diff --git a/lib/plausible_web/templates/auth/password_form.html.eex b/lib/plausible_web/templates/auth/password_form.html.eex deleted file mode 100644 index 70a5d9a00c61..000000000000 --- a/lib/plausible_web/templates/auth/password_form.html.eex +++ /dev/null @@ -1,14 +0,0 @@ -<%= form_for @conn, "/password", [class: "bg-white dark:bg-gray-800 max-w-md w-full mx-auto shadow-md rounded px-8 py-6 mt-8"], fn f -> %> -

Set your password

-
-

Min 6 characters

- <%= password_input f, :password, class: "transition bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500" %> - <%= if @conn.assigns[:changeset] do %> - <%= error_tag @changeset, :password %> - <% end %> -
- <%= submit "Set password →", class: "button mt-4 w-full" %> -

- Don't have an account? <%= link("Register", to: "/register", class: "underline text-gray-800 dark:text-gray-200") %> instead. -

-<% end %> diff --git a/lib/plausible_web/templates/auth/password_reset_form.html.eex b/lib/plausible_web/templates/auth/password_reset_form.html.eex deleted file mode 100644 index c405aefbfd8e..000000000000 --- a/lib/plausible_web/templates/auth/password_reset_form.html.eex +++ /dev/null @@ -1,15 +0,0 @@ -<%= form_for @conn, "/password/reset", [class: "bg-white dark:bg-gray-800 max-w-md w-full mx-auto shadow-md rounded px-8 py-6 mt-8"], fn f -> %> -

Reset your password

-
-

Min 6 characters

- <%= password_input f, :password, class: "transition bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500" %> - <%= if @conn.assigns[:changeset] do %> - <%= error_tag @changeset, :password %> - <% end %> -
- <%= hidden_input f, :token, value: @token %> - <%= submit "Set password →", class: "button mt-4 w-full" %> -

- Don't have an account? <%= link("Register", to: "/register", class: "underline text-gray-800 dark:text-gray-200") %> instead. -

-<% end %> diff --git a/lib/plausible_web/templates/auth/password_reset_request_form.html.eex b/lib/plausible_web/templates/auth/password_reset_request_form.html.eex deleted file mode 100644 index 53cadd4a86de..000000000000 --- a/lib/plausible_web/templates/auth/password_reset_request_form.html.eex +++ /dev/null @@ -1,22 +0,0 @@ -<%= form_for @conn, "/password/request-reset", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"], fn f -> %> -

Reset your password

-
Enter your email so we can send a password reset link
-
- <%= email_input f, :email, class: "transition bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500", placeholder: "user@example.com" %> -
- <%= if @conn.assigns[:error] do %> -
<%= @conn.assigns[:error] %>
- <% end %> - - <%= if PlausibleWeb.Captcha.enabled?() do %> -
-
- <%= if assigns[:captcha_error] do %> -
<%= @captcha_error %>
- <% end %> - -
- <% end %> - - <%= submit "Send reset link →", class: "button mt-4 w-full" %> -<% end %> diff --git a/lib/plausible_web/templates/auth/password_reset_request_success.html.eex b/lib/plausible_web/templates/auth/password_reset_request_success.html.eex deleted file mode 100644 index 21bdd8f41cf3..000000000000 --- a/lib/plausible_web/templates/auth/password_reset_request_success.html.eex +++ /dev/null @@ -1,17 +0,0 @@ -
-

Success!

-
- We have sent an email with password reset instructions to <%= @email %> if it exists in our database. To protect your account's security, we cannot confirm whether or not the email address you entered is registered in our database. -
-
- Didn't receive an email within a few minutes? -
-
- You might have used a wrong email address. Please check what email address you used to create your Plausible account and try to reset the password for that address. Do also check your spam folder. - <%= if Application.get_env(:plausible, :is_selfhost) do %> - If you are positive that you are using the correct email address but still aren't receiving the password reset email, please ask on our community-supported forum. - <% else %> - If you are positive that you are using the correct email address but still aren't receiving the password reset email, please contact support@plausible.io. - <% end %> -
-
diff --git a/lib/plausible_web/templates/auth/register_form.html.eex b/lib/plausible_web/templates/auth/register_form.html.eex deleted file mode 100644 index 0b63c07baf42..000000000000 --- a/lib/plausible_web/templates/auth/register_form.html.eex +++ /dev/null @@ -1,65 +0,0 @@ -
-

Register your 30-day unlimited-use free trial

-
Set up privacy-friendly analytics with just a few clicks
-
- -
- <%= form_for @changeset, "/register", [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8", id: "register-form"], fn f -> %> -

Enter your details

-
- <%= label f, :name, "Full name", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= text_input f, :name, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300", placeholder: "Jane Doe" %> -
- <%= error_tag f, :name %> -
-
-
- <%= label f, :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -

No spam, guaranteed.

-
-
- <%= email_input f, :email, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300", placeholder: "example@email.com" %> -
- <%= error_tag f, :email %> -
- -
-
- <%= label f, :password, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -

Min 6 characters

-
-
- <%= password_input f, :password, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300" %> -
- <%= error_tag f, :password %> -
- -
- <%= label f, :password_confirmation, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= password_input f, :password_confirmation, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300" %> -
- <%= error_tag f, :password_confirmation %> -
- - <%= if PlausibleWeb.Captcha.enabled?() do %> -
-
- <%= if assigns[:captcha_error] do %> -
<%= @captcha_error %>
- <% end %> - -
- <% end %> - - <%= submit "Start my free trial →", class: "button mt-4 w-full" %> - -

- Already have an account? <%= link("Log in", to: "/login", class: "underline text-gray-800 dark:text-gray-50") %> instead. -

- <% end %> - -
diff --git a/lib/plausible_web/templates/auth/register_from_invitation_form.html.eex b/lib/plausible_web/templates/auth/register_from_invitation_form.html.eex deleted file mode 100644 index b5fdef97f021..000000000000 --- a/lib/plausible_web/templates/auth/register_from_invitation_form.html.eex +++ /dev/null @@ -1,61 +0,0 @@ -
-

Register your Plausible Analytics account

-
Set up privacy-friendly analytics with just a few clicks
-
- -<%= form_for @changeset, Routes.auth_path(@conn, :register_from_invitation_form, @invitation.invitation_id), [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8"], fn f -> %> -

Enter your details

-
-
- <%= label f, :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -

No spam, guaranteed.

-
-
- <%= email_input f, :email, class: "bg-gray-100 dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300", placeholder: "example@email.com", value: @invitation.email, readonly: "readonly" %> -
- <%= error_tag f, :email %> -
- -
- <%= label f, :name, "Full name", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= text_input f, :name, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300", placeholder: "Jane Doe" %> -
- <%= error_tag f, :name %> -
- -
-
- <%= label f, :password, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -

Min 6 characters

-
-
- <%= password_input f, :password, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300" %> -
- <%= error_tag f, :password %> -
- -
- <%= label f, :password_confirmation, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= password_input f, :password_confirmation, class: "dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-300" %> -
- <%= error_tag f, :password_confirmation %> -
- - <%= if PlausibleWeb.Captcha.enabled?() do %> -
-
- <%= if assigns[:captcha_error] do %> -
<%= @captcha_error %>
- <% end %> - -
- <% end %> - - <%= submit "Create my account →", class: "button mt-4 w-full" %> - -

- Already have an account? <%= link("Log in", to: "/login", class: "underline text-gray-800 dark:text-gray-50") %> instead. -

-<% end %> diff --git a/lib/plausible_web/templates/auth/register_success.html.eex b/lib/plausible_web/templates/auth/register_success.html.eex deleted file mode 100644 index 6fb2c51be111..000000000000 --- a/lib/plausible_web/templates/auth/register_success.html.eex +++ /dev/null @@ -1,34 +0,0 @@ -
-

Register your 30-day unlimited-use free trial

-
Set up privacy-friendly analytics with just a few clicks
-
- - -
- <%= form_for @conn, "/claim-activation", [class: "w-full max-w-lg mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8", id: "register-form"], fn f -> %> -

Activate your account

- -
- Please enter the 4-digit code we sent to <%= @email %> -
- -
-
- <%= text_input f, :code, class: "tracking-widest font-medium shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-36 px-8 border-gray-300 dark:border-gray-500 rounded-l-md dark:text-gray-200 dark:bg-gray-900", oninput: "this.value=this.value.replace(/[^0-9]/g, ''); if (this.value.length >= 4) document.getElementById('submit').focus()", onclick: "this.select();", maxlength: "4", placeholder: "••••", style: "letter-spacing: 10px;" %> - <%= error_tag f, :name %> -
- -
- -
- Didn't receive an email? -
-
- Please check your spam folder and contact support@plausible.io if the problem persists -
- <% end %> - - -
diff --git a/lib/plausible_web/templates/auth/user_settings.html.eex b/lib/plausible_web/templates/auth/user_settings.html.eex deleted file mode 100644 index a8020f035874..000000000000 --- a/lib/plausible_web/templates/auth/user_settings.html.eex +++ /dev/null @@ -1,281 +0,0 @@ -<%= if !Application.get_env(:plausible, :is_selfhost) do %> -
-
-

Subscription Plan

- <%= if @subscription do %> - - <%= present_subscription_status(@subscription.status) %> - - <% end %> -
- -
- - <%= if @subscription && @subscription.status == "deleted" do %> -
-
-
- - - -

- <%= if @subscription.next_bill_date && Timex.compare(@subscription.next_bill_date, Timex.today()) >= 0 do %> - Your subscription is cancelled but you have access to your stats until <%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>. Upgrade below to make sure you don't lose access. - <% else %> - Your subscription is cancelled. Upgrade below to get access to your stats again. - <% end %> -

-
-
-
- <% end %> - -
-
-

Monthly quota

- <%= if @subscription do %> -
<%= subscription_quota(@subscription) %> pageviews
- <%= case @subscription.status do %> - <% "active" -> %> - <%= link("Change plan", to: "/billing/change-plan", class: "text-sm text-indigo-500 font-medium") %> - <% "past_due" -> %> - Change plan - <% _ -> %> - <% end %> - <% else %> -
Free trial
- <%= link("Upgrade", to: "/billing/upgrade", class: "text-sm text-indigo-500 font-medium") %> - <% end %> -
-
-

Next bill amount

- <%= if @subscription && @subscription.status in ["active", "past_due"] do %> -
<%= PlausibleWeb.BillingView.present_currency(@subscription.currency_code) %><%= @subscription.next_bill_amount %>
- <%= if @subscription.update_url do %> - <%= link("Update billing info", to: @subscription.update_url, class: "text-sm text-indigo-500 font-medium") %> - <% end %> - <% else %> -
---
- <% end %> -
-
-

Next bill date

- - <%= if @subscription && @subscription.next_bill_date && @subscription.status in ["active", "past_due"] do %> -
<%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>
-
(<%= subscription_interval(@subscription) %> billing)
- <% else %> -
---
- <% end %> -
-
- -

Your usage

-

Last 30 days total usage across all of your sites

-
-
-
-
-
- - - - - - - - - - - - - - - - -
- Pageviews - - <%= delimit_integer(@usage_pageviews) %> -
- Custom events - - <%= delimit_integer(@usage_custom_events) %> -
- Total billable pageviews - - <%= delimit_integer(@usage_pageviews + @usage_custom_events) %> -
-
-
-
-
-
- - <%= cond do %> - <% @subscription && @subscription.status in ["active", "past_due", "paused"] && @subscription.cancel_url -> %> -
- <%= link("Cancel my subscription", to: @subscription.cancel_url, class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150") %> -
- <% true -> %> -
- <%= link("Upgrade", to: "/billing/upgrade", class: "inline-block px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150") %> -
- <% end %> -
- -<%= case @invoices do %> - <% {:error, :no_subscription} -> %> - - <% {:error, :request_failed} -> %> -
-

Invoices

-
-

- Something went wrong -

-
- - <% invoice_list -> %> -
-

Invoices

-
- - - - - - - - - <%= for invoice <- format_invoices(invoice_list) do %> - - - - - - - - <% end %> -
- Date - - Amount - - Invoice -
- <%= invoice.date %> - - <%= invoice.currency <> invoice.amount %> - - <%= link("Link", to: invoice.url, target: "_blank" ) %> -
-
- <% end %> - -<% end %> - -
-

Dashboard Appearance

- -
- - <%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %> -
- <%= label f, :theme, "Theme Selection", class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300" %> - <%= select f, :theme, Plausible.Themes.options(), class: "dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer" %> -
- <%= submit "Save", class: "button mt-4" %> - <% end %> -
- -
-

Account settings

- -
- - <%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %> -
- <%= label f, :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= text_input f, :name, class: "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" %> - <%= error_tag f, :name %> -
-
-
- <%= label f, :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- <%= email_input f, :email, class: "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md" %> - <%= error_tag f, :email %> -
-
- <%= submit "Save changes", class: "button mt-4" %> - <% end %> -
- -
-

API keys

- -
- -
-
-
- - <%= if Enum.any?(@user.api_keys) do %> -
- - - - - - - - - - <%= for api_key <- @user.api_keys do %> - - - - - - <% end %> - -
- Name - - Key - - Revoke -
- <%= api_key.name %> - - <%= api_key.key_prefix %><%= String.duplicate("*", 32 - 6) %> - - <%= button("Revoke", to: "/settings/api-keys/#{api_key.id}", class: "text-red-600 hover:text-red-900", method: :delete, "data-confirm": "Are you sure you want to revoke this key? This action cannot be reversed.") %> -
-
- <% end %> - - <%= link "+ New API key", to: "/settings/api-keys/new", class: "button mt-4" %> -
-
-
-
- -
-
-

Delete account

-
- -
- -

Deleting your account removes all sites and stats you've collected

- - <%= if @subscription && @subscription.status == "active" do %> - Delete my account -

Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first.

- <% else %> - <%= link("Delete my account", to: "/me", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete", data: [confirm: "Deleting your account will also delete all the sites and data that you own. This action cannot be reversed. Are you sure?"]) %> - <% end %> -
diff --git a/lib/plausible_web/templates/billing/_paddle_script.html.eex b/lib/plausible_web/templates/billing/_paddle_script.html.eex deleted file mode 100644 index 0f430fb8f58c..000000000000 --- a/lib/plausible_web/templates/billing/_paddle_script.html.eex +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/lib/plausible_web/templates/billing/_plan_option.html.eex b/lib/plausible_web/templates/billing/_plan_option.html.eex deleted file mode 100644 index 1c7cdf91c68c..000000000000 --- a/lib/plausible_web/templates/billing/_plan_option.html.eex +++ /dev/null @@ -1,7 +0,0 @@ - - <%= @plan[:volume] %> - - / mo - / yr - - diff --git a/lib/plausible_web/templates/billing/change_enterprise_plan.html.eex b/lib/plausible_web/templates/billing/change_enterprise_plan.html.eex deleted file mode 100644 index 3c1e452c7946..000000000000 --- a/lib/plausible_web/templates/billing/change_enterprise_plan.html.eex +++ /dev/null @@ -1,68 +0,0 @@ - -
-

Change subscription plan

-
- - - -
-
-
- -
-
- -
- Questions? Contact <%= link("support@plausible.io", to: "mailto:support@plausible.io", class: "text-indigo-500") %> -
- -<%= render("_paddle_script.html") %> diff --git a/lib/plausible_web/templates/billing/change_enterprise_plan_contact_us.html.eex b/lib/plausible_web/templates/billing/change_enterprise_plan_contact_us.html.eex deleted file mode 100644 index 67a85d883de2..000000000000 --- a/lib/plausible_web/templates/billing/change_enterprise_plan_contact_us.html.eex +++ /dev/null @@ -1,16 +0,0 @@ -
-

Change subscription plan

-
- -
-
-
- Need to change your limits? -
- -
    - Your account is on an enterprise plan. If you want to increase or decrease the limits on your account, please contact us at enterprise@plausible.io -
- -
-
diff --git a/lib/plausible_web/templates/billing/change_plan.html.eex b/lib/plausible_web/templates/billing/change_plan.html.eex deleted file mode 100644 index 584bdc97fc6c..000000000000 --- a/lib/plausible_web/templates/billing/change_plan.html.eex +++ /dev/null @@ -1,122 +0,0 @@ - - - -
-

Change subscription plan

-
- -
-
-
- Select your new plan -
- -
- Depending on which plan you choose, your card might be charged immediately and your - next payment date could change. You can preview these changes before committing. -
- -
- - - - - - -
- - - -
- - -
-
- -
- Questions? Contact <%= link("support@plausible.io", to: "mailto:support@plausible.io", class: "text-indigo-500") %> -
- -<%= render("_paddle_script.html") %> diff --git a/lib/plausible_web/templates/billing/change_plan_preview.html.eex b/lib/plausible_web/templates/billing/change_plan_preview.html.eex deleted file mode 100644 index 3480a12daac5..000000000000 --- a/lib/plausible_web/templates/billing/change_plan_preview.html.eex +++ /dev/null @@ -1,84 +0,0 @@ -
-

Confirm new subscription plan

-
- -
-
-
Due now
-
- Your card will be charged a pro-rated amount for the current billing period -
- -
-
-
- - - - - - - - - - - - - -
- Amount - - Date -
<%= present_currency(@preview_info["immediate_payment"]["currency"]) %><%= @preview_info["immediate_payment"]["amount"] %><%= present_date(@preview_info["immediate_payment"]["date"]) %>
-
-
-
- -
- -
Next payment
- - -
-
-
- - - - - - - - - - - - - -
- Amount - - Date -
<%= present_currency(@preview_info["immediate_payment"]["currency"]) %><%= @preview_info["next_payment"]["amount"] %><%= present_date(@preview_info["next_payment"]["date"]) %>
-
-
-
- -
- - - Back - - - - <%= button("Confirm plan change", to: "/billing/change-plan/#{@preview_info["plan_id"]}", method: :post, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150") %> - -
-
-
- -
- Questions? Contact <%= link("support@plausible.io", to: "mailto:support@plausible.io", class: "text-indigo-500") %> -
- -<%= render("_paddle_script.html") %> diff --git a/lib/plausible_web/templates/billing/upgrade.html.eex b/lib/plausible_web/templates/billing/upgrade.html.eex deleted file mode 100644 index c594c2ba3e4b..000000000000 --- a/lib/plausible_web/templates/billing/upgrade.html.eex +++ /dev/null @@ -1,128 +0,0 @@ - - - -
-

Upgrade your free trial

-
- -
-
-
-
- You've used <%= PlausibleWeb.AuthView.delimit_integer(@usage) %> billable pageviews in the last 30 days -
- -
- - - - - - -
- - - -
- -
-
Due today:
-
+ VAT if applicable
- - - -
-
- -
-

- What happens if I go over my page views limit? -

-
- You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly.

- If your page views exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point. -
- -
-
-
- -
- Questions? Contact <%= link("support@plausible.io", to: "mailto:support@plausible.io", class: "text-indigo-500") %> -
- -<%= render("_paddle_script.html") %> diff --git a/lib/plausible_web/templates/billing/upgrade_enterprise_plan.html.eex b/lib/plausible_web/templates/billing/upgrade_enterprise_plan.html.eex deleted file mode 100644 index 3604ce423d0e..000000000000 --- a/lib/plausible_web/templates/billing/upgrade_enterprise_plan.html.eex +++ /dev/null @@ -1,43 +0,0 @@ -
-

Upgrade your free trial

-
- -
-
-
-
- You've used <%= PlausibleWeb.AuthView.delimit_integer(@usage) %> billable pageviews in the last 30 days -
- -
- With this link you can upgrade to an enterprise plan with <%= PlausibleWeb.StatsView.large_number_format(@plan[:limit]) %> monthly pageviews, billed on a <%= @plan[:cycle] %> basis. -
- -
- - - -
-
- -
-

- What happens if I go over my page views limit? -

-
- You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly.

- If your page views exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point. -
- -
-
-
- -
- Questions? Contact <%= link("support@plausible.io", to: "mailto:support@plausible.io", class: "text-indigo-500") %> -
- -<%= render("_paddle_script.html") %> diff --git a/lib/plausible_web/templates/billing/upgrade_success.html.eex b/lib/plausible_web/templates/billing/upgrade_success.html.eex deleted file mode 100644 index 4990bbacfa34..000000000000 --- a/lib/plausible_web/templates/billing/upgrade_success.html.eex +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
- Subscription created successfully -
- -
- Thank you for upgrading your subscription. You will be redirected to your - account within 5 seconds. -
- -
- - -
-
diff --git a/lib/plausible_web/templates/billing/upgrade_to_plan.html.eex b/lib/plausible_web/templates/billing/upgrade_to_plan.html.eex deleted file mode 100644 index 65f6bfdbe5a7..000000000000 --- a/lib/plausible_web/templates/billing/upgrade_to_plan.html.eex +++ /dev/null @@ -1,43 +0,0 @@ -
-

Upgrade your free trial

-
- -
-
-
-
- You've used <%= PlausibleWeb.AuthView.delimit_integer(@usage) %> billable pageviews in the last 30 days -
- -
- With this link you can upgrade to an enterprise plan with <%= PlausibleWeb.StatsView.large_number_format(@user.enterprise_plan.monthly_pageview_limit) %> monthly pageviews, billed on a <%= @user.enterprise_plan.billing_interval %> basis. -
- -
- - - -
-
- -
-

- What happens if I go over my page views limit? -

-
- You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly.

- If your page views exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point. -
- -
-
-
- -
- Questions? Contact <%= link("support@plausible.io", to: "mailto:support@plausible.io", class: "text-indigo-500") %> -
- -<%= render("_paddle_script.html") %> \ No newline at end of file diff --git a/lib/plausible_web/templates/email/activation_email.html.eex b/lib/plausible_web/templates/email/activation_email.html.eex deleted file mode 100644 index 66367c2f9bfa..000000000000 --- a/lib/plausible_web/templates/email/activation_email.html.eex +++ /dev/null @@ -1,3 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Enter <%= @code %> to verify your email address. This code will expire in 4 hours. diff --git a/lib/plausible_web/templates/email/cancellation_email.html.eex b/lib/plausible_web/templates/email/cancellation_email.html.eex deleted file mode 100644 index 119e46001fd3..000000000000 --- a/lib/plausible_web/templates/email/cancellation_email.html.eex +++ /dev/null @@ -1,10 +0,0 @@ -Hi <%= @name %>, -

-Thanks for being a Plausible Analytics subscriber and I'm sorry to see you go.

- -I'd love to hear about your experience and how you think we can improve Plausible Analytics for our other subscribers (and for you if you decide to come back). Please reply back to this email with your feedback.

- -If you decide you'd like to continue keeping track of your site traffic while respecting the privacy of your visitors, obviously we'd love to have you back. You can restart your subscription from your account settings.

- -Hope to see you around!
-Uku Taht diff --git a/lib/plausible_web/templates/email/check_stats_email.html.eex b/lib/plausible_web/templates/email/check_stats_email.html.eex deleted file mode 100644 index 5c746e62859e..000000000000 --- a/lib/plausible_web/templates/email/check_stats_email.html.eex +++ /dev/null @@ -1,15 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Plausible is tracking your website stats without compromising the user experience and the privacy of your visitors. -

-<%= link("View your Plausible dashboard now", to: "#{plausible_url()}") %> for the most valuable traffic insights at a glance. -

-Do reply back to this email if you have any questions or need some guidance. -

-Thanks,
-Uku Taht -

--- -

-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/create_site_email.html.eex b/lib/plausible_web/templates/email/create_site_email.html.eex deleted file mode 100644 index e34eafb26eda..000000000000 --- a/lib/plausible_web/templates/email/create_site_email.html.eex +++ /dev/null @@ -1,13 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-You've activated your free 30-day trial of Plausible, a simple and privacy-friendly website analytics tool. -

-<%= link("Click here", to: "#{plausible_url()}/sites/new") %> to add your website URL, your timezone and install our one-line JavaScript snippet to start collecting visitor statistics. -

-Do reply back to this email if you have any questions or need some guidance. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/dashboard_locked.html.eex b/lib/plausible_web/templates/email/dashboard_locked.html.eex deleted file mode 100644 index 031070c5351a..000000000000 --- a/lib/plausible_web/templates/email/dashboard_locked.html.eex +++ /dev/null @@ -1,23 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Last week we sent a reminder that your traffic has exceeded your Plausible Analytics subscription tier two months in a row. -

-Your dashboard is now locked. We're still counting your stats, but you no longer have access to the stats. As you have outgrown your subscription tier, we kindly ask you to upgrade your subscription to accommodate your new traffic levels. -

-In the last billing cycle (<%= date_format(@last_cycle.first) %> to <%= date_format(@last_cycle.last) %>), your account has used <%= PlausibleWeb.StatsView.large_number_format(@usage) %> billable pageviews. -<%= if @usage <= 20_000_000 do %> -Based on that we recommend you select the <%= @suggested_plan[:volume] %>/mo plan. -

-Click here to go to your site settings. You can upgrade your subscription tier by clicking the 'change plan' link. -<% else %> -This is more than our standard plans, so please reply back to this email to get a quote for your volume. -<% end %> -

-The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire. We will unlock your dashboard immediately after your account has been upgraded to the appropriate tier. -

-Thanks for understanding and for being a Plausible subscriber! -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/enterprise_over_limit.html.eex b/lib/plausible_web/templates/email/enterprise_over_limit.html.eex deleted file mode 100644 index af82475e05f5..000000000000 --- a/lib/plausible_web/templates/email/enterprise_over_limit.html.eex +++ /dev/null @@ -1,9 +0,0 @@ -Automated notice about an enterprise account that has gone over their limits.

- -Customer email: <%= @user.email %>
-Last billing cycle: <%= date_format(@last_cycle.first) %> to <%= date_format(@last_cycle.last) %>
-Pageview Usage: <%= PlausibleWeb.StatsView.large_number_format(@usage) %> billable pageviews
-Site usage: <%= @site_usage %> / <%= @site_allowance %> allowed sites
- ---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/existing_user_invitation.html.eex b/lib/plausible_web/templates/email/existing_user_invitation.html.eex deleted file mode 100644 index c1bc0354bbd2..000000000000 --- a/lib/plausible_web/templates/email/existing_user_invitation.html.eex +++ /dev/null @@ -1,10 +0,0 @@ -Hey, -

-<%= @invitation.inviter.email %> has invited you to the <%= @invitation.site.domain %> site on Plausible Analytics. -<%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :index)) %> to view and respond to the invitation. The invitation -will expire 48 hours after this email is sent. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/google_analytics_import.html.eex b/lib/plausible_web/templates/email/google_analytics_import.html.eex deleted file mode 100644 index d56f3f0cf407..000000000000 --- a/lib/plausible_web/templates/email/google_analytics_import.html.eex +++ /dev/null @@ -1,13 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-<%= if @success do %> - Your Google Analytics import has completed. -

- View dashboard: @link -<% else %> - Unfortunately, your Google Analytics import failed. -<% end %> -

--- -

-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/invitation_accepted.html.eex b/lib/plausible_web/templates/email/invitation_accepted.html.eex deleted file mode 100644 index f437183d29b6..000000000000 --- a/lib/plausible_web/templates/email/invitation_accepted.html.eex +++ /dev/null @@ -1,9 +0,0 @@ -Hey <%= user_salutation(@invitation.inviter) %>, -

-<%= @invitation.email %> has accepted your invitation to <%= @invitation.site.domain %>. -<%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :settings_general, @invitation.site.domain)) %> to view site settings. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/invitation_rejected.html.eex b/lib/plausible_web/templates/email/invitation_rejected.html.eex deleted file mode 100644 index ef40174ba432..000000000000 --- a/lib/plausible_web/templates/email/invitation_rejected.html.eex +++ /dev/null @@ -1,9 +0,0 @@ -Hey <%= user_salutation(@invitation.inviter) %>, -

-<%= @invitation.email %> has rejected your invitation to <%= @invitation.site.domain %>. -<%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :settings_general, @invitation.site.domain)) %> to view site settings. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/new_user_invitation.html.eex b/lib/plausible_web/templates/email/new_user_invitation.html.eex deleted file mode 100644 index d21a004ab260..000000000000 --- a/lib/plausible_web/templates/email/new_user_invitation.html.eex +++ /dev/null @@ -1,11 +0,0 @@ -Hey, -

-<%= @invitation.inviter.email %> has invited you to join the <%= @invitation.site.domain %> site on Plausible Analytics. -<%= link("Click here", to: Routes.auth_url(PlausibleWeb.Endpoint, :register_from_invitation_form, @invitation.invitation_id)) %> to create your account. The link is valid for 48 hours after this email is sent. -

-Plausible is a lightweight and open-source website analytics tool. We hope you like our simple and ethical approach to tracking website visitors. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/over_limit.html.eex b/lib/plausible_web/templates/email/over_limit.html.eex deleted file mode 100644 index 265344984502..000000000000 --- a/lib/plausible_web/templates/email/over_limit.html.eex +++ /dev/null @@ -1,28 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Thanks for being a Plausible Analytics subscriber! -

-This is a notice that your traffic has exceeded your subscription tier two months in a row. Congrats on all that traffic! -

-In order to keep your stats running, we require you to upgrade your account to accommodate your new traffic levels. If you do not upgrade your account within the next 7 days, we will lock your stats and they won't be accessible. -

-In the last billing cycle (<%= date_format(@last_cycle.first) %> to <%= date_format(@last_cycle.last) %>), your account has used <%= PlausibleWeb.StatsView.large_number_format(@usage) %> billable pageviews. -<%= if @usage <= 20_000_000 do %> -Based on that we recommend you select the <%= @suggested_plan[:volume] %>/mo plan. -

-You can upgrade your subscription using our self-serve platform. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire. -

-Click here to go to your site settings. You can upgrade your subscription tier by clicking the 'change plan' link. -<% else %> -This is more than our standard plans, so please reply back to this email to get a quote for your volume. -<% end %> -

-Have questions or need help with anything? Just reply to this email and we'll gladly help. -

-Thanks again for using our product and for your support! -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/ownership_transfer_accepted.html.eex b/lib/plausible_web/templates/email/ownership_transfer_accepted.html.eex deleted file mode 100644 index 1d8dcf53ce38..000000000000 --- a/lib/plausible_web/templates/email/ownership_transfer_accepted.html.eex +++ /dev/null @@ -1,10 +0,0 @@ -Hey <%= user_salutation(@invitation.inviter) %>, -

-<%= @invitation.email %> has accepted the ownership transfer of <%= @invitation.site.domain %>. They will be responsible for billing of it going -forward and your role has been changed to admin. -<%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :settings_general, @invitation.site.domain)) %> to view site settings. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/ownership_transfer_rejected.html.eex b/lib/plausible_web/templates/email/ownership_transfer_rejected.html.eex deleted file mode 100644 index b67ca63e0a69..000000000000 --- a/lib/plausible_web/templates/email/ownership_transfer_rejected.html.eex +++ /dev/null @@ -1,9 +0,0 @@ -Hey <%= user_salutation(@invitation.inviter) %>, -

-<%= @invitation.email %> has rejected the ownership transfer of <%= @invitation.site.domain %>. -<%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :settings_general, @invitation.site.domain)) %> to view site settings. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/ownership_transfer_request.html.eex b/lib/plausible_web/templates/email/ownership_transfer_request.html.eex deleted file mode 100644 index 4d48468f13b3..000000000000 --- a/lib/plausible_web/templates/email/ownership_transfer_request.html.eex +++ /dev/null @@ -1,15 +0,0 @@ -Hey, -

-<%= @invitation.inviter.email %> has request to transfer the ownership of <%= @invitation.site.domain %> site on Plausible Analytics to you. -<%= if @new_owner_account do %> - <%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :index)) %> to view and respond to the invitation. -<% else %> - <%= link("Click here", to: Routes.auth_url(PlausibleWeb.Endpoint, :register_form), invitation: @invitation.invitation_id) %> to create your account. -

- Plausible is a lightweight and open-source website analytics tool. We hope you like our simple and ethical approach to tracking website visitors. -<% end %> -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/password_reset_email.html.eex b/lib/plausible_web/templates/email/password_reset_email.html.eex deleted file mode 100644 index 612c7f117a6e..000000000000 --- a/lib/plausible_web/templates/email/password_reset_email.html.eex +++ /dev/null @@ -1,2 +0,0 @@ -Click here to reset your Plausible password.

-This link will expire in 1 hour. If you don't use it by then, you can request another login link. diff --git a/lib/plausible_web/templates/email/site_member_removed.html.eex b/lib/plausible_web/templates/email/site_member_removed.html.eex deleted file mode 100644 index 1634b7dcf355..000000000000 --- a/lib/plausible_web/templates/email/site_member_removed.html.eex +++ /dev/null @@ -1,10 +0,0 @@ -Hey <%= user_salutation(@membership.user) %>, -

-An administrator of <%= @membership.site.domain %> has removed you as a member. You won't be able to see the stats anymore. -

-<%= link("Click here", to: Routes.site_url(PlausibleWeb.Endpoint, :index)) %> to view your sites. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/site_setup_help_email.html.eex b/lib/plausible_web/templates/email/site_setup_help_email.html.eex deleted file mode 100644 index 0f4b2b0a3662..000000000000 --- a/lib/plausible_web/templates/email/site_setup_help_email.html.eex +++ /dev/null @@ -1,17 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-<%= if Plausible.Billing.on_trial?(@user) do %> - You signed up for a free 30-day trial of Plausible, a simple and privacy-friendly website analytics tool. -

-<% end %> -To finish your setup for <%= @site.domain %>, you need to install <%= link("this lightweight line of JavaScript code", to: "#{plausible_url()}/#{URI.encode_www_form(@site.domain)}/snippet") %> into your site to start collecting visitor statistics. -

-This Plausible script is 45 times smaller than Google Analytics script so you’ll have a fast loading site while getting all the important traffic insights on one single page. -

-Do reply back to this email if you have any questions or need some guidance. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/site_setup_success_email.html.eex b/lib/plausible_web/templates/email/site_setup_success_email.html.eex deleted file mode 100644 index e1be6de5fd7e..000000000000 --- a/lib/plausible_web/templates/email/site_setup_success_email.html.eex +++ /dev/null @@ -1,19 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Congrats! The Plausible script has been installed correctly on <%= link(@site.domain, to: "https://#{@site.domain}") %>. Your website traffic is now being tracked without compromising the user experience and the privacy of your visitors. -

-<%= link("Check your stats", to: "#{plausible_url()}/#{URI.encode_www_form(@site.domain)}") %> -

-<%= if Plausible.Billing.on_trial?(@user) do %> - You're on a 30-day unlimited-use free trial with no obligations so do take your time to explore your simple and privacy-friendly website analytics dashboard. -

-<% end %> -PS: Plausible is fully open-source and our public roadmap is defined by the community. <%= link("Leave your feedback", to: "#{plausible_url()}/feedback") %> and have your say on metrics and features we should be adding next. -

-Do reply back to this email if you have any questions. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/spike_notification.html.eex b/lib/plausible_web/templates/email/spike_notification.html.eex deleted file mode 100644 index 47a9f27a87fa..000000000000 --- a/lib/plausible_web/templates/email/spike_notification.html.eex +++ /dev/null @@ -1,18 +0,0 @@ -There are currently <%= @current_visitors %> visitors on <%= link(@site.domain, to: "https://" <> @site.domain) %>. -<%= if Enum.count(@sources) > 0 do %> -
-
- The top sources for current visitors:
- <%= for %{name: source, count: visitors} <- @sources do %> - <%= source %> - <%= visitors %> visitor<%= if visitors > 1, do: "s" %>
- <% end %> -<% end %> - -<%= if @link do %> -

-View dashboard: @link -<% end %> -

--- -

-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/trial_one_week_reminder.html.eex b/lib/plausible_web/templates/email/trial_one_week_reminder.html.eex deleted file mode 100644 index b28a4015a668..000000000000 --- a/lib/plausible_web/templates/email/trial_one_week_reminder.html.eex +++ /dev/null @@ -1,15 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Time flies! Your 30-day free trial of Plausible will end next week. -

-Over the last three weeks, I hope you got to experience the potential benefits of having website stats in a simple dashboard while respecting the privacy of your visitors, not annoying them with the cookie and privacy notices and still having a fast loading site. -

-In order to continue receiving valuable website traffic insights at a glance, you’ll need to <%= link("Upgrade your account", to: "#{plausible_url()}/billing/upgrade") %>. -

-If you have any questions or feedback for me, feel free to reply to this email. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/trial_over_email.html.eex b/lib/plausible_web/templates/email/trial_over_email.html.eex deleted file mode 100644 index 03148de0b74d..000000000000 --- a/lib/plausible_web/templates/email/trial_over_email.html.eex +++ /dev/null @@ -1,15 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Your free Plausible trial has now expired. Upgrade your account to continue receiving valuable website traffic insights at a glance while respecting the privacy of your visitors and still having a fast loading site.

- -<%= link("Upgrade now", to: "#{plausible_url()}/billing/upgrade") %> -

- -We will keep recording stats for another month to give you time to upgrade. - -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/trial_upgrade_email.html.eex b/lib/plausible_web/templates/email/trial_upgrade_email.html.eex deleted file mode 100644 index 1c1dd960f58e..000000000000 --- a/lib/plausible_web/templates/email/trial_upgrade_email.html.eex +++ /dev/null @@ -1,20 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Thanks for exploring Plausible, a simple and privacy-friendly alternative to Google Analytics. Your free 30-day trial is ending <%= @day %>, but you can keep using Plausible by upgrading to a paid plan. -

-In the last month, your account has used <%= PlausibleWeb.AuthView.delimit_integer(@usage) %> billable pageviews<%= if @custom_events > 0, do: " and custom events in total", else: "" %>. -<%= if @usage <= 10_000_000 do %> -Based on that we recommend you select the <%= @suggested_plan[:volume] %>/mo plan. -

-<%= link("Upgrade now", to: "#{plausible_url()}/billing/upgrade") %> -

-Have a question, feedback or need some guidance? Just reply to this email to get in touch! -<% else %> -This is more than our standard plans, so please reply back to this email to get a quote for your volume. -<% end %> -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/weekly_report.html.eex b/lib/plausible_web/templates/email/weekly_report.html.eex deleted file mode 100644 index 3e61ec0a781a..000000000000 --- a/lib/plausible_web/templates/email/weekly_report.html.eex +++ /dev/null @@ -1,697 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/plausible_web/templates/email/welcome_email.html.eex b/lib/plausible_web/templates/email/welcome_email.html.eex deleted file mode 100644 index 303cb26da0ec..000000000000 --- a/lib/plausible_web/templates/email/welcome_email.html.eex +++ /dev/null @@ -1,25 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-We are building Plausible to provide a simple and ethical approach to tracking website visitors. -We're super excited to have you on board! -

-Here's how to get the most out of your Plausible experience: -

-* <%= link("Enable email reports", to: "https://docs.plausible.io/email-reports/") %> and notifications for <%= link("traffic spikes", to: "https://plausible.io/docs/traffic-spikes") %>
-* <%= link("Integrate with Search Console", to: "https://plausible.io/docs/google-search-console-integration") %> to get keyword phrases people find your site with
-* <%= link("Invite team members and other collaborators", to: "https://plausible.io/docs/users-roles") %>
-* Set up some easy goals including <%= link("404 error pages", to: "https://plausible.io/docs/error-pages-tracking-404") %> and <%= link("outbound link clicks", to: "https://plausible.io/docs/outbound-link-click-tracking/") %>
-* <%= link("Opt out from counting your own visits", to: "https://plausible.io/docs/excluding") %>
-* If you're concerned about adblockers, <%= link("set up a proxy to bypass them", to: "https://plausible.io/docs/proxy/introduction") %>
-

-Then you're ready to start exploring your fast loading, ethical and actionable <%= link("Plausible dashboard", to: "https://plausible.io/sites") %>. -

-Have a question, feedback or need some guidance? Do reply back to this email. -

-Thanks,
-Uku and Marko -
--- -
-<%= plausible_url() %>
-{{{ pm:unsubscribe }}} diff --git a/lib/plausible_web/templates/email/yearly_expiration_notification.html.eex b/lib/plausible_web/templates/email/yearly_expiration_notification.html.eex deleted file mode 100644 index d3103dc35317..000000000000 --- a/lib/plausible_web/templates/email/yearly_expiration_notification.html.eex +++ /dev/null @@ -1,14 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Time flies! This is a reminder that your annual subscription for Plausible Analytics will expire on <%= @date %>. -

-You need to renew your subscription on <%= link("account settings page", to: "#{plausible_url()}/billing/upgrade") %> if you want to continue using Plausible to count your website stats in a privacy-friendly way. -

-If you don't want to continue your subscription, there's no action required. You will lose access to the stats on <%= @date %>. -

-Have a question, feedback or need some guidance? Just reply to this email to get in touch! -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/email/yearly_renewal_notification.html.eex b/lib/plausible_web/templates/email/yearly_renewal_notification.html.eex deleted file mode 100644 index 8d7e650b7979..000000000000 --- a/lib/plausible_web/templates/email/yearly_renewal_notification.html.eex +++ /dev/null @@ -1,14 +0,0 @@ -Hey <%= user_salutation(@user) %>, -

-Time flies! This is a reminder that your annual subscription for Plausible Analytics is due to renew on <%= @date %>. We will automatically charge <%= PlausibleWeb.BillingView.present_currency(@currency) %><%= @next_bill_amount %> from your preferred billing method. -

-There's no action required if you're happy to continue using Plausible to count your website stats in a privacy-friendly way. -

-If you don't want to continue your subscription, you can cancel it on your <%= link("account settings page", to: "#{plausible_url()}/billing/upgrade") %>. -

-Have a question, feedback or need some guidance? Do reply back to this email. -

-Thanks,
-Uku and Marko
---
-<%= plausible_url() %>
diff --git a/lib/plausible_web/templates/error/error.html.eex b/lib/plausible_web/templates/error/error.html.eex index f5a01e7ce5af..abab1ac4ff7c 100644 --- a/lib/plausible_web/templates/error/error.html.eex +++ b/lib/plausible_web/templates/error/error.html.eex @@ -15,7 +15,6 @@

<%= @status %>

<%= @message %>
- <%= link("Go to the homepage", to: PlausibleWeb.LayoutView.home_dest(@conn), class: "button mt-4") %>
diff --git a/lib/plausible_web/templates/layout/_flash.html.eex b/lib/plausible_web/templates/layout/_flash.html.eex deleted file mode 100644 index 4f8f0deaee91..000000000000 --- a/lib/plausible_web/templates/layout/_flash.html.eex +++ /dev/null @@ -1,65 +0,0 @@ -<%= if get_flash(@conn, :success) do %> -
-
-
-
-
-
- - - -
-
-

- <%= get_flash(@conn, :success_title) || "Success!" %> -

-

- <%= get_flash(@conn, :success) %> -

-
-
- -
-
-
-
-
-
-<% end %> - -<%= if get_flash(@conn, :error) do %> -
-
-
-
-
-
- -
-
-

- <%= get_flash(@conn, :error_title) || "Error" %> -

-

- <%= get_flash(@conn, :error) %> -

-
-
- -
-
-
-
-
-
-<% end %> diff --git a/lib/plausible_web/templates/layout/_footer.html.eex b/lib/plausible_web/templates/layout/_footer.html.eex deleted file mode 100644 index 371d569477d0..000000000000 --- a/lib/plausible_web/templates/layout/_footer.html.eex +++ /dev/null @@ -1,156 +0,0 @@ -
-
-
-
-

- - Plausible Analytics -

-

- <%= if !Application.get_env(:plausible, :is_selfhost) do %> - Made and hosted in the EU 🇪🇺
- <% end %> - 100% self-funded and independent
- Built by @ukutaht and @markosaric -

- <%= if Application.get_env(:plausible, :is_selfhost) do %> - - <% end %> -
-
- -
-
-

- Community -

- -
- -
-
-
-
-
diff --git a/lib/plausible_web/templates/layout/_header.html.eex b/lib/plausible_web/templates/layout/_header.html.eex index 5d136da918d7..df78e551b5e9 100644 --- a/lib/plausible_web/templates/layout/_header.html.eex +++ b/lib/plausible_web/templates/layout/_header.html.eex @@ -3,77 +3,13 @@ diff --git a/lib/plausible_web/templates/layout/_notice.html.eex b/lib/plausible_web/templates/layout/_notice.html.eex deleted file mode 100644 index 1572644f4441..000000000000 --- a/lib/plausible_web/templates/layout/_notice.html.eex +++ /dev/null @@ -1,96 +0,0 @@ -<%= if @conn.private[:phoenix_flash] do %> - <%= render("_flash.html", assigns) %> -<% end %> - - -<%= if on_grace_period?(@conn.assigns[:current_user]) do %> -
-
-
-
- -
-
-

- Please upgrade your account -

-
-

- In order to keep your stats running, we require you to upgrade your account. If you do not upgrade your account <%= grace_period_end(@conn.assigns[:current_user]) %>, we will lock your sites and they won't be accessible. <%= link("Upgrade now →", to: "/settings", class: "text-sm font-medium text-yellow-800") %> -

-
-
-
-
-
-<% end %> - -<%= if grace_period_over?(@conn.assigns[:current_user]) do %> -
-
-
-
- -
-
-

- Dashboard locked -

-
-

- As you have outgrown your subscription tier, we kindly ask you to upgrade your subscription to accommodate your new traffic levels. <%= link("Upgrade now →", to: "/settings", class: "text-sm font-medium text-yellow-800") %> -

-
-
-
-
-
-<% end %> - -<%= if @conn.assigns[:current_user] && @conn.assigns[:current_user].subscription && @conn.assigns[:current_user].subscription.status == "past_due" do %> -
-
-
-
- - - -

- Your latest payment failed. Please provide valid payment details to keep using Plausible. -

-
-
-
- <%= link("Update billing info", to: @conn.assigns[:current_user].subscription.update_url, class: "flex items-center justify-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-200 focus:outline-none focus:ring transition ease-in-out duration-150") %> -
-
-
-
-
-<% end %> - -<%= if @conn.assigns[:current_user] && @conn.assigns[:current_user].subscription && @conn.assigns[:current_user].subscription.status == "paused" do %> -
-
-
-
- - - -

- Your subscription is paused due to failed payments. Please provide valid payment details to keep using Plausible. -

-
-
-
- <%= link("Update billing info", to: @conn.assigns[:current_user].subscription.update_url, class: "flex items-center justify-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-200 focus:outline-none focus:ring transition ease-in-out duration-150") %> -
-
-
-
-
-<% end %> diff --git a/lib/plausible_web/templates/layout/_settings_tab.html.eex b/lib/plausible_web/templates/layout/_settings_tab.html.eex deleted file mode 100644 index 1fe10c793acc..000000000000 --- a/lib/plausible_web/templates/layout/_settings_tab.html.eex +++ /dev/null @@ -1,5 +0,0 @@ -<%= if is_current_tab(@conn, @this_tab) do %> - "><%= @text %> -<% else %> - "><%= @text %> -<% end %> diff --git a/lib/plausible_web/templates/layout/_tracking.html.eex b/lib/plausible_web/templates/layout/_tracking.html.eex deleted file mode 100644 index e364ea969c63..000000000000 --- a/lib/plausible_web/templates/layout/_tracking.html.eex +++ /dev/null @@ -1,17 +0,0 @@ -<%= if !Application.get_env(:plausible, :is_selfhost) && !@conn.assigns[:skip_plausible_tracking] do %> - <%= if Application.get_env(:plausible, :environment) == "prod" do %> - - - - <% end %> - - <%= if Application.get_env(:plausible, :environment) == "staging" do %> - - - <% end %> - - <%= if Application.get_env(:plausible, :environment) == "dev" do %> - - - <% end %> -<% end %> diff --git a/lib/plausible_web/templates/layout/app.html.eex b/lib/plausible_web/templates/layout/app.html.eex index af87952946b8..cb43c9d23ece 100644 --- a/lib/plausible_web/templates/layout/app.html.eex +++ b/lib/plausible_web/templates/layout/app.html.eex @@ -9,25 +9,15 @@ <%= assigns[:title] || "Plausible · Simple, privacy-friendly alternative to Google Analytics" %> "/> - <%= render("_tracking.html", assigns) %> <%= if @conn.assigns[:background], do: "background-color: #{@conn.assigns[:background]}" %>"> - <%= if !@conn.assigns[:embedded] do %> - <%= render("_header.html", assigns) %> - <%= render("_notice.html", assigns) %> - <% end %> + <%= render("_header.html", assigns) %>
<%= Map.get(assigns, :inner_layout) || @inner_content %>
- <%= if @conn.assigns[:embedded] do %> -
- - <% else %> - <%= render("_footer.html", assigns) %> - <% end %> diff --git a/lib/plausible_web/templates/layout/embedded.html.eex b/lib/plausible_web/templates/layout/embedded.html.eex deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lib/plausible_web/templates/layout/focus.html.eex b/lib/plausible_web/templates/layout/focus.html.eex deleted file mode 100644 index 95f7c6be2ae3..000000000000 --- a/lib/plausible_web/templates/layout/focus.html.eex +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - "> - <%= assigns[:title] || "Plausible · Web analytics" %> - "/> - <%= render("_tracking.html", assigns) %> - - - - - - <%= if @conn.private[:phoenix_flash] do %> - <%= render("_flash.html", assigns) %> - <% end %> - <%= @inner_content %> - - -

- ©<%= DateTime.utc_now().year() %> Plausible Analytics. All rights reserved. -

- - - - diff --git a/lib/plausible_web/templates/layout/site_settings.html.eex b/lib/plausible_web/templates/layout/site_settings.html.eex deleted file mode 100644 index 91e78cc64914..000000000000 --- a/lib/plausible_web/templates/layout/site_settings.html.eex +++ /dev/null @@ -1,26 +0,0 @@ -<%= render_layout "app.html", assigns do %> -
- <%= link("← Back to stats", to: "/#{URI.encode_www_form(@site.domain)}", class: "text-sm text-indigo-600 font-bold") %> -
-

- Settings for <%= @site.domain %> -

-
-
-
- <%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/recipients", [class: "lg:hidden"], fn f -> %> - <%= select f, :tab, settings_tabs(@conn), class: "dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100", onchange: "location.href = location.href.replace(/[^\/\/]*$/, event.target.value)", selected: List.last(@conn.path_info) %> - <% end %> - -
- -
- <%= @inner_content %> -
-
-
-<% end %> diff --git a/lib/plausible_web/templates/page/index.html.eex b/lib/plausible_web/templates/page/index.html.eex deleted file mode 100644 index d9551964f5dc..000000000000 --- a/lib/plausible_web/templates/page/index.html.eex +++ /dev/null @@ -1,3 +0,0 @@ -
- You will be redirected... If it doesn't work, please click login. -
diff --git a/lib/plausible_web/templates/site/edit_shared_link.html.eex b/lib/plausible_web/templates/site/edit_shared_link.html.eex deleted file mode 100644 index e36d12fb91b2..000000000000 --- a/lib/plausible_web/templates/site/edit_shared_link.html.eex +++ /dev/null @@ -1,12 +0,0 @@ -<%= form_for @changeset, "/sites/#{URI.encode_www_form(@site.domain)}/shared-links/#{@changeset.data.slug}", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

Edit shared link

-
- <%= label f, :name, "Name", class: "block text-sm font-medium text-gray-700 dark:text-gray-100" %> -
- <%= text_input f, :name, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md", required: "required" %> - <%= error_tag f, :name %> -
-
- - <%= submit "Update", class: "button mt-4 w-full" %> -<% end %> diff --git a/lib/plausible_web/templates/site/index.html.eex b/lib/plausible_web/templates/site/index.html.eex deleted file mode 100644 index d1b4dc156b68..000000000000 --- a/lib/plausible_web/templates/site/index.html.eex +++ /dev/null @@ -1,188 +0,0 @@ -
- - <%= if @needs_to_upgrade == {true, :no_active_subscription} do %> -
-
-
- -
-
-

- Payment required -

-
-

- To access the sites you own, you need to subscribe to a monthly or yearly payment plan. - <%= link("Upgrade now →", to: "/settings", class: "text-sm font-medium text-yellow-800") %> -

-
-
-
-
- <% end %> - -
-

- My sites -

- + Add a website -
- -
    - <%= if Enum.empty?(@sites ++ @invitations) do %> -

    You don't have any sites yet

    - <% end %> - - <%= for invitation <- @invitations do %> -
    -
  • -
    - -
    -

    <%= invitation.site.domain %>

    -
    - - - Pending invitation - -
    -
    - - - <%= PlausibleWeb.StatsView.large_number_format(Map.get(@visitors, invitation.site.domain, 0)) %> visitor<%= if Map.get(@visitors, invitation.site.domain, 0) != 1 do %>s<% end %> in last 24h - - -
    -
  • -
    - <% end %> - - <%= for site <- @sites do %> -
    - <%= link(to: "/" <> URI.encode_www_form(site.domain)) do %> -
  • -
    - -
    -

    <%= site.domain %>

    -
    -
    -
    - - - <%= PlausibleWeb.StatsView.large_number_format(Map.get(@visitors, site.domain, 0)) %> visitor<%= if Map.get(@visitors, site.domain, 0) != 1 do %>s<% end %> in last 24h - - -
    -
  • - <% end %> - <%= if List.first(site.memberships).role != :viewer do %> - <%= link(to: "/" <> URI.encode_www_form(site.domain) <> "/settings", class: "absolute top-0 right-0 p-4 mt-1") do %> - - <% end %> - <% end %> -
    - <% end %> -
- - <%= if @pagination.total_pages > 1 do %> - <%= pagination @conn, @pagination, [current_class: "is-current"], fn p -> %> - - <% end %> - <% end %> - - <%= if Enum.any?(@invitations) do %> - - <% end %> -
diff --git a/lib/plausible_web/templates/site/membership/invite_member_form.html.eex b/lib/plausible_web/templates/site/membership/invite_member_form.html.eex deleted file mode 100644 index 81467a65c29f..000000000000 --- a/lib/plausible_web/templates/site/membership/invite_member_form.html.eex +++ /dev/null @@ -1,63 +0,0 @@ -<%= form_for @conn, Routes.membership_path(@conn, :invite_member, @site.domain), [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

Invite member to <%= @site.domain %>

- -

- Enter the email address and role of the person you want to invite. We will contact them over email to offer them access to - <%= @site.domain %> analytics. -

- -

- The invitation will expire in 48 hours -

- - <%= if @conn.assigns[:error] do %> -
<%= @conn.assigns[:error] %>
- <% end %> - -
- <%= label f, :email, "Email address", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
-
- -
- <%= email_input(f, :email, class: "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-full rounded-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500", placeholder: "john.doe@example.com", required: "true") %> -
- <%= error_tag f, :email %> -
- -
- <%= label f, :role, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
- - - -
-
- -
- <%= submit(class: "button w-full") do %> - - Invite - <% end %> -
-<% end %> diff --git a/lib/plausible_web/templates/site/membership/transfer_ownership_form.html.eex b/lib/plausible_web/templates/site/membership/transfer_ownership_form.html.eex deleted file mode 100644 index 5a35d70800cb..000000000000 --- a/lib/plausible_web/templates/site/membership/transfer_ownership_form.html.eex +++ /dev/null @@ -1,30 +0,0 @@ -<%= form_for @conn, Routes.membership_path(@conn, :transfer_ownership, @site.domain), [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

Transfer ownership of <%= @site.domain %>

- -

- Enter the email address of the new owner. We will contact them over email to offer them the ownership of <%= @site.domain %>. -

-

- If they accept the transfer request, the new owner will be responsible for billing. Your access will be downgraded to admin and - any other member roles will stay the same. If they don't respond in 48 hours, the request will expire automatically. -

- - <%= if @conn.assigns[:error] do %> -
<%= @conn.assigns[:error] %>
- <% end %> - -
- <%= label f, :email, "Email address", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -
-
- -
- <%= email_input(f, :email, class: "focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-md pl-10 sm:text-sm border-gray-300", placeholder: "john.doe@example.com", required: "true") %> -
- <%= error_tag f, :email %> -
- -
- <%= submit("Request transfer", class: "button w-full") %> -
-<% end %> diff --git a/lib/plausible_web/templates/site/new.html.eex b/lib/plausible_web/templates/site/new.html.eex deleted file mode 100644 index 260aec287e45..000000000000 --- a/lib/plausible_web/templates/site/new.html.eex +++ /dev/null @@ -1,79 +0,0 @@ -
- <%= form_for @changeset, "/sites", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

Your website details

- - <%= if @is_at_limit do %> -
-
-
- -
-
-

- Upgrade required -

-
-

- Your account is limited to <%= @site_limit %> sites. Please contact hello@plausible.io to add more sites. -

-
-
-
-
- <% end %> - - <%= if is_nil(@current_user.trial_expiry_date) do %> -
-
-
- -
-
-
-

- When you create your first site, your account will enter a 30 day free trial. -

-
-
-
-
- <% end %> - -
- <%= label f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -

Just the naked domain or subdomain without 'www'

-
- - https:// - - <%= text_input f, :domain, class: "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md sm:text-sm border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300", placeholder: "example.com", disabled: @is_at_limit %> -
- <%= error_tag f, :domain %> -
-
- <%= label f, :timezone, "Reporting Timezone", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %> -

To make sure we agree on what 'today' means

- -
- <%= select f, :timezone, Plausible.Timezones.options(), id: "tz-select", selected: "Etc/Greenwich", class: "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md", disabled: @is_at_limit %> -
-
- - - <%= submit "Add snippet →", class: "button mt-4 w-full", disabled: @is_at_limit %> - <% end %> - - <%= if @is_first_site do %> - - <% end %> -
diff --git a/lib/plausible_web/templates/site/new_goal.html.eex b/lib/plausible_web/templates/site/new_goal.html.eex deleted file mode 100644 index 32d700501428..000000000000 --- a/lib/plausible_web/templates/site/new_goal.html.eex +++ /dev/null @@ -1,40 +0,0 @@ -<%= form_for @changeset, "/#{URI.encode_www_form(@site.domain)}/goals", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

Add goal for <%= @site.domain %>

-
Goal trigger
-
-
Pageview
-
Custom event
-
-
-
- <%= label f, :page_path, class: "block text-sm font-bold dark:text-gray-100" %> - <%= text_input f, :page_path, class: "transition mt-3 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500", placeholder: "/success" %> - <%= error_tag f, :page_path %> -
- -
- - <%= submit "Add goal →", class: "button mt-4 w-full" %> -<% end %> - - diff --git a/lib/plausible_web/templates/site/new_shared_link.html.eex b/lib/plausible_web/templates/site/new_shared_link.html.eex deleted file mode 100644 index 533a183fabd1..000000000000 --- a/lib/plausible_web/templates/site/new_shared_link.html.eex +++ /dev/null @@ -1,24 +0,0 @@ -<%= form_for @changeset, "/sites/#{URI.encode_www_form(@site.domain)}/shared-links", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

New shared link

-
-
-
- <%= label f, :name, "Name", class: "block text-sm font-medium text-gray-700 dark:text-gray-100" %> -
- <%= text_input f, :name, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md", required: "required", autocomplete: "off" %> - <%= error_tag f, :name %> -
-
-
- <%= label f, :password, "Password (optional)", class: "block text-sm font-medium text-gray-700 dark:text-gray-100" %> -
- <%= password_input f, :password, class: "shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md", autocomplete: "off" %> - <%= error_tag f, :password %> -

- Password protection is optional. Please make sure you save it in a secure place. Once the link is created, we cannot reveal the password. -

-
-
- - <%= submit "Create shared link", class: "button mt-4 w-full" %> -<% end %> diff --git a/lib/plausible_web/templates/site/settings_custom_domain.html.eex b/lib/plausible_web/templates/site/settings_custom_domain.html.eex deleted file mode 100644 index b1508cd40b14..000000000000 --- a/lib/plausible_web/templates/site/settings_custom_domain.html.eex +++ /dev/null @@ -1,39 +0,0 @@ -
-
-
- -
-
-

- Deprecated feature -

-
-

- We are moving away from CNAME-based custom domains. If you're concerned about adblockers, we recommend - <%= link("setting up a proxy", class: "underline text-yellow-800 dark:text-yellow-400", to: "https://plausible.io/docs/proxy/introduction", target: "_blank", rel: "noferrer") %> for your analytics script instead. -

-
-
-
-
- -
-
-

Custom domain

-

Serve the tracking script from your domain name as a first-party resource instead of loading the script from our domain.

- - <%= link(to: "https://docs.plausible.io/custom-domain/", target: "_blank") do %> - - <% end %> -
-
- <%= if @site.custom_domain do %> - - Configured domain: <%= @site.custom_domain.domain %> - - <%= link("Remove custom domain", to: "/sites/#{URI.encode_www_form(@site.domain)}/custom-domains/#{@site.custom_domain.id}", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete") %> - <% end %> -
-
diff --git a/lib/plausible_web/templates/site/settings_danger_zone.html.eex b/lib/plausible_web/templates/site/settings_danger_zone.html.eex deleted file mode 100644 index a9f2343b5c10..000000000000 --- a/lib/plausible_web/templates/site/settings_danger_zone.html.eex +++ /dev/null @@ -1,32 +0,0 @@ -
-
-
-

Danger zone

-

Destructive actions below can result in irrecoverable data loss. Be careful.

-
-
  • -
    -

    - Reset stats -

    -

    - Removes all pageviews but keeps the site configuration -

    -
    - <%= link("Reset #{@site.domain} stats", to: "/#{URI.encode_www_form(@site.domain)}/stats", method: :delete, class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", data: [confirm: "Resetting the stats cannot be reversed. Are you sure?"]) %> -
  • -
    - -
  • -
    -

    - Delete site -

    -

    - Removes all stats along with the site configuration -

    -
    - <%= link "Delete #{@site.domain}", to: "/#{URI.encode_www_form(@site.domain)}", method: :delete, class: "inline-block px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-800 bg-red-100 dark:bg-red-200 hover:bg-red-50 dark:hover:bg-red-300 focus:outline-none focus:border-red-300 focus:ring active:bg-red-200 transition ease-in-out duration-150 sm:text-sm sm:leading-5", data: [confirm: "Deleting the site data cannot be reversed. Are you sure?"] %> -
  • -
    -
    diff --git a/lib/plausible_web/templates/site/settings_email_reports.html.eex b/lib/plausible_web/templates/site/settings_email_reports.html.eex deleted file mode 100644 index 614340067598..000000000000 --- a/lib/plausible_web/templates/site/settings_email_reports.html.eex +++ /dev/null @@ -1,191 +0,0 @@ -
    -
    -

    Email reports

    -

    Send weekly/monthly analytics reports to as many addresses as you wish

    - <%= link(to: "https://plausible.io/docs/email-reports", target: "_blank", rel: "noferrer") do %> - - <% end %> -
    - -
    - <%= if @weekly_report do %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - <% else %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - <% end %> - Send a weekly email report every Monday -
    - <%= if @weekly_report do %> -
    -

    Weekly report recipients

    - <%= for recipient <- @weekly_report.recipients do %> -
    - - <%= recipient %> - - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/recipients/#{recipient}", method: :delete) do %> - - <% end %> -
    - <% end %> - <%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/weekly-report/recipients", fn f -> %> -
    -
    -
    -
    - -
    - <%= email_input f, :recipient, class: "focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100", placeholder: "recipient@example.com", required: "true" %> -
    - - <%= submit class: "-ml-px relative button rounded-l-none" do %> - - Add recipient - <% end %> -
    -
    - <% end %> -
    - <% end %> -
    -
    - <%= if @monthly_report do %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - <% else %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - <% end %> - Send a monthly email report on 1st of the month -
    - <%= if @monthly_report do %> -
    -

    Monthly report recipients

    - <%= for recipient <- @monthly_report.recipients do %> -
    - - <%= recipient %> - - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/recipients/#{recipient}", method: :delete) do %> - - <% end %> -
    - <% end %> - <%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/monthly-report/recipients", fn f -> %> -
    -
    -
    -
    - -
    - <%= email_input f, :recipient, class: "focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100", placeholder: "recipient@example.com", required: "true" %> -
    - - <%= submit class: "-ml-px relative button rounded-l-none" do %> - - Add recipient - <% end %> -
    -
    - <% end %> -
    - <% end %> -
    - -
    -
    -

    Traffic spike notifications

    -

    Get notified when your site has unusually high number of current visitors

    - <%= link(to: "https://plausible.io/docs/traffic-spikes", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - -
    - <%= if @spike_notification do %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/disable", method: :post, class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - <% else %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/enable", method: :post, class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - <% end %> - Send notifications of traffic spikes -
    - - <%= if @spike_notification do %> -
    - - <%= form_for Plausible.Site.SpikeNotification.changeset(@spike_notification, %{}), "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification", fn f -> %> -

    Current visitor threshold

    -
    -
    -
    - - -
    - <%= number_input f, :threshold, class: "focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:text-gray-100" %> -
    - -
    - <% end %> -

    Notification recipients

    - <%= for recipient <- @spike_notification.recipients do %> -
    - - <%= recipient %> - - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/recipients/#{recipient}", method: :delete) do %> - - <% end %> -
    - <% end %> - <%= form_for @conn, "/sites/#{URI.encode_www_form(@site.domain)}/spike-notification/recipients", fn f -> %> -
    -
    -
    -
    - -
    - <%= email_input f, :recipient, class: "focus:ring-indigo-500 dark:bg-gray-900 focus:border-indigo-500 block w-full rounded-none rounded-l-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500 dark:placeholder-gray-400 dark:text-gray-100", placeholder: "recipient@example.com", required: "true" %> -
    - - <%= submit class: "-ml-px relative button rounded-l-none" do %> - - Add recipient - <% end %> -
    -
    - <% end %> -
    - <% end %> -
    diff --git a/lib/plausible_web/templates/site/settings_general.html.eex b/lib/plausible_web/templates/site/settings_general.html.eex deleted file mode 100644 index 689f4698878d..000000000000 --- a/lib/plausible_web/templates/site/settings_general.html.eex +++ /dev/null @@ -1,118 +0,0 @@ -<%= form_for @changeset, "/#{URI.encode_www_form(@site.domain)}/settings", fn f -> %> -
    -
    -
    -

    General information

    -

    Update your reporting timezone.

    - <%= link(to: "https://plausible.io/docs/general/", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - -
    -
    <%= label f, :domain, class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300" %> <%= text_input f, :domain, class: "dark:bg-gray-900 mt-1 block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:text-gray-100", disabled: "disabled" %> -
    - -
    - <%= label f, :timezone, "Reporting Timezone", class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300" %> - <%= select f, :timezone, Plausible.Timezones.options(), class: "dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer" %> -
    -
    -
    -
    - - <%= submit "Save", class: "button" %> - -
    -
    -<% end %> - -<%= form_for @conn, "/", [class: "shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6"], fn f -> %> -
    -

    Javascript snippet

    -

    Include this snippet in the <head> of your website.

    - - <%= link(to: "https://plausible.io/docs/plausible-script", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - -
    -
    - <%= textarea f, :domain, id: "snippet_code", class: "transition overflow-hidden bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white focus:border-gray-300 dark:focus:border-gray-500 text-xs mt-2 resize-none", value: snippet(@site), rows: 2 %> - - - -
    -
    -<% end %> - -
    -
    -

    Data Import from Google Analytics

    -

    Import existing data from your Google Analytics account.

    - <%= link(to: "https://docs.plausible.io/import-data/", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - - <%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %> - <%= cond do %> - <% @imported_data && @imported_data.status == "importing" -> %> -
    -
    We are importing data from <%= @imported_data.source %> in the background... You will receive an email when it's completed
    - - <% @imported_data && @imported_data.status == "ok" -> %> -
  • -
    -

    - Forget Imported Data -

    -

    - Removes all data imported from <%= @imported_data.source %> -

    -
    - <%= link("Forget imported stats", to: "/#{URI.encode_www_form(@site.domain)}/settings/forget-imported", method: :delete, class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150") %> -
  • - - <%= if @site.google_auth do %> - <%= link("Unlink Google account", to: "/#{URI.encode_www_form(@site.domain)}/settings/google-import", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete") %> - <% end %> - - <% @site.google_auth -> %> -
    - Linked Google account: <%= @site.google_auth.email %> - - <%= case @google_profiles do %> - <% {:ok, profiles} -> %> -

    - Select the Google Analytics profile you would like to import data from. -

    - - <%= form_for @conn, "/#{URI.encode_www_form(@site.domain)}/settings/google-import", [class: "max-w-xs"], fn f -> %> -
    -
    - <%= select f, :profile, profiles, prompt: "(Choose profile)", class: "dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100" %> -
    -
    - <%= submit "Import", class: "button" %> - <% end %> - - <% {:error, error} -> %> -

    The following error occurred when fetching your Google Analytics profiles.

    -

    <%= error %>

    - <% end %> - - <%= link("Unlink Google account", to: "/#{URI.encode_www_form(@site.domain)}/settings/google-import", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete") %> - - <% true -> %> - <%= button("Continue with Google", to: Plausible.Google.Api.authorize_url(@site.id, "general"), class: "button mt-8") %> - <% end %> - <% else %> -
    - -

    An extra step is needed to set up your Plausible Analytics Self Hosted for the Google Search Console integration. - Find instructions <%= link("here", to: "https://plausible.io/docs/self-hosting-configuration#google-search-integration", class: "text-indigo-500") %>

    -
    - <% end %> -
    diff --git a/lib/plausible_web/templates/site/settings_goals.html.eex b/lib/plausible_web/templates/site/settings_goals.html.eex deleted file mode 100644 index 746983436d9f..000000000000 --- a/lib/plausible_web/templates/site/settings_goals.html.eex +++ /dev/null @@ -1,26 +0,0 @@ -
    -
    -

    Goals

    -

    Define actions that you want your users to take like visiting a certain page, submitting a form, etc.

    - <%= link(to: "https://docs.plausible.io/goal-conversions/", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - - <%= if Enum.count(@goals) > 0 do %> -
    - <%= for goal <- @goals do %> -
    - <%= goal_name(goal) %> - <%= button(to: "/#{URI.encode_www_form(@site.domain)}/goals/#{goal.id}", method: :delete, class: "text-sm text-red-600", data: [confirm: "Are you sure you want to remove goal #{goal_name(goal)}? This will just affect the UI, all of your analytics data will stay intact."]) do %> - - <% end %> -
    - <% end %> -
    - <% else %> -
    No goals configured for this site yet
    - <% end %> - - <%= link("+ Add goal", to: "/#{URI.encode_www_form(@site.domain)}/goals/new", class: "button mt-6") %> -
    diff --git a/lib/plausible_web/templates/site/settings_people.html.eex b/lib/plausible_web/templates/site/settings_people.html.eex deleted file mode 100644 index 8840129585ef..000000000000 --- a/lib/plausible_web/templates/site/settings_people.html.eex +++ /dev/null @@ -1,148 +0,0 @@ -
    -
    -

    People

    -

    Invite your friend or coworkers

    - <%= link(to: "https://plausible.io/docs/users-roles", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    -
    -
      - <%= for membership <- @site.memberships do %> -
    • -
      -
      - <%= gravatar(membership.user.email, class: "h-8 w-8 rounded-full") %> -
      -
      -

      - <%= membership.user.name %> -

      -

      - <%= membership.user.email %> -

      -
      - -
      - -
        - <%= if membership.role == :owner do %> -
      • -
        -

        Owner

        -

        Site owner cannot be assigned to any other role

        -
        - - - - -
      • - <%= if @conn.assigns[:current_user_role] == :owner do %> -
      • - <%= link("Transfer ownership →", to: Routes.membership_path(@conn, :transfer_ownership_form, @site.domain), class: "inline-block w-full p-4 text-sm text-red-600 font-medium") %> -
      • - <% end %> - <% else %> - <%= link(to: Routes.membership_path(@conn, :update_role, @site.domain, membership.id, "admin"), method: :put, class: "p-4 flex justify-between text-sm group hover:bg-indigo-500") do %> -
        -

        Admin

        -

        View stats and edit site settings

        -
        - - <%= if membership.role == :admin do %> - - - - <% end %> - <% end %> - <%= link(to: Routes.membership_path(@conn, :update_role, @site.domain, membership.id, "viewer"), method: :put, class: "p-4 flex justify-between text-sm group hover:bg-indigo-500") do %> -
        -

        Viewer

        -

        View stats only

        -
        - - <%= if membership.role == :viewer do %> - - - - <% end %> - <% end %> - - <%= link(to: Routes.membership_path(@conn, :remove_member, @site.domain, membership.id), method: :delete, class: "p-4 flex hover:bg-gray-100 hover:bg-gray-900 text-red-600") do %> -

        Remove member

        - <% end %> - <% end %> -
      -
      -
      -
    • - <% end %> -
    - - <%= if Enum.count(@site.invitations) > 0 do %> -
    -

    Pending invitations

    -
    -
    -
    -
    -
    - - - - - - - - - - <%= for invitation <- @site.invitations do %> - - - - - - <% end %> - -
    - Email - - Role - - Edit -
    - <%= invitation.email %> - - <%= invitation.role |> Atom.to_string |> String.capitalize %> - - <%= link("Remove", to: "/sites/invitations/#{invitation.invitation_id}", method: :delete, class: "text-red-600 hover:text-red-900") %> -
    -
    -
    -
    -
    - <% end %> -
    - -
    - <%= link(to: Routes.membership_path(@conn, :invite_member_form, @site.domain), class: "button") do %> - - Invite - <% end %> -
    -
    diff --git a/lib/plausible_web/templates/site/settings_search_console.html.eex b/lib/plausible_web/templates/site/settings_search_console.html.eex deleted file mode 100644 index 5181e906e01f..000000000000 --- a/lib/plausible_web/templates/site/settings_search_console.html.eex +++ /dev/null @@ -1,56 +0,0 @@ -
    -
    -

    Google Search Console integration

    -

    You can integrate with Google Search Console to get all of your important search results stats such as keyword phrases people find your site with.

    - <%= link(to: "https://docs.plausible.io/google-search-console-integration/", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - - <%= if Keyword.get(Application.get_env(:plausible, :google), :client_id) do %> - <%= if @site.google_auth do %> -
    - Linked Google account: <%= @site.google_auth.email %> - - <%= link("Unlink Google account", to: "/#{URI.encode_www_form(@site.domain)}/settings/google-search", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete") %> - - <%= case @search_console_domains do %> - <% {:ok, domains} -> %> - <%= if @site.google_auth.property && !(@site.google_auth.property in domains) do %> -

    - NB: Your Google account does not have access to your currently configured property, <%= @site.google_auth.property %>. Please select a verified property from the list below. -

    - <% else %> -

    - Select the Google Search Console property you would like to pull keyword data from. If you don't see your domain, <%= link("set it up and verify", to: "https://docs.plausible.io/google-search-console-integration", class: "text-indigo-500") %> on Search Console first. -

    - <% end %> - - <%= form_for Plausible.Site.GoogleAuth.changeset(@site.google_auth), "/#{URI.encode_www_form(@site.domain)}/settings/google", [class: "max-w-xs"], fn f -> %> -
    -
    - <%= select f, :property, domains, prompt: "(Choose property)", class: "dark:bg-gray-800 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 outline-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100" %> -
    -
    - - <%= submit "Save", class: "button" %> - <% end %> - <% {:error, error} -> %> -

    The following error happened when fetching your Google Search Console domains.

    -

    <%= error %>

    - <% end %> - <% else %> - <%= button("Continue with Google", to: Plausible.Google.Api.authorize_url(@site.id, "search-console"), class: "button mt-8") %> - -
    - NB: You also need to set up your site on <%= link("Google Search Console", to: "https://search.google.com/search-console/about") %> for the integration to work. <%= link("Read the docs", to: "https://plausible.io/docs/google-search-console-integration", class: "text-indigo-500", rel: "noreferrer") %> -
    - <% end %> - <% else %> -
    - -

    An extra step is needed to set up your Plausible Analytics Self Hosted for the Google Search Console integration. - Find instructions <%= link("here", to: "https://plausible.io/docs/self-hosting-configuration#google-search-integration", class: "text-indigo-500") %>

    -
    - <% end %> -
    diff --git a/lib/plausible_web/templates/site/settings_visibility.html.eex b/lib/plausible_web/templates/site/settings_visibility.html.eex deleted file mode 100644 index 6a0c570f4223..000000000000 --- a/lib/plausible_web/templates/site/settings_visibility.html.eex +++ /dev/null @@ -1,118 +0,0 @@ -
    -
    -

    Public dashboard

    -

    Share your stats publicly or keep them private

    - <%= link(to: "https://docs.plausible.io/visibility", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - - <%= if @site.public do %> -
    - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/make-private", method: "POST", class: "bg-indigo-600 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - Make stats publicly available on URI.encode_www_form(@site.domain)%>" class="text-indigo-500"><%= plausible_url() <> "/" <> URI.encode_www_form(@site.domain)%> -
    - <% else %> -
    - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/make-public", method: "POST", class: "bg-gray-200 dark:bg-gray-700 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring") do %> - - <% end %> - Make stats publicly available on URI.encode_www_form(@site.domain)%>" class="text-indigo-500"><%= plausible_url() <> "/" <> URI.encode_www_form(@site.domain)%> -
    - <% end %> -
    - -
    -
    -

    Shared links

    -

    You can share your stats privately by generating a shared link. The links are impossible to guess and you can add password protection for extra security.

    - <%= link(to: "https://docs.plausible.io/shared-links", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - -
    - <%= for link <- @shared_links do %> -
    - -
    - - - <%= link(to: "/sites/#{URI.encode_www_form(@site.domain)}/shared-links/#{link.slug}/edit", class: "px-4 py-2 inline-flex items-center text-indigo-800 bg-gray-200 border-r border-gray-300 rounded-none dark:bg-gray-850 dark:text-indigo-500 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-825") do %> - - <% end %> - <%= button(to: "/sites/#{URI.encode_www_form(@site.domain)}/shared-links/#{link.slug}", method: :delete, class: "py-2 px-4 inline-flex items-center bg-gray-200 dark:bg-gray-850 text-red-600 dark:text-red-500 rounded-l-none hover:bg-gray-300 dark:hover:bg-gray-825", data: [confirm: "Are you sure you want to delete this shared link? The stats will not be accessible with this link anymore."]) do %> - - <% end %> -
    -
    - <% end %> - - <%= link("+ New link", to: "/sites/#{URI.encode_www_form(@site.domain)}/shared-links/new", class: "button mt-4") %> -
    -
    - -
    -
    -

    Embed dashboard

    -

    You can use shared links to embed your stats in any other webpage using an iframe. Copy & paste a shared link into the form below to generate the embed code.

    - <%= link(to: "https://plausible.io/docs/embed-dashboard", target: "_blank", rel: "noreferrer") do %> - - <% end %> -
    - -
    -
    - -

    Only public shared links without password protection can be embedded

    -
    - -
    -
    - -
    - - -
    - -
    - -

    Hint: try using `transparent` background to blend the dashboard with your site background

    -
    - -
    -
    -
    - - - - -
    -
    - - -
    - - - - -
    -
    -
    -
    diff --git a/lib/plausible_web/templates/site/snippet.html.eex b/lib/plausible_web/templates/site/snippet.html.eex deleted file mode 100644 index 066c6b3309db..000000000000 --- a/lib/plausible_web/templates/site/snippet.html.eex +++ /dev/null @@ -1,22 +0,0 @@ -
    - <%= form_for @conn, "/", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

    Add javascript snippet

    -
    -

    Paste this snippet in the <head> of your website.

    - -
    - <%= textarea f, :domain, id: "snippet_code", class: "transition overflow-hidden bg-gray-100 dark:bg-gray-900 appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-400 dark:focus:border-gray-500 text-xs mt-4 resize-none", value: snippet(@site), rows: 3, readonly: "readonly" %> - - - -
    -
    - <%= link("Start collecting data →", class: "button mt-4 w-full", to: "/#{URI.encode_www_form(@site.domain)}") %> - <% end %> - - <%= if @is_first_site do %> - - <% end %> -
    diff --git a/lib/plausible_web/templates/stats/shared_link_password.html.eex b/lib/plausible_web/templates/stats/shared_link_password.html.eex deleted file mode 100644 index 38a84f9bab32..000000000000 --- a/lib/plausible_web/templates/stats/shared_link_password.html.eex +++ /dev/null @@ -1,17 +0,0 @@ -<%= form_for @conn, "/share/#{@link.slug}/authenticate", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> -

    Enter password

    -
    - This link is password-protected. Please enter the password to continue to the dashboard. -
    - -
    - <%= label f, :password, "Password", class: "block text-sm font-bold dark:text-gray-100" %> - <%= password_input f, :password, class: "transition mt-3 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500" %> - - <%= if @conn.assigns[:error] do %> -
    <%= @conn.assigns[:error] %>
    - <% end %> -
    - - <%= submit "Continue", class: "button mt-4 w-full" %> -<% end %> diff --git a/lib/plausible_web/templates/stats/site_locked.html.eex b/lib/plausible_web/templates/stats/site_locked.html.eex deleted file mode 100644 index 4d217cab5d49..000000000000 --- a/lib/plausible_web/templates/stats/site_locked.html.eex +++ /dev/null @@ -1,49 +0,0 @@ -
    -
    -
    -
    - -
    -

    - Site locked -

    - - <%= case @conn.assigns[:current_user_role] do %> - <% :owner -> %> -
    -

    - This site is locked because you don't have an active subscription. We are still counting stats in the background but your access to the dashboard is restricted. Subscribe with the link below to access your stats again. -

    -
    -
    - <%= link("Manage my subscription", to: "/settings", class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm") %> -
    - <% role when role in [:admin, :viewer] -> %> -
    -

    - This site is currently locked and cannot be accessed. The site owner <%= @owner.email %> must upgrade their subscription plan in order to - unlock the site. -

    -
    -

    Want to pay for this site with the account you're logged in with?

    -

    Contact <%= @owner.email %> and ask them to <%= link("transfer the ownership", class: "text-indigo-500", to: "https://plausible.io/docs/transfer-ownership", rel: "noreferrer") %> of the site over to you

    -
    -
    -
    - <%= link("Back to my sites", to: "/sites", class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm") %> -
    - <% _ -> %> -
    -

    - This site is currently locked and cannot be accessed. You can check back later or contact the site owner to unlock it. -

    -
    - <%= if @conn.assigns[:current_user] do %> -
    - <%= link("Back to my sites", to: "/sites", class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm") %> -
    - <% end %> - <% end %> -
    -
    -
    diff --git a/lib/plausible_web/templates/stats/stats.html.eex b/lib/plausible_web/templates/stats/stats.html.eex index a47aac1505a4..4ea2675f8743 100644 --- a/lib/plausible_web/templates/stats/stats.html.eex +++ b/lib/plausible_web/templates/stats/stats.html.eex @@ -1,41 +1,7 @@
    " data-site-domain="<%= @site.domain %>"> - <%= if @offer_email_report do %> - - <% end %> - - <%= if @site.locked do %> - - <% end %> -
    -
    +
    - <%= if !@conn.assigns[:current_user] && @conn.assigns[:demo] do %> -
    -
    -

    - Want these stats for your website? -
    - Start your free trial today. -

    - -
    -
    - <% end %> +
    diff --git a/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex b/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex index ea34a30efc52..c09b9ed6b9e7 100644 --- a/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex +++ b/lib/plausible_web/templates/stats/waiting_first_pageview.html.eex @@ -27,11 +27,6 @@

    Need to see the snippet again? <%= link("Click here", to: "/#{URI.encode_www_form(@site.domain)}/snippet", class: "text-indigo-600 dark:text-indigo-500 text-underline")%>
    Not working? <%= link("Troubleshoot the integration", to: "https://plausible.io/docs/troubleshoot-integration#keep-seeing-a-blinking-green-dot", class: "text-indigo-600 dark:text-indigo-500 text-underline", rel: "noreferrer") %> with our guide - <%= if Application.get_env(:plausible, :is_selfhost) do %> - first
    Still not working? Ask on our <%= link("community-supported forum", to: "https://github.com/plausible/analytics/discussions", class: "text-indigo-600 dark:text-indigo-500 text-underline" ) %> - <% else %> - first
    Still not working? <%= link("Contact us", to: "https://plausible.io/contact", class: "text-indigo-600 dark:text-indigo-500 text-underline" ) %> and we will help you with your setup - <% end %>

    diff --git a/lib/plausible_web/templates/unsubscribe/success.html.eex b/lib/plausible_web/templates/unsubscribe/success.html.eex deleted file mode 100644 index 443d1f65666e..000000000000 --- a/lib/plausible_web/templates/unsubscribe/success.html.eex +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    Unsubscribe successful

    -

    You will no longer receive a <%= @interval %> analytics report for <%= @site %>

    -
    diff --git a/lib/plausible_web/views/auth_view.ex b/lib/plausible_web/views/auth_view.ex deleted file mode 100644 index 32976e2c5119..000000000000 --- a/lib/plausible_web/views/auth_view.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule PlausibleWeb.AuthView do - use PlausibleWeb, :view - alias Plausible.Billing.Plans - - def admin_email do - Application.get_env(:plausible, :admin_email) - end - - def base_domain do - PlausibleWeb.Endpoint.host() - end - - def plausible_url do - PlausibleWeb.Endpoint.url() - end - - def subscription_quota(subscription) do - Plans.allowance(subscription) |> PlausibleWeb.StatsView.large_number_format() - end - - def subscription_interval(subscription) do - Plans.subscription_interval(subscription) - end - - def format_invoices(invoice_list) do - Enum.map(invoice_list, fn invoice -> - %{ - date: - invoice["payout_date"] |> Date.from_iso8601!() |> Timex.format!("{Mshort} {D}, {YYYY}"), - amount: (invoice["amount"] / 1) |> :erlang.float_to_binary(decimals: 2), - currency: invoice["currency"] |> PlausibleWeb.BillingView.present_currency(), - url: invoice["receipt_url"] - } - end) - end - - def delimit_integer(number) do - Integer.to_charlist(number) - |> :lists.reverse() - |> delimit_integer([]) - |> String.Chars.to_string() - end - - defp delimit_integer([a, b, c, d | tail], acc) do - delimit_integer([d | tail], [",", c, b, a | acc]) - end - - defp delimit_integer(list, acc) do - :lists.reverse(list) ++ acc - end - - def present_subscription_status("active"), do: "Active" - def present_subscription_status("past_due"), do: "Past due" - def present_subscription_status("deleted"), do: "Cancelled" - def present_subscription_status("paused"), do: "Paused" - def present_subscription_status(status), do: status - - def subscription_colors("active"), do: "bg-green-100 text-green-800" - def subscription_colors("past_due"), do: "bg-yellow-100 text-yellow-800" - def subscription_colors("paused"), do: "bg-red-100 text-red-800" - def subscription_colors("deleted"), do: "bg-red-100 text-red-800" - def subscription_colors(_), do: "" -end diff --git a/lib/plausible_web/views/billing_view.ex b/lib/plausible_web/views/billing_view.ex deleted file mode 100644 index 6a4abdd9ec59..000000000000 --- a/lib/plausible_web/views/billing_view.ex +++ /dev/null @@ -1,40 +0,0 @@ -defmodule PlausibleWeb.BillingView do - use PlausibleWeb, :view - - def admin_email do - Application.get_env(:plausible, :admin_email) - end - - def base_domain do - PlausibleWeb.Endpoint.host() - end - - def plausible_url do - PlausibleWeb.Endpoint.url() - end - - def present_date(date) do - Date.from_iso8601!(date) - |> Timex.format!("{D} {Mshort} {YYYY}") - end - - def present_currency("USD"), do: "$" - def present_currency("EUR"), do: "€" - def present_currency("GBP"), do: "£" - - def reccommended_plan(usage) do - cond do - usage < 9000 -> - "10k / mo" - - usage < 90_000 -> - "100k / mo" - - usage < 900_000 -> - "1m / mo" - - true -> - "custom" - end - end -end diff --git a/lib/plausible_web/views/email_view.ex b/lib/plausible_web/views/email_view.ex deleted file mode 100644 index 5d43bd66779f..000000000000 --- a/lib/plausible_web/views/email_view.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule PlausibleWeb.EmailView do - use PlausibleWeb, :view - - def admin_email do - Application.get_env(:plausible, :admin_email) - end - - def plausible_url do - PlausibleWeb.Endpoint.url() - end - - def base_domain() do - PlausibleWeb.Endpoint.host() - end - - def user_salutation(user) do - if user.name do - String.split(user.name) |> List.first() - else - "" - end - end - - def date_format(date) do - Timex.format!(date, "{D} {Mshort} {YYYY}") - end -end diff --git a/lib/plausible_web/views/layout_view.ex b/lib/plausible_web/views/layout_view.ex index bba14ea7b442..1b4c39e51a28 100644 --- a/lib/plausible_web/views/layout_view.ex +++ b/lib/plausible_web/views/layout_view.ex @@ -13,47 +13,7 @@ defmodule PlausibleWeb.LayoutView do PlausibleWeb.Endpoint.url() end - def home_dest(conn) do - if conn.assigns[:current_user] do - "/sites" - else - "/" - end - end - - def settings_tabs(conn) do - [ - [key: "General", value: "general"], - [key: "People", value: "people"], - [key: "Visibility", value: "visibility"], - [key: "Goals", value: "goals"], - [key: "Search Console", value: "search-console"], - [key: "Email reports", value: "email-reports"], - if !is_selfhost() && conn.assigns[:site].custom_domain do - [key: "Custom domain", value: "custom-domain"] - else - nil - end, - if conn.assigns[:current_user_role] == :owner do - [key: "Danger zone", value: "danger-zone"] - else - nil - end - ] - end - - def trial_notificaton(user) do - case Plausible.Billing.trial_days_left(user) do - days when days > 1 -> - "#{days} trial days left" - - days when days == 1 -> - "Trial ends tomorrow" - - days when days == 0 -> - "Trial ends today" - end - end + def trial_notificaton(_user), do: "Trial ends tomorrow" def on_grace_period?(nil), do: false @@ -87,8 +47,4 @@ defmodule PlausibleWeb.LayoutView do def is_current_tab(conn, tab) do List.last(conn.path_info) == tab end - - defp is_selfhost() do - Application.get_env(:plausible, :is_selfhost) - end end diff --git a/lib/plausible_web/views/page_view.ex b/lib/plausible_web/views/page_view.ex deleted file mode 100644 index f0fd45dea827..000000000000 --- a/lib/plausible_web/views/page_view.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule PlausibleWeb.PageView do - use PlausibleWeb, :view - - def admin_email do - Application.get_env(:plausible, :admin_email) - end - - def base_domain do - PlausibleWeb.Endpoint.host() - end - - def plausible_url do - PlausibleWeb.Endpoint.url() - end -end diff --git a/lib/plausible_web/views/site/membership_view.ex b/lib/plausible_web/views/site/membership_view.ex deleted file mode 100644 index 20af528c347d..000000000000 --- a/lib/plausible_web/views/site/membership_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule PlausibleWeb.Site.MembershipView do - use PlausibleWeb, :view -end diff --git a/lib/plausible_web/views/site_view.ex b/lib/plausible_web/views/site_view.ex deleted file mode 100644 index 91e817dd0c0b..000000000000 --- a/lib/plausible_web/views/site_view.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule PlausibleWeb.SiteView do - use PlausibleWeb, :view - import Phoenix.Pagination.HTML - - def admin_email do - Application.get_env(:plausible, :admin_email) - end - - def plausible_url do - PlausibleWeb.Endpoint.url() - end - - def base_domain() do - PlausibleWeb.Endpoint.host() - end - - def goal_name(%Plausible.Goal{page_path: page_path}) when is_binary(page_path) do - "Visit " <> page_path - end - - def goal_name(%Plausible.Goal{event_name: name}) when is_binary(name) do - name - end - - def shared_link_dest(site, link) do - Plausible.Sites.shared_link_url(site, link) - end - - def gravatar(email, opts) do - hash = - email - |> String.trim() - |> String.downcase() - |> :erlang.md5() - |> Base.encode16(case: :lower) - - img = "https://www.gravatar.com/avatar/#{hash}?s=150&d=identicon" - img_tag(img, opts) - end - - def snippet(site) do - tracker = - if site.custom_domain do - "https://" <> site.custom_domain.domain <> "/js/index.js" - else - "#{plausible_url()}/js/plausible.js" - end - - """ - - """ - end - - def with_indefinite_article(word) do - if String.starts_with?(word, ["a", "e", "i", "o", "u"]) do - "an " <> word - else - "a " <> word - end - end -end diff --git a/lib/plausible_web/views/unsubscribe_view.ex b/lib/plausible_web/views/unsubscribe_view.ex deleted file mode 100644 index ecb1b20eda7e..000000000000 --- a/lib/plausible_web/views/unsubscribe_view.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule PlausibleWeb.UnsubscribeView do - use PlausibleWeb, :view -end diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex deleted file mode 100644 index 8f333a8bbbd9..000000000000 --- a/lib/workers/check_usage.ex +++ /dev/null @@ -1,142 +0,0 @@ -defmodule Plausible.Workers.CheckUsage do - use Plausible.Repo - use Oban.Worker, queue: :check_usage - - defmacro yesterday() do - quote do - fragment("now() - INTERVAL '1 day'") - end - end - - defmacro last_day_of_month(day) do - quote do - fragment( - "(date_trunc('month', ?::date) + interval '1 month' - interval '1 day')::date", - unquote(day) - ) - end - end - - defmacro day_of_month(date) do - quote do - fragment("EXTRACT(day from ?::date)", unquote(date)) - end - end - - defmacro least(left, right) do - quote do - fragment("least(?, ?)", unquote(left), unquote(right)) - end - end - - @impl Oban.Worker - def perform(_job, billing_mod \\ Plausible.Billing, today \\ Timex.today()) do - yesterday = today |> Timex.shift(days: -1) - - active_subscribers = - Repo.all( - from u in Plausible.Auth.User, - join: s in Plausible.Billing.Subscription, - on: s.user_id == u.id, - left_join: ep in Plausible.Billing.EnterprisePlan, - on: ep.user_id == u.id, - where: is_nil(u.grace_period), - where: s.status == "active", - where: not is_nil(s.last_bill_date), - # Accounts for situations like last_bill_date==2021-01-31 AND today==2021-03-01. Since February never reaches the 31st day, the account is checked on 2021-03-01. - where: - least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) == - day_of_month(^yesterday), - preload: [subscription: s, enterprise_plan: ep] - ) - - for subscriber <- active_subscribers do - if subscriber.enterprise_plan do - check_enterprise_subscriber(subscriber, billing_mod) - else - check_regular_subscriber(subscriber, billing_mod) - end - end - - :ok - end - - def check_enterprise_subscriber(subscriber, billing_mod) do - pageview_limit = check_pageview_limit(subscriber, billing_mod) - site_limit = check_site_limit(subscriber) - - case {pageview_limit, site_limit} do - {{:within_limit, _}, {:within_limit, _}} -> - nil - - {{_, {last_cycle, last_cycle_usage}}, {_, {site_usage, site_allowance}}} -> - template = - PlausibleWeb.Email.enterprise_over_limit_email( - subscriber, - last_cycle_usage, - last_cycle, - site_usage, - site_allowance - ) - - Plausible.Mailer.send_email_safe(template) - end - end - - defp check_regular_subscriber(subscriber, billing_mod) do - case check_pageview_limit(subscriber, billing_mod) do - {:over_limit, {last_cycle, last_cycle_usage}} -> - suggested_plan = Plausible.Billing.Plans.suggested_plan(subscriber, last_cycle_usage) - - template = - PlausibleWeb.Email.over_limit_email( - subscriber, - last_cycle_usage, - last_cycle, - suggested_plan - ) - - Plausible.Mailer.send_email_safe(template) - Plausible.Auth.User.start_grace_period(subscriber, last_cycle_usage) |> Repo.update() - - _ -> - nil - end - end - - defp check_pageview_limit(subscriber, billing_mod) do - allowance = - case Plausible.Billing.Plans.allowance(subscriber.subscription) do - allowance when is_number(allowance) -> - allowance * 1.1 - - _allowance -> - Sentry.capture_message("Unable to calculate allowance", - user: subscriber, - subscription: subscriber.subscription - ) - end - - {_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber) - - {last_last_cycle_usage, last_cycle_usage} = - billing_mod.last_two_billing_months_usage(subscriber) - - if last_last_cycle_usage >= allowance && last_cycle_usage >= allowance do - {:over_limit, {last_cycle, last_cycle_usage}} - else - {:within_limit, {last_cycle, last_cycle_usage}} - end - end - - defp check_site_limit(subscriber) do - allowance = subscriber.enterprise_plan.site_limit - total_sites = Plausible.Sites.count_owned_by(subscriber) - - if total_sites >= allowance do - {:over_limit, {total_sites, allowance}} - else - {:within_limit, {total_sites, allowance}} - end - end -end diff --git a/lib/workers/clean_email_verification_codes.ex b/lib/workers/clean_email_verification_codes.ex deleted file mode 100644 index 77061462f652..000000000000 --- a/lib/workers/clean_email_verification_codes.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Plausible.Workers.CleanEmailVerificationCodes do - use Plausible.Repo - use Oban.Worker, queue: :clean_email_verification_codes - - @impl Oban.Worker - def perform(_job) do - Repo.update_all( - from(c in "email_verification_codes", - where: not is_nil(c.user_id), - where: c.issued_at < fragment("now() - INTERVAL '4 hours'") - ), - set: [user_id: nil] - ) - - :ok - end -end diff --git a/lib/workers/clean_invitations.ex b/lib/workers/clean_invitations.ex deleted file mode 100644 index 17cf841d064b..000000000000 --- a/lib/workers/clean_invitations.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Plausible.Workers.CleanInvitations do - use Plausible.Repo - use Oban.Worker, queue: :clean_invitations - - @impl Oban.Worker - def perform(_job) do - Repo.delete_all( - from i in Plausible.Auth.Invitation, - where: i.inserted_at < fragment("now() - INTERVAL '48 hours'") - ) - - :ok - end -end diff --git a/lib/workers/import_google_analytics.ex b/lib/workers/import_google_analytics.ex deleted file mode 100644 index 0e32b8c109a4..000000000000 --- a/lib/workers/import_google_analytics.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Plausible.Workers.ImportGoogleAnalytics do - use Plausible.Repo - - use Oban.Worker, - queue: :google_analytics_imports, - max_attempts: 1, - unique: [fields: [:args], period: 60] - - @impl Oban.Worker - def perform( - %Oban.Job{args: %{"site_id" => site_id, "profile" => profile}}, - google_api \\ Plausible.Google.Api - ) do - site = - Repo.get(Plausible.Site, site_id) - |> Repo.preload([:google_auth, [memberships: :user]]) - - case google_api.import_analytics(site, profile) do - {:ok, _} -> - Plausible.Site.import_success(site) - |> Repo.update!() - - Enum.each(site.memberships, fn membership -> - if membership.role in [:owner, :admin] do - PlausibleWeb.Email.import_success(membership.user, site) - |> Plausible.Mailer.send_email_safe() - end - end) - - :ok - - {:error, error} -> - Plausible.Site.import_failure(site) - |> Repo.update!() - - Enum.each(site.memberships, fn membership -> - if membership.role in [:owner, :admin] do - PlausibleWeb.Email.import_failure(membership.user, site) - |> Plausible.Mailer.send_email_safe() - end - end) - - {:error, error} - end - end -end diff --git a/lib/workers/lock_sites.ex b/lib/workers/lock_sites.ex deleted file mode 100644 index 69356d6cd7a7..000000000000 --- a/lib/workers/lock_sites.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Plausible.Workers.LockSites do - use Plausible.Repo - use Oban.Worker, queue: :lock_sites - - @impl Oban.Worker - def perform(_job) do - subscription_q = from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at]) - users = Repo.all(from u in Plausible.Auth.User, preload: [subscription: ^subscription_q]) - - for user <- users do - Plausible.Billing.SiteLocker.check_sites_for(user) - end - - :ok - end -end diff --git a/lib/workers/notify_annual_renewal.ex b/lib/workers/notify_annual_renewal.ex deleted file mode 100644 index 3d081543f522..000000000000 --- a/lib/workers/notify_annual_renewal.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule Plausible.Workers.NotifyAnnualRenewal do - use Plausible.Repo - use Oban.Worker, queue: :notify_annual_renewal - - @yearly_plans Plausible.Billing.Plans.all_yearly_plan_ids() - - @impl Oban.Worker - @doc """ - Sends a notification at most 7 days and at least 1 day before the renewal of an annual subscription - """ - def perform(_job) do - current_subscriptions = - from( - s in Plausible.Billing.Subscription, - group_by: s.user_id, - select: %{ - user_id: s.user_id, - inserted_at: max(s.inserted_at) - } - ) - - users = - Repo.all( - from u in Plausible.Auth.User, - join: cs in subquery(current_subscriptions), - on: cs.user_id == u.id, - join: s in Plausible.Billing.Subscription, - on: s.inserted_at == cs.inserted_at, - left_join: sent in "sent_renewal_notifications", - on: s.user_id == sent.user_id, - where: s.paddle_plan_id in @yearly_plans, - where: - s.next_bill_date > fragment("now()::date") and - s.next_bill_date <= fragment("now()::date + INTERVAL '7 days'"), - where: is_nil(sent.id) or sent.timestamp < fragment("now() - INTERVAL '1 month'"), - preload: [subscription: s] - ) - - for user <- users do - case user.subscription.status do - "active" -> - template = PlausibleWeb.Email.yearly_renewal_notification(user) - Plausible.Mailer.send_email_safe(template) - - "deleted" -> - template = PlausibleWeb.Email.yearly_expiration_notification(user) - Plausible.Mailer.send_email_safe(template) - - _ -> - Sentry.capture_message("Invalid subscription for renewal", user: user) - end - - Repo.insert_all("sent_renewal_notifications", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - end - - :ok - end -end diff --git a/lib/workers/schedule_email_reports.ex b/lib/workers/schedule_email_reports.ex deleted file mode 100644 index 3a9ebe483601..000000000000 --- a/lib/workers/schedule_email_reports.ex +++ /dev/null @@ -1,97 +0,0 @@ -defmodule Plausible.Workers.ScheduleEmailReports do - use Plausible.Repo - use Oban.Worker, queue: :schedule_email_reports - alias Plausible.Workers.SendEmailReport - require Logger - - @impl Oban.Worker - @doc """ - Email reports should be sent on Monday at 9am according to the timezone - of a site. This job runs every day at midnight to ensure that all sites - have a scheduled job for email reports. - """ - def perform(_job) do - schedule_weekly_emails() - schedule_monthly_emails() - end - - defp schedule_weekly_emails() do - weekly_jobs = - from( - j in Oban.Job, - where: - j.worker == "Plausible.Workers.SendEmailReport" and - fragment("(? ->> 'interval')", j.args) == "weekly" - ) - - sites = - Repo.all( - from s in Plausible.Site, - join: wr in Plausible.Site.WeeklyReport, - on: wr.site_id == s.id, - left_join: job in subquery(weekly_jobs), - on: - fragment("(? -> 'site_id')::int", job.args) == s.id and - job.state not in ["completed", "discarded"], - where: is_nil(job), - where: not s.locked, - preload: [weekly_report: wr] - ) - - for site <- sites do - SendEmailReport.new(%{site_id: site.id, interval: "weekly"}, - scheduled_at: monday_9am(site.timezone) - ) - |> Oban.insert!() - end - - :ok - end - - def monday_9am(timezone) do - Timex.now(timezone) - |> Timex.shift(weeks: 1) - |> Timex.beginning_of_week() - |> Timex.shift(hours: 9) - end - - defp schedule_monthly_emails() do - monthly_jobs = - from( - j in Oban.Job, - where: - j.worker == "Plausible.Workers.SendEmailReport" and - fragment("(? ->> 'interval')", j.args) == "monthly" - ) - - sites = - Repo.all( - from s in Plausible.Site, - join: mr in Plausible.Site.MonthlyReport, - on: mr.site_id == s.id, - left_join: job in subquery(monthly_jobs), - on: - fragment("(? -> 'site_id')::int", job.args) == s.id and - job.state not in ["completed", "discarded"], - where: is_nil(job), - where: not s.locked, - preload: [monthly_report: mr] - ) - - for site <- sites do - SendEmailReport.new(%{site_id: site.id, interval: "monthly"}, - scheduled_at: first_of_month_9am(site.timezone) - ) - |> Oban.insert!() - end - - :ok - end - - def first_of_month_9am(timezone) do - Timex.now(timezone) - |> Timex.shift(months: 1) - |> Timex.beginning_of_month() - |> Timex.shift(hours: 9) - end -end diff --git a/lib/workers/send_check_stats_emails.ex b/lib/workers/send_check_stats_emails.ex deleted file mode 100644 index ee158580d80c..000000000000 --- a/lib/workers/send_check_stats_emails.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Plausible.Workers.SendCheckStatsEmails do - use Plausible.Repo - use Oban.Worker, queue: :check_stats_emails - - @impl Oban.Worker - def perform(_job) do - q = - from(u in Plausible.Auth.User, - left_join: ce in "check_stats_emails", - on: ce.user_id == u.id, - where: is_nil(ce.id), - where: - u.inserted_at > fragment("(now() at time zone 'utc') - '14 days'::interval") and - u.inserted_at < fragment("(now() at time zone 'utc') - '7 days'::interval") and - u.last_seen < fragment("(now() at time zone 'utc') - '7 days'::interval"), - preload: [sites: :weekly_report] - ) - - for user <- Repo.all(q) do - enabled_report = Enum.any?(user.sites, fn site -> site.weekly_report end) - - if Plausible.Auth.has_active_sites?(user) && !enabled_report do - send_check_stats_email(user) - end - end - - :ok - end - - defp send_check_stats_email(user) do - PlausibleWeb.Email.check_stats_email(user) - |> Plausible.Mailer.send_email() - - Repo.insert_all("check_stats_emails", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - end -end diff --git a/lib/workers/send_email_report.ex b/lib/workers/send_email_report.ex deleted file mode 100644 index 5a7eb477b9bc..000000000000 --- a/lib/workers/send_email_report.ex +++ /dev/null @@ -1,84 +0,0 @@ -defmodule Plausible.Workers.SendEmailReport do - use Plausible.Repo - use Oban.Worker, queue: :send_email_reports, max_attempts: 1 - alias Plausible.Stats.Query - alias Plausible.Stats - - @impl Oban.Worker - def perform(%Oban.Job{args: %{"interval" => "weekly", "site_id" => site_id}}) do - site = Repo.get(Plausible.Site, site_id) |> Repo.preload(:weekly_report) - today = Timex.now(site.timezone) |> DateTime.to_date() - date = Timex.shift(today, weeks: -1) |> Timex.end_of_week() |> Date.to_iso8601() - query = Query.from(site, %{"period" => "7d", "date" => date}) - - for email <- site.weekly_report.recipients do - unsubscribe_link = - PlausibleWeb.Endpoint.url() <> - "/sites/#{URI.encode_www_form(site.domain)}/weekly-report/unsubscribe?email=#{email}" - - send_report(email, site, "Weekly", unsubscribe_link, query) - end - - :ok - end - - @impl Oban.Worker - def perform(%Oban.Job{args: %{"interval" => "monthly", "site_id" => site_id}}) do - site = Repo.get(Plausible.Site, site_id) |> Repo.preload(:monthly_report) - - last_month = - Timex.now(site.timezone) - |> Timex.shift(months: -1) - |> Timex.beginning_of_month() - - query = - Query.from(site, %{ - "period" => "month", - "date" => Timex.format!(last_month, "{ISOdate}") - }) - - for email <- site.monthly_report.recipients do - unsubscribe_link = - PlausibleWeb.Endpoint.url() <> - "/sites/#{URI.encode_www_form(site.domain)}/monthly-report/unsubscribe?email=#{email}" - - send_report(email, site, Timex.format!(last_month, "{Mfull}"), unsubscribe_link, query) - end - - :ok - end - - defp send_report(email, site, name, unsubscribe_link, query) do - prev_query = Query.shift_back(query, site) - curr_period = Stats.aggregate(site, query, [:pageviews, :visitors, :bounce_rate]) - prev_period = Stats.aggregate(site, prev_query, [:pageviews, :visitors, :bounce_rate]) - - change_pageviews = Stats.Compare.calculate_change(:pageviews, prev_period, curr_period) - change_visitors = Stats.Compare.calculate_change(:visitors, prev_period, curr_period) - change_bounce_rate = Stats.Compare.calculate_change(:bounce_rate, prev_period, curr_period) - - source_query = Query.put_filter(query, "visit:source", {:is_not, "Direct / None"}) - sources = Stats.breakdown(site, source_query, "visit:source", [:visitors], {5, 1}) - pages = Stats.breakdown(site, query, "event:page", [:visitors], {5, 1}) - user = Plausible.Auth.find_user_by(email: email) - login_link = user && Plausible.Sites.is_member?(user.id, site) - - template = - PlausibleWeb.Email.weekly_report(email, site, - unique_visitors: curr_period[:visitors][:value], - change_visitors: change_visitors, - pageviews: curr_period[:pageviews][:value], - change_pageviews: change_pageviews, - bounce_rate: curr_period[:bounce_rate][:value], - change_bounce_rate: change_bounce_rate, - sources: sources, - unsubscribe_link: unsubscribe_link, - login_link: login_link, - pages: pages, - query: query, - name: name - ) - - Plausible.Mailer.send_email_safe(template) - end -end diff --git a/lib/workers/send_site_setup_emails.ex b/lib/workers/send_site_setup_emails.ex deleted file mode 100644 index fb365ee206a1..000000000000 --- a/lib/workers/send_site_setup_emails.ex +++ /dev/null @@ -1,112 +0,0 @@ -defmodule Plausible.Workers.SendSiteSetupEmails do - use Plausible.Repo - use Oban.Worker, queue: :site_setup_emails - require Logger - - @impl Oban.Worker - def perform(_job) do - send_create_site_emails() - send_setup_help_emails() - send_setup_success_emails() - - :ok - end - - defp send_create_site_emails() do - q = - from(s in Plausible.Auth.User, - left_join: se in "create_site_emails", - on: se.user_id == s.id, - where: is_nil(se.id), - where: - s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval") and - s.inserted_at < fragment("(now() at time zone 'utc') - '48 hours'::interval"), - preload: :sites - ) - - for user <- Repo.all(q) do - if Enum.empty?(user.sites) do - send_create_site_email(user) - end - end - end - - defp send_setup_help_emails() do - q = - from(s in Plausible.Site, - left_join: se in "setup_help_emails", - on: se.site_id == s.id, - where: is_nil(se.id), - where: s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval") - ) - - for site <- Repo.all(q) do - owner = - Plausible.Sites.owner_for(site) - |> Repo.preload(:subscription) - - setup_completed = Plausible.Sites.has_stats?(site) - hours_passed = Timex.diff(Timex.now(), site.inserted_at, :hours) - - if !setup_completed && hours_passed > 47 do - send_setup_help_email(owner, site) - end - end - end - - defp send_setup_success_emails() do - q = - from(s in Plausible.Site, - left_join: se in "setup_success_emails", - on: se.site_id == s.id, - where: is_nil(se.id), - where: s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval") - ) - - for site <- Repo.all(q) do - owner = - Plausible.Sites.owner_for(site) - |> Repo.preload(:subscription) - - if Plausible.Sites.has_stats?(site) do - send_setup_success_email(owner, site) - end - end - end - - defp send_create_site_email(user) do - PlausibleWeb.Email.create_site_email(user) - |> Plausible.Mailer.send_email_safe() - - Repo.insert_all("create_site_emails", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - end - - defp send_setup_success_email(user, site) do - PlausibleWeb.Email.site_setup_success(user, site) - |> Plausible.Mailer.send_email_safe() - - Repo.insert_all("setup_success_emails", [ - %{ - site_id: site.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - end - - defp send_setup_help_email(user, site) do - PlausibleWeb.Email.site_setup_help(user, site) - |> Plausible.Mailer.send_email_safe() - - Repo.insert_all("setup_help_emails", [ - %{ - site_id: site.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - end -end diff --git a/lib/workers/send_trial_notifications.ex b/lib/workers/send_trial_notifications.ex deleted file mode 100644 index 36361eba879c..000000000000 --- a/lib/workers/send_trial_notifications.ex +++ /dev/null @@ -1,74 +0,0 @@ -defmodule Plausible.Workers.SendTrialNotifications do - use Plausible.Repo - - use Oban.Worker, - queue: :trial_notification_emails, - max_attempts: 1 - - require Logger - - @impl Oban.Worker - def perform(_job) do - users = - Repo.all( - from u in Plausible.Auth.User, - left_join: s in Plausible.Billing.Subscription, - on: s.user_id == u.id, - where: is_nil(s.id), - order_by: u.inserted_at - ) - - for user <- users do - case Timex.diff(user.trial_expiry_date, Timex.today(), :days) do - 7 -> - if Plausible.Auth.has_active_sites?(user, [:owner]) do - send_one_week_reminder(user) - end - - 1 -> - if Plausible.Auth.has_active_sites?(user, [:owner]) do - send_tomorrow_reminder(user) - end - - 0 -> - if Plausible.Auth.has_active_sites?(user, [:owner]) do - send_today_reminder(user) - end - - -1 -> - if Plausible.Auth.has_active_sites?(user, [:owner]) do - send_over_reminder(user) - end - - _ -> - nil - end - end - - :ok - end - - defp send_one_week_reminder(user) do - PlausibleWeb.Email.trial_one_week_reminder(user) - |> Plausible.Mailer.send_email_safe() - end - - defp send_tomorrow_reminder(user) do - usage = Plausible.Billing.usage_breakdown(user) - - PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", usage) - |> Plausible.Mailer.send_email_safe() - end - - defp send_today_reminder(user) do - usage = Plausible.Billing.usage_breakdown(user) - - PlausibleWeb.Email.trial_upgrade_email(user, "today", usage) - |> Plausible.Mailer.send_email_safe() - end - - defp send_over_reminder(user) do - PlausibleWeb.Email.trial_over_email(user) - |> Plausible.Mailer.send_email_safe() - end -end diff --git a/lib/workers/spike_notifier.ex b/lib/workers/spike_notifier.ex deleted file mode 100644 index 52cde6cf1a7f..000000000000 --- a/lib/workers/spike_notifier.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule Plausible.Workers.SpikeNotifier do - use Plausible.Repo - alias Plausible.Stats.Query - alias Plausible.Site.SpikeNotification - use Oban.Worker, queue: :spike_notifications - @at_most_every "12 hours" - - @impl Oban.Worker - def perform(_job, clickhouse \\ Plausible.Stats.Clickhouse) do - notifications = - Repo.all( - from sn in SpikeNotification, - where: is_nil(sn.last_sent), - or_where: sn.last_sent < fragment("now() - INTERVAL ?", @at_most_every), - join: s in Plausible.Site, - on: sn.site_id == s.id, - where: not s.locked, - preload: [site: s] - ) - - for notification <- notifications do - query = Query.from(notification.site, %{"period" => "realtime"}) - current_visitors = clickhouse.current_visitors(notification.site, query) - - if current_visitors >= notification.threshold do - sources = clickhouse.top_sources(notification.site, query, 3, 1, true) - notify(notification, current_visitors, sources) - end - end - - :ok - end - - def notify(notification, current_visitors, sources) do - for recipient <- notification.recipients do - send_notification(recipient, notification.site, current_visitors, sources) - end - - notification - |> SpikeNotification.was_sent() - |> Repo.update() - end - - defp send_notification(recipient, site, current_visitors, sources) do - site = Repo.preload(site, :members) - - dashboard_link = - if Enum.member?(site.members, recipient) do - PlausibleWeb.Endpoint.url() <> "/" <> URI.encode_www_form(site.domain) - end - - template = - PlausibleWeb.Email.spike_notification( - recipient, - site, - current_visitors, - sources, - dashboard_link - ) - - Plausible.Mailer.send_email_safe(template) - end -end diff --git a/mix.exs b/mix.exs index 041b03c07efb..4fb37d39a559 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,6 @@ defmodule Plausible.MixProject do defp deps do [ {:bcrypt_elixir, "~> 2.0"}, - {:combination, "~> 0.0.3"}, {:cors_plug, "~> 2.0"}, {:ecto_sql, "< 3.7.2"}, {:elixir_uuid, "~> 1.2", only: :test}, @@ -69,20 +68,13 @@ defmodule Plausible.MixProject do {:tzdata, "~> 1.1.2"}, {:gettext, "~> 0.20.0", override: true}, {:ua_inspector, "~> 2.2"}, - {:bamboo, "~> 2.2"}, {:hackney, "~> 1.18"}, - {:bamboo_phoenix, "~> 1.0.0"}, - {:bamboo_postmark, git: "https://github.com/pablo-co/bamboo_postmark.git", tag: "master"}, - {:bamboo_smtp, "~> 4.1"}, {:sentry, "~> 8.0"}, {:httpoison, "~> 1.4"}, {:ex_machina, "~> 2.3", only: :test}, {:excoveralls, "~> 0.10", only: :test}, {:double, "~> 0.8.0", only: :test}, - {:php_serializer, "~> 2.0"}, {:csv, "~> 2.3"}, - {:oauther, "~> 1.3"}, - {:nanoid, "~> 2.0.2"}, {:siphash, "~> 3.2"}, {:oban, "~> 2.12.0"}, {:geolix, "~> 1.0"}, @@ -93,10 +85,8 @@ defmodule Plausible.MixProject do {:cachex, "~> 3.4"}, {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, - {:kaffy, "~> 0.9.0"}, {:envy, "~> 1.1.1"}, {:phoenix_pagination, "~> 0.7.0"}, - {:hammer, "~> 6.0"}, {:public_suffix, git: "https://github.com/axelson/publicsuffix-elixir"}, {:telemetry, "~> 1.0", override: true}, {:opentelemetry_exporter, "~> 1.6"}, diff --git a/priv/paddle.pem b/priv/paddle.pem deleted file mode 100644 index 110be6cc1537..000000000000 --- a/priv/paddle.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvn1//X4/nkeKtF8aFPi5 -1ngjAvb68oGnzBmft0dOUxl6t4wSEl93efWI9xC24qPDGxippROPqRlelUne96zO -ZdAj3JZT8K77m0lS/EbtHBF6GSvETSr/UceOcF//pbG8Q7EQDN3+/VfiTlHpzG/s -V9gaOGNYJ28dlaS7gNQZYO+eeoWA92RLLk7X73F/lPQtJ70rxop+kLyAA4fDr08k -TR4gWBAyAkBsyC8W0v9b0zHBqxiiGdS2wQRWzMGrPHti//LGiR92PVxvP3QQd8iW -RELlcXOKLOhPX3LzU9DAWNFlRmtdQ4J3F7t642VIFWaAo2zOdeb4J8cmcoViYNp3 -7tWUQPyVQnm2V8gjfIDNYIG1pMjleikCcsL823rDNuV1LWzsxnZHihzX2KvUhdVY -Js2N89w1YbeOEcvO8d6fFYEkSckOkkrhK9MLGla6OY+nHds0/Clykq0BSD2kbSpr -mZ3qWiBZtbR7PEiEGr6pNbupFWVsV7OyOXR4MmU89Ml7bqnlGOIUy+o9cG7E3FfJ -klALjZIpf7FCZyQH7b6KU6IZlZK+hdXEScx2gL3dyQAzNXNV+hFAyvCKhqns7lQO -u6yrbmj0tDHUN6ing4K3UQ01zmfSv5s83usrqRud27ZSnInYXKeKzcj1mYgDELOS -GNGaLnkTNF9TKBP8qYz++/sCAwEAAQ== ------END PUBLIC KEY----- diff --git a/priv/plans_v1.json b/priv/plans_v1.json deleted file mode 100644 index 9ab90fb591ad..000000000000 --- a/priv/plans_v1.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "limit":10000, - "monthly_cost":"$6", - "monthly_product_id":"558018", - "yearly_cost":"$48", - "yearly_product_id":"572810" - }, - { - "limit":100000, - "monthly_cost":"$12", - "monthly_product_id":"558745", - "yearly_cost":"$96", - "yearly_product_id":"590752" - }, - { - "limit":200000, - "monthly_cost":"$18", - "monthly_product_id":"597485", - "yearly_cost":"$144", - "yearly_product_id":"597486" - }, - { - "limit":500000, - "monthly_cost":"$27", - "monthly_product_id":"597487", - "yearly_cost":"$216", - "yearly_product_id":"597488" - }, - { - "limit":1000000, - "monthly_cost":"$48", - "monthly_product_id":"597642", - "yearly_cost":"$384", - "yearly_product_id":"597643" - }, - { - "limit":2000000, - "monthly_cost":"$69", - "monthly_product_id":"597309", - "yearly_cost":"$552", - "yearly_product_id":"597310" - }, - { - "limit":5000000, - "monthly_cost":"$99", - "monthly_product_id":"597311", - "yearly_cost":"$792", - "yearly_product_id":"597312" - }, - { - "limit":10000000, - "monthly_cost":"$150", - "monthly_product_id":"642352", - "yearly_cost":"$1200", - "yearly_product_id":"642354" - }, - { - "limit":20000000, - "monthly_cost":"$225", - "monthly_product_id":"642355", - "yearly_cost":"$1800", - "yearly_product_id":"642356" - }, - { - "limit":50000000, - "monthly_cost":"$330", - "monthly_product_id":"650652", - "yearly_cost":"$2640", - "yearly_product_id":"650653" - } -] diff --git a/priv/plans_v2.json b/priv/plans_v2.json deleted file mode 100644 index 434ac751dcb8..000000000000 --- a/priv/plans_v2.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "limit":10000, - "monthly_cost":"$6", - "monthly_product_id":"654177", - "yearly_cost":"$60", - "yearly_product_id":"653232" - }, - { - "limit":100000, - "monthly_cost":"$12", - "monthly_product_id":"654178", - "yearly_cost":"$120", - "yearly_product_id":"653234" - }, - { - "limit":200000, - "monthly_cost":"$20", - "monthly_product_id":"653237", - "yearly_cost":"$200", - "yearly_product_id":"653236" - }, - { - "limit":500000, - "monthly_cost":"$30", - "monthly_product_id":"653238", - "yearly_cost":"$300", - "yearly_product_id":"653239" - }, - { - "limit":1000000, - "monthly_cost":"$50", - "monthly_product_id":"653240", - "yearly_cost":"$500", - "yearly_product_id":"653242" - }, - { - "limit":2000000, - "monthly_cost":"$70", - "monthly_product_id":"653253", - "yearly_cost":"$700", - "yearly_product_id":"653254" - }, - { - "limit":5000000, - "monthly_cost":"$100", - "monthly_product_id":"653255", - "yearly_cost":"$1000", - "yearly_product_id":"653256" - }, - { - "limit":10000000, - "monthly_cost":"$150", - "monthly_product_id":"654181", - "yearly_cost":"$1500", - "yearly_product_id":"653257" - }, - { - "limit":20000000, - "monthly_cost":"$225", - "monthly_product_id":"654182", - "yearly_cost":"$2250", - "yearly_product_id":"653258" - }, - { - "limit":50000000, - "monthly_cost":"$330", - "monthly_product_id":"654183", - "yearly_cost":"$3300", - "yearly_product_id":"653259" - } -] diff --git a/priv/plans_v3.json b/priv/plans_v3.json deleted file mode 100644 index dc3668e7be73..000000000000 --- a/priv/plans_v3.json +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "limit":10000, - "monthly_cost":"$9", - "monthly_product_id":"749342", - "yearly_cost":"$90", - "yearly_product_id":"749343" - }, - { - "limit":100000, - "monthly_cost":"$19", - "monthly_product_id":"749344", - "yearly_cost":"$190", - "yearly_product_id":"749345" - }, - { - "limit":200000, - "monthly_cost":"$29", - "monthly_product_id":"749346", - "yearly_cost":"$290", - "yearly_product_id":"749347" - }, - { - "limit":500000, - "monthly_cost":"$49", - "monthly_product_id":"749348", - "yearly_cost":"$490", - "yearly_product_id":"749349" - }, - { - "limit":1000000, - "monthly_cost":"$69", - "monthly_product_id":"749350", - "yearly_cost":"$690", - "yearly_product_id":"749352" - }, - - { - "limit":2000000, - "monthly_cost":"$89", - "monthly_product_id":"749353", - "yearly_cost":"$890", - "yearly_product_id":"749355" - }, - { - "limit":5000000, - "monthly_cost":"$129", - "monthly_product_id":"749356", - "yearly_cost":"$1290", - "yearly_product_id":"749357" - }, - { - "limit":10000000, - "monthly_cost":"$169", - "monthly_product_id":"749358", - "yearly_cost":"$1690", - "yearly_product_id":"749359" - } -] diff --git a/test/plausible/auth/auth_test.exs b/test/plausible/auth/auth_test.exs deleted file mode 100644 index d76417bb595b..000000000000 --- a/test/plausible/auth/auth_test.exs +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Plausible.AuthTest do - use Plausible.DataCase - alias Plausible.Auth - - describe "user_completed_setup?" do - test "is false if user does not have any sites" do - user = insert(:user) - - refute Auth.has_active_sites?(user) - end - - test "is false if user does not have any events" do - user = insert(:user) - insert(:site, members: [user]) - - refute Auth.has_active_sites?(user) - end - - test "is true if user does have events" do - user = insert(:user) - insert(:site, members: [user], domain: "test-site.com") - - assert Auth.has_active_sites?(user) - end - - test "can specify which roles we're looking for" do - user = insert(:user) - - insert(:site, - domain: "test-site.com", - memberships: [ - build(:site_membership, user: user, role: :admin) - ] - ) - - refute Auth.has_active_sites?(user, [:owner]) - end - end -end diff --git a/test/plausible/billing/billing_test.exs b/test/plausible/billing/billing_test.exs deleted file mode 100644 index 8e5499f56b18..000000000000 --- a/test/plausible/billing/billing_test.exs +++ /dev/null @@ -1,551 +0,0 @@ -defmodule Plausible.BillingTest do - use Plausible.DataCase - use Bamboo.Test, shared: true - alias Plausible.Billing - import Plausible.TestUtils - - describe "usage" do - test "is 0 with no events" do - user = insert(:user) - - assert Billing.usage(user) == 0 - end - - test "counts the total number of events from all sites the user owns" do - user = insert(:user) - site1 = insert(:site, members: [user]) - site2 = insert(:site, members: [user]) - - populate_stats(site1, [ - build(:pageview), - build(:pageview) - ]) - - populate_stats(site2, [ - build(:pageview), - build(:event, name: "custom events") - ]) - - assert Billing.usage(user) == 4 - end - - test "only counts usage from sites where the user is the owner" do - user = insert(:user) - - insert(:site, - domain: "site-with-no-views.com", - memberships: [ - build(:site_membership, user: user, role: :owner) - ] - ) - - insert(:site, - domain: "test-site.com", - memberships: [ - build(:site_membership, user: user, role: :admin) - ] - ) - - assert Billing.usage(user) == 0 - end - end - - describe "last_two_billing_cycles" do - test "billing on the 1st" do - last_bill_date = ~D[2021-01-01] - today = ~D[2021-01-02] - - user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date)) - - expected_cycles = { - Date.range(~D[2020-11-01], ~D[2020-11-30]), - Date.range(~D[2020-12-01], ~D[2020-12-31]) - } - - assert Billing.last_two_billing_cycles(user, today) == expected_cycles - end - - test "in case of yearly billing, cycles are normalized as if they were paying monthly" do - last_bill_date = ~D[2020-09-01] - today = ~D[2021-02-02] - - user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date)) - - expected_cycles = { - Date.range(~D[2020-12-01], ~D[2020-12-31]), - Date.range(~D[2021-01-01], ~D[2021-01-31]) - } - - assert Billing.last_two_billing_cycles(user, today) == expected_cycles - end - end - - describe "last_two_billing_months_usage" do - test "counts events from last two billing cycles" do - last_bill_date = ~D[2021-01-01] - today = ~D[2021-01-02] - user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date)) - site = insert(:site, members: [user]) - - create_pageviews([ - %{domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]}, - %{domain: site.domain, timestamp: ~N[2020-12-31 00:00:00]}, - %{domain: site.domain, timestamp: ~N[2020-11-01 00:00:00]}, - %{domain: site.domain, timestamp: ~N[2020-10-31 00:00:00]} - ]) - - assert Billing.last_two_billing_months_usage(user, today) == {1, 1} - end - - test "only considers sites that the user owns" do - last_bill_date = ~D[2021-01-01] - today = ~D[2021-01-02] - - user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date)) - - owner_site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner) - ] - ) - - admin_site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :admin) - ] - ) - - create_pageviews([ - %{domain: owner_site.domain, timestamp: ~N[2020-12-31 00:00:00]}, - %{domain: admin_site.domain, timestamp: ~N[2020-12-31 00:00:00]}, - %{domain: owner_site.domain, timestamp: ~N[2020-11-01 00:00:00]}, - %{domain: admin_site.domain, timestamp: ~N[2020-11-01 00:00:00]} - ]) - - assert Billing.last_two_billing_months_usage(user, today) == {1, 1} - end - - test "gets event count from last month and this one" do - user = - insert(:user, - subscription: - build(:subscription, last_bill_date: Timex.today() |> Timex.shift(days: -1)) - ) - - assert Billing.last_two_billing_months_usage(user) == {0, 0} - end - end - - describe "trial_days_left" do - test "is 30 days for new signup" do - user = insert(:user) - - assert Billing.trial_days_left(user) == 30 - end - - test "is based on trial_expiry_date" do - user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 1)) - - assert Billing.trial_days_left(user) == 1 - end - end - - describe "on_trial?" do - test "is true with >= 0 trial days left" do - user = insert(:user) |> Repo.preload(:subscription) - - assert Billing.on_trial?(user) - end - - test "is false with < 0 trial days left" do - user = - insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: -1)) - |> Repo.preload(:subscription) - - refute Billing.on_trial?(user) - end - - test "is false if user has subscription" do - user = insert(:user, subscription: build(:subscription)) - - refute Billing.on_trial?(user) - end - end - - describe "needs_to_upgrade?" do - test "is false for a trial user" do - user = insert(:user) - user = Repo.preload(user, :subscription) - - refute Billing.needs_to_upgrade?(user) - end - - test "is true for a user with an expired trial" do - user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1)) - user = Repo.preload(user, :subscription) - - assert Billing.needs_to_upgrade?(user) - end - - test "is false for a user with an expired trial but an active subscription" do - user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1)) - insert(:subscription, user: user) - user = Repo.preload(user, :subscription) - - refute Billing.needs_to_upgrade?(user) - end - - test "is false for a user with a cancelled subscription IF the billing cycle isn't completed yet" do - user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1)) - insert(:subscription, user: user, status: "deleted", next_bill_date: Timex.today()) - user = Repo.preload(user, :subscription) - - refute Billing.needs_to_upgrade?(user) - end - - test "is true for a user with a cancelled subscription IF the billing cycle is complete" do - user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1)) - - insert(:subscription, - user: user, - status: "deleted", - next_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - user = Repo.preload(user, :subscription) - - assert Billing.needs_to_upgrade?(user) - end - - test "is false for a deleted subscription if not next_bill_date specified" do - user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1)) - insert(:subscription, user: user, status: "deleted", next_bill_date: nil) - user = Repo.preload(user, :subscription) - - assert Billing.needs_to_upgrade?(user) - end - end - - @subscription_id "subscription-123" - @plan_id_10k "654177" - @plan_id_100k "654178" - - describe "subscription_created" do - test "creates a subscription" do - user = insert(:user) - - Billing.subscription_created(%{ - "alert_name" => "subscription_created", - "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id_10k, - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "status" => "active", - "next_bill_date" => "2019-06-01", - "unit_price" => "6.00", - "currency" => "EUR" - }) - - subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.paddle_subscription_id == @subscription_id - assert subscription.next_bill_date == ~D[2019-06-01] - assert subscription.next_bill_amount == "6.00" - assert subscription.currency_code == "EUR" - end - - test "create with email address" do - user = insert(:user) - - Billing.subscription_created(%{ - "passthrough" => "", - "email" => user.email, - "alert_name" => "subscription_created", - "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id_10k, - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "status" => "active", - "next_bill_date" => "2019-06-01", - "unit_price" => "6.00", - "currency" => "EUR" - }) - - subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.paddle_subscription_id == @subscription_id - assert subscription.next_bill_date == ~D[2019-06-01] - assert subscription.next_bill_amount == "6.00" - end - - test "unlocks sites if user has any locked sites" do - user = insert(:user) - site = insert(:site, locked: true, members: [user]) - - Billing.subscription_created(%{ - "alert_name" => "subscription_created", - "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id_10k, - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "status" => "active", - "next_bill_date" => "2019-06-01", - "unit_price" => "6.00", - "currency" => "EUR" - }) - - refute Repo.reload!(site).locked - end - - test "if user upgraded to an enterprise plan, their API key limits are automatically adjusted" do - user = insert(:user) - - plan = - insert(:enterprise_plan, - user: user, - paddle_plan_id: @plan_id_10k, - hourly_api_request_limit: 10_000 - ) - - api_key = insert(:api_key, user: user, hourly_request_limit: 1) - - Billing.subscription_created(%{ - "alert_name" => "subscription_created", - "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id_10k, - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "status" => "active", - "next_bill_date" => "2019-06-01", - "unit_price" => "6.00", - "currency" => "EUR" - }) - - assert Repo.reload!(api_key).hourly_request_limit == plan.hourly_api_request_limit - end - end - - describe "subscription_updated" do - test "updates an existing subscription" do - user = insert(:user) - subscription = insert(:subscription, user: user) - - Billing.subscription_updated(%{ - "alert_name" => "subscription_updated", - "subscription_id" => subscription.paddle_subscription_id, - "subscription_plan_id" => "new-plan-id", - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "status" => "active", - "next_bill_date" => "2019-06-01", - "new_unit_price" => "12.00", - "currency" => "EUR" - }) - - subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.paddle_plan_id == "new-plan-id" - assert subscription.next_bill_amount == "12.00" - end - - test "unlocks sites if subscription is changed from past_due to active" do - user = insert(:user) - subscription = insert(:subscription, user: user, status: "past_due") - site = insert(:site, locked: true, members: [user]) - - Billing.subscription_updated(%{ - "alert_name" => "subscription_updated", - "subscription_id" => subscription.paddle_subscription_id, - "subscription_plan_id" => "new-plan-id", - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "old_status" => "past_due", - "status" => "active", - "next_bill_date" => "2019-06-01", - "new_unit_price" => "12.00", - "currency" => "EUR" - }) - - refute Repo.reload!(site).locked - end - - test "if user upgraded to an enterprise plan, their API key limits are automatically adjusted" do - user = insert(:user) - subscription = insert(:subscription, user: user) - - plan = - insert(:enterprise_plan, - user: user, - paddle_plan_id: "new-plan-id", - hourly_api_request_limit: 10_000 - ) - - api_key = insert(:api_key, user: user, hourly_request_limit: 1) - - Billing.subscription_updated(%{ - "alert_name" => "subscription_updated", - "subscription_id" => subscription.paddle_subscription_id, - "subscription_plan_id" => "new-plan-id", - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "old_status" => "past_due", - "status" => "active", - "next_bill_date" => "2019-06-01", - "new_unit_price" => "12.00", - "currency" => "EUR" - }) - - assert Repo.reload!(api_key).hourly_request_limit == plan.hourly_api_request_limit - end - - test "if user's grace period has ended, upgrading to the proper plan will unlock sites and remove grace period" do - user = - insert(:user, - grace_period: %Plausible.Auth.GracePeriod{ - end_date: Timex.shift(Timex.today(), days: -1), - allowance_required: 11_000 - } - ) - - subscription = insert(:subscription, user: user) - site = insert(:site, locked: true, members: [user]) - - Billing.subscription_updated(%{ - "alert_name" => "subscription_updated", - "subscription_id" => subscription.paddle_subscription_id, - "subscription_plan_id" => @plan_id_100k, - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "old_status" => "past_due", - "status" => "active", - "next_bill_date" => "2019-06-01", - "new_unit_price" => "12.00", - "currency" => "EUR" - }) - - assert Repo.reload!(site).locked == false - assert Repo.reload!(user).grace_period == nil - end - - test "does not remove grace period if upgraded plan allowance is too low" do - user = - insert(:user, - grace_period: %Plausible.Auth.GracePeriod{ - end_date: Timex.shift(Timex.today(), days: -1), - allowance_required: 11_000 - } - ) - - subscription = insert(:subscription, user: user) - site = insert(:site, locked: true, members: [user]) - - Billing.subscription_updated(%{ - "alert_name" => "subscription_updated", - "subscription_id" => subscription.paddle_subscription_id, - "subscription_plan_id" => @plan_id_10k, - "update_url" => "update_url.com", - "cancel_url" => "cancel_url.com", - "passthrough" => user.id, - "old_status" => "past_due", - "status" => "active", - "next_bill_date" => "2019-06-01", - "new_unit_price" => "12.00", - "currency" => "EUR" - }) - - assert Repo.reload!(site).locked == true - assert Repo.reload!(user).grace_period.allowance_required == 11_000 - end - end - - describe "subscription_cancelled" do - test "sets the status to deleted" do - user = insert(:user) - subscription = insert(:subscription, status: "active", user: user) - - Billing.subscription_cancelled(%{ - "alert_name" => "subscription_cancelled", - "subscription_id" => subscription.paddle_subscription_id, - "status" => "deleted" - }) - - subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.status == "deleted" - end - - test "ignores if the subscription cannot be found" do - res = - Billing.subscription_cancelled(%{ - "alert_name" => "subscription_cancelled", - "subscription_id" => "some_nonexistent_id", - "status" => "deleted" - }) - - assert res == {:ok, nil} - end - - test "sends an email to confirm cancellation" do - user = insert(:user) - subscription = insert(:subscription, status: "active", user: user) - - Billing.subscription_cancelled(%{ - "alert_name" => "subscription_cancelled", - "subscription_id" => subscription.paddle_subscription_id, - "status" => "deleted" - }) - - assert_email_delivered_with( - subject: "Your Plausible Analytics subscription has been canceled" - ) - end - end - - describe "subscription_payment_succeeded" do - test "sets the next bill amount and date, last bill date" do - user = insert(:user) - subscription = insert(:subscription, user: user) - - Billing.subscription_payment_succeeded(%{ - "alert_name" => "subscription_payment_succeeded", - "subscription_id" => subscription.paddle_subscription_id - }) - - subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.next_bill_date == ~D[2019-07-10] - assert subscription.next_bill_amount == "6.00" - assert subscription.last_bill_date == ~D[2019-06-10] - end - - test "ignores if the subscription cannot be found" do - res = - Billing.subscription_payment_succeeded(%{ - "alert_name" => "subscription_payment_succeeded", - "subscription_id" => "nonexistent_subscription_id", - "next_bill_date" => Timex.shift(Timex.today(), days: 30), - "unit_price" => "12.00" - }) - - assert res == {:ok, nil} - end - end - - describe "change_plan" do - test "sets the next bill amount and date" do - user = insert(:user) - insert(:subscription, user: user) - - Billing.change_plan(user, "123123") - - subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.paddle_plan_id == "123123" - assert subscription.next_bill_date == ~D[2019-07-10] - assert subscription.next_bill_amount == "6.00" - end - end -end diff --git a/test/plausible/billing/plans_test.exs b/test/plausible/billing/plans_test.exs deleted file mode 100644 index 599a20aabedf..000000000000 --- a/test/plausible/billing/plans_test.exs +++ /dev/null @@ -1,85 +0,0 @@ -defmodule Plausible.Billing.PlansTest do - use Plausible.DataCase - alias Plausible.Billing.Plans - - @v1_plan_id "558018" - @v2_plan_id "654177" - @v3_plan_id "749342" - - describe "plans_for" do - test "shows v1 pricing for users who are already on v1 pricing" do - user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id)) - - assert List.first(Plans.plans_for(user))[:monthly_product_id] == @v1_plan_id - end - - test "shows v2 pricing for users who are already on v2 pricing" do - user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id)) - - assert List.first(Plans.plans_for(user))[:monthly_product_id] == @v2_plan_id - end - - test "shows v2 pricing for users who signed up in 2021" do - user = insert(:user, inserted_at: ~N[2021-12-31 00:00:00]) |> Repo.preload(:subscription) - - assert List.first(Plans.plans_for(user))[:monthly_product_id] == @v2_plan_id - end - - test "shows v3 pricing for everyone else" do - user = insert(:user) |> Repo.preload(:subscription) - - assert List.first(Plans.plans_for(user))[:monthly_product_id] == @v3_plan_id - end - end - - describe "allowance" do - test "is based on the plan if user is on a standard plan" do - user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id)) - - assert Plans.allowance(user.subscription) == 10_000 - end - - test "free_10k has 10k allowance" do - user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k")) - - assert Plans.allowance(user.subscription) == 10_000 - end - - test "is based on the enterprise plan if user is on an enterprise plan" do - user = insert(:user) - - enterprise_plan = - insert(:enterprise_plan, user_id: user.id, monthly_pageview_limit: 100_000) - - subscription = - insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id) - - assert Plans.allowance(subscription) == 100_000 - end - end - - describe "subscription_interval" do - test "is based on the plan if user is on a standard plan" do - user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id)) - - assert Plans.subscription_interval(user.subscription) == "monthly" - end - - test "is N/A for free plan" do - user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k")) - - assert Plans.subscription_interval(user.subscription) == "N/A" - end - - test "is based on the enterprise plan if user is on an enterprise plan" do - user = insert(:user) - - enterprise_plan = insert(:enterprise_plan, user_id: user.id, billing_interval: :yearly) - - subscription = - insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id) - - assert Plans.subscription_interval(subscription) == :yearly - end - end -end diff --git a/test/plausible/billing/site_locker_test.exs b/test/plausible/billing/site_locker_test.exs deleted file mode 100644 index 8c6007a52640..000000000000 --- a/test/plausible/billing/site_locker_test.exs +++ /dev/null @@ -1,194 +0,0 @@ -defmodule Plausible.Billing.SiteLockerTest do - use Plausible.DataCase - use Bamboo.Test, shared: true - alias Plausible.Billing.SiteLocker - - describe "check_sites_for/1" do - test "does not lock sites if user is on trial" do - user = - insert(:user, trial_expiry_date: Timex.today()) - |> Repo.preload(:subscription) - - site = insert(:site, locked: true, members: [user]) - - SiteLocker.check_sites_for(user) - - refute Repo.reload!(site).locked - end - - test "does not lock if user has an active subscription" do - user = insert(:user) - insert(:subscription, status: "active", user: user) - user = Repo.preload(user, :subscription) - site = insert(:site, locked: true, members: [user]) - - SiteLocker.check_sites_for(user) - - refute Repo.reload!(site).locked - end - - test "does not lock user who is past due" do - user = insert(:user) - insert(:subscription, status: "past_due", user: user) - user = Repo.preload(user, :subscription) - site = insert(:site, members: [user]) - - SiteLocker.check_sites_for(user) - - refute Repo.reload!(site).locked - end - - test "does not lock user who cancelled subscription but it hasn't expired yet" do - user = insert(:user) - insert(:subscription, status: "deleted", user: user) - user = Repo.preload(user, :subscription) - site = insert(:site, members: [user]) - - SiteLocker.check_sites_for(user) - - refute Repo.reload!(site).locked - end - - test "does not lock user who has an active subscription and is on grace period" do - user = - insert(:user, - grace_period: %Plausible.Auth.GracePeriod{ - end_date: Timex.shift(Timex.today(), days: 1), - allowance_required: 10_000 - } - ) - - insert(:subscription, status: "active", user: user) - user = Repo.preload(user, :subscription) - site = insert(:site, members: [user]) - - SiteLocker.check_sites_for(user) - - refute Repo.reload!(site).locked - end - - test "locks user who cancelled subscription and the cancelled subscription has expired" do - user = insert(:user) - - insert(:subscription, - status: "deleted", - next_bill_date: Timex.today() |> Timex.shift(days: -1), - user: user - ) - - site = insert(:site, members: [user]) - - user = Repo.preload(user, :subscription) - - SiteLocker.check_sites_for(user) - - refute Repo.reload!(site).locked - end - - test "locks all sites if user has active subscription but grace period has ended" do - user = - insert(:user, - grace_period: %Plausible.Auth.GracePeriod{ - end_date: Timex.shift(Timex.today(), days: -1), - allowance_required: 10_000 - } - ) - - insert(:subscription, status: "active", user: user) - user = Repo.preload(user, :subscription) - site = insert(:site, members: [user]) - - SiteLocker.check_sites_for(user) - - assert Repo.reload!(site).locked - end - - test "sends email if grace period has ended" do - user = - insert(:user, - grace_period: %Plausible.Auth.GracePeriod{ - end_date: Timex.shift(Timex.today(), days: -1), - allowance_required: 10_000 - } - ) - - insert(:subscription, status: "active", user: user) - user = Repo.preload(user, :subscription) - insert(:site, members: [user]) - - SiteLocker.check_sites_for(user) - - assert_email_delivered_with( - to: [user], - subject: "[Action required] Your Plausible dashboard is now locked" - ) - end - - test "does not send grace period email if site is already locked" do - user = - insert(:user, - grace_period: %Plausible.Auth.GracePeriod{ - end_date: Timex.shift(Timex.today(), days: -1), - allowance_required: 10_000, - is_over: false - } - ) - - insert(:subscription, status: "active", user: user) - user = Repo.preload(user, :subscription) - insert(:site, members: [user]) - - SiteLocker.check_sites_for(user) - - assert_email_delivered_with( - to: [user], - subject: "[Action required] Your Plausible dashboard is now locked" - ) - - user = Repo.reload!(user) |> Repo.preload(:subscription) - SiteLocker.check_sites_for(user) - - assert_no_emails_delivered() - end - - test "locks all sites if user has no trial or active subscription" do - user = - insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - |> Repo.preload(:subscription) - - site = insert(:site, locked: true, members: [user]) - - SiteLocker.check_sites_for(user) - - assert Repo.reload!(site).locked - end - - test "only locks sites that the user owns" do - user = - insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - |> Repo.preload(:subscription) - - owner_site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner) - ] - ) - - viewer_site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :viewer) - ] - ) - - SiteLocker.check_sites_for(user) - - owner_site = Repo.reload!(owner_site) - viewer_site = Repo.reload!(viewer_site) - - assert owner_site.locked - refute viewer_site.locked - end - end -end diff --git a/test/plausible/imported/imported_test.exs b/test/plausible/imported/imported_test.exs deleted file mode 100644 index 8ad031a4e421..000000000000 --- a/test/plausible/imported/imported_test.exs +++ /dev/null @@ -1,618 +0,0 @@ -defmodule Plausible.ImportedTest do - use PlausibleWeb.ConnCase - use Timex - import Plausible.TestUtils - - @user_id 123 - - describe "Parse and import third party data fetched from Google Analytics" do - setup [:create_user, :log_in, :create_new_site, :add_imported_data] - - @tag :skip - test "Visitors data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, timestamp: ~N[2021-01-31 00:00:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101"], - "metrics" => [%{"values" => ["1", "1", "0", "1", "60"]}] - }, - %{ - "dimensions" => ["20210131"], - "metrics" => [%{"values" => ["1", "1", "1", "1", "60"]}] - } - ], - site.id, - "imported_visitors" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/main-graph?period=month&date=2021-01-01&with_imported=true" - ) - - assert %{"plot" => plot, "imported_source" => "Google Analytics"} = json_response(conn, 200) - - assert Enum.count(plot) == 31 - assert List.first(plot) == 2 - assert List.last(plot) == 2 - assert Enum.sum(plot) == 4 - end - - @tag :skip - test "Sources are imported", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - referrer_source: "Google", - referrer: "google.com", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - referrer_source: "Google", - referrer: "google.com", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - referrer_source: "DuckDuckGo", - referrer: "duckduckgo.com", - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "duckduckgo.com", "organic", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "0", "60"]}] - }, - %{ - "dimensions" => ["20210131", "google.com", "organic", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - }, - %{ - "dimensions" => ["20210101", "google.com", "paid", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - }, - %{ - "dimensions" => ["20210101", "Twitter", "social", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - }, - %{ - "dimensions" => [ - "20210131", - "A Nice Newsletter", - "email", - "newsletter", - "", - "" - ], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - }, - %{ - "dimensions" => ["20210101", "(direct)", "(none)", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - } - ], - site.id, - "imported_sources" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/sources?period=month&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{"name" => "Google", "visitors" => 4}, - %{"name" => "DuckDuckGo", "visitors" => 2}, - %{"name" => "A Nice Newsletter", "visitors" => 1}, - %{"name" => "Twitter", "visitors" => 1} - ] - end - - @tag :skip - test "UTM mediums data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - utm_medium: "social", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - utm_medium: "social", - timestamp: ~N[2021-01-01 12:00:00] - ) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "Twitter", "social", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - }, - %{ - "dimensions" => ["20210101", "(direct)", "(none)", "", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "60"]}] - } - ], - site.id, - "imported_sources" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/utm_mediums?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "bounce_rate" => 100.0, - "name" => "social", - "visit_duration" => 20, - "visitors" => 3 - } - ] - end - - @tag :skip - test "UTM campaigns data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, utm_campaign: "profile", timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, utm_campaign: "august", timestamp: ~N[2021-01-01 00:00:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "Twitter", "social", "profile", "", ""], - "metrics" => [%{"values" => ["1", "1", "1", "100"]}] - }, - %{ - "dimensions" => ["20210101", "Gmail", "email", "august", "", ""], - "metrics" => [%{"values" => ["1", "1", "0", "100"]}] - }, - %{ - "dimensions" => ["20210101", "Gmail", "email", "(not set)", "", ""], - "metrics" => [%{"values" => ["1", "1", "0", "100"]}] - } - ], - site.id, - "imported_sources" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/utm_campaigns?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "name" => "august", - "visitors" => 2, - "bounce_rate" => 50.0, - "visit_duration" => 50.0 - }, - %{ - "name" => "profile", - "visitors" => 2, - "bounce_rate" => 100.0, - "visit_duration" => 50.0 - } - ] - end - - @tag :skip - test "UTM terms data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, utm_term: "oat milk", timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, utm_term: "Sweden", timestamp: ~N[2021-01-01 00:00:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "Google", "paid", "", "", "oat milk"], - "metrics" => [%{"values" => ["1", "1", "1", "100"]}] - }, - %{ - "dimensions" => ["20210101", "Google", "paid", "", "", "Sweden"], - "metrics" => [%{"values" => ["1", "1", "0", "100"]}] - }, - %{ - "dimensions" => ["20210101", "Google", "paid", "", "", "(not set)"], - "metrics" => [%{"values" => ["1", "1", "0", "100"]}] - } - ], - site.id, - "imported_sources" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/utm_terms?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "name" => "Sweden", - "visitors" => 2, - "bounce_rate" => 50.0, - "visit_duration" => 50.0 - }, - %{ - "name" => "oat milk", - "visitors" => 2, - "bounce_rate" => 100.0, - "visit_duration" => 50.0 - } - ] - end - - @tag :skip - test "UTM contents data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, utm_content: "ad", timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, utm_content: "blog", timestamp: ~N[2021-01-01 00:00:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "Google", "paid", "", "ad", ""], - "metrics" => [%{"values" => ["1", "1", "1", "100"]}] - }, - %{ - "dimensions" => ["20210101", "Google", "paid", "", "blog", ""], - "metrics" => [%{"values" => ["1", "1", "0", "100"]}] - }, - %{ - "dimensions" => ["20210101", "Google", "paid", "", "(not set)", ""], - "metrics" => [%{"values" => ["1", "1", "0", "100"]}] - } - ], - site.id, - "imported_sources" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/utm_contents?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "name" => "blog", - "visitors" => 2, - "bounce_rate" => 50.0, - "visit_duration" => 50.0 - }, - %{ - "name" => "ad", - "visitors" => 2, - "bounce_rate" => 100.0, - "visit_duration" => 50.0 - } - ] - end - - @tag :skip - test "Page event data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - pathname: "/", - hostname: "host-a.com", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/some-other-page", - hostname: "host-a.com", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:15:00] - ) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "host-a.com", "/"], - "metrics" => [%{"values" => ["1", "1", "0", "700"]}] - }, - %{ - "dimensions" => ["20210101", "host-b.com", "/some-other-page"], - "metrics" => [%{"values" => ["1", "2", "1", "60"]}] - }, - %{ - "dimensions" => ["20210101", "host-b.com", "/some-other-page?wat=wot"], - "metrics" => [%{"values" => ["1", "1", "0", "60"]}] - } - ], - site.id, - "imported_pages" - ) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "/"], - "metrics" => [%{"values" => ["1", "3", "10", "1"]}] - } - ], - site.id, - "imported_entry_pages" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/pages?period=day&date=2021-01-01&detailed=true&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "bounce_rate" => nil, - "time_on_page" => 60, - "visitors" => 3, - "pageviews" => 4, - "name" => "/some-other-page" - }, - %{ - "bounce_rate" => 25.0, - "time_on_page" => 800.0, - "visitors" => 2, - "pageviews" => 2, - "name" => "/" - } - ] - end - - @tag :skip - test "Exit page event data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - pathname: "/page1", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/page1", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/page1", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/page2", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:15:00] - ) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "host-a.com", "/page2"], - "metrics" => [%{"values" => ["2", "4", "0", "10"]}] - } - ], - site.id, - "imported_pages" - ) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "/page2"], - "metrics" => [%{"values" => ["2", "3"]}] - } - ], - site.id, - "imported_exit_pages" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/exit-pages?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "name" => "/page2", - "unique_exits" => 3, - "total_exits" => 4, - "exit_rate" => 80.0 - }, - %{"name" => "/page1", "unique_exits" => 2, "total_exits" => 2, "exit_rate" => 66} - ] - end - - @tag :skip - test "Location data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - country_code: "EE", - timestamp: ~N[2021-01-01 00:15:00] - ), - build(:pageview, - country_code: "EE", - timestamp: ~N[2021-01-01 00:15:00] - ), - build(:pageview, - country_code: "GB", - timestamp: ~N[2021-01-01 00:15:00] - ) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "EE", "Tartumaa"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - }, - %{ - "dimensions" => ["20210101", "GB", "Midlothian"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - } - ], - site.id, - "imported_locations" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/countries?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{ - "code" => "EE", - "alpha_3" => "EST", - "name" => "Estonia", - "flag" => "🇪🇪", - "visitors" => 3, - "percentage" => 60 - }, - %{ - "code" => "GB", - "alpha_3" => "GBR", - "name" => "United Kingdom", - "flag" => "🇬🇧", - "visitors" => 2, - "percentage" => 40 - } - ] - end - - @tag :skip - test "Devices data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, screen_size: "Desktop", timestamp: ~N[2021-01-01 00:15:00]), - build(:pageview, screen_size: "Desktop", timestamp: ~N[2021-01-01 00:15:00]), - build(:pageview, screen_size: "Laptop", timestamp: ~N[2021-01-01 00:15:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "mobile"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - }, - %{ - "dimensions" => ["20210101", "Laptop"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - } - ], - site.id, - "imported_devices" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/screen-sizes?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{"name" => "Desktop", "visitors" => 2, "percentage" => 40}, - %{"name" => "Laptop", "visitors" => 2, "percentage" => 40}, - %{"name" => "Mobile", "visitors" => 1, "percentage" => 20} - ] - end - - @tag :skip - test "Browsers data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, browser: "Chrome", timestamp: ~N[2021-01-01 00:15:00]), - build(:pageview, browser: "Firefox", timestamp: ~N[2021-01-01 00:15:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "User-Agent: Mozilla"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - }, - %{ - "dimensions" => ["20210101", "Android Browser"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - } - ], - site.id, - "imported_browsers" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/browsers?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{"name" => "Firefox", "visitors" => 2, "percentage" => 50}, - %{"name" => "Mobile App", "visitors" => 1, "percentage" => 25}, - %{"name" => "Chrome", "visitors" => 1, "percentage" => 25} - ] - end - - @tag :skip - test "OS data imported from Google Analytics", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, operating_system: "Mac", timestamp: ~N[2021-01-01 00:15:00]), - build(:pageview, operating_system: "Mac", timestamp: ~N[2021-01-01 00:15:00]), - build(:pageview, operating_system: "GNU/Linux", timestamp: ~N[2021-01-01 00:15:00]) - ]) - - assert :ok = - Plausible.Imported.from_google_analytics( - [ - %{ - "dimensions" => ["20210101", "Macintosh"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - }, - %{ - "dimensions" => ["20210101", "Linux"], - "metrics" => [%{"values" => ["1", "1", "0", "10"]}] - } - ], - site.id, - "imported_operating_systems" - ) - - conn = - get( - conn, - "/api/stats/#{site.domain}/operating-systems?period=day&date=2021-01-01&with_imported=true" - ) - - assert json_response(conn, 200) == [ - %{"name" => "Mac", "visitors" => 3, "percentage" => 60}, - %{"name" => "GNU/Linux", "visitors" => 2, "percentage" => 40} - ] - end - end -end diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/browsers.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/browsers.csv deleted file mode 100644 index 292175e1729c..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/browsers.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,conversions,conversion_rate -,1,50.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/cities.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/cities.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/cities.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/conversions.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/conversions.csv deleted file mode 100644 index 5c3bf52284c3..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/conversions.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_conversions,total_conversions -Signup,1,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/countries.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/countries.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/countries.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/devices.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/devices.csv deleted file mode 100644 index 64f760da4003..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/devices.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,conversions,conversion_rate -,1,25.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/entry_pages.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/entry_pages.csv deleted file mode 100644 index f2471bfe9d83..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/entry_pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,conversions,conversion_rate -/,1,25.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/exit_pages.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/exit_pages.csv deleted file mode 100644 index 4d92a7a078ea..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/exit_pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,conversions,conversion_rate -/,1,33.3 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/operating_systems.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/operating_systems.csv deleted file mode 100644 index 64f760da4003..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/operating_systems.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,conversions,conversion_rate -,1,25.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/pages.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/pages.csv deleted file mode 100644 index f2471bfe9d83..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,conversions,conversion_rate -/,1,25.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/prop_breakdown.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/prop_breakdown.csv deleted file mode 100644 index cc987c30cb26..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/prop_breakdown.csv +++ /dev/null @@ -1,2 +0,0 @@ -prop,name,unique_conversions,total_conversions -variant,A,1,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/regions.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/regions.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/regions.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/sources.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/sources.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/sources.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_campaigns.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_campaigns.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_campaigns.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_contents.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_contents.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_contents.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_mediums.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_mediums.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_mediums.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_sources.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_sources.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_sources.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_terms.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_terms.csv deleted file mode 100644 index 854a70d52fc5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/utm_terms.csv +++ /dev/null @@ -1 +0,0 @@ -name,conversions,conversion_rate diff --git a/test/plausible_web/controllers/CSVs/30d-filter-goal/visitors.csv b/test/plausible_web/controllers/CSVs/30d-filter-goal/visitors.csv deleted file mode 100644 index f7e11600a7b1..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-goal/visitors.csv +++ /dev/null @@ -1,32 +0,0 @@ -date,visitors,pageviews,bounce_rate,visit_duration -2021-09-20,0,0,100,0 -2021-09-21,0,0,, -2021-09-22,0,0,, -2021-09-23,0,0,, -2021-09-24,0,0,, -2021-09-25,0,0,, -2021-09-26,0,0,, -2021-09-27,0,0,, -2021-09-28,0,0,, -2021-09-29,0,0,, -2021-09-30,0,0,, -2021-10-01,0,0,, -2021-10-02,0,0,, -2021-10-03,0,0,, -2021-10-04,0,0,, -2021-10-05,0,0,, -2021-10-06,0,0,, -2021-10-07,0,0,, -2021-10-08,0,0,, -2021-10-09,0,0,, -2021-10-10,0,0,, -2021-10-11,0,0,, -2021-10-12,0,0,, -2021-10-13,0,0,, -2021-10-14,0,0,, -2021-10-15,0,0,, -2021-10-16,0,0,, -2021-10-17,0,0,, -2021-10-18,0,0,, -2021-10-19,1,0,100,0 -2021-10-20,0,0,0,60 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/browsers.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/browsers.csv deleted file mode 100644 index 7b062ae5bb79..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/browsers.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/cities.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/cities.csv deleted file mode 100644 index 9816303bc6d0..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/cities.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Tallinn,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/conversions.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/conversions.csv deleted file mode 100644 index 583f280ff6ef..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/conversions.csv +++ /dev/null @@ -1 +0,0 @@ -name,unique_conversions,total_conversions diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/countries.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/countries.csv deleted file mode 100644 index 7fd0fa6da7d4..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/countries.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Estonia,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/devices.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/devices.csv deleted file mode 100644 index 7b062ae5bb79..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/devices.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/entry_pages.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/entry_pages.csv deleted file mode 100644 index 9ee95b1a87b6..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/entry_pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_entrances,total_entrances,visit_duration -/,1,1,60 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/exit_pages.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/exit_pages.csv deleted file mode 100644 index 8971817457a8..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/exit_pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_exits,total_exits,exit_rate -/some-other-page,1,1,100.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/operating_systems.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/operating_systems.csv deleted file mode 100644 index 7b062ae5bb79..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/operating_systems.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/pages.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/pages.csv deleted file mode 100644 index 711f9fb0b6aa..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,time_on_page -/some-other-page,1,,60.0 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/prop_breakdown.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/prop_breakdown.csv deleted file mode 100644 index c31750f137c5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/prop_breakdown.csv +++ /dev/null @@ -1 +0,0 @@ -prop,name,unique_conversions,total_conversions diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/regions.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/regions.csv deleted file mode 100644 index 94104deeb529..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/regions.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Harjumaa,1 diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/sources.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/sources.csv deleted file mode 100644 index 773a58cf75d2..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/sources.csv +++ /dev/null @@ -1 +0,0 @@ -name,visitors,bounce_rate,visit_duration diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_campaigns.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/utm_campaigns.csv deleted file mode 100644 index 773a58cf75d2..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_campaigns.csv +++ /dev/null @@ -1 +0,0 @@ -name,visitors,bounce_rate,visit_duration diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_contents.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/utm_contents.csv deleted file mode 100644 index 773a58cf75d2..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_contents.csv +++ /dev/null @@ -1 +0,0 @@ -name,visitors,bounce_rate,visit_duration diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_mediums.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/utm_mediums.csv deleted file mode 100644 index 773a58cf75d2..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_mediums.csv +++ /dev/null @@ -1 +0,0 @@ -name,visitors,bounce_rate,visit_duration diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_sources.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/utm_sources.csv deleted file mode 100644 index 773a58cf75d2..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_sources.csv +++ /dev/null @@ -1 +0,0 @@ -name,visitors,bounce_rate,visit_duration diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_terms.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/utm_terms.csv deleted file mode 100644 index 773a58cf75d2..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/utm_terms.csv +++ /dev/null @@ -1 +0,0 @@ -name,visitors,bounce_rate,visit_duration diff --git a/test/plausible_web/controllers/CSVs/30d-filter-path/visitors.csv b/test/plausible_web/controllers/CSVs/30d-filter-path/visitors.csv deleted file mode 100644 index b79ea927f53c..000000000000 --- a/test/plausible_web/controllers/CSVs/30d-filter-path/visitors.csv +++ /dev/null @@ -1,32 +0,0 @@ -date,visitors,pageviews,bounce_rate,visit_duration -2021-09-20,0,0,, -2021-09-21,0,0,, -2021-09-22,0,0,, -2021-09-23,0,0,, -2021-09-24,0,0,, -2021-09-25,0,0,, -2021-09-26,0,0,, -2021-09-27,0,0,, -2021-09-28,0,0,, -2021-09-29,0,0,, -2021-09-30,0,0,, -2021-10-01,0,0,, -2021-10-02,0,0,, -2021-10-03,0,0,, -2021-10-04,0,0,, -2021-10-05,0,0,, -2021-10-06,0,0,, -2021-10-07,0,0,, -2021-10-08,0,0,, -2021-10-09,0,0,, -2021-10-10,0,0,, -2021-10-11,0,0,, -2021-10-12,0,0,, -2021-10-13,0,0,, -2021-10-14,0,0,, -2021-10-15,0,0,, -2021-10-16,0,0,, -2021-10-17,0,0,, -2021-10-18,0,0,, -2021-10-19,0,0,, -2021-10-20,1,1,, diff --git a/test/plausible_web/controllers/CSVs/30d/browsers.csv b/test/plausible_web/controllers/CSVs/30d/browsers.csv deleted file mode 100644 index b09641f477fc..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/browsers.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,visitors -ABrowserName,2 -,2 diff --git a/test/plausible_web/controllers/CSVs/30d/cities.csv b/test/plausible_web/controllers/CSVs/30d/cities.csv deleted file mode 100644 index 9816303bc6d0..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/cities.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Tallinn,1 diff --git a/test/plausible_web/controllers/CSVs/30d/conversions.csv b/test/plausible_web/controllers/CSVs/30d/conversions.csv deleted file mode 100644 index 5c3bf52284c3..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/conversions.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_conversions,total_conversions -Signup,1,1 diff --git a/test/plausible_web/controllers/CSVs/30d/countries.csv b/test/plausible_web/controllers/CSVs/30d/countries.csv deleted file mode 100644 index 550bdd69caed..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/countries.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Estonia,2 diff --git a/test/plausible_web/controllers/CSVs/30d/devices.csv b/test/plausible_web/controllers/CSVs/30d/devices.csv deleted file mode 100644 index 259a48cdd4a5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/devices.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,4 diff --git a/test/plausible_web/controllers/CSVs/30d/entry_pages.csv b/test/plausible_web/controllers/CSVs/30d/entry_pages.csv deleted file mode 100644 index 016868c18622..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/entry_pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_entrances,total_entrances,visit_duration -/,4,4,15 diff --git a/test/plausible_web/controllers/CSVs/30d/exit_pages.csv b/test/plausible_web/controllers/CSVs/30d/exit_pages.csv deleted file mode 100644 index 9c4fc18759d9..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/exit_pages.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,unique_exits,total_exits,exit_rate -/,3,3,100.0 -/some-other-page,1,1,100.0 diff --git a/test/plausible_web/controllers/CSVs/30d/operating_systems.csv b/test/plausible_web/controllers/CSVs/30d/operating_systems.csv deleted file mode 100644 index 259a48cdd4a5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/operating_systems.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,4 diff --git a/test/plausible_web/controllers/CSVs/30d/pages.csv b/test/plausible_web/controllers/CSVs/30d/pages.csv deleted file mode 100644 index dc8d9adb4f18..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/pages.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,visitors,bounce_rate,time_on_page -/,4,75, -/some-other-page,1,,60.0 diff --git a/test/plausible_web/controllers/CSVs/30d/prop_breakdown.csv b/test/plausible_web/controllers/CSVs/30d/prop_breakdown.csv deleted file mode 100644 index c31750f137c5..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/prop_breakdown.csv +++ /dev/null @@ -1 +0,0 @@ -prop,name,unique_conversions,total_conversions diff --git a/test/plausible_web/controllers/CSVs/30d/regions.csv b/test/plausible_web/controllers/CSVs/30d/regions.csv deleted file mode 100644 index 94104deeb529..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/regions.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Harjumaa,1 diff --git a/test/plausible_web/controllers/CSVs/30d/sources.csv b/test/plausible_web/controllers/CSVs/30d/sources.csv deleted file mode 100644 index 3b22e30a0102..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/sources.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -Google,1,0,60 diff --git a/test/plausible_web/controllers/CSVs/30d/utm_campaigns.csv b/test/plausible_web/controllers/CSVs/30d/utm_campaigns.csv deleted file mode 100644 index 934651b6b9f9..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/utm_campaigns.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -ads,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/30d/utm_contents.csv b/test/plausible_web/controllers/CSVs/30d/utm_contents.csv deleted file mode 100644 index 6f8d1ba25516..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/utm_contents.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -content,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/30d/utm_mediums.csv b/test/plausible_web/controllers/CSVs/30d/utm_mediums.csv deleted file mode 100644 index 1fe1c21a2856..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/utm_mediums.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -search,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/30d/utm_sources.csv b/test/plausible_web/controllers/CSVs/30d/utm_sources.csv deleted file mode 100644 index b32c9660a021..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/utm_sources.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -google,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/30d/utm_terms.csv b/test/plausible_web/controllers/CSVs/30d/utm_terms.csv deleted file mode 100644 index cbd3a7ce4047..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/utm_terms.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -term,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/30d/visitors.csv b/test/plausible_web/controllers/CSVs/30d/visitors.csv deleted file mode 100644 index df94365f9baa..000000000000 --- a/test/plausible_web/controllers/CSVs/30d/visitors.csv +++ /dev/null @@ -1,32 +0,0 @@ -date,visitors,pageviews,bounce_rate,visit_duration -2021-09-20,1,1,100,0 -2021-09-21,0,0,, -2021-09-22,0,0,, -2021-09-23,0,0,, -2021-09-24,0,0,, -2021-09-25,0,0,, -2021-09-26,0,0,, -2021-09-27,0,0,, -2021-09-28,0,0,, -2021-09-29,0,0,, -2021-09-30,0,0,, -2021-10-01,0,0,, -2021-10-02,0,0,, -2021-10-03,0,0,, -2021-10-04,0,0,, -2021-10-05,0,0,, -2021-10-06,0,0,, -2021-10-07,0,0,, -2021-10-08,0,0,, -2021-10-09,0,0,, -2021-10-10,0,0,, -2021-10-11,0,0,, -2021-10-12,0,0,, -2021-10-13,0,0,, -2021-10-14,0,0,, -2021-10-15,0,0,, -2021-10-16,0,0,, -2021-10-17,0,0,, -2021-10-18,0,0,, -2021-10-19,2,1,100,0 -2021-10-20,1,2,0,60 diff --git a/test/plausible_web/controllers/CSVs/6m/browsers.csv b/test/plausible_web/controllers/CSVs/6m/browsers.csv deleted file mode 100644 index f2719de785cc..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/browsers.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,visitors -ABrowserName,3 -,2 diff --git a/test/plausible_web/controllers/CSVs/6m/cities.csv b/test/plausible_web/controllers/CSVs/6m/cities.csv deleted file mode 100644 index 9816303bc6d0..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/cities.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Tallinn,1 diff --git a/test/plausible_web/controllers/CSVs/6m/conversions.csv b/test/plausible_web/controllers/CSVs/6m/conversions.csv deleted file mode 100644 index 5c3bf52284c3..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/conversions.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_conversions,total_conversions -Signup,1,1 diff --git a/test/plausible_web/controllers/CSVs/6m/countries.csv b/test/plausible_web/controllers/CSVs/6m/countries.csv deleted file mode 100644 index 4daa0cc026f0..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/countries.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Estonia,3 diff --git a/test/plausible_web/controllers/CSVs/6m/devices.csv b/test/plausible_web/controllers/CSVs/6m/devices.csv deleted file mode 100644 index 6d03d7369b78..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/devices.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,5 diff --git a/test/plausible_web/controllers/CSVs/6m/entry_pages.csv b/test/plausible_web/controllers/CSVs/6m/entry_pages.csv deleted file mode 100644 index 63a95b447af6..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/entry_pages.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,unique_entrances,total_entrances,visit_duration -/,5,5,12 diff --git a/test/plausible_web/controllers/CSVs/6m/exit_pages.csv b/test/plausible_web/controllers/CSVs/6m/exit_pages.csv deleted file mode 100644 index 037f89fb6d75..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/exit_pages.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,unique_exits,total_exits,exit_rate -/,4,4,100.0 -/some-other-page,1,1,100.0 diff --git a/test/plausible_web/controllers/CSVs/6m/operating_systems.csv b/test/plausible_web/controllers/CSVs/6m/operating_systems.csv deleted file mode 100644 index 6d03d7369b78..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/operating_systems.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -,5 diff --git a/test/plausible_web/controllers/CSVs/6m/pages.csv b/test/plausible_web/controllers/CSVs/6m/pages.csv deleted file mode 100644 index 33187b985b06..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/pages.csv +++ /dev/null @@ -1,3 +0,0 @@ -name,visitors,bounce_rate,time_on_page -/,5,80, -/some-other-page,1,,60.0 diff --git a/test/plausible_web/controllers/CSVs/6m/prop_breakdown.csv b/test/plausible_web/controllers/CSVs/6m/prop_breakdown.csv deleted file mode 100644 index c31750f137c5..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/prop_breakdown.csv +++ /dev/null @@ -1 +0,0 @@ -prop,name,unique_conversions,total_conversions diff --git a/test/plausible_web/controllers/CSVs/6m/regions.csv b/test/plausible_web/controllers/CSVs/6m/regions.csv deleted file mode 100644 index 94104deeb529..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/regions.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors -Harjumaa,1 diff --git a/test/plausible_web/controllers/CSVs/6m/sources.csv b/test/plausible_web/controllers/CSVs/6m/sources.csv deleted file mode 100644 index 635bc508ad22..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/sources.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -Google,2,50,30 diff --git a/test/plausible_web/controllers/CSVs/6m/utm_campaigns.csv b/test/plausible_web/controllers/CSVs/6m/utm_campaigns.csv deleted file mode 100644 index 13779d3f0df2..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/utm_campaigns.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -ads,2,100,0 diff --git a/test/plausible_web/controllers/CSVs/6m/utm_contents.csv b/test/plausible_web/controllers/CSVs/6m/utm_contents.csv deleted file mode 100644 index 6f8d1ba25516..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/utm_contents.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -content,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/6m/utm_mediums.csv b/test/plausible_web/controllers/CSVs/6m/utm_mediums.csv deleted file mode 100644 index 1fe1c21a2856..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/utm_mediums.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -search,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/6m/utm_sources.csv b/test/plausible_web/controllers/CSVs/6m/utm_sources.csv deleted file mode 100644 index b32c9660a021..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/utm_sources.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -google,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/6m/utm_terms.csv b/test/plausible_web/controllers/CSVs/6m/utm_terms.csv deleted file mode 100644 index cbd3a7ce4047..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/utm_terms.csv +++ /dev/null @@ -1,2 +0,0 @@ -name,visitors,bounce_rate,visit_duration -term,1,100,0 diff --git a/test/plausible_web/controllers/CSVs/6m/visitors.csv b/test/plausible_web/controllers/CSVs/6m/visitors.csv deleted file mode 100644 index ed3d3b5e7013..000000000000 --- a/test/plausible_web/controllers/CSVs/6m/visitors.csv +++ /dev/null @@ -1,7 +0,0 @@ -date,visitors,pageviews,bounce_rate,visit_duration -2021-05-01,1,1,100,0 -2021-06-01,0,0,, -2021-07-01,0,0,, -2021-08-01,0,0,, -2021-09-01,1,1,100,0 -2021-10-01,3,3,67,20 diff --git a/test/plausible_web/controllers/admin_auth_controller_test.exs b/test/plausible_web/controllers/admin_auth_controller_test.exs deleted file mode 100644 index f37733fbdb5e..000000000000 --- a/test/plausible_web/controllers/admin_auth_controller_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -defmodule PlausibleWeb.AdminAuthControllerTest do - use PlausibleWeb.ConnCase - - describe "GET /" do - test "no landing page", %{conn: conn} do - set_config(disable_authentication: false) - conn = get(conn, "/") - assert redirected_to(conn) == "/login" - end - - test "logs admin user in automatically when authentication is disabled", %{conn: conn} do - set_config(disable_authentication: true) - - admin_user = - insert(:user, - email: Application.get_env(:plausible, :admin_email), - password: Application.get_env(:plausible, :admin_pwd) - ) - - # goto landing page - conn = get(conn, "/") - assert get_session(conn, :current_user_id) == admin_user.id - assert redirected_to(conn) == "/sites" - - # trying logging out - conn = get(conn, "/logout") - assert redirected_to(conn) == "/" - conn = get(conn, "/") - assert redirected_to(conn) == "/sites" - end - - @tag :skip - test "disable registration", %{conn: conn} do - set_config(disable_registration: true) - conn = get(conn, "/register") - assert redirected_to(conn) == "/login" - end - end - - def set_config(config) do - updated_config = - Keyword.merge( - [disable_authentication: false, disable_registration: false], - config - ) - - Application.put_env( - :plausible, - :selfhost, - updated_config - ) - end -end diff --git a/test/plausible_web/controllers/api/external_sites_controller_test.exs b/test/plausible_web/controllers/api/external_sites_controller_test.exs deleted file mode 100644 index 7165b8f660cd..000000000000 --- a/test/plausible_web/controllers/api/external_sites_controller_test.exs +++ /dev/null @@ -1,414 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalSitesControllerTest do - use PlausibleWeb.ConnCase - use Plausible.Repo - import Plausible.TestUtils - - setup %{conn: conn} do - user = insert(:user) - api_key = insert(:api_key, user: user, scopes: ["sites:provision:*"]) - conn = Plug.Conn.put_req_header(conn, "authorization", "Bearer #{api_key.key}") - {:ok, user: user, api_key: api_key, conn: conn} - end - - describe "POST /api/v1/sites" do - @tag :skip - test "can create a site", %{conn: conn} do - conn = - post(conn, "/api/v1/sites", %{ - "domain" => "some-site.domain", - "timezone" => "Europe/Tallinn" - }) - - assert json_response(conn, 200) == %{ - "domain" => "some-site.domain", - "timezone" => "Europe/Tallinn" - } - end - - @tag :skip - test "timezone defaults to Etc/UTC", %{conn: conn} do - conn = - post(conn, "/api/v1/sites", %{ - "domain" => "some-site.domain" - }) - - assert json_response(conn, 200) == %{ - "domain" => "some-site.domain", - "timezone" => "Etc/UTC" - } - end - - @tag :skip - test "domain is required", %{conn: conn} do - conn = post(conn, "/api/v1/sites", %{}) - - assert json_response(conn, 400) == %{ - "error" => "domain can't be blank" - } - end - - @tag :skip - test "does not allow creating more sites than the limit", %{conn: conn, user: user} do - Application.put_env(:plausible, :site_limit, 3) - insert(:site, members: [user]) - insert(:site, members: [user]) - insert(:site, members: [user]) - - conn = - post(conn, "/api/v1/sites", %{ - "domain" => "some-site.domain", - "timezone" => "Europe/Tallinn" - }) - - assert json_response(conn, 403) == %{ - "error" => - "Your account has reached the limit of 3 sites per account. Please contact hello@plausible.io to unlock more sites." - } - end - - @tag :skip - test "cannot access with a bad API key scope", %{conn: conn, user: user} do - api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) - - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - |> post("/api/v1/sites", %{"site" => %{"domain" => "domain.com"}}) - - assert json_response(conn, 401) == %{ - "error" => - "Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested." - } - end - end - - describe "DELETE /api/v1/sites/:site_id" do - setup :create_new_site - - @tag :skip - test "delete a site by it's domain", %{conn: conn, site: site} do - conn = delete(conn, "/api/v1/sites/" <> site.domain) - - assert json_response(conn, 200) == %{"deleted" => true} - end - - @tag :skip - test "is 404 when site cannot be found", %{conn: conn} do - conn = delete(conn, "/api/v1/sites/foobar.baz") - - assert json_response(conn, 404) == %{"error" => "Site could not be found"} - end - - @tag :skip - test "cannot delete a site that the user does not own", %{conn: conn, user: user} do - site = insert(:site, members: []) - insert(:site_membership, user: user, site: site, role: :admin) - conn = delete(conn, "/api/v1/sites/" <> site.domain) - - assert json_response(conn, 404) == %{"error" => "Site could not be found"} - end - - @tag :skip - test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do - api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) - - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - |> delete("/api/v1/sites/" <> site.domain) - - assert json_response(conn, 401) == %{ - "error" => - "Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested." - } - end - end - - describe "PUT /api/v1/sites/shared-links" do - setup :create_site - - @tag :skip - test "can add a shared link to a site", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/shared-links", %{ - site_id: site.domain, - name: "Wordpress" - }) - - res = json_response(conn, 200) - assert res["name"] == "Wordpress" - assert String.starts_with?(res["url"], "http://") - end - - @tag :skip - test "is idempotent find or create op", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/shared-links", %{ - site_id: site.domain, - name: "Wordpress" - }) - - %{"url" => url} = json_response(conn, 200) - - conn = - put(conn, "/api/v1/sites/shared-links", %{ - site_id: site.domain, - name: "Wordpress" - }) - - assert %{"url" => ^url} = json_response(conn, 200) - end - - @tag :skip - test "returns 400 when site id missing", %{conn: conn} do - conn = - put(conn, "/api/v1/sites/shared-links", %{ - name: "Wordpress" - }) - - res = json_response(conn, 400) - assert res["error"] == "Parameter `site_id` is required to create a shared link" - end - - @tag :skip - test "returns 404 when site id is non existent", %{conn: conn} do - conn = - put(conn, "/api/v1/sites/shared-links", %{ - name: "Wordpress", - site_id: "bad" - }) - - res = json_response(conn, 404) - assert res["error"] == "Site could not be found" - end - - @tag :skip - test "returns 404 when api key owner does not have permissions to create a shared link", %{ - conn: conn, - site: site, - user: user - } do - Repo.update_all( - from(sm in Plausible.Site.Membership, - where: sm.site_id == ^site.id and sm.user_id == ^user.id - ), - set: [role: :viewer] - ) - - conn = - put(conn, "/api/v1/sites/shared-links", %{ - site_id: site.domain, - name: "Wordpress" - }) - - res = json_response(conn, 404) - assert res["error"] == "Site could not be found" - end - end - - describe "PUT /api/v1/sites/goals" do - setup :create_site - - @tag :skip - test "can add a goal as event to a site", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "event", - event_name: "Signup" - }) - - res = json_response(conn, 200) - assert res["goal_type"] == "event" - assert res["event_name"] == "Signup" - end - - @tag :skip - test "can add a goal as page to a site", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "page", - page_path: "/signup" - }) - - res = json_response(conn, 200) - assert res["goal_type"] == "page" - assert res["page_path"] == "/signup" - end - - @tag :skip - test "is idempotent find or create op", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "event", - event_name: "Signup" - }) - - %{"id" => goal_id} = json_response(conn, 200) - - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "event", - event_name: "Signup" - }) - - assert %{"id" => ^goal_id} = json_response(conn, 200) - end - - @tag :skip - test "returns 400 when site id missing", %{conn: conn} do - conn = - put(conn, "/api/v1/sites/goals", %{ - goal_type: "event", - event_name: "Signup" - }) - - res = json_response(conn, 400) - assert res["error"] == "Parameter `site_id` is required to create a goal" - end - - @tag :skip - test "returns 404 when site id is non existent", %{conn: conn} do - conn = - put(conn, "/api/v1/sites/goals", %{ - goal_type: "event", - event_name: "Signup", - site_id: "bad" - }) - - res = json_response(conn, 404) - assert res["error"] == "Site could not be found" - end - - @tag :skip - test "returns 404 when api key owner does not have permissions to create a goal", %{ - conn: conn, - site: site, - user: user - } do - Repo.update_all( - from(sm in Plausible.Site.Membership, - where: sm.site_id == ^site.id and sm.user_id == ^user.id - ), - set: [role: :viewer] - ) - - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "event", - event_name: "Signup" - }) - - res = json_response(conn, 404) - assert res["error"] == "Site could not be found" - end - - @tag :skip - test "returns 400 when goal type missing", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - event_name: "Signup" - }) - - res = json_response(conn, 400) - assert res["error"] == "Parameter `goal_type` is required to create a goal" - end - - @tag :skip - test "returns 400 when goal event name missing", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "event" - }) - - res = json_response(conn, 400) - assert res["error"] == "Parameter `event_name` is required to create a goal" - end - - @tag :skip - test "returns 400 when goal page path missing", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "page" - }) - - res = json_response(conn, 400) - assert res["error"] == "Parameter `page_path` is required to create a goal" - end - end - - describe "DELETE /api/v1/sites/goals/:goal_id" do - setup :create_new_site - - @tag :skip - test "delete a goal by it's id", %{conn: conn, site: site} do - conn = - put(conn, "/api/v1/sites/goals", %{ - site_id: site.domain, - goal_type: "event", - event_name: "Signup" - }) - - %{"id" => goal_id} = json_response(conn, 200) - - conn = - delete(conn, "/api/v1/sites/goals/#{goal_id}", %{ - site_id: site.domain - }) - - assert json_response(conn, 200) == %{"deleted" => true} - end - - @tag :skip - test "is 404 when goal cannot be found", %{conn: conn, site: site} do - conn = - delete(conn, "/api/v1/sites/goals/0", %{ - site_id: site.domain - }) - - assert json_response(conn, 404) == %{"error" => "Goal could not be found"} - end - - @tag :skip - test "cannot delete a goal belongs to a site that the user does not own", %{ - conn: conn, - user: user - } do - site = insert(:site, members: []) - insert(:site_membership, user: user, site: site, role: :viewer) - - conn = - delete(conn, "/api/v1/sites/goals/1", %{ - site_id: site.domain - }) - - assert json_response(conn, 404) == %{"error" => "Site could not be found"} - end - - @tag :skip - test "cannot access with a bad API key scope", %{conn: conn, site: site, user: user} do - api_key = insert(:api_key, user: user, scopes: ["stats:read:*"]) - - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - - conn = - delete(conn, "/api/v1/sites/goals/1", %{ - site_id: site.domain - }) - - assert json_response(conn, 401) == %{ - "error" => - "Invalid API key. Please make sure you're using a valid API key with access to the resource you've requested." - } - end - end -end diff --git a/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs b/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs deleted file mode 100644 index 8e7d9d95194f..000000000000 --- a/test/plausible_web/controllers/api/external_stats_controller/aggregate_test.exs +++ /dev/null @@ -1,813 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalStatsController.AggregateTest do - use PlausibleWeb.ConnCase - import Plausible.TestUtils - - setup [:create_user, :create_new_site, :create_api_key, :use_api_key] - @user_id 123 - - describe "param validation" do - @tag :skip - test "validates that date can be parsed", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-dkjbAS", - "metrics" => "pageviews" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Error parsing `date` parameter: invalid_format. Please specify a valid date in ISO-8601 format." - } - end - - @tag :skip - test "validates that period can be parsed", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "aosuhsacp", - "metrics" => "pageviews" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Error parsing `period` parameter: invalid period `aosuhsacp`. Please find accepted values in our docs: https://plausible.io/docs/stats-api#time-periods" - } - end - - @tag :skip - test "custom period is not valid without a date", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "custom", - "metrics" => "pageviews" - }) - - assert json_response(conn, 400) == %{ - "error" => - "The `date` parameter is required when using a custom period. See https://plausible.io/docs/stats-api#time-periods" - } - end - - @tag :skip - test "validates date format in custom period", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "custom", - "date" => "2020-131-2piaskj,s,a90uac", - "metrics" => "pageviews" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Invalid format for `date` parameter. When using a custom period, please include two ISO-8601 formatted dates joined by a comma. See https://plausible.io/docs/stats-api#time-periods" - } - end - - @tag :skip - test "validates that metrics are all recognized", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "30d", - "metrics" => "pageviews,led_zeppelin" - }) - - assert json_response(conn, 400) == %{ - "error" => - "The metric `led_zeppelin` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#get-apiv1statsbreakdown" - } - end - - @tag :skip - test "validates that session metrics cannot be used with event:name filter", %{ - conn: conn, - site: site - } do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "30d", - "metrics" => "pageviews,visit_duration", - "filters" => "event:name==Signup" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Session metric `visit_duration` cannot be queried when using a filter on `event:name`." - } - end - end - - @tag :skip - test "aggregates a single metric", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, user_id: @user_id, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, user_id: @user_id, domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 3} - } - end - - @tag :skip - test "aggregates visitors, pageviews, visits, bounce rate and visit duration", %{ - conn: conn, - site: site - } do - populate_stats([ - build(:pageview, user_id: @user_id, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, user_id: @user_id, domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visits,visitors,bounce_rate,visit_duration" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 3}, - "visitors" => %{"value" => 2}, - "visits" => %{"value" => 2}, - "bounce_rate" => %{"value" => 50}, - "visit_duration" => %{"value" => 750} - } - end - - describe "comparisons" do - @tag :skip - test "compare period=day with previous period", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2020-12-31 00:00:00]), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "compare" => "previous_period" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 3, "change" => 200}, - "visitors" => %{"value" => 2, "change" => 100}, - "bounce_rate" => %{"value" => 50, "change" => -50}, - "visit_duration" => %{"value" => 750, "change" => 100} - } - end - - @tag :skip - test "compare period=6mo with previous period", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2020-12-31 00:00:00]), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-02-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-03-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "6mo", - "date" => "2021-03-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "compare" => "previous_period" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 4, "change" => 100}, - "visitors" => %{"value" => 3, "change" => 100}, - "bounce_rate" => %{"value" => 100, "change" => 100}, - "visit_duration" => %{"value" => 0, "change" => 0} - } - end - end - - describe "filters" do - @tag :skip - test "can filter by source", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - referrer_source: "Google", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:source==Google" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by no source/referrer", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - referrer_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:source==Direct / None" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by referrer", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - referrer: "https://facebook.com", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:referrer==https://facebook.com" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by utm_medium", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_medium: "social", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:utm_medium==social" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by utm_source", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_source: "Twitter", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:utm_source==Twitter" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by utm_campaign", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_campaign: "profile", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:utm_campaign==profile" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by device type", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - screen_size: "Desktop", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:device==Desktop" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by browser", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - browser: "Chrome", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:browser==Chrome" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by browser version", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - browser: "Chrome", - browser_version: "56", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:browser_version==56" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by operating system", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - operating_system: "Mac", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:os==Mac" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by operating system version", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - operating_system_version: "10.5", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:os_version==10.5" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "can filter by country", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - country_code: "EE", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "visit:country==EE" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "when filtering by page, session metrics treat is like entry_page", %{ - conn: conn, - site: site - } do - populate_stats([ - build(:pageview, - pathname: "/blogpost", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, - pathname: "/blogpost", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "event:page==/blogpost" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 2}, - "visitors" => %{"value" => 2}, - "bounce_rate" => %{"value" => 50}, - "visit_duration" => %{"value" => 750} - } - end - - @tag :skip - test "filtering by event:name", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - domain: site.domain, - user_id: @user_id, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Signup", - domain: site.domain, - user_id: @user_id, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "visitors,pageviews", - "filters" => "event:name==Signup" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 0}, - "visitors" => %{"value" => 2} - } - end - - @tag :skip - test "combining filters", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - pathname: "/blogpost", - country_code: "EE", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, - pathname: "/blogpost", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration", - "filters" => "event:page==/blogpost;visit:country==EE" - }) - - assert json_response(conn, 200)["results"] == %{ - "pageviews" => %{"value" => 1}, - "visitors" => %{"value" => 1}, - "bounce_rate" => %{"value" => 0}, - "visit_duration" => %{"value" => 1500} - } - end - - @tag :skip - test "wildcard page filter", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, pathname: "/en/page1"), - build(:pageview, pathname: "/en/page2"), - build(:pageview, pathname: "/pl/page1") - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "metrics" => "visitors", - "filters" => "event:page==/en/**" - }) - - assert json_response(conn, 200)["results"] == %{"visitors" => %{"value" => 2}} - end - - @tag :skip - test "negated wildcard page filter", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, pathname: "/en/page1"), - build(:pageview, pathname: "/en/page2"), - build(:pageview, pathname: "/pl/page1") - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "metrics" => "visitors", - "filters" => "event:page!=/en/**" - }) - - assert json_response(conn, 200)["results"] == %{"visitors" => %{"value" => 1}} - end - - @tag :skip - test "wildcard and member filter combined", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, pathname: "/en/page1"), - build(:pageview, pathname: "/en/page2"), - build(:pageview, pathname: "/pl/page1"), - build(:pageview, pathname: "/ee/page1") - ]) - - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => site.domain, - "metrics" => "visitors", - "filters" => "event:page==/en/**|/pl/**" - }) - - assert json_response(conn, 200)["results"] == %{"visitors" => %{"value" => 3}} - end - end -end diff --git a/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs b/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs deleted file mode 100644 index 83c3ce0ea422..000000000000 --- a/test/plausible_web/controllers/api/external_stats_controller/auth_test.exs +++ /dev/null @@ -1,113 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalStatsController.AuthTest do - use PlausibleWeb.ConnCase - import Plausible.TestUtils - - setup [:create_user, :create_api_key] - - @tag :skip - test "unauthenticated request - returns 401", %{conn: conn} do - conn = - get(conn, "/api/v1/stats/aggregate", %{ - "site_id" => "some-site.com", - "metrics" => "pageviews" - }) - - assert json_response(conn, 401) == %{ - "error" => "Missing API key. Please use a valid Plausible API key as a Bearer Token." - } - end - - @tag :skip - test "bad API key - returns 401", %{conn: conn} do - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer bad-key") - |> get("/api/v1/stats/aggregate", %{"site_id" => "some-site.com", "metrics" => "pageviews"}) - - assert json_response(conn, 401) == %{ - "error" => - "Invalid API key or site ID. Please make sure you're using a valid API key with access to the site you've requested." - } - end - - @tag :skip - test "good API key but bad site id - returns 401", %{conn: conn, api_key: api_key} do - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key}") - |> get("/api/v1/stats/aggregate", %{"site_id" => "some-site.com", "metrics" => "pageviews"}) - - assert json_response(conn, 401) == %{ - "error" => - "Invalid API key or site ID. Please make sure you're using a valid API key with access to the site you've requested." - } - end - - @tag :skip - test "good API key but missing site id - returns 400", %{conn: conn, api_key: api_key} do - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key}") - |> get("/api/v1/stats/aggregate", %{"metrics" => "pageviews"}) - - assert json_response(conn, 400) == %{ - "error" => - "Missing site ID. Please provide the required site_id parameter with your request." - } - end - - @tag :skip - test "can access with correct API key and site ID", %{conn: conn, user: user, api_key: api_key} do - site = insert(:site, members: [user]) - - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key}") - |> get("/api/v1/stats/aggregate", %{"site_id" => site.domain, "metrics" => "pageviews"}) - - assert json_response(conn, 200) == %{ - "results" => %{"pageviews" => %{"value" => 0}} - } - end - - @tag :skip - test "can access as an admin", %{conn: conn, user: user, api_key: api_key} do - Application.put_env(:plausible, :super_admin_user_ids, [user.id]) - site = insert(:site) - - conn = - conn - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key}") - |> get("/api/v1/stats/aggregate", %{"site_id" => site.domain, "metrics" => "pageviews"}) - - assert json_response(conn, 200) == %{ - "results" => %{"pageviews" => %{"value" => 0}} - } - end - - @tag :skip - test "limits the rate of API requests", %{user: user} do - api_key = insert(:api_key, user_id: user.id, hourly_request_limit: 3) - - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - |> get("/api/v1/stats/aggregate") - - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - |> get("/api/v1/stats/aggregate") - - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - |> get("/api/v1/stats/aggregate") - - conn = - build_conn() - |> Plug.Conn.put_req_header("authorization", "Bearer #{api_key.key}") - |> get("/api/v1/stats/aggregate") - - assert json_response(conn, 429) == %{ - "error" => "Too many API requests. Your API key is limited to 3 requests per hour." - } - end -end diff --git a/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs b/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs deleted file mode 100644 index 3c3f5cf26a6f..000000000000 --- a/test/plausible_web/controllers/api/external_stats_controller/breakdown_test.exs +++ /dev/null @@ -1,1794 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalStatsController.BreakdownTest do - use PlausibleWeb.ConnCase - import Plausible.TestUtils - @user_id 1231 - - setup [:create_user, :create_new_site, :create_api_key, :use_api_key] - - describe "param validation" do - @tag :skip - test "validates that property is required", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain - }) - - assert json_response(conn, 400) == %{ - "error" => - "The `property` parameter is required. Please provide at least one property to show a breakdown by." - } - end - - @tag :skip - test "validates that correct period is used", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "bad_period" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Error parsing `period` parameter: invalid period `bad_period`. Please find accepted values in our docs: https://plausible.io/docs/stats-api#time-periods" - } - end - - @tag :skip - test "fails when an invalid metric is provided", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "property" => "event:page", - "metrics" => "visitors,baa", - "site_id" => site.domain - }) - - assert json_response(conn, 400) == %{ - "error" => - "The metric `baa` is not recognized. Find valid metrics from the documentation: https://plausible.io/docs/stats-api#get-apiv1statsbreakdown" - } - end - - @tag :skip - test "session metrics cannot be used with event:name property", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "property" => "event:name", - "metrics" => "visitors,bounce_rate", - "site_id" => site.domain - }) - - assert json_response(conn, 400) == %{ - "error" => - "Session metric `bounce_rate` cannot be queried for breakdown by `event:name`." - } - end - - @tag :skip - test "session metrics cannot be used with event:props:* property", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "property" => "event:props:url", - "metrics" => "visitors,bounce_rate", - "site_id" => site.domain - }) - - assert json_response(conn, 400) == %{ - "error" => - "Session metric `bounce_rate` cannot be queried for breakdown by `event:props:url`." - } - end - - @tag :skip - test "session metrics cannot be used with event:name filter", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "property" => "event:page", - "filters" => "event:name==Signup", - "metrics" => "visitors,bounce_rate", - "site_id" => site.domain - }) - - assert json_response(conn, 400) == %{ - "error" => - "Session metric `bounce_rate` cannot be queried when using a filter on `event:name`." - } - end - - @tag :skip - test "session metrics cannot be used with event:props:* filter", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "property" => "event:page", - "filters" => "event:props:url==google.com", - "metrics" => "visitors,bounce_rate", - "site_id" => site.domain - }) - - assert json_response(conn, 400) == %{ - "error" => - "Session metric `bounce_rate` cannot be queried when using a filter on `event:props:url`." - } - end - end - - @tag :skip - test "breakdown by visit:source", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - referrer_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - referrer_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - referrer_source: "", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:source" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"source" => "Google", "visitors" => 2}, - %{"source" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:country", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, country_code: "EE", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, country_code: "EE", domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, country_code: "US", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:country" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"country" => "EE", "visitors" => 2}, - %{"country" => "US", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:referrer", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - referrer: "https://ref.com", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - referrer: "https://ref.com", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - referrer: "", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:referrer" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"referrer" => "https://ref.com", "visitors" => 2}, - %{"referrer" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:utm_medium", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_medium: "Search", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - utm_medium: "Search", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - utm_medium: "", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:utm_medium" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"utm_medium" => "Search", "visitors" => 2}, - %{"utm_medium" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:utm_source", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - utm_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - utm_source: "", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:utm_source" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"utm_source" => "Google", "visitors" => 2}, - %{"utm_source" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:utm_campaign", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - utm_campaign: "ads", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - utm_campaign: "ads", - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - utm_campaign: "", - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:utm_campaign" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"utm_campaign" => "ads", "visitors" => 2}, - %{"utm_campaign" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:utm_content", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - utm_content: "Content1", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - utm_content: "Content1", - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - utm_content: "", - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:utm_content" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"utm_content" => "Content1", "visitors" => 2}, - %{"utm_content" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:utm_term", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - utm_term: "Term1", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - utm_term: "Term1", - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - utm_term: "", - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:utm_term" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"utm_term" => "Term1", "visitors" => 2}, - %{"utm_term" => "Direct / None", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:device", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - screen_size: "Desktop", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - screen_size: "Desktop", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - screen_size: "Mobile", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:device" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"device" => "Desktop", "visitors" => 2}, - %{"device" => "Mobile", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:os", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - operating_system: "Mac", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - operating_system: "Mac", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - operating_system: "Windows", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:os" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"os" => "Mac", "visitors" => 2}, - %{"os" => "Windows", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:os_version", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - operating_system_version: "10.5", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - operating_system_version: "10.5", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - operating_system_version: "10.6", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:os_version" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"os_version" => "10.5", "visitors" => 2}, - %{"os_version" => "10.6", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:browser", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, browser: "Firefox", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, browser: "Firefox", domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, browser: "Safari", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:browser" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"browser" => "Firefox", "visitors" => 2}, - %{"browser" => "Safari", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by visit:browser_version", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - browser_version: "56", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - browser_version: "56", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - browser_version: "57", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:browser_version" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"browser_version" => "56", "visitors" => 2}, - %{"browser_version" => "57", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by event:page", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, pathname: "/", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, pathname: "/", domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/", "visitors" => 2}, - %{"page" => "/plausible.io", "visitors" => 1} - ] - } - end - - describe "custom events" do - @tag :skip - test "can breakdown by event:name", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:name" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"name" => "Signup", "visitors" => 2}, - %{"name" => "pageview", "visitors" => 1} - ] - } - end - - @tag :skip - test "can breakdown by event:name with visitors and events metrics", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - domain: site.domain, - pathname: "/non-existing", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "404", - domain: site.domain, - pathname: "/non-existing", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - domain: site.domain, - pathname: "/non-existing", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:01] - ), - build(:pageview, - domain: site.domain, - pathname: "/non-existing", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:02] - ), - build(:event, - name: "404", - domain: site.domain, - pathname: "/non-existing", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:02] - ), - build(:pageview, - domain: site.domain, - pathname: "/non-existing", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:03] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:name", - "metrics" => "visitors,events" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"name" => "pageview", "visitors" => 1, "events" => 4}, - %{"name" => "404", "visitors" => 1, "events" => 2} - ] - } - end - - @tag :skip - test "can breakdown by event:name while filtering for something", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Signup", - pathname: "/pageA", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - pathname: "/pageA", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - pathname: "/pageA", - browser: "Safari", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - pathname: "/pageB", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/pageA", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:name", - "filters" => "event:page==/pageA;visit:browser==Chrome" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"name" => "Signup", "visitors" => 2}, - %{"name" => "pageview", "visitors" => 1} - ] - } - end - - @tag :skip - test "can breakdown by a visit:property when filtering by event:name", %{ - conn: conn, - site: site - } do - populate_stats([ - build(:pageview, - referrer_source: "Google", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - domain: site.domain, - referrer_source: "Twitter", - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:source", - "filters" => "event:name==Signup" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"source" => "Google", "visitors" => 1} - ] - } - end - - @tag :skip - test "can breakdown by event:name when filtering by event:page", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - pathname: "/pageA", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/pageA", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - pathname: "/pageA", - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/pageB", - domain: site.domain, - referrer_source: "Twitter", - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:name", - "filters" => "event:page==/pageA" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"name" => "pageview", "visitors" => 2}, - %{"name" => "Signup", "visitors" => 1} - ] - } - end - - @tag :skip - test "can breakdown by event:page when filtering by event:name", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Signup", - pathname: "/pageA", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - pathname: "/pageA", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - pathname: "/pageB", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/pageB", - domain: site.domain, - referrer_source: "Twitter", - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "filters" => "event:name==Signup" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/pageA", "visitors" => 2}, - %{"page" => "/pageB", "visitors" => 1} - ] - } - end - - @tag :skip - test "can filter event:page with a wildcard", %{ - conn: conn, - site: site - } do - populate_stats(site, [ - build(:pageview, pathname: "/en/page1"), - build(:pageview, pathname: "/en/page2"), - build(:pageview, pathname: "/en/page2"), - build(:pageview, pathname: "/pl/page1") - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "property" => "event:page", - "filters" => "event:page==/en/**" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/en/page2", "visitors" => 2}, - %{"page" => "/en/page1", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by custom event property", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["personal"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Some other event", - "meta.key": ["package"], - "meta.value": ["business"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:props:package", - "filters" => "event:name==Purchase" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"package" => "business", "visitors" => 2}, - %{"package" => "personal", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by custom event property, with (none)", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["16"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["16"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["14"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["14"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:26:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:props:cost", - "filters" => "event:name==Purchase" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"cost" => "16", "visitors" => 2}, - %{"cost" => "14", "visitors" => 2}, - %{"cost" => "(none)", "visitors" => 1} - ] - } - end - - @tag :skip - test "breakdown by custom event property, limited", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["16"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["16"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["18"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["14"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["14"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:26:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:props:cost", - "filters" => "event:name==Purchase", - "limit" => 2 - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"cost" => "14", "visitors" => 2}, - %{"cost" => "16", "visitors" => 2} - ] - } - end - - @tag :skip - test "breakdown by custom event property, paginated", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["16"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["16"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["18"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["14"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["cost"], - "meta.value": ["14"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:26:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:props:cost", - "filters" => "event:name==Purchase", - "limit" => 2, - "page" => 2 - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"cost" => "18", "visitors" => 1} - ] - } - end - end - - describe "breakdown by event:goal" do - @tag :skip - test "custom properties from custom events are returned", %{conn: conn, site: site} do - insert(:goal, %{domain: site.domain, event_name: "404"}) - insert(:goal, %{domain: site.domain, event_name: "Purchase"}) - insert(:goal, %{domain: site.domain, page_path: "/test"}) - - populate_stats([ - build(:pageview, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00], - pathname: "/test" - ), - build(:pageview, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:01], - pathname: "/test", - "meta.key": ["method"], - "meta.value": ["HTTP"] - ), - build(:event, - name: "404", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:02], - "meta.key": ["method"], - "meta.value": ["HTTP"] - ), - build(:event, - name: "Purchase", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:02], - "meta.key": ["method"], - "meta.value": ["HTTPS"] - ), - build(:event, - name: "404", - timestamp: ~N[2021-01-01 00:00:03], - domain: site.domain, - "meta.key": ["OS", "method"], - "meta.value": ["Linux", "HTTP"] - ), - build(:event, - name: "404", - timestamp: ~N[2021-01-01 00:00:04], - domain: site.domain, - "meta.key": ["version"], - "meta.value": ["1"] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:goal" - }) - - res = - Enum.map(json_response(conn, 200)["results"], fn item -> - Map.update(item, "props", [], fn x -> Enum.sort(x) end) - end) - - assert res == [ - %{ - "goal" => "404", - "props" => ["OS", "method", "version"], - "visitors" => 3 - }, - %{ - "goal" => "Visit /test", - "props" => [], - "visitors" => 2 - }, - %{ - "goal" => "Purchase", - "props" => ["method"], - "visitors" => 1 - } - ] - end - end - - describe "filtering" do - @tag :skip - test "event:page filter for breakdown by session props", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - pathname: "/ignore", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - browser: "Safari", - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:browser", - "filters" => "event:page==/plausible.io" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"browser" => "Chrome", "visitors" => 2}, - %{"browser" => "Safari", "visitors" => 1} - ] - } - end - - @tag :skip - test "event:page filter shows traffic sources directly to that page", %{ - conn: conn, - site: site - } do - populate_stats(site, [ - build(:pageview, - pathname: "/ignore", - referrer_source: "Should not show up", - utm_medium: "Should not show up", - utm_source: "Should not show up", - utm_campaign: "Should not show up", - user_id: @user_id - ), - build(:pageview, - pathname: "/plausible.io", - user_id: @user_id - ), - build(:pageview, - pathname: "/plausible.io", - referrer_source: "Google", - utm_medium: "Google", - utm_source: "Google", - utm_campaign: "Google" - ) - ]) - - for property <- ["source", "utm_medium", "utm_source", "utm_campaign"] do - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "property" => "visit:" <> property, - "filters" => "event:page==/plausible.io" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{property => "Google", "visitors" => 1} - ] - } - end - end - - @tag :skip - test "event:goal pageview filter for breakdown by visit source", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - referrer_source: "Bing", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - referrer_source: "Google", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:source", - "filters" => "event:goal == Visit /plausible.io" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"source" => "Google", "visitors" => 1} - ] - } - end - - @tag :skip - test "event:goal custom event filter for breakdown by visit source", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - referrer_source: "Bing", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - referrer_source: "Google", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Register", - user_id: @user_id, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:source", - "filters" => "event:goal == Register" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"source" => "Google", "visitors" => 1} - ] - } - end - - @tag :skip - test "event:goal custom event filter for breakdown by event page", %{conn: conn, site: site} do - populate_stats(site, [ - build(:event, - pathname: "/en/register", - name: "Register" - ), - build(:event, - pathname: "/en/register", - name: "Register" - ), - build(:event, - pathname: "/it/register", - name: "Register" - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "property" => "event:page", - "filters" => "event:goal == Register" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/en/register", "visitors" => 2}, - %{"page" => "/it/register", "visitors" => 1} - ] - } - end - - @tag :skip - test "IN filter for event:page", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - pathname: "/ignore", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/important-page", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "filters" => "event:page == /plausible.io|/important-page" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/plausible.io", "visitors" => 2}, - %{"page" => "/important-page", "visitors" => 1} - ] - } - end - - @tag :skip - test "IN filter for visit:browser", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - pathname: "/ignore", - browser: "Firefox", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - browser: "Safari", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/important-page", - browser: "Safari", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "filters" => "visit:browser == Chrome|Safari" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/plausible.io", "visitors" => 2}, - %{"page" => "/important-page", "visitors" => 1} - ] - } - end - - @tag :skip - test "IN filter for visit:entry_page", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - pathname: "/ignore", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/important-page", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "filters" => "event:page == /plausible.io|/important-page", - "metrics" => "bounce_rate" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"page" => "/plausible.io", "bounce_rate" => 100}, - %{"page" => "/important-page", "bounce_rate" => 100} - ] - } - end - - @tag :skip - test "IN filter for event:name", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Login", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Irrelevant", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:name", - "filters" => "event:name == Signup|Login" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"name" => "Signup", "visitors" => 2}, - %{"name" => "Login", "visitors" => 1} - ] - } - end - - @tag :skip - test "can use a is_not filter", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, browser: "Chrome"), - build(:pageview, browser: "Safari"), - build(:pageview, browser: "Safari"), - build(:pageview, browser: "Edge") - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "filters" => "visit:browser != Chrome", - "property" => "visit:browser" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"browser" => "Safari", "visitors" => 2}, - %{"browser" => "Edge", "visitors" => 1} - ] - } - end - end - - describe "pagination" do - @tag :skip - test "can limit results", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, pathname: "/a", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, pathname: "/b", domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, pathname: "/c", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "limit" => 2 - }) - - res = json_response(conn, 200) - assert Enum.count(res["results"]) == 2 - end - - @tag :skip - test "can paginate results", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, pathname: "/a", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, pathname: "/b", domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, pathname: "/c", domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "limit" => 2, - "page" => 2 - }) - - res = json_response(conn, 200) - assert Enum.count(res["results"]) == 1 - end - end - - describe "metrics" do - @tag :skip - test "all metrics for breakdown by visit prop", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, - user_id: 1, - referrer_source: "Google", - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: 1, - referrer_source: "Google", - timestamp: ~N[2021-01-01 00:10:00] - ), - build(:pageview, - referrer_source: "Google", - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:pageview, - referrer_source: "Twitter", - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:source", - "metrics" => "visitors,visits,pageviews,bounce_rate,visit_duration" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{ - "source" => "Google", - "visitors" => 2, - "visits" => 2, - "bounce_rate" => 50, - "visit_duration" => 300, - "pageviews" => 3 - }, - %{ - "source" => "Twitter", - "visitors" => 1, - "visits" => 1, - "bounce_rate" => 100, - "visit_duration" => 0, - "pageviews" => 1 - } - ] - } - end - - @tag :skip - test "filter by custom event property", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - browser: "Chrome", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - browser: "Safari", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - browser: "Safari", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["personal"], - browser: "IE", - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "visit:browser", - "filters" => "event:name==Purchase;event:props:package==business" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"browser" => "Safari", "visitors" => 2}, - %{"browser" => "Chrome", "visitors" => 1} - ] - } - end - - @tag :skip - test "all metrics for breakdown by event prop", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - user_id: 1, - pathname: "/", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: 1, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:10:00] - ), - build(:pageview, pathname: "/", domain: site.domain, timestamp: ~N[2021-01-01 00:25:00]), - build(:pageview, - pathname: "/plausible.io", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/breakdown", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "property" => "event:page", - "metrics" => "visitors,pageviews,events,bounce_rate,visit_duration" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{ - "page" => "/", - "visitors" => 2, - "bounce_rate" => 50, - "visit_duration" => 300, - "pageviews" => 2, - "events" => 2 - }, - %{ - "page" => "/plausible.io", - "visitors" => 2, - "bounce_rate" => 100, - "visit_duration" => 0, - "pageviews" => 2, - "events" => 2 - } - ] - } - end - end -end diff --git a/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs b/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs deleted file mode 100644 index 9cc7b5d2d0ad..000000000000 --- a/test/plausible_web/controllers/api/external_stats_controller/timeseries_test.exs +++ /dev/null @@ -1,824 +0,0 @@ -defmodule PlausibleWeb.Api.ExternalStatsController.TimeseriesTest do - use PlausibleWeb.ConnCase - import Plausible.TestUtils - - setup [:create_user, :create_new_site, :create_api_key, :use_api_key] - - describe "param validation" do - @tag :skip - test "validates that date can be parsed", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "6mo", - "date" => "2021-dkjbAS" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Error parsing `date` parameter: invalid_format. Please specify a valid date in ISO-8601 format." - } - end - - @tag :skip - test "validates that period can be parsed", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "aosuhsacp" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Error parsing `period` parameter: invalid period `aosuhsacp`. Please find accepted values in our docs: https://plausible.io/docs/stats-api#time-periods" - } - end - - @tag :skip - test "validates that interval is `date` or `month`", %{conn: conn, site: site} do - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "12mo", - "interval" => "alskd" - }) - - assert json_response(conn, 400) == %{ - "error" => - "Error parsing `interval` parameter: invalid interval `alskd`. Valid intervals are `date`, `month`" - } - end - end - - @user_id 123 - @tag :skip - test "shows hourly data for a certain date", %{conn: conn, site: site} do - populate_stats(site, [ - build(:pageview, user_id: @user_id, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, user_id: @user_id, timestamp: ~N[2021-01-01 00:10:00]), - build(:pageview, timestamp: ~N[2021-01-01 23:59:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "day", - "date" => "2021-01-01", - "metrics" => "visitors,pageviews,visits,visit_duration,bounce_rate" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{ - "date" => "2021-01-01 00:00:00", - "visitors" => 1, - "visits" => 1, - "pageviews" => 2, - "visit_duration" => 600, - "bounce_rate" => 0 - }, - %{ - "date" => "2021-01-01 01:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 02:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 03:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 04:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 05:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 06:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 07:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 08:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 09:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 10:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 11:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 12:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 13:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 14:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 15:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 16:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 17:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 18:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 19:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 20:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 21:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 22:00:00", - "visitors" => 0, - "visits" => 0, - "pageviews" => 0, - "visit_duration" => nil, - "bounce_rate" => nil - }, - %{ - "date" => "2021-01-01 23:00:00", - "visitors" => 1, - "visits" => 1, - "pageviews" => 1, - "visit_duration" => 0, - "bounce_rate" => 100 - } - ] - } - end - - @tag :skip - test "shows last 7 days of visitors", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-07 23:59:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "7d", - "date" => "2021-01-07" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"date" => "2021-01-01", "visitors" => 1}, - %{"date" => "2021-01-02", "visitors" => 0}, - %{"date" => "2021-01-03", "visitors" => 0}, - %{"date" => "2021-01-04", "visitors" => 0}, - %{"date" => "2021-01-05", "visitors" => 0}, - %{"date" => "2021-01-06", "visitors" => 0}, - %{"date" => "2021-01-07", "visitors" => 1} - ] - } - end - - @tag :skip - test "shows last 6 months of visitors", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2020-12-31 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "6mo", - "date" => "2021-01-01" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"date" => "2020-08-01", "visitors" => 0}, - %{"date" => "2020-09-01", "visitors" => 0}, - %{"date" => "2020-10-01", "visitors" => 0}, - %{"date" => "2020-11-01", "visitors" => 0}, - %{"date" => "2020-12-01", "visitors" => 1}, - %{"date" => "2021-01-01", "visitors" => 2} - ] - } - end - - @tag :skip - test "shows last 12 months of visitors", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2020-02-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2020-12-31 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "12mo", - "date" => "2021-01-01" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"date" => "2020-02-01", "visitors" => 1}, - %{"date" => "2020-03-01", "visitors" => 0}, - %{"date" => "2020-04-01", "visitors" => 0}, - %{"date" => "2020-05-01", "visitors" => 0}, - %{"date" => "2020-06-01", "visitors" => 0}, - %{"date" => "2020-07-01", "visitors" => 0}, - %{"date" => "2020-08-01", "visitors" => 0}, - %{"date" => "2020-09-01", "visitors" => 0}, - %{"date" => "2020-10-01", "visitors" => 0}, - %{"date" => "2020-11-01", "visitors" => 0}, - %{"date" => "2020-12-01", "visitors" => 1}, - %{"date" => "2021-01-01", "visitors" => 2} - ] - } - end - - @tag :skip - test "shows last 12 months of visitors with interval daily", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2020-02-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2020-12-31 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "12mo", - "interval" => "date" - }) - - res = json_response(conn, 200) - assert Enum.count(res["results"]) in [365, 366] - end - - @tag :skip - test "shows a custom range with daily interval", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-02 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "custom", - "date" => "2021-01-01,2021-01-02" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{"date" => "2021-01-01", "visitors" => 2}, - %{"date" => "2021-01-02", "visitors" => 1} - ] - } - end - - @tag :skip - test "shows a custom range with monthly interval", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, user_id: @user_id, domain: site.domain, timestamp: ~N[2020-12-01 00:00:00]), - build(:pageview, user_id: @user_id, domain: site.domain, timestamp: ~N[2020-12-01 00:05:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-02 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "custom", - "date" => "2020-12-01, 2021-01-02", - "interval" => "month", - "metrics" => "pageviews,visitors,bounce_rate,visit_duration" - }) - - assert json_response(conn, 200) == %{ - "results" => [ - %{ - "date" => "2020-12-01", - "visitors" => 1, - "pageviews" => 2, - "bounce_rate" => 0, - "visit_duration" => 300 - }, - %{ - "date" => "2021-01-01", - "visitors" => 2, - "pageviews" => 2, - "bounce_rate" => 100, - "visit_duration" => 0 - } - ] - } - end - - describe "filters" do - @tag :skip - test "can filter by source", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - referrer_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:source==Google" - }) - - res = json_response(conn, 200) - assert List.first(res["results"]) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by no source/referrer", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]), - build(:pageview, - referrer_source: "Google", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:source==Direct / None" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by referrer", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - referrer: "https://facebook.com", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:referrer==https://facebook.com" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by utm_medium", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_medium: "social", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:utm_medium==social" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by utm_source", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_source: "Twitter", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:utm_source==Twitter" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by utm_campaign", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - utm_campaign: "profile", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:utm_campaign==profile" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by device type", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - screen_size: "Desktop", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:device==Desktop" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by browser", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - browser: "Chrome", - browser_version: "56.1", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - browser: "Chrome", - browser_version: "55", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:browser==Chrome;visit:browser_version==56.1" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by operating system", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - operating_system: "Mac", - operating_system_version: "10.5", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - operating_system: "Something else", - operating_system_version: "10.5", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - operating_system: "Mac", - operating_system_version: "10.4", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:os == Mac;visit:os_version==10.5" - }) - - res = json_response(conn, 200)["results"] - assert List.first(res) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "can filter by country", %{conn: conn, site: site} do - populate_stats([ - build(:pageview, - user_id: @user_id, - country_code: "EE", - operating_system_version: "10.5", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - user_id: @user_id, - country_code: "EE", - operating_system_version: "10.5", - domain: site.domain, - timestamp: ~N[2021-01-01 00:15:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "visit:country==EE", - "metrics" => "visitors,pageviews,bounce_rate,visit_duration" - }) - - res = json_response(conn, 200)["results"] - - assert List.first(res) == %{ - "date" => "2021-01-01", - "visitors" => 1, - "pageviews" => 2, - "bounce_rate" => 0, - "visit_duration" => 900 - } - end - - @tag :skip - test "filtering by page - session metrics consider it like entry_page", %{ - conn: conn, - site: site - } do - populate_stats([ - build(:pageview, - pathname: "/hello", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, - pathname: "/hello", - user_id: @user_id, - domain: site.domain, - timestamp: ~N[2021-01-01 00:05:00] - ), - build(:pageview, - pathname: "/hello", - domain: site.domain, - timestamp: ~N[2021-01-01 05:00:00] - ), - build(:pageview, - pathname: "/goobye", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "event:page==/hello", - "metrics" => "visitors,pageviews,bounce_rate,visit_duration" - }) - - res = json_response(conn, 200)["results"] - - assert List.first(res) == %{ - "date" => "2021-01-01", - "visitors" => 2, - "pageviews" => 3, - "bounce_rate" => 50, - "visit_duration" => 150 - } - end - - @tag :skip - test "can filter by event:name", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Signup", - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:pageview, domain: site.domain, timestamp: ~N[2021-01-01 00:00:00]) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "event:name==Signup" - }) - - res = json_response(conn, 200) - assert List.first(res["results"]) == %{"date" => "2021-01-01", "visitors" => 1} - end - - @tag :skip - test "filter by custom event property", %{conn: conn, site: site} do - populate_stats([ - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:00:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["personal"], - domain: site.domain, - timestamp: ~N[2021-01-01 00:25:00] - ), - build(:event, - name: "Purchase", - "meta.key": ["package"], - "meta.value": ["business"], - domain: site.domain, - timestamp: ~N[2021-01-02 00:25:00] - ) - ]) - - conn = - get(conn, "/api/v1/stats/timeseries", %{ - "site_id" => site.domain, - "period" => "month", - "date" => "2021-01-01", - "filters" => "event:name==Purchase;event:props:package==business" - }) - - %{"results" => [first, second | _rest]} = json_response(conn, 200) - assert first == %{"date" => "2021-01-01", "visitors" => 2} - assert second == %{"date" => "2021-01-02", "visitors" => 1} - end - end -end diff --git a/test/plausible_web/controllers/api/paddle_controller_test.exs b/test/plausible_web/controllers/api/paddle_controller_test.exs deleted file mode 100644 index 9a7e59866ecd..000000000000 --- a/test/plausible_web/controllers/api/paddle_controller_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -defmodule PlausibleWeb.Api.PaddleControllerTest do - use PlausibleWeb.ConnCase - use Plausible.Repo - - @body %{ - "alert_id" => "16173800", - "alert_name" => "subscription_created", - "cancel_url" => - "https://checkout.paddle.com/subscription/cancel?user=1032746&subscription=1869424&hash=eyJpdiI6Ik5XSnhkT1k2RSticStZcEVYZ2FEeTVZTHVSaTNpcFM4XC9aMFJNSjh6TFNrPSIsInZhbHVlIjoiWTMzd3dENGQySTc0UjVPM0U3dzVDUWlBMEQ3dGVzM2lyODRPczBVcjdBRXlncmQzRUVmQjhnZVhpTzJIRjFtNW41MmZOVmJNVUJ4empmeXgyUno1Z3c9PSIsIm1hYyI6ImFjNDMyNDIxNmNmMWNiNmViMmFlNGVkMzQ3ZjYyYTQ5ZWI2YTEyMDQ4YjFhNTEyMjAxNzNlNjEwOWU4OTVhOWMifQ%3D%3D", - "checkout_id" => "38111668-chre6449b9c3cc8-f473e48a86", - "currency" => "USD", - "email" => "josh@joshuae.com", - "event_time" => "2019-08-20 21:44:54", - "marketing_consent" => 0, - "next_bill_date" => "2019-09-20", - "passthrough" => "235", - "quantity" => "1", - "status" => "active", - "subscription_id" => "1869424", - "subscription_plan_id" => "558018", - "unit_price" => "6.00", - "update_url" => - "https://checkout.paddle.com/subscription/update?user=1032746&subscription=1869424&hash=eyJpdiI6IkNUS2VZQlRxcFA5MVlEXC9Oa1ZwRDBNaGN4VVwvaU1RU2srWXc0bU1tQndyTT0iLCJ2YWx1ZSI6ImtTeU1ESkxWcEVrTDFmKzkyZ0FaSFo2Q0VNK3A2XC9NdEU4S2tGVFE2blJicGxBQzZ1XC9mMG1PcUo3MWV2OGY4YURjT1UxY2hpeHh5SFhlMmhXaFpoalE9PSIsIm1hYyI6ImZkNGU5MTg3YzQxZWYxNDJjNDkyMWFkYmZhZjIyMGQ3ZGI2YTVmMTcxZGViY2VkNzI0ZjNlMDRkZTgwNTEwMzUifQ%3D%3D", - "user_id" => "1032746", - "p_signature" => - "qfqKA3dI9d60uie9IORcvkHYV+rd1UaCu/f5kh4miTkeIQNimgusQG8pS1OHobCvN/OktwKCjFcbIwoa4nakOOWGroHJ8FjLJHBK4g1uI37Bp6l73dNl8mB4dNGW1M+atkz7ag6pETRIdEKCmC5tV9afN5CvbcqRV1lsj/x2fAsjAe/sQkmAP1jbDXOMEuHqkWssSB7Q+NGHHLHuNQ67m7YFBnZSgYzLeLMEApkZClJn0j6MokUVjW37ISn5eA5FlUbT7s6Kph54roRzLIpYvC+ff/n6ae2Iu1OsORxRBg4Uv8dqqjqBKXlv84/OB80U89yMIbRw/pbHD6+zF4FxgNV7nk2bjgK2V6h55AOuhJHHUMb4XX9R8i8iG1FOlNJaTwbhkIkvQF3q7nEItKCqizn+l4tFQ9MUcrjw8jytDznbOnSlmNhtcDVlnvXNDaSPkEA7AyR6c+BiZV/Y6I3y8sr8h/F/cBM3OPTwfdKK34jyWW4LRn15nSxq2kjH3SyLPEpTJUMdcRGAgBZc06E4lENU2x22E/JKG5BRi1aDs5OFQtrjYi2hOTI0dyPF3OLNeZcCgBCKBmKq5XIf1T0RPFWAWtKkzXhl/QH+4feNATb9/i6k5xKeUJf0ltWzsI5x84kvsC/m05hn/AuBDmcZGkVnDLXrqttR+zDXY6P1euE=" - } - - describe "webhook verification" do - @tag :skip - test "is verified when signature is correct", %{conn: conn} do - insert(:user, id: 235) - conn = post(conn, "/api/paddle/webhook", @body) - - assert conn.status == 200 - end - - @tag :skip - test "not verified when signature is corrupted", %{conn: conn} do - corrupted = Map.put(@body, "p_signature", Base.encode64("123 fake signature")) - conn = post(conn, "/api/paddle/webhook", corrupted) - assert conn.status == 400 - end - end -end diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs deleted file mode 100644 index 1a05b853595d..000000000000 --- a/test/plausible_web/controllers/auth_controller_test.exs +++ /dev/null @@ -1,632 +0,0 @@ -defmodule PlausibleWeb.AuthControllerTest do - use PlausibleWeb.ConnCase - use Bamboo.Test - use Plausible.Repo - import Plausible.TestUtils - - describe "GET /register" do - @tag :skip - test "shows the register form", %{conn: conn} do - conn = get(conn, "/register") - - assert html_response(conn, 200) =~ "Enter your details" - end - end - - describe "POST /register" do - @tag :skip - test "registering sends an activation link", %{conn: conn} do - post(conn, "/register", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject}) - assert user_email == "user@example.com" - assert subject =~ "is your Plausible email verification code" - end - - @tag :skip - test "user is redirected to activate page after registration", %{conn: conn} do - conn = - post(conn, "/register", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert redirected_to(conn, 302) == "/activate" - end - - @tag :skip - test "creates user record", %{conn: conn} do - post(conn, "/register", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - user = Repo.one(Plausible.Auth.User) - assert user.name == "Jane Doe" - end - - @tag :skip - test "logs the user in", %{conn: conn} do - conn = - post(conn, "/register", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert get_session(conn, :current_user_id) - end - - @tag :skip - test "user is redirected to activation after registration", %{conn: conn} do - conn = - post(conn, "/register", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert redirected_to(conn) == "/activate" - end - end - - describe "GET /register/invitations/:invitation_id" do - @tag :skip - test "shows the register form", %{conn: conn} do - inviter = insert(:user) - site = insert(:site, members: [inviter]) - - invitation = - insert(:invitation, - site_id: site.id, - inviter: inviter, - email: "user@email.co", - role: :admin - ) - - conn = get(conn, "/register/invitation/#{invitation.invitation_id}") - - assert html_response(conn, 200) =~ "Enter your details" - end - end - - describe "POST /register/invitation/:invitation_id" do - setup do - inviter = insert(:user) - site = insert(:site, members: [inviter]) - - invitation = - insert(:invitation, - site_id: site.id, - inviter: inviter, - email: "user@email.co", - role: :admin - ) - - {:ok, %{site: site, invitation: invitation}} - end - - @tag :skip - test "registering sends an activation link", %{conn: conn, invitation: invitation} do - post(conn, "/register/invitation/#{invitation.invitation_id}", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject}) - assert user_email == "user@example.com" - assert subject =~ "is your Plausible email verification code" - end - - @tag :skip - test "user is redirected to activate page after registration", %{ - conn: conn, - invitation: invitation - } do - conn = - post(conn, "/register/invitation/#{invitation.invitation_id}", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert redirected_to(conn, 302) == "/activate" - end - - @tag :skip - test "creates user record", %{conn: conn, invitation: invitation} do - post(conn, "/register/invitation/#{invitation.invitation_id}", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - user = Repo.get_by(Plausible.Auth.User, email: "user@example.com") - assert user.name == "Jane Doe" - end - - @tag :skip - test "leaves trial_expiry_date null when invitation role is not :owner", %{ - conn: conn, - invitation: invitation - } do - post(conn, "/register/invitation/#{invitation.invitation_id}", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - user = Repo.get_by(Plausible.Auth.User, email: "user@example.com") - assert is_nil(user.trial_expiry_date) - end - - @tag :skip - test "logs the user in", %{conn: conn, invitation: invitation} do - conn = - post(conn, "/register/invitation/#{invitation.invitation_id}", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert get_session(conn, :current_user_id) - end - - @tag :skip - test "user is redirected to activation after registration", %{conn: conn} do - conn = - post(conn, "/register", - user: %{ - name: "Jane Doe", - email: "user@example.com", - password: "very-secret", - password_confirmation: "very-secret" - } - ) - - assert redirected_to(conn) == "/activate" - end - end - - describe "GET /activate" do - setup [:create_user, :log_in] - - @tag :skip - test "if user does not have a code: prompts user to request activation code", %{conn: conn} do - conn = get(conn, "/activate") - - assert html_response(conn, 200) =~ "Request activation code" - end - - @tag :skip - test "if user does have a code: prompts user to enter the activation code from their email", - %{conn: conn} do - conn = - post(conn, "/activate/request-code") - |> get("/activate") - - assert html_response(conn, 200) =~ "Please enter the 4-digit code we sent to" - end - end - - describe "POST /activate/request-code" do - setup [:create_user, :log_in] - - @tag :skip - test "associates an activation pin with the user account", %{conn: conn, user: user} do - post(conn, "/activate/request-code") - - code = - Repo.one( - from c in "email_verification_codes", - where: c.user_id == ^user.id, - select: %{user_id: c.user_id, issued_at: c.issued_at} - ) - - assert code[:user_id] == user.id - assert Timex.after?(code[:issued_at], Timex.now() |> Timex.shift(seconds: -10)) - end - - @tag :skip - test "sends activation email to user", %{conn: conn, user: user} do - post(conn, "/activate/request-code") - - assert_delivered_email_matches(%{to: [{_, user_email}], subject: subject}) - assert user_email == user.email - assert subject =~ "is your Plausible email verification code" - end - - @tag :skip - test "redirets user to /activate", %{conn: conn} do - conn = post(conn, "/activate/request-code") - - assert redirected_to(conn, 302) == "/activate" - end - end - - describe "POST /activate" do - setup [:create_user, :log_in] - - @tag :skip - test "with wrong pin - reloads the form with error", %{conn: conn} do - conn = post(conn, "/activate", %{code: "1234"}) - - assert html_response(conn, 200) =~ "Incorrect activation code" - end - - @tag :skip - test "with expired pin - reloads the form with error", %{conn: conn, user: user} do - Repo.insert_all("email_verification_codes", [ - %{ - code: 1234, - user_id: user.id, - issued_at: Timex.shift(Timex.now(), days: -1) - } - ]) - - conn = post(conn, "/activate", %{code: "1234"}) - - assert html_response(conn, 200) =~ "Code is expired, please request another one" - end - - @tag :skip - test "marks the user account as active", %{conn: conn, user: user} do - Repo.update!(Plausible.Auth.User.changeset(user, %{email_verified: false})) - post(conn, "/activate/request-code") - - code = - Repo.one( - from c in "email_verification_codes", where: c.user_id == ^user.id, select: c.code - ) - |> Integer.to_string() - - conn = post(conn, "/activate", %{code: code}) - user = Repo.get_by(Plausible.Auth.User, id: user.id) - - assert user.email_verified - assert redirected_to(conn) == "/sites/new" - end - - @tag :skip - test "redirects to /sites if user has invitation", %{conn: conn, user: user} do - site = insert(:site) - insert(:invitation, inviter: build(:user), site: site, email: user.email) - Repo.update!(Plausible.Auth.User.changeset(user, %{email_verified: false})) - post(conn, "/activate/request-code") - - code = - Repo.one( - from c in "email_verification_codes", where: c.user_id == ^user.id, select: c.code - ) - |> Integer.to_string() - - conn = post(conn, "/activate", %{code: code}) - - assert redirected_to(conn) == "/sites" - end - - @tag :skip - test "removes the user association from the verification code", %{conn: conn, user: user} do - Repo.update!(Plausible.Auth.User.changeset(user, %{email_verified: false})) - post(conn, "/activate/request-code") - - code = - Repo.one( - from c in "email_verification_codes", where: c.user_id == ^user.id, select: c.code - ) - |> Integer.to_string() - - post(conn, "/activate", %{code: code}) - - refute Repo.exists?(from c in "email_verification_codes", where: c.user_id == ^user.id) - end - end - - describe "GET /login_form" do - test "shows the login form", %{conn: conn} do - conn = get(conn, "/login") - assert html_response(conn, 200) =~ "Enter your email and password" - end - end - - describe "POST /login" do - test "valid email and password - logs the user in", %{conn: conn} do - user = insert(:user, password: "password") - - conn = post(conn, "/login", email: user.email, password: "password") - - assert get_session(conn, :current_user_id) == user.id - assert redirected_to(conn) == "/sites" - end - - test "email does not exist - renders login form again", %{conn: conn} do - conn = post(conn, "/login", email: "user@example.com", password: "password") - - assert get_session(conn, :current_user_id) == nil - assert html_response(conn, 200) =~ "Enter your email and password" - end - - test "bad password - renders login form again", %{conn: conn} do - user = insert(:user, password: "password") - conn = post(conn, "/login", email: user.email, password: "wrong") - - assert get_session(conn, :current_user_id) == nil - assert html_response(conn, 200) =~ "Enter your email and password" - end - - test "limits login attempts to 5 per minute" do - user = insert(:user, password: "password") - - build_conn() - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> post("/login", email: user.email, password: "wrong") - - build_conn() - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> post("/login", email: user.email, password: "wrong") - - build_conn() - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> post("/login", email: user.email, password: "wrong") - - build_conn() - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> post("/login", email: user.email, password: "wrong") - - build_conn() - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> post("/login", email: user.email, password: "wrong") - - conn = - build_conn() - |> put_req_header("x-forwarded-for", "1.1.1.1") - |> post("/login", email: user.email, password: "wrong") - - assert get_session(conn, :current_user_id) == nil - assert html_response(conn, 429) =~ "Too many login attempts" - end - end - - describe "GET /password/request-reset" do - @tag :skip - test "renders the form", %{conn: conn} do - conn = get(conn, "/password/request-reset") - assert html_response(conn, 200) =~ "Enter your email so we can send a password reset link" - end - end - - describe "POST /password/request-reset" do - @tag :skip - test "email is empty - renders form with error", %{conn: conn} do - conn = post(conn, "/password/request-reset", %{email: ""}) - - assert html_response(conn, 200) =~ "Enter your email so we can send a password reset link" - end - - @tag :skip - test "email is present and exists - sends password reset email", %{conn: conn} do - user = insert(:user) - conn = post(conn, "/password/request-reset", %{email: user.email}) - - assert html_response(conn, 200) =~ "Success!" - assert_email_delivered_with(subject: "Plausible password reset") - end - end - - describe "GET /password/reset" do - @tag :skip - test "with valid token - shows form", %{conn: conn} do - token = Plausible.Auth.Token.sign_password_reset("email@example.com") - conn = get(conn, "/password/reset", %{token: token}) - - assert html_response(conn, 200) =~ "Reset your password" - end - - @tag :skip - test "with invalid token - shows error page", %{conn: conn} do - conn = get(conn, "/password/reset", %{token: "blabla"}) - - assert html_response(conn, 401) =~ "Your token is invalid" - end - end - - describe "POST /password/reset" do - alias Plausible.Auth.{User, Token, Password} - - @tag :skip - test "with valid token - resets the password", %{conn: conn} do - user = insert(:user) - token = Token.sign_password_reset(user.email) - post(conn, "/password/reset", %{token: token, password: "new-password"}) - - user = Plausible.Repo.get(User, user.id) - assert Password.match?("new-password", user.password_hash) - end - - @tag :skip - test "with valid token - redirects the user to login", %{conn: conn} do - user = insert(:user) - token = Token.sign_password_reset(user.email) - conn = post(conn, "/password/reset", %{token: token, password: "new-password"}) - - assert redirected_to(conn, 302) == "/login" - end - end - - describe "GET /settings" do - setup [:create_user, :log_in] - - @tag :skip - test "shows the form", %{conn: conn} do - conn = get(conn, "/settings") - assert html_response(conn, 200) =~ "Account settings" - end - - @tag :skip - test "shows subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "558018", user: user) - conn = get(conn, "/settings") - assert html_response(conn, 200) =~ "10k pageviews" - assert html_response(conn, 200) =~ "monthly billing" - end - - @tag :skip - test "shows yearly subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "590752", user: user) - conn = get(conn, "/settings") - assert html_response(conn, 200) =~ "100k pageviews" - assert html_response(conn, 200) =~ "yearly billing" - end - - @tag :skip - test "shows free subscription", %{conn: conn, user: user} do - insert(:subscription, paddle_plan_id: "free_10k", user: user) - conn = get(conn, "/settings") - assert html_response(conn, 200) =~ "10k pageviews" - assert html_response(conn, 200) =~ "N/A billing" - end - - @tag :skip - test "shows invoices for subscribed user", %{conn: conn, user: user} do - insert(:subscription, - paddle_plan_id: "558018", - paddle_subscription_id: "redundant", - user: user - ) - - conn = get(conn, "/settings") - assert html_response(conn, 200) =~ "Dec 24, 2020" - assert html_response(conn, 200) =~ "€11.11" - assert html_response(conn, 200) =~ "Nov 24, 2020" - assert html_response(conn, 200) =~ "$22.00" - end - - @tag :skip - test "shows 'something went wrong' on failed invoice request'", %{conn: conn, user: user} do - insert(:subscription, - paddle_plan_id: "558018", - paddle_subscription_id: "invalid_subscription_id", - user: user - ) - - conn = get(conn, "/settings") - assert html_response(conn, 200) =~ "Invoices" - assert html_response(conn, 200) =~ "Something went wrong" - end - - @tag :skip - test "does not show invoice section for a user with no subscription", %{conn: conn} do - conn = get(conn, "/settings") - assert !(html_response(conn, 200) =~ "Invoices") - end - end - - describe "PUT /settings" do - setup [:create_user, :log_in] - - @tag :skip - test "updates user record", %{conn: conn, user: user} do - put(conn, "/settings", %{"user" => %{"name" => "New name"}}) - - user = Plausible.Repo.get(Plausible.Auth.User, user.id) - assert user.name == "New name" - end - - @tag :skip - test "redirects user to /settings", %{conn: conn} do - conn = put(conn, "/settings", %{"user" => %{"name" => "New name"}}) - - assert redirected_to(conn, 302) == "/settings" - end - end - - describe "DELETE /me" do - setup [:create_user, :log_in, :create_new_site] - use Plausible.Repo - - @tag :skip - test "deletes the user", %{conn: conn, user: user, site: site} do - Repo.insert_all("intro_emails", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - - Repo.insert_all("feedback_emails", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - - Repo.insert_all("create_site_emails", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - - Repo.insert_all("check_stats_emails", [ - %{ - user_id: user.id, - timestamp: NaiveDateTime.utc_now() - } - ]) - - insert(:google_auth, site: site, user: user) - insert(:subscription, user: user, status: "deleted") - - conn = delete(conn, "/me") - assert redirected_to(conn) == "/" - end - - @tag :skip - test "deletes sites that the user owns", %{conn: conn, user: user, site: owner_site} do - viewer_site = insert(:site) - insert(:site_membership, site: viewer_site, user: user, role: "viewer") - - delete(conn, "/me") - - assert Repo.get(Plausible.Site, viewer_site.id) - refute Repo.get(Plausible.Site, owner_site.id) - end - end -end diff --git a/test/plausible_web/controllers/billing_controller_test.exs b/test/plausible_web/controllers/billing_controller_test.exs deleted file mode 100644 index 116e090e8ce0..000000000000 --- a/test/plausible_web/controllers/billing_controller_test.exs +++ /dev/null @@ -1,138 +0,0 @@ -defmodule PlausibleWeb.BillingControllerTest do - use PlausibleWeb.ConnCase - import Plausible.TestUtils - - describe "GET /upgrade" do - setup [:create_user, :log_in] - - @tag :skip - test "shows upgrade page when user does not have a subcription already", %{conn: conn} do - conn = get(conn, "/billing/upgrade") - - assert html_response(conn, 200) =~ "Upgrade your free trial" - end - - @tag :skip - test "redirects user to change plan if they already have a plan", %{conn: conn, user: user} do - insert(:subscription, user: user) - conn = get(conn, "/billing/upgrade") - - assert redirected_to(conn) == "/billing/change-plan" - end - - @tag :skip - test "redirects user to enteprise plan page if they are configured with one", %{ - conn: conn, - user: user - } do - plan = insert(:enterprise_plan, user: user) - conn = get(conn, "/billing/upgrade") - - assert redirected_to(conn) == "/billing/upgrade/enterprise/#{plan.id}" - end - end - - describe "GET /upgrade/enterprise/:plan_id" do - setup [:create_user, :log_in] - - @tag :skip - test "renders enteprise plan upgrade page", %{conn: conn, user: user} do - plan = insert(:enterprise_plan, user: user) - - conn = get(conn, "/billing/upgrade/enterprise/#{plan.id}") - - assert html_response(conn, 200) =~ "Upgrade your free trial" - assert html_response(conn, 200) =~ "enterprise plan" - end - end - - describe "GET /change-plan" do - setup [:create_user, :log_in] - - @tag :skip - test "shows change plan page if user has subsription", %{conn: conn, user: user} do - insert(:subscription, user: user) - conn = get(conn, "/billing/change-plan") - - assert html_response(conn, 200) =~ "Change subscription plan" - end - - @tag :skip - test "redirects to /upgrade if user does not have a subscription", %{conn: conn} do - conn = get(conn, "/billing/change-plan") - - assert redirected_to(conn) == "/billing/upgrade" - end - - @tag :skip - test "redirects to enterprise change plan page if user has enterprise plan and existing subscription", - %{conn: conn, user: user} do - insert(:subscription, user: user) - plan = insert(:enterprise_plan, user: user) - conn = get(conn, "/billing/change-plan") - - assert redirected_to(conn) == "/billing/change-plan/enterprise/#{plan.id}" - end - end - - describe "GET /change-plan/enterprise/:plan_id" do - setup [:create_user, :log_in] - - @tag :skip - test "shows change plan page if user has subsription and enterprise plan", %{ - conn: conn, - user: user - } do - insert(:subscription, user: user) - - plan = - insert(:enterprise_plan, - user: user, - monthly_pageview_limit: 1000, - hourly_api_request_limit: 500, - site_limit: 100 - ) - - conn = get(conn, "/billing/change-plan/enterprise/#{plan.id}") - - assert html_response(conn, 200) =~ "Change subscription plan" - assert html_response(conn, 200) =~ "Up to 1k monthly pageviews" - assert html_response(conn, 200) =~ "Up to 500 hourly api requests" - assert html_response(conn, 200) =~ "Up to 100 sites" - end - - test "renders 404 is user does not have enterprise plan", %{conn: conn, user: user} do - insert(:subscription, user: user) - conn = get(conn, "/billing/change-plan/enterprise/123") - - assert conn.status == 404 - end - end - - describe "POST /change-plan" do - setup [:create_user, :log_in] - - @tag :skip - test "calls Paddle API to update subscription", %{conn: conn, user: user} do - insert(:subscription, user: user) - - post(conn, "/billing/change-plan/123123") - - subscription = Plausible.Repo.get_by(Plausible.Billing.Subscription, user_id: user.id) - assert subscription.paddle_plan_id == "123123" - assert subscription.next_bill_date == ~D[2019-07-10] - assert subscription.next_bill_amount == "6.00" - end - end - - describe "GET /billing/upgrade-success" do - setup [:create_user, :log_in] - - @tag :skip - test "shows success page after user subscribes", %{conn: conn} do - conn = get(conn, "/billing/upgrade-success") - - assert html_response(conn, 200) =~ "Subscription created successfully" - end - end -end diff --git a/test/plausible_web/controllers/invitation_controller_test.exs b/test/plausible_web/controllers/invitation_controller_test.exs deleted file mode 100644 index fb8b05fab8ca..000000000000 --- a/test/plausible_web/controllers/invitation_controller_test.exs +++ /dev/null @@ -1,209 +0,0 @@ -defmodule PlausibleWeb.Site.InvitationControllerTest do - use PlausibleWeb.ConnCase - use Plausible.Repo - use Bamboo.Test - import Plausible.TestUtils - - setup [:create_user, :log_in] - - describe "POST /sites/invitations/:invitation_id/accept" do - @tag :skip - test "converts the invitation into a membership", %{conn: conn, user: user} do - site = insert(:site) - - invitation = - insert(:invitation, - site_id: site.id, - inviter: build(:user), - email: user.email, - role: :admin - ) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - refute Repo.exists?(from(i in Plausible.Auth.Invitation, where: i.email == ^user.email)) - - membership = Repo.get_by(Plausible.Site.Membership, user_id: user.id, site_id: site.id) - assert membership.role == :admin - end - - @tag :skip - test "notifies the original inviter", %{conn: conn, user: user} do - inviter = insert(:user) - site = insert(:site) - - invitation = - insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :admin) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - assert_email_delivered_with( - to: [nil: inviter.email], - subject: "[Plausible Analytics] #{user.email} accepted your invitation to #{site.domain}" - ) - end - - @tag :skip - test "ownership transfer - notifies the original inviter with a different email", %{ - conn: conn, - user: user - } do - inviter = insert(:user) - site = insert(:site) - - invitation = - insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :owner) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - assert_email_delivered_with( - to: [nil: inviter.email], - subject: - "[Plausible Analytics] #{user.email} accepted the ownership transfer of #{site.domain}" - ) - end - - @tag :skip - test "ownership transfer - downgrades previous owner to admin", %{conn: conn, user: user} do - old_owner = insert(:user) - site = insert(:site, members: [old_owner]) - - invitation = - insert(:invitation, site_id: site.id, inviter: old_owner, email: user.email, role: :owner) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - refute Repo.exists?(from(i in Plausible.Auth.Invitation, where: i.email == ^user.email)) - - old_owner_membership = - Repo.get_by(Plausible.Site.Membership, user_id: old_owner.id, site_id: site.id) - - assert old_owner_membership.role == :admin - - new_owner_membership = - Repo.get_by(Plausible.Site.Membership, user_id: user.id, site_id: site.id) - - assert new_owner_membership.role == :owner - end - - @tag :skip - test "ownership transfer - will lock the site if new owner does not have an active subscription or trial", - %{ - conn: conn, - user: user - } do - Repo.update_all(from(u in Plausible.Auth.User, where: u.id == ^user.id), - set: [trial_expiry_date: Timex.today() |> Timex.shift(days: -1)] - ) - - inviter = insert(:user) - site = insert(:site, locked: false) - - invitation = - insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :owner) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - assert Repo.reload!(site).locked - end - - @tag :skip - test "ownership transfer - will end the trial of the new owner immediately", %{ - conn: conn, - user: user - } do - Repo.update_all(from(u in Plausible.Auth.User, where: u.id == ^user.id), - set: [trial_expiry_date: Timex.today() |> Timex.shift(days: 7)] - ) - - inviter = insert(:user) - site = insert(:site, locked: false) - - invitation = - insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :owner) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - assert Timex.before?(Repo.reload!(user).trial_expiry_date, Timex.today()) - assert Repo.reload!(site).locked - end - - @tag :skip - test "ownership transfer - if new owner does not have a trial - will set trial_expiry_date to yesterday", - %{ - conn: conn, - user: user - } do - Repo.update_all(from(u in Plausible.Auth.User, where: u.id == ^user.id), - set: [trial_expiry_date: nil] - ) - - inviter = insert(:user) - site = insert(:site, locked: false) - - invitation = - insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :owner) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/accept") - - assert Timex.before?(Repo.reload!(user).trial_expiry_date, Timex.today()) - assert Repo.reload!(site).locked - end - end - - describe "POST /sites/invitations/:invitation_id/reject" do - @tag :skip - test "deletes the invitation", %{conn: conn, user: user} do - site = insert(:site) - - invitation = - insert(:invitation, - site_id: site.id, - inviter: build(:user), - email: user.email, - role: :admin - ) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/reject") - - refute Repo.exists?(from(i in Plausible.Auth.Invitation, where: i.email == ^user.email)) - end - - @tag :skip - test "notifies the original inviter", %{conn: conn, user: user} do - inviter = insert(:user) - site = insert(:site) - - invitation = - insert(:invitation, site_id: site.id, inviter: inviter, email: user.email, role: :admin) - - post(conn, "/sites/invitations/#{invitation.invitation_id}/reject") - - assert_email_delivered_with( - to: [nil: inviter.email], - subject: "[Plausible Analytics] #{user.email} rejected your invitation to #{site.domain}" - ) - end - end - - describe "DELETE /sites/invitations/:invitation_id" do - @tag :skip - test "removes the invitation", %{conn: conn} do - site = insert(:site) - - invitation = - insert(:invitation, - site_id: site.id, - inviter: build(:user), - email: "jane@example.com", - role: :admin - ) - - delete(conn, "/sites/invitations/#{invitation.invitation_id}") - - refute Repo.exists?( - from i in Plausible.Auth.Invitation, where: i.email == "jane@example.com" - ) - end - end -end diff --git a/test/plausible_web/controllers/site/membership_controller_test.exs b/test/plausible_web/controllers/site/membership_controller_test.exs deleted file mode 100644 index b391d696b5bc..000000000000 --- a/test/plausible_web/controllers/site/membership_controller_test.exs +++ /dev/null @@ -1,217 +0,0 @@ -defmodule PlausibleWeb.Site.MembershipControllerTest do - use PlausibleWeb.ConnCase - use Plausible.Repo - use Bamboo.Test - import Plausible.TestUtils - - setup [:create_user, :log_in] - - describe "GET /sites/:website/memberships/invite" do - @tag :skip - test "shows invite form", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - - conn = get(conn, "/sites/#{site.domain}/memberships/invite") - - assert html_response(conn, 200) =~ "Invite member to" - end - end - - describe "POST /sites/:website/memberships/invite" do - @tag :skip - test "creates invitation", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - - conn = - post(conn, "/sites/#{site.domain}/memberships/invite", %{ - email: "john.doe@example.com", - role: "admin" - }) - - invitation = Repo.get_by(Plausible.Auth.Invitation, email: "john.doe@example.com") - - assert invitation.role == :admin - assert redirected_to(conn) == "/#{site.domain}/settings/people" - end - - @tag :skip - test "sends invitation email for new user", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - - post(conn, "/sites/#{site.domain}/memberships/invite", %{ - email: "john.doe@example.com", - role: "admin" - }) - - assert_email_delivered_with( - to: [nil: "john.doe@example.com"], - subject: "[Plausible Analytics] You've been invited to #{site.domain}" - ) - end - - @tag :skip - test "sends invitation email for existing user", %{conn: conn, user: user} do - existing_user = insert(:user) - site = insert(:site, members: [user]) - - post(conn, "/sites/#{site.domain}/memberships/invite", %{ - email: existing_user.email, - role: "admin" - }) - - assert_email_delivered_with( - to: [nil: existing_user.email], - subject: "[Plausible Analytics] You've been invited to #{site.domain}" - ) - end - - @tag :skip - test "renders form with error if the invitee is already a member", %{conn: conn, user: user} do - second_member = insert(:user) - site = insert(:site, members: [user, second_member]) - - conn = - post(conn, "/sites/#{site.domain}/memberships/invite", %{ - email: second_member.email, - role: "admin" - }) - - assert html_response(conn, 200) =~ - "#{second_member.email} is already a member of #{site.domain}" - end - end - - describe "GET /sites/:website/transfer-ownership" do - @tag :skip - test "shows ownership transfer form", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - - conn = get(conn, "/sites/#{site.domain}/transfer-ownership") - - assert html_response(conn, 200) =~ "Transfer ownership of" - end - end - - describe "POST /sites/:website/transfer-ownership" do - @tag :skip - test "creates invitation with :owner role", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - - conn = - post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: "john.doe@example.com"}) - - invitation = Repo.get_by(Plausible.Auth.Invitation, email: "john.doe@example.com") - - assert invitation.role == :owner - assert redirected_to(conn) == "/#{site.domain}/settings/people" - end - - @tag :skip - test "sends ownership transfer email for new user", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - - post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: "john.doe@example.com"}) - - assert_email_delivered_with( - to: [nil: "john.doe@example.com"], - subject: "[Plausible Analytics] Request to transfer ownership of #{site.domain}" - ) - end - - @tag :skip - test "sends invitation email for existing user", %{conn: conn, user: user} do - existing_user = insert(:user) - site = insert(:site, members: [user]) - - post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: existing_user.email}) - - assert_email_delivered_with( - to: [nil: existing_user.email], - subject: "[Plausible Analytics] Request to transfer ownership of #{site.domain}" - ) - end - end - - describe "PUT /sites/memberships/:id/role/:new_role" do - @tag :skip - test "updates a site member's role", %{conn: conn, user: user} do - admin = insert(:user) - - site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner), - build(:site_membership, user: admin, role: :admin) - ] - ) - - membership = Repo.get_by(Plausible.Site.Membership, user_id: admin.id) - - put(conn, "/sites/#{site.domain}/memberships/#{membership.id}/role/viewer") - - membership = Repo.reload!(membership) - - assert membership.role == :viewer - end - - @tag :skip - test "can downgrade yourself from admin to viewer, redirects to stats instead", %{ - conn: conn, - user: user - } do - site = insert(:site, memberships: [build(:site_membership, user: user, role: :admin)]) - - membership = Repo.get_by(Plausible.Site.Membership, user_id: user.id) - - conn = put(conn, "/sites/#{site.domain}/memberships/#{membership.id}/role/viewer") - - membership = Repo.reload!(membership) - - assert membership.role == :viewer - assert redirected_to(conn) == "/#{site.domain}" - end - end - - describe "DELETE /sites/memberships/:id" do - @tag :skip - test "removes a member from a site", %{conn: conn, user: user} do - admin = insert(:user) - - site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner), - build(:site_membership, user: admin, role: :admin) - ] - ) - - membership = Enum.find(site.memberships, &(&1.role == :admin)) - - delete(conn, "/sites/#{site.domain}/memberships/#{membership.id}") - - refute Repo.exists?(from sm in Plausible.Site.Membership, where: sm.user_id == ^admin.id) - end - - @tag :skip - test "notifies the user who has been removed via email", %{conn: conn, user: user} do - admin = insert(:user) - - site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner), - build(:site_membership, user: admin, role: :admin) - ] - ) - - membership = Enum.find(site.memberships, &(&1.role == :admin)) - - delete(conn, "/sites/#{site.domain}/memberships/#{membership.id}") - - assert_email_delivered_with( - to: [nil: admin.email], - subject: "[Plausible Analytics] Your access to #{site.domain} has been revoked" - ) - end - end -end diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs deleted file mode 100644 index b2786a9b1077..000000000000 --- a/test/plausible_web/controllers/site_controller_test.exs +++ /dev/null @@ -1,809 +0,0 @@ -defmodule PlausibleWeb.SiteControllerTest do - use PlausibleWeb.ConnCase - use Plausible.Repo - use Bamboo.Test - use Oban.Testing, repo: Plausible.Repo - import Plausible.TestUtils - - describe "GET /sites/new" do - setup [:create_user, :log_in] - - @tag :skip - test "shows the site form", %{conn: conn} do - conn = get(conn, "/sites/new") - - assert html_response(conn, 200) =~ "Your website details" - end - - @tag :skip - test "shows onboarding steps if it's the first site for the user", %{conn: conn} do - conn = get(conn, "/sites/new") - - assert html_response(conn, 200) =~ "Add site info" - end - - @tag :skip - test "does not show onboarding steps if user has a site already", %{conn: conn, user: user} do - insert(:site, members: [user], domain: "test-site.com") - - conn = get(conn, "/sites/new") - - refute html_response(conn, 200) =~ "Add site info" - end - end - - describe "GET /sites" do - setup [:create_user, :log_in] - - test "shows empty screen if no sites", %{conn: conn} do - conn = get(conn, "/sites") - assert html_response(conn, 200) =~ "You don't have any sites yet" - end - - test "lists all of your sites with last 24h visitors", %{conn: conn, user: user} do - insert(:site, members: [user], domain: "test-site.com") - conn = get(conn, "/sites") - - assert html_response(conn, 200) =~ "test-site.com" - assert html_response(conn, 200) =~ "3 visitors in last 24h" - end - - test "shows invitations for user by email address", %{conn: conn, user: user} do - site = insert(:site) - insert(:invitation, email: user.email, site_id: site.id, inviter: build(:user)) - conn = get(conn, "/sites") - - assert html_response(conn, 200) =~ site.domain - end - - test "invitations are case insensitive", %{conn: conn, user: user} do - site = insert(:site) - - insert(:invitation, - email: String.upcase(user.email), - site_id: site.id, - inviter: build(:user) - ) - - conn = get(conn, "/sites") - - assert html_response(conn, 200) =~ site.domain - end - - test "paginates sites", %{conn: conn, user: user} do - insert(:site, members: [user], domain: "test-site1.com") - insert(:site, members: [user], domain: "test-site2.com") - insert(:site, members: [user], domain: "test-site3.com") - insert(:site, members: [user], domain: "test-site4.com") - - conn = get(conn, "/sites?per_page=2") - - assert html_response(conn, 200) =~ "test-site1.com" - assert html_response(conn, 200) =~ "test-site2.com" - refute html_response(conn, 200) =~ "test-site3.com" - refute html_response(conn, 200) =~ "test-site4.com" - - conn = get(conn, "/sites?per_page=2&page=2") - - refute html_response(conn, 200) =~ "test-site1.com" - refute html_response(conn, 200) =~ "test-site2.com" - assert html_response(conn, 200) =~ "test-site3.com" - assert html_response(conn, 200) =~ "test-site4.com" - end - end - - describe "POST /sites" do - setup [:create_user, :log_in] - - @tag :skip - test "creates the site with valid params", %{conn: conn} do - conn = - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert redirected_to(conn) == "/example.com/snippet" - assert Repo.exists?(Plausible.Site, domain: "example.com") - end - - @tag :skip - test "starts trial if user does not have trial yet", %{conn: conn, user: user} do - Plausible.Auth.User.remove_trial_expiry(user) |> Repo.update!() - - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert Repo.reload!(user).trial_expiry_date - end - - @tag :skip - test "sends welcome email if this is the user's first site", %{conn: conn} do - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert_email_delivered_with(subject: "Welcome to Plausible") - end - - @tag :skip - test "does not send welcome email if user already has a previous site", %{ - conn: conn, - user: user - } do - insert(:site, members: [user]) - - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert_no_emails_delivered() - end - - @tag :skip - test "does not allow site creation when the user is at their site limit", %{ - conn: conn, - user: user - } do - # @tag :skip - # default site limit defined in config/.test.env - insert(:site, members: [user]) - insert(:site, members: [user]) - insert(:site, members: [user]) - - conn = - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert conn.status == 400 - end - - @tag :skip - test "allows accounts registered before 2021-05-05 to go over the limit", %{ - conn: conn, - user: user - } do - Repo.update_all(from(u in "users", where: u.id == ^user.id), - set: [inserted_at: ~N[2020-01-01 00:00:00]] - ) - - insert(:site, members: [user]) - insert(:site, members: [user]) - insert(:site, members: [user]) - insert(:site, members: [user]) - - conn = - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert redirected_to(conn) == "/example.com/snippet" - assert Repo.exists?(Plausible.Site, domain: "example.com") - end - - @tag :skip - test "allows enterprise accounts to create unlimited sites", %{ - conn: conn, - user: user - } do - insert(:enterprise_plan, user: user) - - insert(:site, members: [user]) - insert(:site, members: [user]) - insert(:site, members: [user]) - - conn = - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert redirected_to(conn) == "/example.com/snippet" - assert Repo.exists?(Plausible.Site, domain: "example.com") - end - - @tag :skip - test "cleans up the url", %{conn: conn} do - conn = - post(conn, "/sites", %{ - "site" => %{ - "domain" => "https://www.Example.com/", - "timezone" => "Europe/London" - } - }) - - assert redirected_to(conn) == "/example.com/snippet" - assert Repo.exists?(Plausible.Site, domain: "example.com") - end - - @tag :skip - test "renders form again when domain is missing", %{conn: conn} do - conn = - post(conn, "/sites", %{ - "site" => %{ - "timezone" => "Europe/London" - } - }) - - assert html_response(conn, 200) =~ "can't be blank" - end - - @tag :skip - test "only alphanumeric characters and slash allowed in domain", %{conn: conn} do - conn = - post(conn, "/sites", %{ - "site" => %{ - "timezone" => "Europe/London", - "domain" => "!@£.com" - } - }) - - assert html_response(conn, 200) =~ "only letters, numbers, slashes and period allowed" - end - - @tag :skip - test "renders form again when it is a duplicate domain", %{conn: conn} do - insert(:site, domain: "example.com") - - conn = - post(conn, "/sites", %{ - "site" => %{ - "domain" => "example.com", - "timezone" => "Europe/London" - } - }) - - assert html_response(conn, 200) =~ - "This domain has already been taken. Perhaps one of your team members registered it? If that's not the case, please contact support@plausible.io" - end - end - - describe "GET /:website/snippet" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "shows snippet", %{conn: conn, site: site} do - conn = get(conn, "/#{site.domain}/snippet") - - assert html_response(conn, 200) =~ "Add javascript snippet" - end - end - - describe "GET /:website/settings/general" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "shows settings form", %{conn: conn, site: site} do - conn = get(conn, "/#{site.domain}/settings/general") - - assert html_response(conn, 200) =~ "General information" - end - end - - describe "GET /:website/settings/goals" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "lists goals for the site", %{conn: conn, site: site} do - insert(:goal, domain: site.domain, event_name: "Custom event") - insert(:goal, domain: site.domain, page_path: "/register") - - conn = get(conn, "/#{site.domain}/settings/goals") - - assert html_response(conn, 200) =~ "Custom event" - assert html_response(conn, 200) =~ "Visit /register" - end - end - - describe "PUT /:website/settings" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "updates the timezone", %{conn: conn, site: site} do - conn = - put(conn, "/#{site.domain}/settings", %{ - "site" => %{ - "timezone" => "Europe/London" - } - }) - - updated = Repo.get(Plausible.Site, site.id) - assert updated.timezone == "Europe/London" - assert redirected_to(conn, 302) == "/#{site.domain}/settings/general" - end - end - - describe "POST /sites/:website/make-public" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "makes the site public", %{conn: conn, site: site} do - conn = post(conn, "/sites/#{site.domain}/make-public") - - updated = Repo.get(Plausible.Site, site.id) - assert updated.public - assert redirected_to(conn, 302) == "/#{site.domain}/settings/visibility" - end - end - - describe "POST /sites/:website/make-private" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "makes the site private", %{conn: conn, site: site} do - conn = post(conn, "/sites/#{site.domain}/make-private") - - updated = Repo.get(Plausible.Site, site.id) - refute updated.public - assert redirected_to(conn, 302) == "/#{site.domain}/settings/visibility" - end - end - - describe "DELETE /:website" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "deletes the site", %{conn: conn, user: user} do - site = insert(:site, members: [user]) - insert(:google_auth, user: user, site: site) - insert(:custom_domain, site: site) - insert(:spike_notification, site: site) - - delete(conn, "/#{site.domain}") - - refute Repo.exists?(from s in Plausible.Site, where: s.id == ^site.id) - end - end - - describe "PUT /:website/settings/google" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "updates google auth property", %{conn: conn, user: user, site: site} do - insert(:google_auth, user: user, site: site) - - conn = - put(conn, "/#{site.domain}/settings/google", %{ - "google_auth" => %{"property" => "some-new-property.com"} - }) - - updated_auth = Repo.one(Plausible.Site.GoogleAuth) - assert updated_auth.property == "some-new-property.com" - assert redirected_to(conn, 302) == "/#{site.domain}/settings/search-console" - end - end - - describe "DELETE /:website/settings/google" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "deletes associated google auth", %{conn: conn, user: user, site: site} do - insert(:google_auth, user: user, site: site) - conn = delete(conn, "/#{site.domain}/settings/google-search") - - refute Repo.exists?(Plausible.Site.GoogleAuth) - assert redirected_to(conn, 302) == "/#{site.domain}/settings/search-console" - end - end - - describe "GET /:website/goals/new" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "shows form to create a new goal", %{conn: conn, site: site} do - conn = get(conn, "/#{site.domain}/goals/new") - - assert html_response(conn, 200) =~ "Add goal" - end - end - - describe "POST /:website/goals" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "creates a pageview goal for the website", %{conn: conn, site: site} do - conn = - post(conn, "/#{site.domain}/goals", %{ - goal: %{ - page_path: "/success", - event_name: "" - } - }) - - goal = Repo.one(Plausible.Goal) - - assert goal.page_path == "/success" - assert goal.event_name == nil - assert redirected_to(conn, 302) == "/#{site.domain}/settings/goals" - end - - @tag :skip - test "creates a custom event goal for the website", %{conn: conn, site: site} do - conn = - post(conn, "/#{site.domain}/goals", %{ - goal: %{ - page_path: "", - event_name: "Signup" - } - }) - - goal = Repo.one(Plausible.Goal) - - assert goal.event_name == "Signup" - assert goal.page_path == nil - assert redirected_to(conn, 302) == "/#{site.domain}/settings/goals" - end - end - - describe "DELETE /:website/goals/:id" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "lists goals for the site", %{conn: conn, site: site} do - goal = insert(:goal, domain: site.domain, event_name: "Custom event") - - conn = delete(conn, "/#{site.domain}/goals/#{goal.id}") - - assert Repo.aggregate(Plausible.Goal, :count, :id) == 0 - assert redirected_to(conn, 302) == "/#{site.domain}/settings/goals" - end - end - - describe "POST /sites/:website/weekly-report/enable" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "creates a weekly report record with the user email", %{ - conn: conn, - site: site, - user: user - } do - post(conn, "/sites/#{site.domain}/weekly-report/enable") - - report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - assert report.recipients == [user.email] - end - end - - describe "POST /sites/:website/weekly-report/disable" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "deletes the weekly report record", %{conn: conn, site: site} do - insert(:weekly_report, site: site) - - post(conn, "/sites/#{site.domain}/weekly-report/disable") - - refute Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - end - end - - describe "POST /sites/:website/weekly-report/recipients" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "adds a recipient to the weekly report", %{conn: conn, site: site} do - insert(:weekly_report, site: site) - - post(conn, "/sites/#{site.domain}/weekly-report/recipients", recipient: "user@email.com") - - report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - assert report.recipients == ["user@email.com"] - end - end - - describe "DELETE /sites/:website/weekly-report/recipients/:recipient" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "removes a recipient from the weekly report", %{conn: conn, site: site} do - insert(:weekly_report, site: site, recipients: ["recipient@email.com"]) - - delete(conn, "/sites/#{site.domain}/weekly-report/recipients/recipient@email.com") - - report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - assert report.recipients == [] - end - end - - describe "POST /sites/:website/monthly-report/enable" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "creates a monthly report record with the user email", %{ - conn: conn, - site: site, - user: user - } do - post(conn, "/sites/#{site.domain}/monthly-report/enable") - - report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - assert report.recipients == [user.email] - end - end - - describe "POST /sites/:website/monthly-report/disable" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "deletes the monthly report record", %{conn: conn, site: site} do - insert(:monthly_report, site: site) - - post(conn, "/sites/#{site.domain}/monthly-report/disable") - - refute Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - end - end - - describe "POST /sites/:website/monthly-report/recipients" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "adds a recipient to the monthly report", %{conn: conn, site: site} do - insert(:monthly_report, site: site) - - post(conn, "/sites/#{site.domain}/monthly-report/recipients", recipient: "user@email.com") - - report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - assert report.recipients == ["user@email.com"] - end - end - - describe "DELETE /sites/:website/monthly-report/recipients/:recipient" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "removes a recipient from the monthly report", %{conn: conn, site: site} do - insert(:monthly_report, site: site, recipients: ["recipient@email.com"]) - - delete(conn, "/sites/#{site.domain}/monthly-report/recipients/recipient@email.com") - - report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - assert report.recipients == [] - end - end - - describe "POST /sites/:website/spike-notification/enable" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "creates a spike notification record with the user email", %{ - conn: conn, - site: site, - user: user - } do - post(conn, "/sites/#{site.domain}/spike-notification/enable") - - notification = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - assert notification.recipients == [user.email] - end - - @tag :skip - test "does not allow duplicate spike notification to be created", %{ - conn: conn, - site: site - } do - post(conn, "/sites/#{site.domain}/spike-notification/enable") - post(conn, "/sites/#{site.domain}/spike-notification/enable") - - assert Repo.aggregate( - from(s in Plausible.Site.SpikeNotification, where: s.site_id == ^site.id), - :count - ) == 1 - end - end - - describe "POST /sites/:website/spike-notification/disable" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "deletes the spike notification record", %{conn: conn, site: site} do - insert(:spike_notification, site: site) - - post(conn, "/sites/#{site.domain}/spike-notification/disable") - - refute Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - end - end - - describe "PUT /sites/:website/spike-notification" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "updates spike notification threshold", %{conn: conn, site: site} do - insert(:spike_notification, site: site, threshold: 10) - - put(conn, "/sites/#{site.domain}/spike-notification", %{ - "spike_notification" => %{"threshold" => "15"} - }) - - notification = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - assert notification.threshold == 15 - end - end - - describe "POST /sites/:website/spike-notification/recipients" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "adds a recipient to the spike notification", %{conn: conn, site: site} do - insert(:spike_notification, site: site) - - post(conn, "/sites/#{site.domain}/spike-notification/recipients", - recipient: "user@email.com" - ) - - report = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - assert report.recipients == ["user@email.com"] - end - end - - describe "DELETE /sites/:website/spike-notification/recipients/:recipient" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "removes a recipient from the spike notification", %{conn: conn, site: site} do - insert(:spike_notification, site: site, recipients: ["recipient@email.com"]) - - delete(conn, "/sites/#{site.domain}/spike-notification/recipients/recipient@email.com") - - report = Repo.get_by(Plausible.Site.SpikeNotification, site_id: site.id) - assert report.recipients == [] - end - end - - describe "GET /sites/:website/shared-links/new" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "shows form for new shared link", %{conn: conn, site: site} do - conn = get(conn, "/sites/#{site.domain}/shared-links/new") - - assert html_response(conn, 200) =~ "New shared link" - end - end - - describe "POST /sites/:website/shared-links" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "creates shared link without password", %{conn: conn, site: site} do - post(conn, "/sites/#{site.domain}/shared-links", %{ - "shared_link" => %{"name" => "Link name"} - }) - - link = Repo.one(Plausible.Site.SharedLink) - - refute is_nil(link.slug) - assert is_nil(link.password_hash) - assert link.name == "Link name" - end - - @tag :skip - test "creates shared link with password", %{conn: conn, site: site} do - post(conn, "/sites/#{site.domain}/shared-links", %{ - "shared_link" => %{"password" => "password", "name" => "New name"} - }) - - link = Repo.one(Plausible.Site.SharedLink) - - refute is_nil(link.slug) - refute is_nil(link.password_hash) - assert link.name == "New name" - end - end - - describe "GET /sites/:website/shared-links/edit" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "shows form to edit shared link", %{conn: conn, site: site} do - link = insert(:shared_link, site: site) - conn = get(conn, "/sites/#{site.domain}/shared-links/#{link.slug}/edit") - - assert html_response(conn, 200) =~ "Edit shared link" - end - end - - describe "PUT /sites/:website/shared-links/:slug" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "can update link name", %{conn: conn, site: site} do - link = insert(:shared_link, site: site) - - put(conn, "/sites/#{site.domain}/shared-links/#{link.slug}", %{ - "shared_link" => %{"name" => "Updated link name"} - }) - - link = Repo.one(Plausible.Site.SharedLink) - - assert link.name == "Updated link name" - end - end - - describe "DELETE /sites/:website/shared-links/:slug" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "shows form for new shared link", %{conn: conn, site: site} do - link = insert(:shared_link, site: site) - - conn = delete(conn, "/sites/#{site.domain}/shared-links/#{link.slug}") - - refute Repo.one(Plausible.Site.SharedLink) - assert redirected_to(conn, 302) =~ "/#{site.domain}/settings" - end - end - - describe "DELETE sites/:website/custom-domains/:id" do - setup [:create_user, :log_in, :create_site] - - @tag :skip - test "lists goals for the site", %{conn: conn, site: site} do - domain = insert(:custom_domain, site: site) - - delete(conn, "/sites/#{site.domain}/custom-domains/#{domain.id}") - - assert Repo.aggregate(Plausible.Site.CustomDomain, :count, :id) == 0 - end - end - - describe "POST /:website/settings/google-import" do - setup [:create_user, :log_in, :create_new_site] - - @tag :skip - test "adds in-progress imported tag to site", %{conn: conn, site: site} do - post(conn, "/#{site.domain}/settings/google-import", %{"profile" => "123"}) - - imported_data = Repo.reload(site).imported_data - - assert imported_data - assert imported_data.source == "Google Analytics" - assert imported_data.end_date == Timex.today() - assert imported_data.status == "importing" - end - - @tag :skip - test "schedules an import job in Oban", %{conn: conn, site: site} do - post(conn, "/#{site.domain}/settings/google-import", %{"profile" => "123"}) - - assert_enqueued( - worker: Plausible.Workers.ImportGoogleAnalytics, - args: %{"site_id" => site.id, "profile" => "123"} - ) - end - end - - describe "DELETE /:website/settings/:forget_imported" do - setup [:create_user, :log_in, :create_new_site] - - @tag :skip - test "removes imported_data field from site", %{conn: conn, site: site} do - delete(conn, "/#{site.domain}/settings/forget-imported") - - assert Repo.reload(site).imported_data == nil - end - end -end diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 465f314a330b..20b421901301 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -46,7 +46,7 @@ defmodule PlausibleWeb.StatsControllerTest do end describe "GET /:website - as a super admin" do - setup [:create_user, :make_user_super_admin, :log_in] + setup [:create_user, :log_in] test "can view a private dashboard with stats", %{conn: conn} do site = insert(:site) @@ -91,10 +91,6 @@ defmodule PlausibleWeb.StatsControllerTest do end end - defp make_user_super_admin(%{user: user}) do - Application.put_env(:plausible, :super_admin_user_ids, [user.id]) - end - describe "GET /:website/export" do setup [:create_user, :create_new_site, :log_in] diff --git a/test/plausible_web/controllers/unsubscribe_controller_test.exs b/test/plausible_web/controllers/unsubscribe_controller_test.exs deleted file mode 100644 index ad55458d986f..000000000000 --- a/test/plausible_web/controllers/unsubscribe_controller_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -defmodule PlausibleWeb.UnsubscribeControllerTest do - use PlausibleWeb.ConnCase - use Plausible.Repo - - describe "GET /sites/:website/weekly-report/unsubscribe" do - @tag :skip - test "removes a recipient from the weekly report without them having to log in", %{conn: conn} do - site = insert(:site) - insert(:weekly_report, site: site, recipients: ["recipient@email.com"]) - - conn = - get(conn, "/sites/#{site.domain}/weekly-report/unsubscribe?email=recipient@email.com") - - assert html_response(conn, 200) =~ "Unsubscribe successful" - - report = Repo.get_by(Plausible.Site.WeeklyReport, site_id: site.id) - assert report.recipients == [] - end - - @tag :skip - test "renders success if site or weekly report does not exist in the database", %{conn: conn} do - conn = - get(conn, "/sites/nonexistent.com/weekly-report/unsubscribe?email=recipient@email.com") - - assert html_response(conn, 200) =~ "Unsubscribe successful" - end - end - - describe "GET /sites/:website/monthly-report/unsubscribe" do - @tag :skip - test "removes a recipient from the weekly report without them having to log in", %{conn: conn} do - site = insert(:site) - insert(:monthly_report, site: site, recipients: ["recipient@email.com"]) - - conn = - get(conn, "/sites/#{site.domain}/monthly-report/unsubscribe?email=recipient@email.com") - - assert html_response(conn, 200) =~ "Unsubscribe successful" - - report = Repo.get_by(Plausible.Site.MonthlyReport, site_id: site.id) - assert report.recipients == [] - end - - @tag :skip - test "renders success if site or weekly report does not exist in the database", %{conn: conn} do - conn = - get(conn, "/sites/nonexistent.com/monthly-report/unsubscribe?email=recipient@email.com") - - assert html_response(conn, 200) =~ "Unsubscribe successful" - end - end -end diff --git a/test/plausible_web/plugs/auth_plug_test.exs b/test/plausible_web/plugs/auth_plug_test.exs deleted file mode 100644 index 9598610ecdd2..000000000000 --- a/test/plausible_web/plugs/auth_plug_test.exs +++ /dev/null @@ -1,44 +0,0 @@ -defmodule PlausibleWeb.AuthPlugTest do - use Plausible.DataCase - use Plug.Test - alias PlausibleWeb.AuthPlug - - test "does nothing if user is not logged in" do - conn = - conn(:get, "/") - |> init_test_session(%{}) - |> AuthPlug.call(%{}) - - assert is_nil(conn.assigns[:current_user]) - end - - test "looks up current user if they are logged in" do - user = insert(:user) - subscription = insert(:subscription, user: user) - - conn = - conn(:get, "/") - |> init_test_session(%{current_user_id: user.id}) - |> AuthPlug.call(%{}) - - assert conn.assigns[:current_user].id == user.id - assert conn.assigns[:current_user].subscription.id == subscription.id - end - - test "looks up the latest subscription" do - user = insert(:user) - - _old_subscription = - insert(:subscription, user: user, inserted_at: Timex.now() |> Timex.shift(days: -1)) - - subscription = insert(:subscription, user: user, inserted_at: Timex.now()) - - conn = - conn(:get, "/") - |> init_test_session(%{current_user_id: user.id}) - |> AuthPlug.call(%{}) - - assert conn.assigns[:current_user].id == user.id - assert conn.assigns[:current_user].subscription.id == subscription.id - end -end diff --git a/test/plausible_web/plugs/firewall_test.exs b/test/plausible_web/plugs/firewall_test.exs deleted file mode 100644 index a698e2f14a36..000000000000 --- a/test/plausible_web/plugs/firewall_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -defmodule PlausibleWeb.FirewallTest do - use Plausible.DataCase - use Plug.Test - alias PlausibleWeb.Firewall - - @allowed_ip "127.0.0.1" - @blocked_ip "127.0.0.2" - @opts [blocklist: [@blocked_ip]] - - setup do - Application.put_env(:plausible, PlausibleWeb.Firewall, blocklist: [@blocked_ip]) - :ok - end - - test "ignores request if IP is allowed" do - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", @allowed_ip) - |> Firewall.call(@opts) - - assert conn.status == nil - end - - test "responds with 404 if IP is blocked" do - conn = - conn(:get, "/") - |> put_req_header("x-forwarded-for", @blocked_ip) - |> Firewall.call(@opts) - - assert conn.status == 404 - end -end diff --git a/test/plausible_web/plugs/session_timeout_plug_test.exs b/test/plausible_web/plugs/session_timeout_plug_test.exs deleted file mode 100644 index 7859a9021e8b..000000000000 --- a/test/plausible_web/plugs/session_timeout_plug_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -defmodule PlausibleWeb.SessionTimeoutPlugTest do - use ExUnit.Case, async: true - use Plug.Test - alias PlausibleWeb.SessionTimeoutPlug - @opts %{timeout_after_seconds: 10} - - test "does nothing if user is not logged in" do - conn = - conn(:get, "/") - |> init_test_session(%{}) - |> SessionTimeoutPlug.call(@opts) - - refute get_session(conn, :session_timeout_at) - end - - test "sets session timeout if user is logged in" do - conn = - conn(:get, "/") - |> init_test_session(%{current_user_id: 1}) - |> SessionTimeoutPlug.call(@opts) - - timeout = get_session(conn, :session_timeout_at) - now = DateTime.utc_now() |> DateTime.to_unix() - assert timeout > now - end - - test "logs user out if timeout passed" do - conn = - conn(:get, "/") - |> init_test_session(%{current_user_id: 1, session_timeout_at: 1}) - |> SessionTimeoutPlug.call(@opts) - - assert conn.private[:plug_session_info] == :drop - end -end diff --git a/test/plausible_web/views/email_view_test.exs b/test/plausible_web/views/email_view_test.exs deleted file mode 100644 index 37521055dcc3..000000000000 --- a/test/plausible_web/views/email_view_test.exs +++ /dev/null @@ -1,16 +0,0 @@ -defmodule PlausibleWeb.EmailViewTest do - use PlausibleWeb.ConnCase, async: true - alias PlausibleWeb.EmailView - - describe "user salutation" do - test "picks first name if full name has two parts" do - user1 = %Plausible.Auth.User{name: "Jane"} - user2 = %Plausible.Auth.User{name: "Jane Doe"} - user3 = %Plausible.Auth.User{name: "Jane Alice Doe"} - - assert EmailView.user_salutation(user1) == "Jane" - assert EmailView.user_salutation(user2) == "Jane" - assert EmailView.user_salutation(user3) == "Jane" - end - end -end diff --git a/test/support/factory.ex b/test/support/factory.ex index c2eccb6ae828..20b11b76fc28 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -15,12 +15,6 @@ defmodule Plausible.Factory do merge_attributes(user, attrs) end - def spike_notification_factory do - %Plausible.Site.SpikeNotification{ - threshold: 10 - } - end - def site_factory do domain = sequence(:domain, &"example-#{&1}.com") @@ -104,191 +98,6 @@ defmodule Plausible.Factory do } end - def goal_factory do - %Plausible.Goal{} - end - - def subscription_factory do - %Plausible.Billing.Subscription{ - paddle_subscription_id: sequence(:paddle_subscription_id, &"subscription-#{&1}"), - paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"), - cancel_url: "cancel.com", - update_url: "cancel.com", - status: "active", - next_bill_amount: "6.00", - next_bill_date: Timex.today(), - last_bill_date: Timex.today(), - currency_code: "USD" - } - end - - def enterprise_plan_factory do - %Plausible.Billing.EnterprisePlan{ - paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"), - billing_interval: :monthly, - monthly_pageview_limit: 1_000_000, - hourly_api_request_limit: 3000, - site_limit: 100 - } - end - - def google_auth_factory do - %Plausible.Site.GoogleAuth{ - email: sequence(:google_auth_email, &"email-#{&1}@email.com"), - refresh_token: "123", - access_token: "123", - expires: Timex.now() |> Timex.shift(days: 1) - } - end - - def custom_domain_factory do - %Plausible.Site.CustomDomain{ - domain: sequence(:custom_domain, &"domain-#{&1}.com") - } - end - - def weekly_report_factory do - %Plausible.Site.WeeklyReport{} - end - - def monthly_report_factory do - %Plausible.Site.MonthlyReport{} - end - - def shared_link_factory do - %Plausible.Site.SharedLink{ - name: "Link name", - slug: Nanoid.generate() - } - end - - def invitation_factory do - %Plausible.Auth.Invitation{ - invitation_id: Nanoid.generate(), - email: sequence(:email, &"email-#{&1}@example.com"), - role: :admin - } - end - - def api_key_factory do - key = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) - - %Plausible.Auth.ApiKey{ - name: "api-key-name", - key: key, - key_hash: Plausible.Auth.ApiKey.do_hash(key), - key_prefix: binary_part(key, 0, 6) - } - end - - def imported_visitors_factory do - %{ - table: "imported_visitors", - date: Timex.today(), - visitors: 1, - pageviews: 1, - bounces: 0, - visits: 1, - visit_duration: 10 - } - end - - def imported_sources_factory do - %{ - table: "imported_sources", - date: Timex.today(), - source: "", - visitors: 1, - visits: 1, - bounces: 0, - visit_duration: 10 - } - end - - def imported_pages_factory do - %{ - table: "imported_pages", - date: Timex.today(), - page: "", - visitors: 1, - pageviews: 1, - exits: 0, - time_on_page: 10 - } - end - - def imported_entry_pages_factory do - %{ - table: "imported_entry_pages", - date: Timex.today(), - entry_page: "", - visitors: 1, - entrances: 1, - bounces: 0, - visit_duration: 10 - } - end - - def imported_exit_pages_factory do - %{ - table: "imported_exit_pages", - date: Timex.today(), - exit_page: "", - visitors: 1, - exits: 1 - } - end - - def imported_locations_factory do - %{ - table: "imported_locations", - date: Timex.today(), - country: "", - region: "", - city: 0, - visitors: 1, - visits: 1, - bounces: 0, - visit_duration: 10 - } - end - - def imported_devices_factory do - %{ - table: "imported_devices", - date: Timex.today(), - device: "", - visitors: 1, - visits: 1, - bounces: 0, - visit_duration: 10 - } - end - - def imported_browsers_factory do - %{ - table: "imported_browsers", - date: Timex.today(), - browser: "", - visitors: 1, - visits: 1, - bounces: 0, - visit_duration: 10 - } - end - - def imported_operating_systems_factory do - %{ - table: "imported_operating_systems", - date: Timex.today(), - operating_system: "", - visitors: 1, - visits: 1, - bounces: 0, - visit_duration: 10 - } - end - defp hash_key() do Keyword.fetch!( Application.get_env(:plausible, PlausibleWeb.Endpoint), diff --git a/test/support/google_api_mock.ex b/test/support/google_api_mock.ex deleted file mode 100644 index 5aa33a7180b7..000000000000 --- a/test/support/google_api_mock.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Plausible.Google.Api.Mock do - def fetch_stats(_auth, _query, _limit) do - {:ok, - [ - %{"name" => "simple web analytics", "count" => 6}, - %{"name" => "open-source analytics", "count" => 2} - ]} - end -end diff --git a/test/support/paddle_api_mock.ex b/test/support/paddle_api_mock.ex deleted file mode 100644 index 42ad51a375b0..000000000000 --- a/test/support/paddle_api_mock.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Plausible.PaddleApi.Mock do - def get_subscription(_) do - {:ok, - %{ - "next_payment" => %{ - "date" => "2019-07-10", - "amount" => 6 - }, - "last_payment" => %{ - "date" => "2019-06-10", - "amount" => 6 - } - }} - end - - def update_subscription(_, %{plan_id: new_plan_id}) do - new_plan_id = String.to_integer(new_plan_id) - - {:ok, - %{ - "plan_id" => new_plan_id, - "next_payment" => %{ - "date" => "2019-07-10", - "amount" => 6 - } - }} - end - - def get_invoices(nil), do: {:error, :no_subscription} - - def get_invoices(subscription) do - case subscription.paddle_subscription_id do - "invalid_subscription_id" -> - {:error, :request_failed} - - _ -> - [ - %{ - "amount" => 11.11, - "currency" => "EUR", - "payout_date" => "2020-12-24", - "receipt_url" => "https://some-receipt-url.com" - }, - %{ - "amount" => 22, - "currency" => "USD", - "payout_date" => "2020-11-24", - "receipt_url" => "https://other-receipt-url.com" - } - ] - end - end -end diff --git a/test/workers/check_usage_test.exs b/test/workers/check_usage_test.exs deleted file mode 100644 index 37b71bce01d8..000000000000 --- a/test/workers/check_usage_test.exs +++ /dev/null @@ -1,279 +0,0 @@ -defmodule Plausible.Workers.CheckUsageTest do - use Plausible.DataCase - use Bamboo.Test - import Double - import Plausible.TestUtils - alias Plausible.Workers.CheckUsage - - setup [:create_user, :create_site] - @paddle_id_10k "558018" - - test "ignores user without subscription" do - CheckUsage.perform(nil) - - assert_no_emails_delivered() - end - - test "ignores user with subscription but no usage", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil) - - assert_no_emails_delivered() - assert Repo.reload(user).grace_period == nil - end - - test "does not send an email if account has been over the limit for one billing month", %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - |> stub(:last_two_billing_months_usage, fn _user -> {9_000, 11_000} end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_no_emails_delivered() - assert Repo.reload(user).grace_period == nil - end - - test "does not send an email if account is over the limit by less than 10%", %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - |> stub(:last_two_billing_months_usage, fn _user -> {10_999, 11_000} end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_no_emails_delivered() - assert Repo.reload(user).grace_period == nil - end - - test "sends an email when an account is over their limit for two consecutive billing months", %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_email_delivered_with( - to: [user], - subject: "[Action required] You have outgrown your Plausible subscription tier" - ) - - assert Repo.reload(user).grace_period.end_date == Timex.shift(Timex.today(), days: 7) - end - - test "skips checking users who already have a grace period", %{user: user} do - Plausible.Auth.User.start_grace_period(user, 12_000) |> Repo.update() - - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_no_emails_delivered() - assert Repo.reload(user).grace_period.allowance_required == 12_000 - end - - test "reccommends a plan to upgrade to", %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_delivered_email_matches(%{ - html_body: html_body - }) - - # Should find 2 visiors - assert html_body =~ ~s(Based on that we recommend you select the 100k/mo plan.) - end - - describe "enterprise customers" do - test "checks billable pageview usage for enterprise customer, sends usage information to enterprise@plausible.io", - %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {1_100_000, 1_100_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - enterprise_plan = insert(:enterprise_plan, user: user, monthly_pageview_limit: 1_000_000) - - insert(:subscription, - user: user, - paddle_plan_id: enterprise_plan.paddle_plan_id, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_email_delivered_with( - to: [{nil, "enterprise@plausible.io"}], - subject: "#{user.email} has outgrown their enterprise plan" - ) - end - - test "checks site limit for enterprise customer, sends usage information to enterprise@plausible.io", - %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {1, 1} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - enterprise_plan = insert(:enterprise_plan, user: user, site_limit: 2) - - insert(:site, members: [user]) - insert(:site, members: [user]) - insert(:site, members: [user]) - - insert(:subscription, - user: user, - paddle_plan_id: enterprise_plan.paddle_plan_id, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_email_delivered_with( - to: [{nil, "enterprise@plausible.io"}], - subject: "#{user.email} has outgrown their enterprise plan" - ) - end - end - - describe "timing" do - test "checks usage one day after the last_bill_date", %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, billing_stub) - - assert_email_delivered_with( - to: [user], - subject: "[Action required] You have outgrown your Plausible subscription tier" - ) - end - - test "does not check exactly one month after last_bill_date", %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: ~D[2021-03-28] - ) - - CheckUsage.perform(nil, billing_stub, ~D[2021-03-28]) - - assert_no_emails_delivered() - end - - test "for yearly subscriptions, checks usage multiple months + one day after the last_bill_date", - %{ - user: user - } do - billing_stub = - Plausible.Billing - |> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end) - |> stub(:last_two_billing_cycles, fn _user -> - {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: ~D[2021-06-29] - ) - - CheckUsage.perform(nil, billing_stub, ~D[2021-08-30]) - - assert_email_delivered_with( - to: [user], - subject: "[Action required] You have outgrown your Plausible subscription tier" - ) - end - end -end diff --git a/test/workers/clean_email_verification_codes_test.exs b/test/workers/clean_email_verification_codes_test.exs deleted file mode 100644 index be1e54086612..000000000000 --- a/test/workers/clean_email_verification_codes_test.exs +++ /dev/null @@ -1,35 +0,0 @@ -defmodule Plausible.Workers.CleanEmailVerificationCodesTest do - use Plausible.DataCase - alias Plausible.Workers.CleanEmailVerificationCodes - - defp issue_code(user, issued_at) do - code = - Repo.one( - from(c in "email_verification_codes", where: is_nil(c.user_id), select: c.code, limit: 1) - ) - - Repo.update_all(from(c in "email_verification_codes", where: c.code == ^code), - set: [user_id: user.id, issued_at: issued_at] - ) - end - - test "cleans codes that are more than 4 hours old" do - user = insert(:user) - issue_code(user, Timex.now() |> Timex.shift(hours: -5)) - issue_code(user, Timex.now() |> Timex.shift(days: -5)) - - CleanEmailVerificationCodes.perform(nil) - - refute Repo.exists?(from c in "email_verification_codes", where: c.user_id == ^user.id) - end - - @tag :skip - test "does not clean code from 2 hours ago" do - user = insert(:user) - issue_code(user, Timex.now() |> Timex.shift(hours: -2)) - - CleanEmailVerificationCodes.perform(nil) - - assert Repo.exists?(from c in "email_verification_codes", where: c.user_id == ^user.id) - end -end diff --git a/test/workers/clean_invitations_test.exs b/test/workers/clean_invitations_test.exs deleted file mode 100644 index 6973fda3de00..000000000000 --- a/test/workers/clean_invitations_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Plausible.Workers.CleanInvitationsTest do - use Plausible.DataCase - alias Plausible.Workers.CleanInvitations - - @tag :skip - test "cleans invitation that is more than 48h old" do - insert(:invitation, - inserted_at: Timex.shift(Timex.now(), hours: -49), - site: build(:site), - inviter: build(:user) - ) - - CleanInvitations.perform(nil) - - refute Repo.exists?(Plausible.Auth.Invitation) - end - - @tag :skip - test "does not clean invitation that is less than 48h old" do - insert(:invitation, - inserted_at: Timex.shift(Timex.now(), hours: -47), - site: build(:site), - inviter: build(:user) - ) - - CleanInvitations.perform(nil) - - assert Repo.exists?(Plausible.Auth.Invitation) - end -end diff --git a/test/workers/import_google_analytics_test.exs b/test/workers/import_google_analytics_test.exs deleted file mode 100644 index d126144b9920..000000000000 --- a/test/workers/import_google_analytics_test.exs +++ /dev/null @@ -1,86 +0,0 @@ -defmodule Plausible.Workers.ImportGoogleAnalyticsTest do - use Plausible.DataCase - use Bamboo.Test - import Double - alias Plausible.Workers.ImportGoogleAnalytics - - @imported_data %Plausible.Site.ImportedData{ - end_date: Timex.today(), - source: "Google Analytics", - status: "importing" - } - - test "updates the imported_data field for the site after succesful import" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: 1)) - site = insert(:site, members: [user], imported_data: @imported_data) - - api_stub = - stub(Plausible.Google.Api, :import_analytics, fn _site, _profile -> - {:ok, nil} - end) - - ImportGoogleAnalytics.perform( - %Oban.Job{args: %{"site_id" => site.id, "profile" => "profile"}}, - api_stub - ) - - assert Repo.reload!(site).imported_data.status == "ok" - end - - test "sends email to owner after succesful import" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: 1)) - site = insert(:site, members: [user], imported_data: @imported_data) - - api_stub = - stub(Plausible.Google.Api, :import_analytics, fn _site, _profile -> - {:ok, nil} - end) - - ImportGoogleAnalytics.perform( - %Oban.Job{args: %{"site_id" => site.id, "profile" => "profile"}}, - api_stub - ) - - assert_email_delivered_with( - to: [user], - subject: "Google Analytics data imported for #{site.domain}" - ) - end - - test "updates site record after failed import" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: 1)) - site = insert(:site, members: [user], imported_data: @imported_data) - - api_stub = - stub(Plausible.Google.Api, :import_analytics, fn _site, _profile -> - {:error, "Something went wrong"} - end) - - ImportGoogleAnalytics.perform( - %Oban.Job{args: %{"site_id" => site.id, "profile" => "profile"}}, - api_stub - ) - - assert Repo.reload!(site).imported_data.status == "error" - end - - test "sends email to owner after failed import" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: 1)) - site = insert(:site, members: [user], imported_data: @imported_data) - - api_stub = - stub(Plausible.Google.Api, :import_analytics, fn _site, _profile -> - {:error, "Something went wrong"} - end) - - ImportGoogleAnalytics.perform( - %Oban.Job{args: %{"site_id" => site.id, "profile" => "profile"}}, - api_stub - ) - - assert_email_delivered_with( - to: [user], - subject: "Google Analytics import failed for #{site.domain}" - ) - end -end diff --git a/test/workers/lock_sites_test.exs b/test/workers/lock_sites_test.exs deleted file mode 100644 index 935c85beff4b..000000000000 --- a/test/workers/lock_sites_test.exs +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Plausible.Workers.LockSitesTest do - use Plausible.DataCase - alias Plausible.Workers.LockSites - - test "does not lock trial user's site" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: 1)) - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - refute Repo.reload!(site).locked - end - - test "locks site for user whose trial has expired" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - assert Repo.reload!(site).locked - end - - test "does not lock active subsriber's sites" do - user = insert(:user) - insert(:subscription, status: "active", user: user) - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - refute Repo.reload!(site).locked - end - - test "does not lock user who is past due" do - user = insert(:user) - insert(:subscription, status: "past_due", user: user) - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - refute Repo.reload!(site).locked - end - - test "does not lock user who cancelled subscription but it hasn't expired yet" do - user = insert(:user) - insert(:subscription, status: "deleted", user: user) - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - refute Repo.reload!(site).locked - end - - test "locks user who cancelled subscription and the cancelled subscription has expired" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - - insert(:subscription, - status: "deleted", - next_bill_date: Timex.today() |> Timex.shift(days: -1), - user: user - ) - - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - assert Repo.reload!(site).locked - end - - test "does not lock if user has an old cancelled subscription and a new active subscription" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - - insert(:subscription, - status: "deleted", - next_bill_date: Timex.today() |> Timex.shift(days: -1), - user: user, - inserted_at: Timex.now() |> Timex.shift(days: -1) - ) - - insert(:subscription, status: "active", user: user) - - site = insert(:site, members: [user]) - - LockSites.perform(nil) - - refute Repo.reload!(site).locked - end - - describe "locking" do - test "only locks sites that the user owns" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - - owner_site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :owner) - ] - ) - - viewer_site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :viewer) - ] - ) - - LockSites.perform(nil) - - owner_site = Repo.reload!(owner_site) - viewer_site = Repo.reload!(viewer_site) - - assert owner_site.locked - refute viewer_site.locked - end - end -end diff --git a/test/workers/notify_annual_renewal_test.exs b/test/workers/notify_annual_renewal_test.exs deleted file mode 100644 index c729d616a697..000000000000 --- a/test/workers/notify_annual_renewal_test.exs +++ /dev/null @@ -1,173 +0,0 @@ -defmodule Plausible.Workers.NotifyAnnualRenewalTest do - use Plausible.DataCase - use Bamboo.Test - import Plausible.TestUtils - alias Plausible.Workers.NotifyAnnualRenewal - - setup [:create_user, :create_site] - @monthly_plan "558018" - @yearly_plan "572810" - @v2_pricing_yearly_plan "653232" - - test "ignores user without subscription" do - NotifyAnnualRenewal.perform(nil) - - assert_no_emails_delivered() - end - - test "ignores user with monthly subscription", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @monthly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 7) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_no_emails_delivered() - end - - test "ignores user with yearly subscription that's not due for renewal in 7 days", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 10) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_no_emails_delivered() - end - - test "ignores user with old yearly subscription that's been superseded by a newer one", %{ - user: user - } do - insert(:subscription, - inserted_at: Timex.shift(Timex.now(), days: -1), - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 5) - ) - - insert(:subscription, - inserted_at: Timex.now(), - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 30) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_no_emails_delivered() - end - - test "sends renewal notification to user whose subscription is due for renewal in 7 days", %{ - user: user - } do - insert(:subscription, - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 7) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible subscription is up for renewal" - ) - end - - test "sends renewal notification to user whose subscription is due for renewal in 2 days", %{ - user: user - } do - insert(:subscription, - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 2) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible subscription is up for renewal" - ) - end - - test "does not send renewal notification multiple times", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 7) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible subscription is up for renewal" - ) - - NotifyAnnualRenewal.perform(nil) - - assert_no_emails_delivered() - end - - test "sends a renewal notification again a year after the previous one", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 7) - ) - - Repo.insert_all("sent_renewal_notifications", [ - %{ - user_id: user.id, - timestamp: Timex.shift(Timex.today(), years: -1) |> Timex.to_naive_datetime() - } - ]) - - NotifyAnnualRenewal.perform(nil) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible subscription is up for renewal" - ) - end - - test "sends renewal notification to user on v2 yearly pricing plans", %{ - user: user - } do - insert(:subscription, - user: user, - paddle_plan_id: @v2_pricing_yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 7) - ) - - NotifyAnnualRenewal.perform(nil) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible subscription is up for renewal" - ) - end - - describe "expiration" do - test "if user subscription is 'deleted', notify them about expiration instead", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @yearly_plan, - next_bill_date: Timex.shift(Timex.today(), days: 7), - status: "deleted" - ) - - NotifyAnnualRenewal.perform(nil) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible subscription is about to expire" - ) - end - end -end diff --git a/test/workers/schedule_email_reports_test.exs b/test/workers/schedule_email_reports_test.exs deleted file mode 100644 index d8e51b2180f3..000000000000 --- a/test/workers/schedule_email_reports_test.exs +++ /dev/null @@ -1,101 +0,0 @@ -defmodule Plausible.Workers.ScheduleEmailReportsTest do - use Plausible.DataCase - use Oban.Testing, repo: Plausible.Repo - alias Plausible.Workers.{ScheduleEmailReports, SendEmailReport} - - describe "weekly reports" do - @tag :skip - test "schedules weekly report on Monday 9am local timezone" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - - assert_enqueued( - worker: SendEmailReport, - args: %{site_id: site.id, interval: "weekly"}, - scheduled_at: ScheduleEmailReports.monday_9am(site.timezone) - ) - end - - @tag :skip - test "does not schedule more than one weekly report at a time" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - perform_job(ScheduleEmailReports, %{}) - - assert Enum.count(all_enqueued(worker: SendEmailReport)) == 1 - end - - test "does not schedule a weekly report for locked site" do - site = insert(:site, locked: true, domain: "test-site.com", timezone: "US/Eastern") - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - - assert Enum.empty?(all_enqueued(worker: SendEmailReport)) - end - - @tag :skip - test "schedules a new report as soon as a previous one is completed" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - Repo.update_all("oban_jobs", set: [state: "completed"]) - assert Enum.empty?(all_enqueued(worker: SendEmailReport)) - perform_job(ScheduleEmailReports, %{}) - assert Enum.count(all_enqueued(worker: SendEmailReport)) == 1 - end - end - - describe "monthly_reports" do - @tag :skip - test "schedules monthly report on first of the next month at 9am local timezone" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:monthly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - - assert_enqueued( - worker: SendEmailReport, - args: %{site_id: site.id, interval: "monthly"}, - scheduled_at: ScheduleEmailReports.first_of_month_9am(site.timezone) - ) - end - - @tag :skip - test "does not schedule more than one monthly report at a time" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:monthly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - perform_job(ScheduleEmailReports, %{}) - - assert Enum.count(all_enqueued(worker: SendEmailReport)) == 1 - end - - test "does not schedule a monthly report for locked site" do - site = insert(:site, locked: true, domain: "test-site.com", timezone: "US/Eastern") - insert(:monthly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - - assert Enum.empty?(all_enqueued(worker: SendEmailReport)) - end - - @tag :skip - test "schedules a new report as soon as a previous one is completed" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:monthly_report, site: site, recipients: ["user@email.com"]) - - perform_job(ScheduleEmailReports, %{}) - Repo.update_all("oban_jobs", set: [state: "completed"]) - assert Enum.empty?(all_enqueued(worker: SendEmailReport)) - perform_job(ScheduleEmailReports, %{}) - assert Enum.count(all_enqueued(worker: SendEmailReport)) == 1 - end - end -end diff --git a/test/workers/send_check_stats_emails_test.exs b/test/workers/send_check_stats_emails_test.exs deleted file mode 100644 index 9bf1885eb08a..000000000000 --- a/test/workers/send_check_stats_emails_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -defmodule Plausible.Workers.SendCheckStatsEmailsTest do - use Plausible.DataCase - use Oban.Testing, repo: Plausible.Repo - use Bamboo.Test - alias Plausible.Workers.SendCheckStatsEmails - - test "does not send an email before a week has passed" do - user = insert(:user, inserted_at: days_ago(6), last_seen: days_ago(6)) - insert(:site, domain: "test-site.com", members: [user]) - - perform_job(SendCheckStatsEmails, %{}) - - assert_no_emails_delivered() - end - - test "does not send an email if the user has logged in recently" do - user = insert(:user, inserted_at: days_ago(9), last_seen: days_ago(6)) - insert(:site, domain: "test-site.com", members: [user]) - - perform_job(SendCheckStatsEmails, %{}) - - assert_no_emails_delivered() - end - - test "does not send an email if the user has configured a weekly report" do - user = insert(:user, inserted_at: days_ago(9), last_seen: days_ago(7)) - site = insert(:site, domain: "test-site.com", members: [user]) - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - - perform_job(SendCheckStatsEmails, %{}) - - assert_no_emails_delivered() - end - - test "sends an email after a week of signup if the user hasn't logged in" do - user = insert(:user, inserted_at: days_ago(8), last_seen: days_ago(8)) - insert(:site, domain: "test-site.com", members: [user]) - - perform_job(SendCheckStatsEmails, %{}) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Check your Plausible website stats" - ) - end - - defp days_ago(days) do - NaiveDateTime.utc_now() - |> NaiveDateTime.truncate(:second) - |> Timex.shift(days: -days) - end -end diff --git a/test/workers/send_email_report_test.exs b/test/workers/send_email_report_test.exs deleted file mode 100644 index 63e15a625e5c..000000000000 --- a/test/workers/send_email_report_test.exs +++ /dev/null @@ -1,138 +0,0 @@ -defmodule Plausible.Workers.SendEmailReportTest do - import Plausible.TestUtils - use Plausible.DataCase - use Bamboo.Test - use Oban.Testing, repo: Plausible.Repo - alias Plausible.Workers.SendEmailReport - alias Timex.Timezone - - describe "weekly reports" do - @tag :skip - test "sends weekly report to all recipients" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:weekly_report, site: site, recipients: ["user@email.com", "user2@email.com"]) - - perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) - - assert_email_delivered_with( - subject: "Weekly report for #{site.domain}", - to: [nil: "user@email.com"] - ) - - assert_email_delivered_with( - subject: "Weekly report for #{site.domain}", - to: [nil: "user2@email.com"] - ) - end - - @tag :skip - test "calculates timezone correctly" do - site = insert(:site, timezone: "US/Eastern") - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - - now = Timex.now(site.timezone) - last_monday = Timex.shift(now, weeks: -1) |> Timex.beginning_of_week() - last_sunday = Timex.shift(now, weeks: -1) |> Timex.end_of_week() - sunday_before_last = Timex.shift(last_monday, minutes: -1) - this_monday = Timex.beginning_of_week(now) - - create_pageviews([ - # Sunday before last, not counted - %{domain: site.domain, timestamp: Timezone.convert(sunday_before_last, "UTC")}, - # Sunday before last, not counted - %{domain: site.domain, timestamp: Timezone.convert(sunday_before_last, "UTC")}, - # Last monday, counted - %{domain: site.domain, timestamp: Timezone.convert(last_monday, "UTC")}, - # Last sunday, counted - %{domain: site.domain, timestamp: Timezone.convert(last_sunday, "UTC")}, - # This monday, not counted - %{domain: site.domain, timestamp: Timezone.convert(this_monday, "UTC")}, - # This monday, not counted - %{domain: site.domain, timestamp: Timezone.convert(this_monday, "UTC")} - ]) - - perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) - - assert_delivered_email_matches(%{ - to: [nil: "user@email.com"], - html_body: html_body - }) - - # Should find 2 visiors - assert html_body =~ - ~s(2) - end - - @tag :skip - test "includes the correct stats" do - site = insert(:site, domain: "test-site.com") - insert(:weekly_report, site: site, recipients: ["user@email.com"]) - now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) - - populate_stats(site, [ - build(:pageview, - referrer_source: "Google", - user_id: 123, - timestamp: Timex.shift(now, days: -7) - ), - build(:pageview, user_id: 123, timestamp: Timex.shift(now, days: -7)), - build(:pageview, timestamp: Timex.shift(now, days: -7)) - ]) - - perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "weekly"}) - - assert_delivered_email_matches(%{ - to: [nil: "user@email.com"], - html_body: html_body - }) - - {:ok, document} = Floki.parse_document(html_body) - - visitors = Floki.find(document, "#visitors") |> Floki.text() - assert visitors == "2" - - pageviews = Floki.find(document, "#pageviews") |> Floki.text() - assert pageviews == "3" - - referrer = Floki.find(document, ".referrer") |> List.first() - referrer_name = referrer |> Floki.find("#referrer-name") |> Floki.text() - referrer_count = referrer |> Floki.find("#referrer-count") |> Floki.text() - - assert referrer_name == "Google" - assert referrer_count == "1" - - page = Floki.find(document, ".page") |> List.first() - page_name = page |> Floki.find("#page-name") |> Floki.text() - page_count = page |> Floki.find("#page-count") |> Floki.text() - - assert page_name == "/" - assert page_count == "2" - end - end - - describe "monthly_reports" do - @tag :skip - test "sends monthly report to all recipients" do - site = insert(:site, domain: "test-site.com", timezone: "US/Eastern") - insert(:monthly_report, site: site, recipients: ["user@email.com", "user2@email.com"]) - - last_month = - Timex.now(site.timezone) - |> Timex.shift(months: -1) - |> Timex.beginning_of_month() - |> Timex.format!("{Mfull}") - - perform_job(SendEmailReport, %{"site_id" => site.id, "interval" => "monthly"}) - - assert_email_delivered_with( - subject: "#{last_month} report for #{site.domain}", - to: [nil: "user@email.com"] - ) - - assert_email_delivered_with( - subject: "#{last_month} report for #{site.domain}", - to: [nil: "user2@email.com"] - ) - end - end -end diff --git a/test/workers/send_site_setup_emails_test.exs b/test/workers/send_site_setup_emails_test.exs deleted file mode 100644 index 76cc6ead0572..000000000000 --- a/test/workers/send_site_setup_emails_test.exs +++ /dev/null @@ -1,100 +0,0 @@ -defmodule Plausible.Workers.SendSiteSetupEmailsTest do - use Plausible.DataCase - use Bamboo.Test - use Oban.Testing, repo: Plausible.Repo - import Plausible.TestUtils - alias Plausible.Workers.SendSiteSetupEmails - - describe "when user has not managed to set up the site" do - test "does not send an email 47 hours after site creation" do - user = insert(:user) - insert(:site, members: [user], inserted_at: hours_ago(47)) - - perform_job(SendSiteSetupEmails, %{}) - - assert_no_emails_delivered() - end - - test "sends a setup help email 48 hours after site has been created" do - user = insert(:user) - insert(:site, members: [user], inserted_at: hours_ago(49)) - - perform_job(SendSiteSetupEmails, %{}) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible setup: Waiting for the first page views" - ) - end - - test "does not send an email more than 72 hours after signup" do - user = insert(:user) - insert(:site, members: [user], inserted_at: hours_ago(73)) - - perform_job(SendSiteSetupEmails, %{}) - - assert_no_emails_delivered() - end - end - - describe "when user has managed to set up their site" do - test "sends the setup completed email as soon as possible" do - user = insert(:user) - insert(:site, members: [user], domain: "test-site.com") - - perform_job(SendSiteSetupEmails, %{}) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Plausible is now tracking your website stats" - ) - end - - test "sends the setup completed email after the help email has been sent" do - user = insert(:user) - site = insert(:site, members: [user], inserted_at: hours_ago(49)) - - perform_job(SendSiteSetupEmails, %{}) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible setup: Waiting for the first page views" - ) - - create_pageviews([%{domain: site.domain}]) - perform_job(SendSiteSetupEmails, %{}) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Plausible is now tracking your website stats" - ) - end - end - - describe "trial user who has not set up a website" do - test "does not send an email before 48h have passed" do - insert(:user, inserted_at: hours_ago(47)) - - perform_job(SendSiteSetupEmails, %{}) - - assert_no_emails_delivered() - end - - test "sends the create site email after 48h" do - user = insert(:user, inserted_at: hours_ago(49)) - - perform_job(SendSiteSetupEmails, %{}) - - assert_email_delivered_with( - to: [{user.name, user.email}], - subject: "Your Plausible setup: Add your website details" - ) - end - end - - defp hours_ago(hours) do - NaiveDateTime.utc_now() - |> NaiveDateTime.truncate(:second) - |> Timex.shift(hours: -hours) - end -end diff --git a/test/workers/send_trial_notifications_test.exs b/test/workers/send_trial_notifications_test.exs deleted file mode 100644 index 58c7cec468a8..000000000000 --- a/test/workers/send_trial_notifications_test.exs +++ /dev/null @@ -1,201 +0,0 @@ -defmodule Plausible.Workers.SendTrialNotificationsTest do - use Plausible.DataCase - use Bamboo.Test - use Oban.Testing, repo: Plausible.Repo - import Plausible.TestUtils - alias Plausible.Workers.SendTrialNotifications - - test "does not send a notification if user didn't create a site" do - insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7)) - insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 1)) - insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 0)) - insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: -1)) - - perform_job(SendTrialNotifications, %{}) - - assert_no_emails_delivered() - end - - test "does not send a notification if user created a site but there are no pageviews" do - user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7)) - insert(:site, members: [user]) - - perform_job(SendTrialNotifications, %{}) - - assert_no_emails_delivered() - end - - test "does not send a notification if user is a collaborator on sites but not an owner" do - user = insert(:user, trial_expiry_date: Timex.now()) - - site = - insert(:site, - memberships: [ - build(:site_membership, user: user, role: :admin) - ] - ) - - populate_stats(site, [build(:pageview, domain: site.domain)]) - - perform_job(SendTrialNotifications, %{}) - - assert_no_emails_delivered() - end - - describe "with site and pageviews" do - test "sends a reminder 7 days before trial ends (16 days after user signed up)" do - user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7)) - site = insert(:site, members: [user]) - populate_stats(site, [build(:pageview, domain: site.domain)]) - - perform_job(SendTrialNotifications, %{}) - - assert_delivered_email(PlausibleWeb.Email.trial_one_week_reminder(user)) - end - - test "sends an upgrade email the day before the trial ends" do - user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 1)) - site = insert(:site, members: [user]) - - populate_stats(site, [ - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain) - ]) - - perform_job(SendTrialNotifications, %{}) - - assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", {3, 0})) - end - - test "sends an upgrade email the day the trial ends" do - user = insert(:user, trial_expiry_date: Timex.today()) - site = insert(:site, members: [user]) - - populate_stats(site, [ - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain) - ]) - - perform_job(SendTrialNotifications, %{}) - - assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "today", {3, 0})) - end - - test "does not include custom event note if user has not used custom events" do - user = insert(:user, trial_expiry_date: Timex.today()) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 0}) - - assert email.html_body =~ - "In the last month, your account has used 9,000 billable pageviews." - end - - test "includes custom event note if user has used custom events" do - user = insert(:user, trial_expiry_date: Timex.today()) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 100}) - - assert email.html_body =~ - "In the last month, your account has used 9,100 billable pageviews and custom events in total." - end - - test "sends a trial over email the day after the trial ends" do - user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) - site = insert(:site, members: [user]) - - populate_stats(site, [ - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain) - ]) - - perform_job(SendTrialNotifications, %{}) - - assert_delivered_email(PlausibleWeb.Email.trial_over_email(user)) - end - - test "does not send a notification if user has a subscription" do - user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7)) - site = insert(:site, members: [user]) - - populate_stats(site, [ - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain), - build(:pageview, domain: site.domain) - ]) - - insert(:subscription, user: user) - - perform_job(SendTrialNotifications, %{}) - - assert_no_emails_delivered() - end - end - - describe "Suggested plans" do - test "suggests 10k/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 0}) - assert email.html_body =~ "we recommend you select the 10k/mo plan." - end - - test "suggests 100k/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {90_000, 0}) - assert email.html_body =~ "we recommend you select the 100k/mo plan." - end - - test "suggests 200k/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {180_000, 0}) - assert email.html_body =~ "we recommend you select the 200k/mo plan." - end - - test "suggests 500k/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {450_000, 0}) - assert email.html_body =~ "we recommend you select the 500k/mo plan." - end - - test "suggests 1m/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {900_000, 0}) - assert email.html_body =~ "we recommend you select the 1M/mo plan." - end - - test "suggests 2m/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {1_800_000, 0}) - assert email.html_body =~ "we recommend you select the 2M/mo plan." - end - - test "suggests 5m/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {4_500_000, 0}) - assert email.html_body =~ "we recommend you select the 5M/mo plan." - end - - test "suggests 10m/mo plan" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000_000, 0}) - assert email.html_body =~ "we recommend you select the 10M/mo plan." - end - - test "does not suggest a plan above that" do - user = insert(:user) - - email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {20_000_000, 0}) - assert email.html_body =~ "please reply back to this email to get a quote for your volume" - end - end -end diff --git a/test/workers/spike_notifier_test.exs b/test/workers/spike_notifier_test.exs deleted file mode 100644 index 055cffe6f334..000000000000 --- a/test/workers/spike_notifier_test.exs +++ /dev/null @@ -1,88 +0,0 @@ -defmodule Plausible.Workers.SpikeNotifierTest do - use Plausible.DataCase - use Bamboo.Test - import Double - alias Plausible.Workers.SpikeNotifier - - test "does not notify anyone if current visitors does not exceed notification threshold" do - site = insert(:site) - - insert(:spike_notification, - site: site, - threshold: 10, - recipients: ["jerod@example.com", "uku@example.com"] - ) - - clickhouse_stub = - stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 5 end) - |> stub(:top_sources, fn _site, _query, _limit, _page, _show_noref -> [] end) - - SpikeNotifier.perform(nil, clickhouse_stub) - - assert_no_emails_delivered() - end - - test "notifies all recipients when traffic is higher than configured threshold" do - site = insert(:site) - - insert(:spike_notification, - site: site, - threshold: 10, - recipients: ["jerod@example.com", "uku@example.com"] - ) - - clickhouse_stub = - stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 10 end) - |> stub(:top_sources, fn _site, _query, _limit, _page, _show_noref -> [] end) - - SpikeNotifier.perform(nil, clickhouse_stub) - - assert_email_delivered_with( - subject: "Traffic spike on #{site.domain}", - to: [nil: "jerod@example.com"] - ) - - assert_email_delivered_with( - subject: "Traffic spike on #{site.domain}", - to: [nil: "uku@example.com"] - ) - end - - test "does not check site if it is locked" do - site = insert(:site, locked: true) - - insert(:spike_notification, - site: site, - threshold: 10, - recipients: ["uku@example.com"] - ) - - clickhouse_stub = - stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 10 end) - |> stub(:top_sources, fn _site, _query, _limit, _page, _show_noref -> [] end) - - SpikeNotifier.perform(nil, clickhouse_stub) - - assert_no_emails_delivered() - end - - test "does not notify anyone if a notification already went out in the last 12 hours" do - site = insert(:site) - insert(:spike_notification, site: site, threshold: 10, recipients: ["uku@example.com"]) - - clickhouse_stub = - stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 10 end) - |> stub(:top_sources, fn _site, _query, _limit, _page, _show_noref -> [] end) - - SpikeNotifier.perform(nil, clickhouse_stub) - - assert_email_delivered_with( - subject: "Traffic spike on #{site.domain}", - to: [nil: "uku@example.com"] - ) - - SpikeNotifier.perform(nil, clickhouse_stub) - - assert_no_emails_delivered() - end -end diff --git a/tracker/LICENSE.md b/tracker/LICENSE.md deleted file mode 100644 index 38d02b46b109..000000000000 --- a/tracker/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2020 Plausible Insights OÜ - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tracker/compile.js b/tracker/compile.js deleted file mode 100644 index b0aa42e75178..000000000000 --- a/tracker/compile.js +++ /dev/null @@ -1,29 +0,0 @@ -const uglify = require("uglify-js"); -const fs = require('fs') -const path = require('path') -const Handlebars = require("handlebars"); -const g = require("generatorics"); - -function relPath(segment) { - return path.join(__dirname, segment) -} - -function compilefile(input, output, templateVars = {}) { - const code = fs.readFileSync(input).toString() - const template = Handlebars.compile(code) - const rendered = template(templateVars) - const result = uglify.minify(rendered) - fs.writeFileSync(output, result.code) -} - -const base_variants = ["hash", "outbound-links", "exclusions", "compat", "local", "manual"] -const variants = [...g.clone.powerSet(base_variants)].filter(a => a.length > 0).map(a => a.sort()); - -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/plausible.js')) -compilefile(relPath('src/plausible.js'), relPath('../priv/tracker/js/analytics.js')) -compilefile(relPath('src/p.js'), relPath('../priv/tracker/js/p.js')) - -variants.map(variant => { - const options = variant.map(variant => variant.replace('-', '_')).reduce((acc, curr) => (acc[curr] = true, acc), {}) - compilefile(relPath('src/plausible.js'), relPath(`../priv/tracker/js/plausible.${variant.join('.')}.js`), options) -}) diff --git a/tracker/package-lock.json b/tracker/package-lock.json deleted file mode 100644 index f75c6ff886ca..000000000000 --- a/tracker/package-lock.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "name": "tracker", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "license": "MIT", - "dependencies": { - "generatorics": "^1.1.0", - "handlebars": "^4.7.7", - "uglify-js": "^3.9.4" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/generatorics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/generatorics/-/generatorics-1.1.0.tgz", - "integrity": "sha1-aVBgu42IuQmzAXGlyz1CR2hmETg=", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uglify-js": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.4.tgz", - "integrity": "sha512-8RZBJq5smLOa7KslsNsVcSH+KOXf1uDU8yqLeNuVKwmT0T3FA0ZoXlinQfRad7SDcbZZRZE4ov+2v71EnxNyCA==", - "dependencies": { - "commander": "~2.20.3" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "generatorics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/generatorics/-/generatorics-1.1.0.tgz", - "integrity": "sha1-aVBgu42IuQmzAXGlyz1CR2hmETg=" - }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "uglify-js": { - "version": "3.9.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.4.tgz", - "integrity": "sha512-8RZBJq5smLOa7KslsNsVcSH+KOXf1uDU8yqLeNuVKwmT0T3FA0ZoXlinQfRad7SDcbZZRZE4ov+2v71EnxNyCA==", - "requires": { - "commander": "~2.20.3" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - } - } -} diff --git a/tracker/package.json b/tracker/package.json deleted file mode 100644 index 88ce3cb434d0..000000000000 --- a/tracker/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "scripts": { - "deploy": "node compile.js" - }, - "license": "MIT", - "dependencies": { - "generatorics": "^1.1.0", - "handlebars": "^4.7.7", - "uglify-js": "^3.9.4" - } -} diff --git a/tracker/src/p.js b/tracker/src/p.js deleted file mode 100644 index 501a40f3fead..000000000000 --- a/tracker/src/p.js +++ /dev/null @@ -1,123 +0,0 @@ -// NOTE: This file is deprecated and only kept around so we don't break compatibility -// with some early customers. This script uses a cookie but this was an old version of Plausible. -// Current script can be found in the tracker/src/plausible.js file - -(function(){ - 'use strict'; - - var scriptEl = window.document.currentScript; - var plausibleHost = new URL(scriptEl.src).origin - - function setCookie(name,value) { - var date = new Date(); - date.setTime(date.getTime() + (3*365*24*60*60*1000)); // 3 YEARS - var expires = "; expires=" + date.toUTCString(); - document.cookie = name + "=" + (value || "") + expires + "; samesite=strict; path=/"; - } - - function getCookie(name) { - var matches = document.cookie.match(new RegExp( - "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" - )); - return matches ? decodeURIComponent(matches[1]) : null; - } - - function ignore(reason) { - console.warn('[Plausible] Ignoring event because ' + reason); - } - - function getUserData() { - var userData = JSON.parse(getCookie('plausible_user')) - - if (userData) { - return { - initial_referrer: userData.initial_referrer && decodeURIComponent(userData.initial_referrer), - initial_source: userData.initial_source && decodeURIComponent(userData.initial_source) - } - } else { - userData = { - initial_referrer: window.document.referrer || null, - initial_source: getSourceFromQueryParam(), - } - - setCookie('plausible_user', JSON.stringify({ - initial_referrer: userData.initial_referrer && encodeURIComponent(userData.initial_referrer), - initial_source: userData.initial_source && encodeURIComponent(userData.initial_source), - })) - - return userData - } - } - - function trigger(eventName, options) { - if (/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(window.location.hostname)) return ignore('website is running locally'); - if (window.location.protocol === 'file:') return ignore('website is running locally'); - if (window.document.visibilityState === 'prerender') return ignore('document is prerendering'); - - var payload = CONFIG['trackAcquisition'] ? getUserData() : {} - payload.n = eventName - payload.u = window.location.href - payload.d = CONFIG['domain'] - payload.r = window.document.referrer || null - payload.w = window.innerWidth - - var request = new XMLHttpRequest(); - request.open('POST', plausibleHost + '/api/event', true); - request.setRequestHeader('Content-Type', 'text/plain'); - - request.send(JSON.stringify(payload)); - - request.onreadystatechange = function() { - if (request.readyState == XMLHttpRequest.DONE) { - options && options.callback && options.callback() - } - } - } - - function page(options) { - trigger('pageview', options) - } - - function trackPushState() { - var his = window.history - if (his.pushState) { - var originalFn = his['pushState'] - his.pushState = function() { - originalFn.apply(this, arguments) - page(); - } - } - window.addEventListener('popstate', page) - } - - function configure(key, val) { - CONFIG[key] = val - } - - try { - var CONFIG = { - domain: window.location.hostname - } - - var functions = { - page: page, - trigger: trigger, - trackPushState: trackPushState, - configure: configure - } - - var queue = window.plausible.q || [] - - window.plausible = function() { - var args = [].slice.call(arguments); - var funcName = args.shift(); - functions[funcName].apply(this, args); - }; - - for (var i = 0; i < queue.length; i++) { - window.plausible.apply(this, queue[i]) - } - } catch (e) { - new Image().src = plausibleHost + '/api/error?message=' + encodeURIComponent(e.message); - } -})(); diff --git a/tracker/src/plausible.js b/tracker/src/plausible.js deleted file mode 100644 index 207f2e1a8bd9..000000000000 --- a/tracker/src/plausible.js +++ /dev/null @@ -1,164 +0,0 @@ -(function(){ - 'use strict'; - - var location = window.location - var document = window.document - - {{#if compat}} - var scriptEl = document.getElementById('plausible'); - {{else}} - var scriptEl = document.currentScript; - {{/if}} - var endpoint = scriptEl.getAttribute('data-api') || defaultEndpoint(scriptEl) - {{#if exclusions}} - var excludedPaths = scriptEl && scriptEl.getAttribute('data-exclude').split(','); - {{/if}} - - function warn(reason) { - console.warn('Ignoring Event: ' + reason); - } - - function defaultEndpoint(el) { - {{#if compat}} - var pathArray = el.src.split( '/' ); - var protocol = pathArray[0]; - var host = pathArray[2]; - return protocol + '//' + host + '/api/event'; - {{else}} - return new URL(el.src).origin + '/api/event' - {{/if}} - } - - - function trigger(eventName, options) { - {{#unless local}} - if (/^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/.test(location.hostname) || location.protocol === 'file:') return warn('localhost'); - {{/unless}} - if (window._phantom || window.__nightmare || window.navigator.webdriver || window.Cypress) return; - try { - if (window.localStorage.plausible_ignore=="true") { - return warn('localStorage flag') - } - } catch (e) { - - } - {{#if exclusions}} - if (excludedPaths) - for (var i = 0; i < excludedPaths.length; i++) - if (eventName == "pageview" && location.pathname.match(new RegExp('^' + excludedPaths[i].trim().replace(/\*\*/g, '.*').replace(/([^\.])\*/g, '$1[^\\s\/]*') + '\/?$'))) - return warn('exclusion rule'); - {{/if}} - - var payload = {} - payload.n = eventName - {{#if manual}} - payload.u = options && options.u ? options.u : location.href - {{else}} - payload.u = location.href - {{/if}} - payload.d = scriptEl.getAttribute('data-domain') - payload.r = document.referrer || null - payload.w = window.innerWidth - if (options && options.meta) { - payload.m = JSON.stringify(options.meta) - } - if (options && options.props) { - payload.p = JSON.stringify(options.props) - } - {{#if hash}} - payload.h = 1 - {{/if}} - - var request = new XMLHttpRequest(); - request.open('POST', endpoint, true); - request.setRequestHeader('Content-Type', 'text/plain'); - - request.send(JSON.stringify(payload)); - - request.onreadystatechange = function() { - if (request.readyState == 4) { - options && options.callback && options.callback() - } - } - } - - {{#if outbound_links}} - function handleOutbound(event) { - var link = event.target; - var middle = event.type == "auxclick" && event.which == 2; - var click = event.type == "click"; - while(link && (typeof link.tagName == 'undefined' || link.tagName.toLowerCase() != 'a' || !link.href)) { - link = link.parentNode - } - - if (link && link.href && link.host && link.host !== location.host) { - if (middle || click) - plausible('Outbound Link: Click', {props: {url: link.href}}) - - // Delay navigation so that Plausible is notified of the click - if(!link.target || link.target.match(/^_(self|parent|top)$/i)) { - if (!(event.ctrlKey || event.metaKey || event.shiftKey) && click) { - setTimeout(function() { - location.href = link.href; - }, 150); - event.preventDefault(); - } - } - } - } - - function registerOutboundLinkEvents() { - document.addEventListener('click', handleOutbound) - document.addEventListener('auxclick', handleOutbound) - } - {{/if}} - - {{#if outbound_links}} - registerOutboundLinkEvents() - {{/if}} - - var queue = (window.plausible && window.plausible.q) || [] - window.plausible = trigger - for (var i = 0; i < queue.length; i++) { - trigger.apply(this, queue[i]) - } - - {{#unless manual}} - var lastPage; - - function page() { - {{#unless hash}} - if (lastPage === location.pathname) return; - {{/unless}} - lastPage = location.pathname - trigger('pageview') - } - - {{#if hash}} - window.addEventListener('hashchange', page) - {{else}} - var his = window.history - if (his.pushState) { - var originalPushState = his['pushState'] - his.pushState = function() { - originalPushState.apply(this, arguments) - page(); - } - window.addEventListener('popstate', page) - } - {{/if}} - - function handleVisibilityChange() { - if (!lastPage && document.visibilityState === 'visible') { - page() - } - } - - - if (document.visibilityState === 'prerender') { - document.addEventListener("visibilitychange", handleVisibilityChange); - } else { - page() - } - {{/unless}} -})();