fix(privacy): remove third-party trackers, native support chat (#37)

o11y is a self-hosted observability tool that was phoning home to three
third-party SaaS vendors with our users' data — inherited from the upstream
fork and, worse, enabled by DEFAULT: vite.config.ts gated each on
`env.VITE_X_ENABLED !== 'false'` (opt-OUT), so any operator who never set the
vars shipped all three. index.html injected two vendor <script> tags on every
page load, and AppRoutes HMAC-hashed the logged-in user's email to identify
them to the support-chat vendor.

Removed end to end:

- Go: web.Settings/SettingsConfig drop the three trackers, their configs,
  defaults, and the O11Y_WEB_SETTINGS_* env surface.
- Schema: docs/config/web-settings.json declares only sentry;
  types/generated/webSettings.ts regenerated from it (not hand-edited).
- Frontend: both vendor <script> loaders deleted from index.html; vite
  defines, env types, example.env, window typings and widget styles gone.
  The identify + theme/bubble effects in AppRoutes go with them, along with
  the now-dead crypto-js Hex/HmacSHA256 and useIsDarkMode imports, and a
  useEffect that existed only to compute the widget's feature-flag gates.

Support chat is now Hanzo Chat, native and one way: utils/supportChat.ts
exposes a single openSupportChat(); the three inline widget call sites
(SideNav, Support, LaunchChatSupport) all route through it. It delegates to
the existing openInNewTab helper rather than calling window.open itself —
the repo's own o11y/no-raw-absolute-path rule mandates that helper.

openInNewTab now passes `noopener`. window.open, unlike <a target="_blank">,
does not imply it, so all 57 call sites were leaving the opened tab able to
navigate us via window.opener (reverse tabnabbing). No caller can be affected:
the function returns void.

Sentry stays — it is our own fork (hanzoai/sentry), and it is now opt-IN
(`=== 'true'`) rather than opt-out.

Also drops two pre-existing dead references that the lint gate surfaced in
the touched files: an unused brand-logo import in SideNav and an empty
`declare interface Window {}` in global.d.ts (the real Window augmentation
lives in typings/window.ts — now the only one).

Verified: go test ./pkg/web/... ok (web + routerweb); tsgo --noEmit clean;
oxlint 0 errors; zero vendor references and zero outbound vendor script
loads remain.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
This commit is contained in:
antje
2026-07-16 12:04:44 -07:00
committed by GitHub
co-authored by Hanzo AI
parent a132573506
commit e99f62d225
20 changed files with 65 additions and 372 deletions
+1 -49
View File
@@ -1,48 +1,9 @@
{
"required": [
"posthog",
"appcues",
"sentry",
"pylon"
"sentry"
],
"additionalProperties": false,
"definitions": {
"Appcues": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"Posthog": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"Pylon": {
"required": [
"enabled"
],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"Sentry": {
"required": [
"enabled"
@@ -57,15 +18,6 @@
}
},
"properties": {
"appcues": {
"$ref": "#/definitions/Appcues"
},
"posthog": {
"$ref": "#/definitions/Posthog"
},
"pylon": {
"$ref": "#/definitions/Pylon"
},
"sentry": {
"$ref": "#/definitions/Sentry"
}
-3
View File
@@ -1,8 +1,5 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
CI="1"
+8 -54
View File
@@ -105,60 +105,14 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
var pylonSettings =
((window.o11yBootData || {}).settings || {}).pylon || {};
var pylonEnabled = pylonSettings.enabled !== false;
if (PYLON_APP_ID && pylonEnabled) {
(function () {
var e = window;
var t = document;
var n = function () {
n.e(arguments);
};
n.q = [];
n.e = function (e) {
n.q.push(e);
};
e.Pylon = n;
var r = function () {
var e = t.createElement('script');
e.setAttribute('type', 'text/javascript');
e.setAttribute('async', 'true');
e.setAttribute(
'src',
'https://widget.usepylon.com/widget/' + PYLON_APP_ID,
);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
if (t.readyState === 'complete') {
r();
} else if (e.addEventListener) {
e.addEventListener('load', r, false);
}
})();
}
</script>
<script type="text/javascript">
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
var appcuesSettings =
((window.o11yBootData || {}).settings || {}).appcues || {};
var appcuesEnabled = appcuesSettings.enabled !== false;
if (APPCUES_APP_ID && appcuesEnabled) {
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');
}
</script>
<!--
NO third-party widgets or trackers are loaded here, and none may be
added. The upstream fork injected a support-chat widget and an
onboarding tracker, both enabled by default — a self-hosted
observability tool must never make outbound calls to third parties
with our users' data. Support chat is Hanzo Chat (utils/supportChat);
analytics is Hanzo Insights. Sentry is opt-in, on our own fork.
-->
<link rel="stylesheet" href="css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
+1 -101
View File
@@ -11,15 +11,12 @@ import { CmdKPalette } from 'components/cmdKPalette/cmdKPalette';
import NotFound from 'components/NotFound';
import { ShiftHoldOverlayController } from 'components/ShiftOverlay/ShiftHoldOverlayController';
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
import { useThemeConfig } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
@@ -55,7 +52,6 @@ function App(): JSX.Element {
isFetchingActiveLicense,
activeLicenseFetchError,
userFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
org,
@@ -117,19 +113,6 @@ function App(): JSX.Element {
if (domain) {
logEvent('Domain Identified', groupTraits, 'group');
}
if (window && window.Appcues) {
window.Appcues.identify(id, {
name: displayName,
deployment_name: hostNameParts[0],
data_region: hostNameParts[1],
deployment_url: hostname,
company_domain: domain,
companyName: orgName,
email,
paidUser: !!trialInfo?.trialConvertedToSubscription,
});
}
insights?.identify(id, {
email,
name: displayName,
@@ -243,89 +226,6 @@ function App(): JSX.Element {
});
}, [isLoggedInState, isAIAssistantEnabled]);
const isDarkMode = useIsDarkMode();
useEffect(() => {
window.Pylon?.('setTheme', isDarkMode ? 'dark' : 'light');
}, [isDarkMode]);
useEffect(() => {
if (
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
pathname.startsWith('/ai-assistant/')
) {
window.Pylon?.('hideChatBubble');
} else {
window.Pylon?.('showChatBubble');
}
}, [pathname]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
// to something went wrong which would ideally need a reload.
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
activeLicense &&
trialInfo
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
const showAddCreditCardModal =
!isPremiumSupportEnabled && !trialInfo?.trialConvertedToSubscription;
if (
isLoggedInState &&
isChatSupportEnabled &&
!showAddCreditCardModal &&
(isCloudUser || isEnterpriseSelfHostedUser) &&
(window.o11yBootData?.settings?.pylon.enabled ?? true)
) {
const email = user.email || '';
const secret = process.env.PYLON_IDENTITY_SECRET || '';
let emailHash = '';
if (email && secret) {
emailHash = HmacSHA256(email, Hex.parse(secret)).toString(Hex);
}
window.pylon = {
chat_settings: {
app_id: process.env.PYLON_APP_ID,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
},
};
}
}
}, [
isLoggedInState,
user,
pathname,
trialInfo?.trialConvertedToSubscription,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
activeLicense,
trialInfo,
isCloudUser,
isEnterpriseSelfHostedUser,
]);
useEffect(() => {
if (!isFetchingUser && isCloudUser && user && user.email) {
enableAnalytics(user);
@@ -9,19 +9,20 @@ import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es';
import { CircleHelp, CreditCard, X } from 'components/ui/icons';
import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { openSupportChat } from 'utils/supportChat';
import './LaunchChatSupport.styles.scss';
export interface LaunchChatSupportProps {
eventName: string;
attributes: Record<string, unknown>;
/** Reserved for the in-app @hanzo/chat integration (see utils/supportChat). */
message?: string;
buttonText?: string;
className?: string;
@@ -33,7 +34,6 @@ export interface LaunchChatSupportProps {
function LaunchChatSupport({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
@@ -112,8 +112,8 @@ function LaunchChatSupport({
setIsAddCreditCardModalOpen(true);
} else {
logEvent(eventName, attributes);
if (window.pylon && !chatMessageDisabled) {
window.Pylon('showNewMessage', defaultTo(message, ''));
if (!chatMessageDisabled) {
openSupportChat();
}
}
};
+3 -7
View File
@@ -72,12 +72,10 @@ import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app';
import { isModifierKeyPressed } from 'utils/app';
import { checkVersionState, isModifierKeyPressed } from 'utils/app';
import { showErrorNotification } from 'utils/error';
import { openInNewTab } from 'utils/navigation';
import o11yBrandLogoUrl from '@/assets/Logos/o11y-brand-logo.svg';
import { openSupportChat } from 'utils/supportChat';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config';
@@ -891,9 +889,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
}
break;
case 'chat-support':
if (window.pylon) {
window.Pylon('show');
}
openSupportChat();
break;
case 'changelog-1':
case 'changelog-2':
+3 -2
View File
@@ -23,6 +23,7 @@ import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import { openSupportChat } from 'utils/supportChat';
import './Support.styles.scss';
@@ -163,8 +164,8 @@ export default function Support(): JSX.Element {
page: pathname,
});
setIsAddCreditCardModalOpen(true);
} else if (window.pylon) {
window.Pylon('show');
} else {
openSupportChat();
}
};
-8
View File
@@ -790,14 +790,6 @@ notifications - 2050
background: color-mix(in srgb, var(--l3-background) 20%, transparent);
}
body.ai-assistant-panel-open {
.PylonChat-bubbleFrameContainer,
.PylonChat-chatWindowFrameContainer {
right: calc(var(--ai-assistant-panel-width, 380px) + 8px) !important;
left: auto !important;
}
}
[role='tab'] {
color: var(--foreground) !important;
}
@@ -1,20 +1,8 @@
/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */
export interface WebSettings {
appcues: Appcues;
posthog: Posthog;
pylon: Pylon;
sentry: Sentry;
}
export interface Appcues {
enabled: boolean;
}
export interface Posthog {
enabled: boolean;
}
export interface Pylon {
enabled: boolean;
}
export interface Sentry {
enabled: boolean;
}
-4
View File
@@ -20,7 +20,3 @@ declare interface WindowEventMap {
AFTER_LOGIN: CustomEvent;
LOGOUT: CustomEvent;
}
declare interface Window {
Pylon: (command: string, ...args: unknown[]) => void;
}
-2
View File
@@ -5,8 +5,6 @@ import type { WebSettings } from 'types/generated/webSettings';
declare global {
interface Window {
store: Store;
pylon: any;
Appcues: Record<string, any>;
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
o11yBootData?: { settings: WebSettings | null };
}
+5 -1
View File
@@ -1,5 +1,9 @@
import { withBasePath } from 'utils/basePath';
// `noopener` is required: window.open (unlike <a target="_blank">) does NOT imply
// it, so without it the opened tab can reach back through window.opener and
// navigate us — reverse tabnabbing. Safe for every caller: the return is void, so
// nobody can be relying on the WindowProxy that noopener suppresses.
export const openInNewTab = (path: string): void => {
window.open(withBasePath(path), '_blank');
window.open(withBasePath(path), '_blank', 'noopener');
};
+18
View File
@@ -0,0 +1,18 @@
import { openInNewTab } from 'utils/navigation';
/**
* The ONE way to open support chat.
*
* Replaces the upstream fork's third-party support widget (which loaded an
* external script and HMAC-hashed user emails for a third party). Support chat
* is Hanzo Chat — native, ours. Every call site goes through this helper; do not
* reintroduce a per-component chat integration.
*
* Follow-up: embed `@hanzo/chat` in-app (built on @hanzo/ai + @hanzo/gui) so
* support is a panel rather than a new tab. `@hanzo/ui` is already a dependency.
*/
export const HANZO_CHAT_URL = 'https://hanzo.chat';
export function openSupportChat(): void {
openInNewTab(HANZO_CHAT_URL);
}
-3
View File
@@ -13,9 +13,6 @@ declare module '*.md?raw' {
interface ImportMetaEnv {
readonly VITE_FRONTEND_API_ENDPOINT: string;
readonly VITE_WEBSOCKET_API_ENDPOINT: string;
readonly VITE_PYLON_APP_ID: string;
readonly VITE_PYLON_IDENTITY_SECRET: string;
readonly VITE_APPCUES_APP_ID: string;
readonly VITE_INSIGHTS_KEY: string;
readonly VITE_SENTRY_AUTH_TOKEN: string;
readonly VITE_SENTRY_ORG: string;
+5 -14
View File
@@ -29,11 +29,11 @@ function bootSettingsPlugin(env: Record<string, string>): Plugin {
return {
name: 'boot-settings',
transformIndexHtml(html): string {
// No third-party trackers. Analytics is Hanzo Insights; support chat
// is Hanzo Chat. Sentry is opt-IN (=== 'true'), pointed at our own
// fork — never opt-out, which shipped it enabled by default.
const settings = {
posthog: { enabled: env.VITE_POSTHOG_ENABLED !== 'false' },
appcues: { enabled: env.VITE_APPCUES_ENABLED !== 'false' },
sentry: { enabled: env.VITE_SENTRY_ENABLED !== 'false' },
pylon: { enabled: env.VITE_PYLON_ENABLED !== 'false' },
sentry: { enabled: env.VITE_SENTRY_ENABLED === 'true' },
};
return html.replaceAll('[[.Settings]]', JSON.stringify(settings));
},
@@ -68,10 +68,7 @@ export default defineConfig(({ mode }): UserConfig => {
react(),
createHtmlPlugin({
inject: {
data: {
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
},
data: {},
},
}),
vitePluginChecker({
@@ -152,12 +149,6 @@ export default defineConfig(({ mode }): UserConfig => {
'process.env.WEBSOCKET_API_ENDPOINT': JSON.stringify(
env.VITE_WEBSOCKET_API_ENDPOINT,
),
'process.env.PYLON_APP_ID': JSON.stringify(env.VITE_PYLON_APP_ID),
'process.env.PYLON_IDENTITY_SECRET': JSON.stringify(
env.VITE_PYLON_IDENTITY_SECRET,
),
'process.env.APPCUES_APP_ID': JSON.stringify(env.VITE_APPCUES_APP_ID),
'process.env.POSTHOG_KEY': JSON.stringify(env.VITE_POSTHOG_KEY),
'process.env.SENTRY_ORG': JSON.stringify(env.VITE_SENTRY_ORG),
'process.env.SENTRY_PROJECT_ID': JSON.stringify(env.VITE_SENTRY_PROJECT_ID),
'process.env.SENTRY_DSN': JSON.stringify(env.VITE_SENTRY_DSN),
+5 -31
View File
@@ -20,23 +20,12 @@ type Config struct {
}
// SettingsConfig holds the configuration for web settings.
//
// Third-party trackers (product analytics, onboarding tours, support chat) are
// NOT part of Hanzo o11y — see settings.go. Analytics is Hanzo Insights; support
// chat is Hanzo Chat. Only Sentry remains, pointed at our own fork and opt-in.
type SettingsConfig struct {
Posthog PosthogConfig `mapstructure:"posthog"`
Appcues AppcuesConfig `mapstructure:"appcues"`
Sentry SentryConfig `mapstructure:"sentry"`
Pylon PylonConfig `mapstructure:"pylon"`
}
type PosthogConfig struct {
Enabled bool `mapstructure:"enabled"`
Key string `mapstructure:"key"`
APIHost string `mapstructure:"api_host"`
UIHost string `mapstructure:"ui_host"`
}
type AppcuesConfig struct {
Enabled bool `mapstructure:"enabled"`
AppID string `mapstructure:"app_id"`
Sentry SentryConfig `mapstructure:"sentry"`
}
type SentryConfig struct {
@@ -45,12 +34,6 @@ type SentryConfig struct {
Tunnel string `mapstructure:"tunnel"`
}
type PylonConfig struct {
Enabled bool `mapstructure:"enabled"`
AppID string `mapstructure:"app_id"`
IdentitySecret string `mapstructure:"identity_secret"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("web"), newConfig)
}
@@ -61,18 +44,9 @@ func newConfig() factory.Config {
Index: "index.html",
Directory: "/etc/o11y/web",
Settings: SettingsConfig{
Posthog: PosthogConfig{
Enabled: false,
},
Appcues: AppcuesConfig{
Enabled: false,
},
Sentry: SentryConfig{
Enabled: false,
},
Pylon: PylonConfig{
Enabled: false,
},
},
}
}
-9
View File
@@ -51,18 +51,9 @@ func TestSettingsConfigWithEnvProvider(t *testing.T) {
value string
expected SettingsConfig
}{
{name: "posthog_enabled", env: "O11Y_WEB_SETTINGS_POSTHOG_ENABLED", value: "true", expected: SettingsConfig{Posthog: PosthogConfig{Enabled: true}}},
{name: "posthog_key", env: "O11Y_WEB_SETTINGS_POSTHOG_KEY", value: "phc_examplekey", expected: SettingsConfig{Posthog: PosthogConfig{Key: "phc_examplekey"}}},
{name: "posthog_api_host", env: "O11Y_WEB_SETTINGS_POSTHOG_API__HOST", value: "https://eu.i.posthog.com", expected: SettingsConfig{Posthog: PosthogConfig{APIHost: "https://eu.i.posthog.com"}}},
{name: "posthog_ui_host", env: "O11Y_WEB_SETTINGS_POSTHOG_UI__HOST", value: "https://eu.posthog.com", expected: SettingsConfig{Posthog: PosthogConfig{UIHost: "https://eu.posthog.com"}}},
{name: "appcues_enabled", env: "O11Y_WEB_SETTINGS_APPCUES_ENABLED", value: "true", expected: SettingsConfig{Appcues: AppcuesConfig{Enabled: true}}},
{name: "appcues_app_id", env: "O11Y_WEB_SETTINGS_APPCUES_APP__ID", value: "12345-abcde", expected: SettingsConfig{Appcues: AppcuesConfig{AppID: "12345-abcde"}}},
{name: "sentry_enabled", env: "O11Y_WEB_SETTINGS_SENTRY_ENABLED", value: "true", expected: SettingsConfig{Sentry: SentryConfig{Enabled: true}}},
{name: "sentry_dsn", env: "O11Y_WEB_SETTINGS_SENTRY_DSN", value: "https://examplePublicKey@o0.ingest.sentry.io/0", expected: SettingsConfig{Sentry: SentryConfig{DSN: "https://examplePublicKey@o0.ingest.sentry.io/0"}}},
{name: "sentry_tunnel", env: "O11Y_WEB_SETTINGS_SENTRY_TUNNEL", value: "https://example.com/tunnel", expected: SettingsConfig{Sentry: SentryConfig{Tunnel: "https://example.com/tunnel"}}},
{name: "pylon_enabled", env: "O11Y_WEB_SETTINGS_PYLON_ENABLED", value: "true", expected: SettingsConfig{Pylon: PylonConfig{Enabled: true}}},
{name: "pylon_app_id", env: "O11Y_WEB_SETTINGS_PYLON_APP__ID", value: "pylon-app-id", expected: SettingsConfig{Pylon: PylonConfig{AppID: "pylon-app-id"}}},
{name: "pylon_identity_secret", env: "O11Y_WEB_SETTINGS_PYLON_IDENTITY__SECRET", value: "pylon-secret", expected: SettingsConfig{Pylon: PylonConfig{IdentitySecret: "pylon-secret"}}},
}
for _, testCase := range testCases {
-30
View File
@@ -119,49 +119,19 @@ func TestServeTemplatedIndex(t *testing.T) {
Index: "valid_template.html",
Directory: "testdata",
Settings: web.SettingsConfig{
Posthog: web.PosthogConfig{
Enabled: true,
Key: "phc_examplekey",
APIHost: "https://us.i.posthog.com",
UIHost: "https://us.posthog.com",
},
Appcues: web.AppcuesConfig{
Enabled: true,
AppID: "12345-abcde",
},
Sentry: web.SentryConfig{
Enabled: true,
DSN: "https://examplePublicKey@o0.ingest.sentry.io/0",
Tunnel: "https://example.com/tunnel",
},
Pylon: web.PylonConfig{
Enabled: true,
AppID: "pylon-app-id",
IdentitySecret: "pylon-secret",
},
},
},
expected: expectedHTML("/", web.Settings{
Posthog: web.Posthog{
Enabled: true,
Key: "phc_examplekey",
APIHost: "https://us.i.posthog.com",
UIHost: "https://us.posthog.com",
},
Appcues: web.Appcues{
Enabled: true,
AppID: "12345-abcde",
},
Sentry: web.Sentry{
Enabled: true,
DSN: "https://examplePublicKey@o0.ingest.sentry.io/0",
Tunnel: "https://example.com/tunnel",
},
Pylon: web.Pylon{
Enabled: true,
AppID: "pylon-app-id",
IdentitySecret: "pylon-secret",
},
}),
},
}
+11 -37
View File
@@ -1,22 +1,17 @@
package web
// Settings is what the SPA reads at boot.
//
// Hanzo o11y does NOT ship third-party trackers. The upstream fork wired in
// product analytics, onboarding tours and a support-chat widget (all enabled by
// default in the frontend build), which meant a self-hosted observability tool
// phoned home to third parties with our users' data. They are removed: analytics
// is Hanzo Insights, support chat is Hanzo Chat. Do not reintroduce them.
//
// Sentry stays because we run our own fork (hanzoai/sentry) and it is opt-in —
// it only activates when an operator sets a DSN pointing at our instance.
type Settings struct {
Posthog Posthog `json:"posthog" required:"true"`
Appcues Appcues `json:"appcues" required:"true"`
Sentry Sentry `json:"sentry" required:"true"`
Pylon Pylon `json:"pylon" required:"true"`
}
type Posthog struct {
Enabled bool `json:"enabled" required:"true"`
Key string `json:"key"`
APIHost string `json:"apiHost"`
UIHost string `json:"uiHost"`
}
type Appcues struct {
Enabled bool `json:"enabled" required:"true"`
AppID string `json:"appId"`
Sentry Sentry `json:"sentry" required:"true"`
}
type Sentry struct {
@@ -25,33 +20,12 @@ type Sentry struct {
Tunnel string `json:"tunnel"`
}
type Pylon struct {
Enabled bool `json:"enabled" required:"true"`
AppID string `json:"appId"`
IdentitySecret string `json:"identitySecret"`
}
func NewSettings(config Config) Settings {
return Settings{
Posthog: Posthog{
Enabled: config.Settings.Posthog.Enabled,
Key: config.Settings.Posthog.Key,
APIHost: config.Settings.Posthog.APIHost,
UIHost: config.Settings.Posthog.UIHost,
},
Appcues: Appcues{
Enabled: config.Settings.Appcues.Enabled,
AppID: config.Settings.Appcues.AppID,
},
Sentry: Sentry{
Enabled: config.Settings.Sentry.Enabled,
DSN: config.Settings.Sentry.DSN,
Tunnel: config.Settings.Sentry.Tunnel,
},
Pylon: Pylon{
Enabled: config.Settings.Pylon.Enabled,
AppID: config.Settings.Pylon.AppID,
IdentitySecret: config.Settings.Pylon.IdentitySecret,
},
}
}
@@ -412,7 +412,7 @@ test.describe('Dashboard Detail — Configure drawer', () => {
await dialog.getByRole('tab', { name: 'Variables' }).click();
const tabpanel = dialog.getByRole('tabpanel', { name: 'Variables' });
// Hover the row to reveal the edit button (Pylon overlay can intercept,
// Hover the row to reveal the edit button (an overlay can intercept,
// so dispatchEvent fires the click directly on the React onClick).
const nameCell = tabpanel.getByText('tb_env', { exact: true }).first();
await nameCell.hover();