mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
177 lines
5.6 KiB
TypeScript
177 lines
5.6 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 } from "@/services/config.queries";
|
|
import { queryClient } from "@/composables/query/queryClient";
|
|
|
|
// Chrome, Firefox and Safari word a failed dynamic import differently; Vite adds its own CSS preload error.
|
|
const CHUNK_LOAD_ERROR_PATTERN =
|
|
/Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed|Unable to preload CSS|Loading (CSS )?chunk \S+ failed/i;
|
|
|
|
/**
|
|
* Approach:
|
|
* 1. When app loads, config API is called (by main.ts) and stored in Vuex store
|
|
* 2. We store the initial commit hash from that config in sessionStorage (per tab)
|
|
* 3. On chunk/resource errors, fetch config API again to check if commit hash changed
|
|
* 4. Cache config responses to avoid excessive API calls
|
|
* 5. Only treat as stale build if commit hash actually changed
|
|
*/
|
|
|
|
class BuildVersionChecker {
|
|
private readonly STORAGE_KEY = "o2_initial_commit_hash";
|
|
private isChecking = false;
|
|
private lastCheckTime = 0;
|
|
private cacheDuration = 5 * 60 * 1000; // Cache for 5 minutes
|
|
private cachedConfig: any = null;
|
|
|
|
private initialVersion: string | null = null;
|
|
|
|
/**
|
|
* Get this tab's baseline version, or null if not set
|
|
*/
|
|
private getStoredVersion(): string | null {
|
|
if (this.initialVersion) return this.initialVersion;
|
|
try {
|
|
return sessionStorage.getItem(this.STORAGE_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the build this tab loaded; called from main.ts on every page load.
|
|
*/
|
|
public setInitialVersion(commitHash: string): void {
|
|
this.initialVersion = commitHash;
|
|
try {
|
|
sessionStorage.setItem(this.STORAGE_KEY, commitHash);
|
|
// The baseline must stay per tab; a shared one hides staleness from older tabs.
|
|
localStorage.removeItem(this.STORAGE_KEY);
|
|
} catch {
|
|
// Storage can be blocked; the in-memory baseline still works for this tab.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch commit hash from config API
|
|
*/
|
|
async fetchCommitHash(): Promise<string> {
|
|
const now = Date.now();
|
|
|
|
// Return cached version if still fresh
|
|
if (this.cachedConfig && now - this.lastCheckTime < this.cacheDuration) {
|
|
return this.cachedConfig.commit_hash;
|
|
}
|
|
|
|
const options = configQuery();
|
|
await queryClient.invalidateQueries({
|
|
queryKey: options.queryKey,
|
|
exact: true,
|
|
refetchType: "none",
|
|
});
|
|
this.cachedConfig = await queryClient.fetchQuery(options);
|
|
this.lastCheckTime = now;
|
|
|
|
return this.cachedConfig.commit_hash;
|
|
}
|
|
|
|
/**
|
|
* Check if a new version is available
|
|
*/
|
|
async checkForNewVersion(): Promise<boolean> {
|
|
if (this.isChecking) return false;
|
|
|
|
const initialVersion = this.getStoredVersion();
|
|
|
|
// Skip version check if we don't have an initial version
|
|
if (!initialVersion) {
|
|
return false;
|
|
}
|
|
|
|
this.isChecking = true;
|
|
try {
|
|
const currentServerHash = await this.fetchCommitHash();
|
|
|
|
// Compare short hashes (first 7 chars)
|
|
const initialShort = initialVersion.substring(0, 7);
|
|
const currentShort = currentServerHash.substring(0, 7);
|
|
|
|
return currentShort !== initialShort;
|
|
} catch (error) {
|
|
return false;
|
|
} finally {
|
|
this.isChecking = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shared helper: Check if current build is stale
|
|
* Returns false if version check fails (network/backend error)
|
|
*/
|
|
private async checkIfVersionIsStale(): Promise<boolean> {
|
|
try {
|
|
return await this.checkForNewVersion();
|
|
} catch (error) {
|
|
// Config API failed - this is a true network/backend error
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: Extract error message from Error or ErrorEvent
|
|
* Uses type guards to safely extract message without unsafe type assertions
|
|
*/
|
|
private getErrorMessage(error: Error | ErrorEvent): string {
|
|
if (error instanceof Error) {
|
|
return error.message;
|
|
}
|
|
if ("message" in error && typeof error.message === "string") {
|
|
return error.message;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Smart detection: Check if chunk load error is due to stale build
|
|
* This distinguishes between stale build (404) and network errors
|
|
*/
|
|
async isStaleChunkError(error: Error | ErrorEvent): Promise<boolean> {
|
|
const errorMessage = this.getErrorMessage(error);
|
|
|
|
// Check if it's a chunk load error
|
|
if (!CHUNK_LOAD_ERROR_PATTERN.test(errorMessage)) {
|
|
return false;
|
|
}
|
|
|
|
return this.checkIfVersionIsStale();
|
|
}
|
|
|
|
/**
|
|
* Check if resource load error (script/link tag) is due to stale build
|
|
*/
|
|
async isStaleResourceError(): Promise<boolean> {
|
|
return this.checkIfVersionIsStale();
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const buildVersionChecker = new BuildVersionChecker();
|
|
|
|
/** True when the error is a failed code-split chunk or CSS preload. */
|
|
export function isChunkLoadError(error: unknown): boolean {
|
|
const message = (error as { message?: unknown } | null | undefined)?.message;
|
|
return typeof message === "string" && CHUNK_LOAD_ERROR_PATTERN.test(message);
|
|
}
|