Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Frontend/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const localPlugin = {
},
};

// Configure console rule: error in CI to catch leftover debug calls before merge,
// off locally so developers get no IDE warnings and can use console freely during development.
const consoleRuleSeverity = process.env.CI === "true" ? "error" : "off";

export default tseslint.config(
{
ignores: ["node_modules/**", "dist/**", "public/js/app.constants.js", "public/mockServiceWorker.js"],
Expand All @@ -33,6 +37,7 @@ export default tseslint.config(
eqeqeq: ["error", "smart"],
"no-throw-literal": "warn",
"local/no-raw-fetch": "error",
"no-console": consoleRuleSeverity,
},
}
);
14 changes: 3 additions & 11 deletions src/Frontend/src/AuthApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useAuthStore } from "@/stores/AuthStore";
import routeLinks from "@/router/routeLinks";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import App from "./App.vue";
import logger from "@/logger";

const { authenticate } = useAuth();
const authStore = useAuthStore();
Expand All @@ -18,21 +19,18 @@ onMounted(async () => {

// If authentication is not enabled, skip authentication
if (!authStore.authEnabled) {
console.debug("Authentication is disabled");
authStore.setAuthenticating(false);
return;
}

// Check if auth config is available
if (!authStore.authConfig) {
console.debug("Authentication is enabled but configuration not available");
authStore.setAuthenticating(false);
return;
}

// If the user is on an anonymous route (like logged-out), don't trigger authentication
if (window.location.hash === `#${routeLinks.loggedOut}`) {
console.debug("User is on logged-out page, skipping authentication");
authStore.setAuthenticating(false);
return;
}
Expand All @@ -41,15 +39,9 @@ onMounted(async () => {
// 1. Check if we're returning from the identity provider (handle callback)
// 2. Check for an existing valid session
// 3. If no valid session, redirect to the identity provider
const authenticated = await authenticate(authStore.authConfig);

if (authenticated) {
console.debug("User authenticated successfully");
} else {
console.debug("Redirecting to identity provider for authentication...");
}
await authenticate(authStore.authConfig);
} catch (error) {
console.error("Failed to authenticate on app load:", error);
logger.error("Failed to authenticate on app load:", error);
authStore.setAuthenticating(false);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SagaName from "@/components/SagaName.vue";
import { useLayout } from "@/components/messages/FlowDiagram/useLayout";
import { formatTypeName } from "@/composables/formatUtils";
import TextEllipses from "@/components/TextEllipses.vue";
import logger from "@/logger";

enum MessageType {
Event = "Event message",
Expand Down Expand Up @@ -143,23 +144,19 @@ function constructEdges(nodes: Node<NodeData>[]): DefaultEdge[] {
if (m === undefined) return false;
return m.receiving_endpoint !== undefined && m.sending_endpoint !== undefined && m.message_id === relatedTo && m.message_intent !== MessageIntent.Publish;
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but something that should be fixed: if the lint command throws an error we don't fail the script as we should, now it is failing silently: https://github.com/Particular/ServicePulse/actions/runs/23470361269/job/68291497710?pr=2917#step:5:24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a bug that needs to be fixed.

I think it has to do with Powershell build.ps1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build should definitely fail, same as our C# builds fail with invalid code styles

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #2921


if (parentMessages.length === 0) {
console.debug(`Fall back to match only on RelatedToMessageId for message with Id '${message.message_id}' matched but link could be invalid.`);
}
}

switch (parentMessages.length) {
case 0:
console.warn(
logger.warn(
`No parent could be resolved for the message with Id '${message.message_id}' which has RelatedToMessageId set. This can happen if the parent has been purged due to retention expiration, an ServiceControl node to be unavailable, or because the parent message not been stored (yet).`
);
break;
case 1:
// Log nothing, this is what it should be
break;
default:
console.warn(`Multiple parents matched for message id '${message.message_id}' possibly due to more-than-once processing, linking to all as it is unknown which processing attempt generated the message.`);
logger.warn(`Multiple parents matched for message id '${message.message_id}' possibly due to more-than-once processing, linking to all as it is unknown which processing attempt generated the message.`);
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useSagaDiagramStore } from "@/stores/SagaDiagramStore";
import { ref, watch, computed } from "vue";
import { EditorView } from "@codemirror/view";
import { parse, stringify } from "lossless-json";
import logger from "@/logger";
// Import the images directly
import CommandIcon from "@/assets/command.svg";
Expand Down Expand Up @@ -82,7 +83,7 @@ const processState = (state: string | undefined): object => {
stateObj = parsedState as Record<string, unknown>;
} catch (e) {
console.error("Error parsing state:", e);
logger.error("Error parsing state:", e);
hasParsingError.value = true;
return {};
}
Expand Down
3 changes: 2 additions & 1 deletion src/Frontend/src/components/monitoring/EndpointInstances.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CriticalTime, InstanceName, ProcessingTime, ScheduledRetries, Throughpu
import FAIcon from "@/components/FAIcon.vue";
import { faEnvelope, faTrash } from "@fortawesome/free-solid-svg-icons";
import monitoringClient from "./monitoringClient";
import logger from "@/logger";

const isRemovingEndpointEnabled = ref<boolean>(false);
const router = useRouter();
Expand All @@ -27,7 +28,7 @@ async function removeEndpoint(endpointName: string, instance: ExtendedEndpointIn
router.push(routeLinks.monitoring.root);
}
} catch (err) {
console.log(err);
logger.error(err);
return false;
}
}
Expand Down
10 changes: 1 addition & 9 deletions src/Frontend/src/composables/autoRefresh.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { watch, ref, shallowReadonly, type WatchStopHandle } from "vue";
import { useCounter, useDocumentVisibility, useTimeoutPoll } from "@vueuse/core";

export default function useFetchWithAutoRefresh(name: string, fetchFn: () => Promise<void>, intervalMs: number) {
export default function useFetchWithAutoRefresh(_name: string, fetchFn: () => Promise<void>, intervalMs: number) {
let watchStop: WatchStopHandle | null = null;
const { count, inc, dec, reset } = useCounter(0);
const interval = ref(intervalMs);
Expand All @@ -28,21 +28,17 @@ export default function useFetchWithAutoRefresh(name: string, fetchFn: () => Pro
const start = async () => {
inc();
if (count.value === 1) {
console.debug(`[AutoRefresh] Starting auto-refresh for ${name} every ${interval.value}ms`);
resume();
watchStop = watch(visibility, (current, previous) => {
if (current === "visible" && previous === "hidden") {
console.debug(`[AutoRefresh] Resuming auto-refresh for ${name} as document became visible`);
resume();
}

if (current === "hidden" && previous === "visible") {
console.debug(`[AutoRefresh] Pausing auto-refresh for ${name} as document became hidden`);
pause();
}
});
} else {
console.debug(`[AutoRefresh] Incremented refCount for ${name} to ${count.value}`);
// Because another component has started using the auto-refresh, do an immediate refresh to ensure it has up-to-date data
await fetchWrapper();
}
Expand All @@ -51,21 +47,17 @@ export default function useFetchWithAutoRefresh(name: string, fetchFn: () => Pro
const stop = () => {
dec();
if (count.value <= 0) {
console.debug(`[AutoRefresh] Stopping auto-refresh for ${name}`);
pause();
watchStop?.();
watchStop = null;
reset();
} else {
console.debug(`[AutoRefresh] Decremented refCount for ${name} to ${count.value}`);
}
};

const updateInterval = (newIntervalMs: number) => {
if (interval.value === newIntervalMs) return;

interval.value = newIntervalMs;
console.debug(`[AutoRefresh] updated polling ${name} to ${newIntervalMs}ms`);

if (isActive.value) {
// We need to do this hack, because useTimeoutPoll doesn't react to interval changes while active
Expand Down
20 changes: 7 additions & 13 deletions src/Frontend/src/composables/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAuthStore } from "@/stores/AuthStore";
import type { AuthConfig } from "@/types/auth";
import { UserManager, type User } from "oidc-client-ts";
import logger from "@/logger";

let userManager: UserManager | null = null;

Expand All @@ -17,31 +18,27 @@ export function useAuth() {

// Set up event handlers
userManager.events.addUserLoaded((user: User) => {
console.debug("User loaded:", user.profile);
authStore.setToken(user.access_token);
});

userManager.events.addUserUnloaded(() => {
console.debug("User unloaded");
authStore.clearToken();
});

userManager.events.addAccessTokenExpiring(async () => {
console.debug("Access token expiring, attempting silent renewal...");
try {
await userManager?.signinSilent();
} catch (error) {
console.error("Silent token renewal failed:", error);
logger.error("Silent token renewal failed:", error);
}
});

userManager.events.addAccessTokenExpired(() => {
console.debug("Access token expired");
authStore.clearToken();
});

userManager.events.addSilentRenewError((error) => {
console.error("Silent renew error:", error);
logger.error("Silent renew error:", error);
});
}

Expand Down Expand Up @@ -75,19 +72,17 @@ export function useAuth() {

if (hasCode && hasState) {
// This is an OAuth callback with authorization code
console.debug("Processing OAuth callback...");
authStore.setAuthenticating(true);
try {
const user = await manager.signinCallback();
console.debug("Signin callback successful");
if (user) {
authStore.setToken(user.access_token);
// Clean up URL by removing OAuth parameters
window.history.replaceState({}, document.title, window.location.pathname);
return true;
}
} catch (error) {
console.error("Signin callback error details:", {
logger.error("Signin callback error details:", {
error,
errorMessage: error instanceof Error ? error.message : "Unknown error",
errorStack: error instanceof Error ? error.stack : undefined,
Expand All @@ -101,15 +96,14 @@ export function useAuth() {
} else if (hasError) {
// OAuth error in callback
const errorDescription = params.get("error_description") || params.get("error");
console.error("OAuth error:", errorDescription);
logger.error("OAuth error:", errorDescription);
authStore.setAuthError(errorDescription || "Authentication failed");
return false;
}

// Check for existing valid user session
const user = await manager.getUser();
if (user && !user.expired) {
console.debug("Existing user session found", user.profile);
authStore.setToken(user.access_token);
return true;
}
Expand All @@ -122,7 +116,7 @@ export function useAuth() {
authStore.setAuthenticating(false);
const errorMessage = error instanceof Error ? error.message : "Unknown authentication error";
authStore.setAuthError(errorMessage);
console.error("Authentication error:", error);
logger.error("Authentication error:", error);
throw error;
}
}
Expand All @@ -146,7 +140,7 @@ export function useAuth() {
authStore.clearToken();
}
} catch (error) {
console.error("Logout error:", error);
logger.error("Logout error:", error);
authStore.clearToken();
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/Frontend/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Centralised logger — the only place in the codebase that touches `console` directly.
// Using this wrapper keeps ESLint's no-console rule satisfied everywhere else and gives
// a single point of control if log behaviour ever needs to change (e.g. send errors to
// a remote service, suppress output in specific environments, etc.).

/* eslint-disable no-console */
const logger = {
error: ((...args) => console.error(...args)) as typeof console.error,
warn: ((...args) => console.warn(...args)) as typeof console.warn,
};
/* eslint-enable no-console */

export default logger;
3 changes: 2 additions & 1 deletion src/Frontend/src/mount.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createApp } from "vue";
import type { Router } from "vue-router";
import logger from "@/logger";
import AuthApp from "./AuthApp.vue";
import Toast, { type PluginOptions, POSITION } from "vue-toastification";
import VueTippy from "vue-tippy";
Expand All @@ -26,7 +27,7 @@ export function mount({ router }: { router: Router }) {
app.mount(`#app`);

app.config.errorHandler = (err, instance) => {
console.error(instance, err);
logger.error(instance, err);
};

return app;
Expand Down
3 changes: 2 additions & 1 deletion src/Frontend/src/stores/AuthStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { acceptHMRUpdate, defineStore } from "pinia";
import logger from "@/logger";
import { ref } from "vue";
import type { AuthConfig } from "@/types/auth";
import { WebStorageStateStore } from "oidc-client-ts";
Expand Down Expand Up @@ -40,7 +41,7 @@ export const useAuthStore = defineStore("auth", () => {
const [, data] = await serviceControlClient.fetchTypedFromServiceControl<AuthConfigResponse>("authentication/configuration");
return data;
} catch (err) {
console.error("Error fetching auth configuration", err);
logger.error("Error fetching auth configuration", err);
return null;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Frontend/src/stores/ConfigurationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { acceptHMRUpdate, defineStore } from "pinia";
import { computed, ref } from "vue";
import Configuration from "@/resources/Configuration";
import serviceControlClient from "@/components/serviceControlClient";
import logger from "@/logger";

export const useConfigurationStore = defineStore("ConfigurationStore", () => {
const configuration = ref<Configuration | null>(null);
Expand All @@ -15,7 +16,7 @@ export const useConfigurationStore = defineStore("ConfigurationStore", () => {
return configuration.value;
})
.catch((error) => {
console.error("Failed to fetch configuration:", error);
logger.error("Failed to fetch configuration:", error);
});

return {
Expand Down
3 changes: 2 additions & 1 deletion src/Frontend/src/stores/ConnectionsAndStatsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { acceptHMRUpdate, defineStore } from "pinia";
import logger from "@/logger";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { FailedMessageStatus } from "@/resources/FailedMessage";
import { ConnectionState } from "@/resources/ConnectionState";
Expand Down Expand Up @@ -96,7 +97,7 @@ async function fetchAndSetConnectionState(fetchFunction: () => Promise<number |
connectionState.unableToConnect = true;
connectionState.connectedRecently = false;
connectionState.connecting = false;
console.log(err);
logger.error(err);
}
} catch {
connectionState.connecting = false;
Expand Down
3 changes: 2 additions & 1 deletion src/Frontend/src/stores/EnvironmentAndVersionsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isSupported, isUpgradeAvailable } from "@/composables/serviceSemVer";
import logger from "@/logger";
import Release from "@/resources/Release";
import RootUrls from "@/resources/RootUrls";
import { useMemoize } from "@vueuse/core";
Expand Down Expand Up @@ -111,7 +112,7 @@ async function getData(url: string, authenticated = false) {
const response = await (authenticated ? authFetch(url) : fetch(url)); // this needs to be an unauthenticated call
return (await response.json()) as unknown as Release[];
} catch (e) {
console.log(e);
logger.error(e);
return [
{
tag: "Unknown",
Expand Down
Loading