// 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 . import { createApp } from "vue"; import store from "./stores"; import App from "./App.vue"; import createRouter from "./router"; import i18n from "./locales"; import "./styles/tailwind.css"; import config from "./aws-exports"; import configService from "./services/config"; import { openobserveRum } from "@openobserve/browser-rum"; import { openobserveLogs } from "@openobserve/browser-logs"; import { useReo } from "./services/reodotdev_analytics"; import { contextRegistry, createDefaultContextProvider, } from "./composables/contextProviders"; import { buildVersionChecker } from "./utils/buildVersionChecker"; import { toast } from "@/lib/feedback/Toast/useToast"; import { bootstrapTheme } from "@/utils/themeManager"; // 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 app = createApp(App); const router = createRouter(store); app.use(i18n); // const router = createRouter(store); app.use(store).use(router); // Initialize default context provider globally const defaultProvider = createDefaultContextProvider(router, store); contextRegistry.register("default", defaultProvider); const { reoInit } = useReo(); reoInit(); // app.use(SearchPlugin); interface ConfigResponse { data?: { telemetry_enabled?: boolean; commit_hash?: string; rum?: { enabled: boolean; client_token: string; application_id: string; site: string; service: string; env: string; version?: string; organization_identifier: string; insecure_http?: boolean; api_version?: string; }; }; } const getConfig = async () => { await configService.get_config().then((res: ConfigResponse) => { if (!res.data) return; 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); } if (res.data.rum?.enabled) { const options = { clientToken: res.data.rum.client_token, applicationId: res.data.rum.application_id, site: res.data.rum.site, service: res.data.rum.service, env: res.data.rum.env, version: res.data.rum.version || "0.0.1", organizationIdentifier: res.data.rum.organization_identifier, insecureHTTP: res.data.rum.insecure_http || false, apiVersion: res.data.rum.api_version || "v1", }; openobserveRum.init({ applicationId: options.applicationId, // required, any string identifying your application clientToken: options.clientToken, site: options.site, organizationIdentifier: options.organizationIdentifier, service: options.service, env: options.env, version: options.version, trackResources: true, trackLongTasks: true, trackUserInteractions: true, actionNameAttribute: "data-test", apiVersion: options.apiVersion, insecureHTTP: options.insecureHTTP, defaultPrivacyLevel: "allow", allowedTracingUrls: [ { match: store.state.API_ENDPOINT + "/api", propagatorTypes: ["openobserve", "tracecontext"], }, ], beforeSend: (event) => { // Filter out specific errors before sending to RUM if (event.type === "error") { const errorMessage = event.error?.message || ""; const errorStack = event.error?.stack || ""; // List of error patterns to ignore const ignoredErrorPatterns = [ /ResizeObserver loop/i, /RS SDK/i, /reo.dev/i, /Cannot set properties of null \(setting 'innerHTML'\)/, ]; // Check if error matches any ignored pattern const shouldIgnore = ignoredErrorPatterns.some( (pattern) => pattern.test(errorMessage) || pattern.test(errorStack), ); if (shouldIgnore) { return false; // Don't send this error } } // Allow all other events to be sent return true; }, }); openobserveLogs.init({ clientToken: options.clientToken, site: options.site, organizationIdentifier: options.organizationIdentifier, service: options.service, env: options.env, version: options.version, forwardErrorsToLogs: true, insecureHTTP: options.insecureHTTP, apiVersion: options.apiVersion, beforeSend: (log) => { // Filter out specific logs before sending const logMessage = log.message || ""; // List of log patterns to ignore const ignoredLogPatterns = [/ResizeObserver loop/i]; // Check if log matches any ignored pattern const shouldIgnore = ignoredLogPatterns.some((pattern) => pattern.test(logMessage), ); if (shouldIgnore) { return false; // Don't send this log } // Allow all other logs to be sent return true; }, }); // Don't start session replay automatically - it will be started after login // to avoid capturing login page with "Unknown" user // Session replay will be started in Login.vue and MainLayout.vue after user is authenticated } }); }; 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: i18n.global.t("common.chunkLoadErrorMsg"), timeout: 0, // Don't auto-dismiss action: { label: i18n.global.t("common.refresh"), handler: () => { window.location.reload(); }, }, position: "top-center", }); } // 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) => { const chunkFailedPattern = /Loading chunk [\d]+ failed|Failed to fetch dynamically imported module/i; if (chunkFailedPattern.test(error.message)) { // Smart detection: Check if it's stale build or network error const isStale = await buildVersionChecker.isStaleChunkError(error); if (isStale) { showNewVersionNotification(); } } }); app.mount("#app");