Files
openobserve/web/src/main.ts
T
quyentonndbsandKai Tanaka 908e6d720b chore: improve openobserve maintenance path (#12508)
Summary:
- Add or tighten focused edge-case tests or type assertions in
web/src/main.ts, web/src/App.spec.ts related to TypeScript, frontend
tooling, test stability, build fixes; avoid docs-only changes and broad
refactors.
- Keep the change narrow so it is straightforward to review.

Notes:
- I kept this scoped to the relevant implementation and tests.

Co-authored-by: Kai Tanaka <275430420+quyentonndbs@users.noreply.github.com>
2026-06-30 16:04:05 +00:00

278 lines
8.7 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 { 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");