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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
-4
@@ -20,7 +20,3 @@ declare interface WindowEventMap {
|
||||
AFTER_LOGIN: CustomEvent;
|
||||
LOGOUT: CustomEvent;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
Pylon: (command: string, ...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Vendored
-3
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user