Compare commits

...

1 Commits

Author SHA1 Message Date
SagarRajput-7
7807a6df44 feat(boot-settings): move SDK config from build-time env vars to runtime boot data injection 2026-05-22 18:39:13 +05:30
7 changed files with 178 additions and 43 deletions

View File

@@ -94,12 +94,15 @@
}
})();
</script>
<script>
window.signozBootData = { settings: [[.BootSettings]] };
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>
var PYLON_APP_ID = '<%- PYLON_APP_ID %>';
if (PYLON_APP_ID) {
var pylonAppId = (window.signozBootData?.settings?.pylon || {}).appId || '';
if (pylonAppId) {
(function () {
var e = window;
var t = document;
@@ -115,10 +118,7 @@
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,
);
e.setAttribute('src', 'https://widget.usepylon.com/widget/' + pylonAppId);
var n = t.getElementsByTagName('script')[0];
n.parentNode.insertBefore(e, n);
};
@@ -130,16 +130,15 @@
})();
}
</script>
<script type="text/javascript">
window.AppcuesSettings = { enableURLDetection: true };
</script>
<script>
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
if (APPCUES_APP_ID) {
var appcuesAppId =
(window.signozBootData?.settings?.appcues || {}).appId || '';
if (appcuesAppId) {
window.AppcuesSettings = { enableURLDetection: true };
(function (d, t) {
var a = d.createElement(t);
a.async = 1;
a.src = '//fast.appcues.com/' + APPCUES_APP_ID + '.js';
a.src = '//fast.appcues.com/' + appcuesAppId + '.js';
var s = d.getElementsByTagName(t)[0];
s.parentNode.insertBefore(a, s);
})(document, 'script');

View File

@@ -35,6 +35,7 @@ import { PreferenceContextProvider } from 'providers/preferences/context/Prefere
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { extractDomain } from 'utils/app';
import { bootSettings } from 'utils/bootData';
import { Home } from './pageComponents';
import PrivateRoute from './Private';
@@ -293,7 +294,7 @@ function App(): JSX.Element {
(isCloudUser || isEnterpriseSelfHostedUser)
) {
const email = user.email || '';
const secret = process.env.PYLON_IDENTITY_SECRET || '';
const secret = bootSettings.pylon.identSecret ?? '';
let emailHash = '';
if (email && secret) {
@@ -302,7 +303,7 @@ function App(): JSX.Element {
window.pylon = {
chat_settings: {
app_id: process.env.PYLON_APP_ID,
app_id: bootSettings.pylon.appId,
email: user.email,
name: user.displayName || user.email,
email_hash: emailHash,
@@ -332,8 +333,8 @@ function App(): JSX.Element {
useEffect(() => {
if (isCloudUser || isEnterpriseSelfHostedUser) {
if (process.env.POSTHOG_KEY) {
posthog.init(process.env.POSTHOG_KEY, {
if (bootSettings.posthog.key) {
posthog.init(bootSettings.posthog.key, {
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
});
@@ -341,8 +342,8 @@ function App(): JSX.Element {
if (!isSentryInitialized) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tunnel: process.env.TUNNEL_URL,
dsn: bootSettings.sentry.dsn,
tunnel: bootSettings.sentry.tunnelUrl,
environment: 'production',
integrations: [
Sentry.browserTracingIntegration(),

View File

@@ -1,5 +1,6 @@
// eslint-disable-next-line no-restricted-imports
import { compose, Store } from 'redux';
import type { SignozBootSettings } from 'utils/bootData';
declare global {
interface Window {
@@ -7,6 +8,7 @@ declare global {
pylon: any;
Appcues: Record<string, any>;
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
signozBootData?: { settings?: Partial<SignozBootSettings> };
}
}

View File

@@ -0,0 +1,99 @@
export {};
type BootData = typeof import('../bootData');
function loadModule(settings?: object): BootData {
(window as any).signozBootData =
settings !== undefined ? { settings } : undefined;
let mod!: BootData;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../bootData');
});
return mod;
}
afterEach(() => {
delete (window as any).signozBootData;
});
describe('when window.signozBootData is absent', () => {
it('all sub-objects are defined and empty', () => {
const { bootSettings } = loadModule();
expect(bootSettings.sentry).toStrictEqual({});
expect(bootSettings.posthog).toStrictEqual({});
expect(bootSettings.pylon).toStrictEqual({});
expect(bootSettings.appcues).toStrictEqual({});
expect(bootSettings.roles).toStrictEqual({});
});
it('optional fields are undefined', () => {
const { bootSettings } = loadModule();
expect(bootSettings.sentry.dsn).toBeUndefined();
expect(bootSettings.sentry.tunnelUrl).toBeUndefined();
expect(bootSettings.posthog.key).toBeUndefined();
expect(bootSettings.pylon.appId).toBeUndefined();
expect(bootSettings.pylon.identSecret).toBeUndefined();
expect(bootSettings.appcues.appId).toBeUndefined();
expect(bootSettings.roles.isRolesDetailEnabled).toBeUndefined();
});
});
describe('when window.signozBootData.settings is populated', () => {
it('reads sentry config', () => {
const { bootSettings } = loadModule({
sentry: { dsn: 'https://abc@sentry.io/1', tunnelUrl: '/tunnel' },
});
expect(bootSettings.sentry.dsn).toBe('https://abc@sentry.io/1');
expect(bootSettings.sentry.tunnelUrl).toBe('/tunnel');
});
it('reads posthog config', () => {
const { bootSettings } = loadModule({ posthog: { key: 'phk_xxx' } });
expect(bootSettings.posthog.key).toBe('phk_xxx');
});
it('reads pylon config', () => {
const { bootSettings } = loadModule({
pylon: { appId: 'pylon-abc', identSecret: 'secret-xyz' },
});
expect(bootSettings.pylon.appId).toBe('pylon-abc');
expect(bootSettings.pylon.identSecret).toBe('secret-xyz');
});
it('reads appcues config', () => {
const { bootSettings } = loadModule({ appcues: { appId: 'appcues-123' } });
expect(bootSettings.appcues.appId).toBe('appcues-123');
});
it('reads roles config', () => {
const { bootSettings } = loadModule({
roles: { isRolesDetailEnabled: true },
});
expect(bootSettings.roles.isRolesDetailEnabled).toBe(true);
});
it('missing sub-namespaces fall back to empty objects', () => {
const { bootSettings } = loadModule({
sentry: { dsn: 'https://abc@sentry.io/1' },
});
expect(bootSettings.posthog).toStrictEqual({});
expect(bootSettings.posthog.key).toBeUndefined();
expect(bootSettings.pylon).toStrictEqual({});
expect(bootSettings.appcues).toStrictEqual({});
expect(bootSettings.roles).toStrictEqual({});
});
});
describe('when window.signozBootData exists but settings is undefined', () => {
it('all sub-objects are empty', () => {
(window as any).signozBootData = {};
let mod!: BootData;
jest.isolateModules(() => {
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
mod = require('../bootData');
});
expect(mod.bootSettings.sentry).toStrictEqual({});
expect(mod.bootSettings.posthog).toStrictEqual({});
});
});

View File

@@ -0,0 +1,35 @@
export interface SentryConfig {
dsn?: string;
tunnelUrl?: string;
}
export interface PosthogConfig {
key?: string;
}
export interface PylonConfig {
appId?: string;
identSecret?: string;
}
export interface AppcuesConfig {
appId?: string;
}
export interface RolesConfig {
isRolesDetailEnabled?: boolean;
}
export interface SignozBootSettings {
sentry: SentryConfig;
posthog: PosthogConfig;
pylon: PylonConfig;
appcues: AppcuesConfig;
roles: RolesConfig;
}
const raw = window.signozBootData?.settings;
export const bootSettings: Readonly<SignozBootSettings> = {
sentry: raw?.sentry ?? {},
posthog: raw?.posthog ?? {},
pylon: raw?.pylon ?? {},
appcues: raw?.appcues ?? {},
roles: raw?.roles ?? {},
};

View File

@@ -13,16 +13,9 @@ 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_POSTHOG_KEY: string;
readonly VITE_SENTRY_AUTH_TOKEN: string;
readonly VITE_SENTRY_ORG: string;
readonly VITE_SENTRY_PROJECT_ID: string;
readonly VITE_SENTRY_DSN: string;
readonly VITE_TUNNEL_URL: string;
readonly VITE_TUNNEL_DOMAIN: string;
readonly VITE_DOCS_BASE_URL: string;
}

View File

@@ -6,7 +6,6 @@ import type { Plugin, TransformResult, UserConfig } from 'vite';
import { defineConfig, loadEnv } from 'vite';
import vitePluginChecker from 'vite-plugin-checker';
import viteCompression from 'vite-plugin-compression';
import { createHtmlPlugin } from 'vite-plugin-html';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
import tsconfigPaths from 'vite-tsconfig-paths';
@@ -23,6 +22,29 @@ function devBasePathPlugin(basePath: string): Plugin {
};
}
function devBootDataPlugin(env: Record<string, string>): Plugin {
return {
name: 'dev-boot-data',
apply: 'serve',
transformIndexHtml(html): string {
const bootSettings = {
sentry: {
dsn: env.VITE_SENTRY_DSN || undefined,
tunnelUrl: env.VITE_TUNNEL_URL || undefined,
},
posthog: { key: env.VITE_POSTHOG_KEY || undefined },
pylon: {
appId: env.VITE_PYLON_APP_ID || undefined,
identSecret: env.VITE_PYLON_IDENTITY_SECRET || undefined,
},
appcues: { appId: env.VITE_APPCUES_APP_ID || undefined },
roles: {},
};
return html.replaceAll('[[.BootSettings]]', JSON.stringify(bootSettings));
},
};
}
function rawMarkdownPlugin(): Plugin {
return {
name: 'raw-markdown',
@@ -47,15 +69,8 @@ export default defineConfig(({ mode }): UserConfig => {
tsconfigPaths(),
rawMarkdownPlugin(),
devBasePathPlugin(basePath),
devBootDataPlugin(env),
react(),
createHtmlPlugin({
inject: {
data: {
PYLON_APP_ID: env.VITE_PYLON_APP_ID || '',
APPCUES_APP_ID: env.VITE_APPCUES_APP_ID || '',
},
},
}),
vitePluginChecker({
typescript: true,
// this doubles the build tim
@@ -126,17 +141,8 @@ 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),
'process.env.TUNNEL_URL': JSON.stringify(env.VITE_TUNNEL_URL),
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
},
// In production, use relative paths so assets work with any base path injected by the backend.