mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(portal): Track page views and sign ups using Mixpanel and HubSpot on public pages (#5050)
Fixes firezone/gtm#253 Fixes firezone/gtm#278
This commit is contained in:
15
elixir/apps/domain/lib/domain/analytics.ex
Normal file
15
elixir/apps/domain/lib/domain/analytics.ex
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -156,6 +156,7 @@ defmodule Web do
|
||||
import Web.FormComponents
|
||||
import Web.TableComponents
|
||||
import Web.PageComponents
|
||||
import Web.AnalyticsComponents
|
||||
import Web.Gettext
|
||||
end
|
||||
end
|
||||
|
||||
51
elixir/apps/web/lib/web/components/analytics_components.ex
Normal file
51
elixir/apps/web/lib/web/components/analytics_components.ex
Normal file
@@ -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"""
|
||||
<div id="analytics" class="hidden" phx-hook="Analytics">
|
||||
<.mixpanel_tracker token={@mixpanel_token} />
|
||||
<.hubspot_tracker hubspot_workspace_id={@hubspot_workspace_id} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def hubspot_tracker(assigns) do
|
||||
~H"""
|
||||
<script
|
||||
:if={not is_nil(@hubspot_workspace_id)}
|
||||
type="text/javascript"
|
||||
id="hs-script-loader"
|
||||
async
|
||||
defer
|
||||
src={"//js.hs-analytics.net/analytics/1716219600000/#{@hubspot_workspace_id}.js"}
|
||||
>
|
||||
</script>
|
||||
<script :if={not is_nil(@hubspot_workspace_id)} type="text/javascript">
|
||||
var _hsq = window._hsq = window._hsq || [];
|
||||
_hsq.push(["setPath", window.location.pathname + window.location.search]);
|
||||
</script>
|
||||
"""
|
||||
end
|
||||
|
||||
def mixpanel_tracker(assigns) do
|
||||
~H"""
|
||||
<script :if={not is_nil(@token)} type="text/javascript">
|
||||
(function (f, b) { if (!b.__SV) { var e, g, i, h; window.mixpanel = b; b._i = []; b.init = function (e, f, c) { function g(a, d) { var b = d.split("."); 2 == b.length && ((a = a[b[0]]), (d = b[1])); a[d] = function () { a.push([d].concat(Array.prototype.slice.call(arguments, 0))); }; } var a = b; "undefined" !== typeof c ? (a = b[c] = []) : (c = "mixpanel"); a.people = a.people || []; a.toString = function (a) { var d = "mixpanel"; "mixpanel" !== c && (d += "." + c); a || (d += " (stub)"); return d; }; a.people.toString = function () { return a.toString(1) + ".people (stub)"; }; i = "disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split( " "); for (h = 0; h < i.length; h++) g(a, i[h]); var j = "set set_once union unset remove delete".split(" "); a.get_group = function () { function b(c) { d[c] = function () { call2_args = arguments; call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); a.push([e, call2]); }; } for ( var d = {}, e = ["get_group"].concat( Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++) b(j[c]); return d; }; b._i.push([e, f, c]); }; b.__SV = 1.2; e = f.createElement("script"); e.type = "text/javascript"; e.async = !0; e.src = "undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL ? MIXPANEL_CUSTOM_LIB_URL : "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//) ? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js" : "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"; g = f.getElementsByTagName("script")[0]; g.parentNode.insertBefore(e, g); } })(document, window.mixpanel || []);
|
||||
</script>
|
||||
<script :if={not is_nil(@token)} type="text/javascript">
|
||||
mixpanel.init("<%= @token %>", {track_pageview: "url-with-path-and-query-string"});
|
||||
mixpanel.set_config({debug: true});
|
||||
</script>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -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",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<.trackers />
|
||||
<main class="h-auto pt-16">
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
||||
@@ -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"
|
||||
<a
|
||||
class={[button_style("info"), button_size("md"), "w-full space-x-1"]}
|
||||
href={~p"/#{@account}/sign_in/providers/#{@provider}/redirect?#{@params}"}
|
||||
>
|
||||
<.provider_icon adapter={@provider.adapter} class="w-5 h-5 mr-2" /> Sign in with
|
||||
<strong><%= @provider.name %></strong>
|
||||
</.button>
|
||||
</a>
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -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} ->
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
###############################
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user