Files
openobserve/web/src/main.ts
T
70a89a7c34 feat(license): gate features by license in the UI (#16233)
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-11-04 10:12:58 +00:00

211 lines
7.4 KiB
TypeScript

// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { configQuery, isFullConfigLoaded } from "@/services/config.queries";
import { createApp } from "vue";
import { VueQueryPlugin } from "@tanstack/vue-query";
import store from "./stores";
import App from "./App.vue";
import createRouter from "./router";
import i18n, { applyDocumentLocale, getLocale, loadLocaleMessages } from "./locales";
import "./styles/tailwind.css";
import "./styles/rtl.css";
// Global generated-content stylesheet: syntax classes (.log-key, .log-string, …)
// applied to v-html-highlighted log output across logs/traces/RUM. Loaded once
// here instead of re-@imported inside each consumer's <style> block.
import "./assets/styles/log-highlighting.css";
import config from "./aws-exports";
import { useReo } from "./services/reodotdev_analytics";
import { contextRegistry, createDefaultContextProvider } from "./composables/contextProviders";
import { buildVersionChecker, isChunkLoadError } from "./utils/buildVersionChecker";
import { queryClient, setMutationNotifier } from "./composables/query/queryClient";
import { toast } from "@/lib/feedback/Toast/useToast";
import { bootstrapTheme } from "@/utils/themeManager";
import { raw } from "@/types/i18n";
import { bindAccess } from "@/composables/useAccessStore";
import { installFetchLicenseReceiver } from "@/services/licenseDenialReceiver";
installFetchLicenseReceiver();
// Apply the resolved theme synchronously before the app mounts so the first
// paint already uses the correct colors (no flash of the base stylesheet theme).
bootstrapTheme();
const activeLocale = getLocale();
applyDocumentLocale(activeLocale);
const app = createApp(App);
const router = createRouter(store);
app.use(i18n);
// const router = createRouter(store);
app.use(store).use(router);
// Bound before mount: a 403 while the layout still shows its skeleton has no boundary to bind it yet.
bindAccess(router, store);
app.use(VueQueryPlugin, { queryClient });
// Mutation success/error feedback. Injected rather than imported by the query
// client so that module stays free of UI and i18n at runtime.
setMutationNotifier((variant, message) => toast({ variant, message }));
// Initialize default context provider globally
const defaultProvider = createDefaultContextProvider(router, store);
contextRegistry.register("default", defaultProvider);
const { reoInit } = useReo();
reoInit();
// app.use(SearchPlugin);
// Unauthenticated bootstrap payload: only what the login page needs. The full
// configuration is fetched authenticated (per org) once the user is signed in.
interface ConfigResponse {
data?: {
telemetry_enabled?: boolean;
build_type?: string;
commit_hash?: string;
};
}
const getConfig = async () => {
// Seeds the shared bootstrap `/config` query — Login and the version checker
// read the same cached entry instead of each issuing their own request. The
// authenticated full config is a separate entry (`configFullQuery`), fetched
// per org once the user is signed in.
await queryClient.fetchQuery(configQuery()).then((data: ConfigResponse["data"]) => {
const res: ConfigResponse = { data };
if (!res.data) return;
// Never clobber the authenticated full config with the bootstrap subset if
// the full fetch (MainLayout) happens to resolve first.
if (!isFullConfigLoaded(store.state.zoConfig)) {
store.dispatch("setConfig", res.data);
}
config.enableAnalytics = res.data.telemetry_enabled?.toString() ?? "false";
// Store initial commit hash for version checking
if (res.data.commit_hash) {
buildVersionChecker.setInitialVersion(res.data.commit_hash);
}
});
};
getConfig();
// ===== Smart Stale Build Detection (Industry Standard) =====
// Detects stale build errors (new deployment, old chunks deleted) and prompts user to refresh
// Track if notification is already shown to prevent duplicates
let staleNotificationShown = false;
// Helper: Show new version notification (only once)
function showNewVersionNotification() {
if (staleNotificationShown) {
return;
}
staleNotificationShown = true;
toast({
variant: "error",
message: raw(i18n.global.t("common.chunkLoadErrorMsg")),
timeout: 0, // Don't auto-dismiss
action: {
label: raw(i18n.global.t("common.refresh")),
handler: () => {
window.location.reload();
},
},
});
}
// Handle resource load errors (script/link tags)
window.addEventListener(
"error",
async (event) => {
const target = event.target as HTMLScriptElement | HTMLLinkElement;
// Check if the error is from loading a script or stylesheet
if (target && (target.tagName === "SCRIPT" || target.tagName === "LINK")) {
const url =
target.tagName === "SCRIPT"
? (target as HTMLScriptElement).src
: (target as HTMLLinkElement).href;
if (!url) return;
// Only check for OpenObserve's own chunks (relative URLs or same origin)
// Ignore external CDN resources (absolute URLs with different origins)
const isOpenObserveResource = (() => {
try {
// Resolve relative URLs against current origin
const resourceUrl = new URL(url, window.location.origin);
// Only check resources from same origin
return resourceUrl.origin === window.location.origin;
} catch (e) {
// Invalid URL - assume it's a relative path (OpenObserve resource)
return true;
}
})();
if (isOpenObserveResource) {
// Smart detection: Check if it's stale build
const isStale = await buildVersionChecker.isStaleResourceError();
if (isStale) {
showNewVersionNotification();
event.preventDefault();
}
}
}
},
true,
);
// Handle dynamic import errors (for code splitting)
router.onError(async (error) => {
if (isChunkLoadError(error)) {
// Smart detection: Check if it's stale build or network error
const isStale = await buildVersionChecker.isStaleChunkError(error);
if (isStale) {
showNewVersionNotification();
}
}
});
// router.onError only sees route chunks; lazy components (e.g. the alert dialog) fail through Vite's preload hook.
window.addEventListener("vite:preloadError", async (event) => {
if (
isChunkLoadError(event.payload) &&
(await buildVersionChecker.isStaleChunkError(event.payload))
) {
showNewVersionNotification();
}
});
// Ensure the active locale's messages are loaded (en-us is bundled; any other
// language is fetched as a code-split chunk) before the first render.
loadLocaleMessages(activeLocale)
// On a locale-chunk load failure, fall back to the bundled en-us messages
// (no console noise). The app must mount regardless.
.catch(() => {})
.finally(() => {
app.mount("#app");
});