diff --git a/elixir/apps/domain/lib/domain/analytics.ex b/elixir/apps/domain/lib/domain/analytics.ex new file mode 100644 index 000000000..60ecef0b4 --- /dev/null +++ b/elixir/apps/domain/lib/domain/analytics.ex @@ -0,0 +1,15 @@ +defmodule Domain.Analytics do + def get_mixpanel_token do + config!() + |> Keyword.get(:mixpanel_token) + end + + def get_hubspot_workspace_id do + config!() + |> Keyword.get(:hubspot_workspace_id) + end + + defp config! do + Application.fetch_env!(:domain, __MODULE__) + end +end diff --git a/elixir/apps/domain/lib/domain/config/definitions.ex b/elixir/apps/domain/lib/domain/config/definitions.ex index 761ae4da1..5c30c770f 100644 --- a/elixir/apps/domain/lib/domain/config/definitions.ex +++ b/elixir/apps/domain/lib/domain/config/definitions.ex @@ -125,6 +125,11 @@ defmodule Domain.Config.Definitions do :telemetry_metrics_reporter_opts, :logger_formatter, :logger_formatter_opts + ]}, + {"Analytics", + [ + :mixpanel_token, + :hubspot_workspace_id ]} ] end @@ -649,4 +654,18 @@ defmodule Domain.Config.Definitions do Boolean flag to turn API Client UI functionality on/off for all accounts. """ defconfig(:feature_rest_api_enabled, :boolean, default: false) + + ############################################## + ## Analytics + ############################################## + + @doc """ + Mixpanel token to use for tracking analytics. + """ + defconfig(:mixpanel_token, :string, default: nil) + + @doc """ + HubSpot account ID to use for user tracking. + """ + defconfig(:hubspot_workspace_id, :string, default: nil) end diff --git a/elixir/apps/web/assets/js/hooks.js b/elixir/apps/web/assets/js/hooks.js index 6d10815d9..41860985e 100644 --- a/elixir/apps/web/assets/js/hooks.js +++ b/elixir/apps/web/assets/js/hooks.js @@ -14,6 +14,39 @@ Hooks.Tabs = { }, }; +Hooks.Analytics = { + mounted() { + this.handleEvent("identify", ({ id, account_id, name, email }) => { + var mixpanel = window.mixpanel || null; + if (mixpanel) { + mixpanel.identify(id); + mixpanel.people.set({ $name: name, $email: email, account_id: account_id }); + mixpanel.set_group("account", account_id); + } + + var _hsq = window._hsq || null; + if (_hsq) { + _hsq.push(["identify", { id: id, email: email }]); + } + }); + + this.handleEvent("track_event", ({ name, properties }) => { + var mixpanel = window.mixpanel || null; + if (mixpanel) { + mixpanel.track(name, properties); + } + + var _hsq = window._hsq || null; + if (_hsq) { + _hsq.push(["trackCustomBehavioralEvent", { + name: name, + properties: properties + }]); + } + }); + } +} + Hooks.Refocus = { mounted() { this.el.addEventListener("click", (ev) => { diff --git a/elixir/apps/web/lib/web.ex b/elixir/apps/web/lib/web.ex index a34818b75..7e1799f4c 100644 --- a/elixir/apps/web/lib/web.ex +++ b/elixir/apps/web/lib/web.ex @@ -156,6 +156,7 @@ defmodule Web do import Web.FormComponents import Web.TableComponents import Web.PageComponents + import Web.AnalyticsComponents import Web.Gettext end end diff --git a/elixir/apps/web/lib/web/components/analytics_components.ex b/elixir/apps/web/lib/web/components/analytics_components.ex new file mode 100644 index 000000000..3e20d7864 --- /dev/null +++ b/elixir/apps/web/lib/web/components/analytics_components.ex @@ -0,0 +1,51 @@ +defmodule Web.AnalyticsComponents do + @moduledoc """ + The components that are responsible for embedding tracking codes into Firezone. + """ + use Phoenix.Component + alias Domain.Analytics + + def trackers(assigns) do + assigns = + assigns + |> assign_new(:mixpanel_token, &Analytics.get_mixpanel_token/0) + |> assign_new(:hubspot_workspace_id, &Analytics.get_hubspot_workspace_id/0) + + ~H""" + + """ + end + + def hubspot_tracker(assigns) do + ~H""" + + + """ + end + + def mixpanel_tracker(assigns) do + ~H""" + + + """ + end +end diff --git a/elixir/apps/web/lib/web/components/form_components.ex b/elixir/apps/web/lib/web/components/form_components.ex index dd6da61c9..e17ce1a44 100644 --- a/elixir/apps/web/lib/web/components/form_components.ex +++ b/elixir/apps/web/lib/web/components/form_components.ex @@ -527,7 +527,7 @@ defmodule Web.FormComponents do """ end - defp button_style do + def button_style do [ "flex items-center justify-center", "rounded", @@ -535,7 +535,7 @@ defmodule Web.FormComponents do ] end - defp button_style("warning") do + def button_style("warning") do button_style() ++ [ "text-primary-500", @@ -544,7 +544,7 @@ defmodule Web.FormComponents do ] end - defp button_style("danger") do + def button_style("danger") do button_style() ++ [ "text-red-600", @@ -553,7 +553,7 @@ defmodule Web.FormComponents do ] end - defp button_style("info") do + def button_style("info") do button_style() ++ [ "text-neutral-900", @@ -562,7 +562,7 @@ defmodule Web.FormComponents do ] end - defp button_style(_style) do + def button_style(_style) do button_style() ++ [ "text-white", @@ -571,7 +571,7 @@ defmodule Web.FormComponents do ] end - defp button_size(size) do + def button_size(size) do text = %{ "xs" => "text-xs", "sm" => "text-sm", diff --git a/elixir/apps/web/lib/web/components/layouts/public.html.heex b/elixir/apps/web/lib/web/components/layouts/public.html.heex index 422fbcc71..513af85a4 100644 --- a/elixir/apps/web/lib/web/components/layouts/public.html.heex +++ b/elixir/apps/web/lib/web/components/layouts/public.html.heex @@ -1,3 +1,4 @@ +<.trackers />
<%= @inner_content %>
diff --git a/elixir/apps/web/lib/web/live/sign_in.ex b/elixir/apps/web/lib/web/live/sign_in.ex index fd59df6a1..4170bfead 100644 --- a/elixir/apps/web/lib/web/live/sign_in.ex +++ b/elixir/apps/web/lib/web/live/sign_in.ex @@ -225,14 +225,13 @@ defmodule Web.SignIn do def openid_connect_button(assigns) do ~H""" - <.button - navigate={~p"/#{@account}/sign_in/providers/#{@provider}/redirect?#{@params}"} - class="w-full space-x-1" - style="info" + <.provider_icon adapter={@provider.adapter} class="w-5 h-5 mr-2" /> Sign in with <%= @provider.name %> - + """ end diff --git a/elixir/apps/web/lib/web/live/sign_up.ex b/elixir/apps/web/lib/web/live/sign_up.ex index 384565241..20fea07fa 100644 --- a/elixir/apps/web/lib/web/live/sign_up.ex +++ b/elixir/apps/web/lib/web/live/sign_up.ex @@ -327,7 +327,7 @@ defmodule Web.SignUp do registration = Ecto.Changeset.apply_changes(changeset) case register_account(registration) do - {:ok, %{account: account, provider: provider, identity: identity}} -> + {:ok, %{account: account, provider: provider, identity: identity, actor: actor}} -> {:ok, account} = Domain.Billing.provision_account(account) {:ok, _} = @@ -339,7 +339,30 @@ defmodule Web.SignUp do ) |> Web.Mailer.deliver() - socket = assign(socket, account: account, provider: provider, identity: identity) + socket = + assign(socket, + account: account, + provider: provider, + identity: identity + ) + + socket = + push_event(socket, "identify", %{ + id: actor.id, + account_id: account.id, + name: actor.name, + email: identity.provider_identifier + }) + + socket = + push_event(socket, "track_event", %{ + name: "Sign Up", + properties: %{ + account_id: account.id, + identity_id: identity.id + } + }) + {:noreply, socket} {:error, :account, err_changeset, _effects_so_far} -> diff --git a/elixir/apps/web/lib/web/router.ex b/elixir/apps/web/lib/web/router.ex index 6398df67c..76c5be79a 100644 --- a/elixir/apps/web/lib/web/router.ex +++ b/elixir/apps/web/lib/web/router.ex @@ -2,16 +2,12 @@ defmodule Web.Router do use Web, :router import Web.Auth - pipeline :browser do - plug :accepts, ["html"] + pipeline :public do + plug :accepts, ["html", "xml"] plug :fetch_session plug :protect_from_forgery plug :fetch_live_flash - plug :put_root_layout, {Web.Layouts, :root} - end - - pipeline :public do - plug :accepts, ["html", "xml"] + plug :put_root_layout, html: {Web.Layouts, :root} end pipeline :account do @@ -19,12 +15,12 @@ defmodule Web.Router do plug :fetch_subject end - pipeline :home do - plug :accepts, ["html", "xml"] + pipeline :control_plane do + plug :accepts, ["html"] plug :fetch_session plug :protect_from_forgery plug :fetch_live_flash - plug :put_root_layout, {Web.Layouts, :root} + plug :put_root_layout, html: {Web.Layouts, :root} end pipeline :ensure_authenticated_admin do @@ -39,7 +35,7 @@ defmodule Web.Router do end scope "/", Web do - pipe_through :home + pipe_through :public get "/", HomeController, :home post "/", HomeController, :redirect_to_sign_in @@ -65,13 +61,13 @@ defmodule Web.Router do end scope "/sign_up", Web do - pipe_through :browser + pipe_through :public live "/", SignUp end scope "/:account_id_or_slug", Web do - pipe_through [:browser, :account, :redirect_if_user_is_authenticated] + pipe_through [:public, :account, :redirect_if_user_is_authenticated] live_session :redirect_if_user_is_authenticated, on_mount: [ @@ -87,7 +83,7 @@ defmodule Web.Router do end scope "/:account_id_or_slug", Web do - pipe_through [:browser, :account] + pipe_through [:control_plane, :account] get "/sign_in/client_redirect", SignInController, :client_redirect get "/sign_in/client_auth_error", SignInController, :client_auth_error @@ -107,13 +103,13 @@ defmodule Web.Router do end scope "/:account_id_or_slug", Web do - pipe_through [:browser, :account] + pipe_through [:control_plane, :account] get "/sign_out", AuthController, :sign_out end scope "/:account_id_or_slug", Web do - pipe_through [:browser, :account, :ensure_authenticated_admin] + pipe_through [:control_plane, :account, :ensure_authenticated_admin] live_session :ensure_authenticated, on_mount: [ diff --git a/elixir/config/config.exs b/elixir/config/config.exs index adabdaf64..9bd933500 100644 --- a/elixir/config/config.exs +++ b/elixir/config/config.exs @@ -39,6 +39,10 @@ config :domain, Domain.Gateways, config :domain, Domain.Telemetry, metrics_reporter: nil +config :domain, Domain.Analytics, + mixpanel_token: nil, + hubspot_workspace_id: nil + config :domain, Domain.Auth.Adapters.GoogleWorkspace.APIClient, endpoint: "https://admin.googleapis.com", finch_transport_opts: [] @@ -133,11 +137,10 @@ config :web, config :web, Web.Plugs.SecureHeaders, csp_policy: [ - "default-src 'self' 'nonce-${nonce}'", - "frame-src 'self' https://js.stripe.com", - "script-src 'self' https://js.stripe.com", - "img-src 'self' data: https://www.gravatar.com", - "style-src 'self' 'unsafe-inline'" + "default-src 'self' 'nonce-${nonce}' https://api-js.mixpanel.com", + "img-src 'self' data: https://www.gravatar.com https://track.hubspot.com", + "style-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline' https://cdn.mxpnl.com https://*.hs-analytics.net" ] config :web, api_url_override: "ws://localhost:13001/" diff --git a/elixir/config/dev.exs b/elixir/config/dev.exs index 81ab4e876..ff6940ffa 100644 --- a/elixir/config/dev.exs +++ b/elixir/config/dev.exs @@ -64,11 +64,10 @@ config :phoenix_live_reload, :dirs, [ config :web, Web.Plugs.SecureHeaders, csp_policy: [ - "default-src 'self' 'nonce-${nonce}' https://cdn.tailwindcss.com/", - "img-src 'self' data: https://www.gravatar.com", + "default-src 'self' 'nonce-${nonce}' https://api-js.mixpanel.com", + "img-src 'self' data: https://www.gravatar.com https://track.hubspot.com", "style-src 'self' 'unsafe-inline'", - "frame-src 'self' https://js.stripe.com", - "script-src 'self' 'unsafe-inline' https://js.stripe.com https://cdn.tailwindcss.com/" + "script-src 'self' 'unsafe-inline' http://cdn.mxpnl.com http://*.hs-analytics.net" ] # Note: on Linux you may need to add `--add-host=host.docker.internal:host-gateway` diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index b4a9393a8..4074526c9 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -53,6 +53,10 @@ if config_env() == :prod do client_logs_enabled: compile_config!(:instrumentation_client_logs_enabled), client_logs_bucket: compile_config!(:instrumentation_client_logs_bucket) + config :domain, Domain.Analytics, + mixpanel_token: compile_config!(:mixpanel_token), + hubspot_workspace_id: compile_config!(:hubspot_workspace_id) + config :domain, :enabled_features, idp_sync: compile_config!(:feature_idp_sync_enabled), sign_up: compile_config!(:feature_sign_up_enabled), diff --git a/elixir/config/test.exs b/elixir/config/test.exs index 992c36f6e..df8a68eb1 100644 --- a/elixir/config/test.exs +++ b/elixir/config/test.exs @@ -39,11 +39,10 @@ config :web, Web.Endpoint, config :web, Web.Plugs.SecureHeaders, csp_policy: [ - "default-src 'self' 'nonce-${nonce}' https://cdn.tailwindcss.com/", - "img-src 'self' data: https://www.gravatar.com", + "default-src 'self' 'nonce-${nonce}' https://api-js.mixpanel.com", + "img-src 'self' data: https://www.gravatar.com https://track.hubspot.com", "style-src 'self' 'unsafe-inline'", - "frame-src 'self' https://js.stripe.com", - "script-src 'self' 'unsafe-inline' https://js.stripe.com https://cdn.tailwindcss.com/" + "script-src 'self' 'unsafe-inline' https://cdn.mxpnl.com https://*.hs-analytics.net" ] ############################### diff --git a/terraform/environments/production/portal.tf b/terraform/environments/production/portal.tf index 658b37b92..acb22c430 100644 --- a/terraform/environments/production/portal.tf +++ b/terraform/environments/production/portal.tf @@ -313,6 +313,16 @@ locals { name = "INSTRUMENTATION_CLIENT_LOGS_BUCKET" value = google_storage_bucket.client-logs.name }, + # Analytics + { + name = "MIXPANEL_TOKEN" + # Note: this token is public + value = "b0ab1d66424a27555ed45a27a4fd0cd2" + }, + { + name = "HUBSPOT_WORKSPACE_ID" + value = "23723443" + }, # Emails { name = "OUTBOUND_EMAIL_ADAPTER" @@ -485,7 +495,7 @@ module "web" { { name = "BACKGROUND_JOBS_ENABLED" value = "false" - }, + } ], local.shared_application_environment_variables) application_labels = { diff --git a/terraform/environments/staging/portal.tf b/terraform/environments/staging/portal.tf index 749703e01..870262901 100644 --- a/terraform/environments/staging/portal.tf +++ b/terraform/environments/staging/portal.tf @@ -296,6 +296,12 @@ locals { name = "INSTRUMENTATION_CLIENT_LOGS_BUCKET" value = google_storage_bucket.client-logs.name }, + # Analytics + { + name = "MIXPANEL_TOKEN" + # Note: this token is public + value = "313bdddc66b911f4afeb2c3242a78113" + }, # Emails { name = "OUTBOUND_EMAIL_ADAPTER"