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"""
+
+ <.mixpanel_tracker token={@mixpanel_token} />
+ <.hubspot_tracker hubspot_workspace_id={@hubspot_workspace_id} />
+
+ """
+ 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"