mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-03-25 01:23:35 +00:00
Compare commits
1 Commits
master
...
frontend_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6473096409 |
@@ -7,10 +7,39 @@ export enum MetricScenario {
|
||||
DatabaseLoad = "DatabaseLoad",
|
||||
}
|
||||
|
||||
export interface WebVitals {
|
||||
lcp?: number; // Largest Contentful Paint
|
||||
inp?: number; // Interaction to Next Paint
|
||||
cls?: number; // Cumulative Layout Shift
|
||||
fcp?: number; // First Contentful Paint
|
||||
ttfb?: number; // Time to First Byte
|
||||
}
|
||||
|
||||
export interface PhaseTimings {
|
||||
endTimeISO: string; // When the phase completed
|
||||
durationMs: number; // Duration from scenario start to phase completion
|
||||
}
|
||||
|
||||
// Generic metric emission event describing scenario outcome.
|
||||
export interface MetricEvent {
|
||||
// === Existing required fields (unchanged) ===
|
||||
readonly platform: Platform;
|
||||
readonly api: ApiType;
|
||||
readonly scenario: MetricScenario;
|
||||
readonly healthy: boolean;
|
||||
|
||||
// === New optional fields ===
|
||||
readonly durationMs?: number;
|
||||
readonly timedOut?: boolean;
|
||||
readonly documentHidden?: boolean;
|
||||
readonly hasExpectedFailure?: boolean;
|
||||
|
||||
readonly completedPhases?: string[];
|
||||
readonly failedPhases?: string[];
|
||||
readonly phaseTimings?: Record<string, PhaseTimings>;
|
||||
|
||||
readonly startTimeISO?: string;
|
||||
readonly endTimeISO?: string;
|
||||
|
||||
readonly vitals?: WebVitals;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { fetchWithTimeout } from "../Utils/FetchWithTimeout";
|
||||
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
||||
import { MetricScenario } from "./Constants";
|
||||
import { reportMetric } from "./MetricEvents";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { Response } = require("node-fetch");
|
||||
@@ -21,11 +22,16 @@ describe("MetricEvents", () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("reportHealthy success includes auth header", async () => {
|
||||
test("reportMetric success includes auth header", async () => {
|
||||
const mockResponse = new Response(null, { status: 200 });
|
||||
mockFetchWithTimeout.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
|
||||
const result = await reportMetric({
|
||||
platform: Platform.Portal,
|
||||
api: "SQL",
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: true,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.ok).toBe(true);
|
||||
@@ -47,39 +53,77 @@ describe("MetricEvents", () => {
|
||||
expect(getAuthorizationHeader).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("reportUnhealthy failure status", async () => {
|
||||
const mockResponse = new Response("Failure", { status: 500 });
|
||||
test("reportMetric sends full enriched payload", async () => {
|
||||
const mockResponse = new Response(null, { status: 200 });
|
||||
mockFetchWithTimeout.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await reportUnhealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
|
||||
const event = {
|
||||
platform: Platform.Portal,
|
||||
api: "SQL" as const,
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: true,
|
||||
durationMs: 2847,
|
||||
timedOut: false,
|
||||
documentHidden: false,
|
||||
hasExpectedFailure: false,
|
||||
completedPhases: ["ExplorerInitialized", "Interactive"],
|
||||
failedPhases: [] as string[],
|
||||
phaseTimings: {
|
||||
ExplorerInitialized: { endTimeISO: "2026-03-13T10:00:02.500Z", durationMs: 2500 },
|
||||
Interactive: { endTimeISO: "2026-03-13T10:00:02.847Z", durationMs: 2847 },
|
||||
},
|
||||
startTimeISO: "2026-03-13T10:00:00.000Z",
|
||||
endTimeISO: "2026-03-13T10:00:02.847Z",
|
||||
vitals: { lcp: 1850, inp: 120, cls: 0.05, fcp: 980, ttfb: 340 },
|
||||
};
|
||||
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.status).toBe(500);
|
||||
const result = await reportMetric(event);
|
||||
expect(result.status).toBe(200);
|
||||
|
||||
const callArgs = mockFetchWithTimeout.mock.calls[0];
|
||||
const body = JSON.parse(callArgs[1]?.body as string);
|
||||
expect(body.healthy).toBe(true);
|
||||
expect(body.durationMs).toBe(2847);
|
||||
expect(body.timedOut).toBe(false);
|
||||
expect(body.documentHidden).toBe(false);
|
||||
expect(body.hasExpectedFailure).toBe(false);
|
||||
expect(body.completedPhases).toEqual(["ExplorerInitialized", "Interactive"]);
|
||||
expect(body.failedPhases).toEqual([]);
|
||||
expect(body.phaseTimings.ExplorerInitialized.durationMs).toBe(2500);
|
||||
expect(body.startTimeISO).toBe("2026-03-13T10:00:00.000Z");
|
||||
expect(body.endTimeISO).toBe("2026-03-13T10:00:02.847Z");
|
||||
expect(body.vitals.lcp).toBe(1850);
|
||||
expect(body.vitals.cls).toBe(0.05);
|
||||
});
|
||||
|
||||
test("reportMetric omits undefined optional fields", async () => {
|
||||
const mockResponse = new Response(null, { status: 200 });
|
||||
mockFetchWithTimeout.mockResolvedValue(mockResponse);
|
||||
|
||||
const event = {
|
||||
platform: Platform.Portal,
|
||||
api: "SQL" as const,
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: false,
|
||||
};
|
||||
|
||||
await reportMetric(event);
|
||||
|
||||
const callArgs = mockFetchWithTimeout.mock.calls[0];
|
||||
const body = JSON.parse(callArgs[1]?.body as string);
|
||||
expect(body.healthy).toBe(false);
|
||||
});
|
||||
|
||||
test("helpers healthy/unhealthy", async () => {
|
||||
mockFetchWithTimeout.mockResolvedValue(new Response(null, { status: 201 }));
|
||||
|
||||
const healthyResult = await reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
|
||||
const unhealthyResult = await reportUnhealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL");
|
||||
|
||||
expect(healthyResult.status).toBe(201);
|
||||
expect(unhealthyResult.status).toBe(201);
|
||||
expect(mockFetchWithTimeout).toHaveBeenCalledTimes(2);
|
||||
expect(body.durationMs).toBeUndefined();
|
||||
expect(body.vitals).toBeUndefined();
|
||||
expect(body.completedPhases).toBeUndefined();
|
||||
});
|
||||
|
||||
test("throws when backend endpoint missing", async () => {
|
||||
const original = configContext.PORTAL_BACKEND_ENDPOINT;
|
||||
(configContext as { PORTAL_BACKEND_ENDPOINT: string }).PORTAL_BACKEND_ENDPOINT = "";
|
||||
|
||||
await expect(reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL")).rejects.toThrow(
|
||||
"baseUri is null or empty",
|
||||
);
|
||||
await expect(
|
||||
reportMetric({ platform: Platform.Portal, api: "SQL", scenario: MetricScenario.ApplicationLoad, healthy: true }),
|
||||
).rejects.toThrow("baseUri is null or empty");
|
||||
|
||||
expect(mockFetchWithTimeout).not.toHaveBeenCalled();
|
||||
(configContext as { PORTAL_BACKEND_ENDPOINT: string }).PORTAL_BACKEND_ENDPOINT = original;
|
||||
@@ -88,17 +132,17 @@ describe("MetricEvents", () => {
|
||||
test("propagates fetch errors", async () => {
|
||||
mockFetchWithTimeout.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
await expect(reportHealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL")).rejects.toThrow(
|
||||
"Network error",
|
||||
);
|
||||
await expect(
|
||||
reportMetric({ platform: Platform.Portal, api: "SQL", scenario: MetricScenario.ApplicationLoad, healthy: true }),
|
||||
).rejects.toThrow("Network error");
|
||||
});
|
||||
|
||||
test("propagates timeout errors", async () => {
|
||||
const abortError = new DOMException("The operation was aborted", "AbortError");
|
||||
mockFetchWithTimeout.mockRejectedValue(abortError);
|
||||
|
||||
await expect(reportUnhealthy(MetricScenario.ApplicationLoad, Platform.Portal, "SQL")).rejects.toThrow(
|
||||
"The operation was aborted",
|
||||
);
|
||||
await expect(
|
||||
reportMetric({ platform: Platform.Portal, api: "SQL", scenario: MetricScenario.ApplicationLoad, healthy: false }),
|
||||
).rejects.toThrow("The operation was aborted");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
// Metrics module: scenario metric emission logic.
|
||||
import { MetricEvent, MetricScenario } from "Metrics/Constants";
|
||||
import { createUri } from "../Common/UrlUtility";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import { ApiType } from "../UserContext";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { fetchWithTimeout } from "../Utils/FetchWithTimeout";
|
||||
|
||||
const RELATIVE_PATH = "/api/dataexplorer/metrics/health"; // Endpoint retains 'health' for backend compatibility.
|
||||
|
||||
export const reportHealthy = (scenario: MetricScenario, platform: Platform, api: ApiType): Promise<Response> =>
|
||||
send({ platform, api, scenario, healthy: true });
|
||||
|
||||
export const reportUnhealthy = (scenario: MetricScenario, platform: Platform, api: ApiType): Promise<Response> =>
|
||||
send({ platform, api, scenario, healthy: false });
|
||||
/** Send a full enriched MetricEvent to the backend. */
|
||||
export const reportMetric = (event: MetricEvent): Promise<Response> => send(event);
|
||||
|
||||
const send = async (event: MetricEvent): Promise<Response> => {
|
||||
// Skip metrics emission during local development
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PhaseTimings, WebVitals } from "Metrics/Constants";
|
||||
import MetricScenario from "./MetricEvents";
|
||||
|
||||
// Common phases shared across all scenarios
|
||||
@@ -15,25 +16,12 @@ export enum ApplicationMetricPhase {
|
||||
// Combined type for all metric phases
|
||||
export type MetricPhase = CommonMetricPhase | ApplicationMetricPhase;
|
||||
|
||||
export interface WebVitals {
|
||||
lcp?: number; // Largest Contentful Paint
|
||||
inp?: number; // Interaction to Next Paint
|
||||
cls?: number; // Cumulative Layout Shift
|
||||
fcp?: number; // First Contentful Paint
|
||||
ttfb?: number; // Time to First Byte
|
||||
}
|
||||
|
||||
export interface ScenarioConfig<TPhase extends string = MetricPhase> {
|
||||
requiredPhases: TPhase[];
|
||||
timeoutMs: number;
|
||||
validate?: (ctx: ScenarioContextSnapshot<TPhase>) => boolean; // Optional custom validation
|
||||
}
|
||||
|
||||
export interface PhaseTimings {
|
||||
endTimeISO: string; // When the phase completed
|
||||
durationMs: number; // Duration from scenario start to phase completion
|
||||
}
|
||||
|
||||
export interface ScenarioContextSnapshot<TPhase extends string = MetricPhase> {
|
||||
scenario: MetricScenario;
|
||||
startTimeISO: string; // Human-readable ISO timestamp
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
||||
import MetricScenario, { reportMetric } from "./MetricEvents";
|
||||
import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig";
|
||||
import { scenarioMonitor } from "./ScenarioMonitor";
|
||||
|
||||
@@ -15,8 +14,7 @@ jest.mock("./MetricEvents", () => ({
|
||||
ApplicationLoad: "ApplicationLoad",
|
||||
DatabaseLoad: "DatabaseLoad",
|
||||
},
|
||||
reportHealthy: jest.fn().mockResolvedValue({ ok: true }),
|
||||
reportUnhealthy: jest.fn().mockResolvedValue({ ok: true }),
|
||||
reportMetric: jest.fn().mockResolvedValue({ ok: true }),
|
||||
}));
|
||||
|
||||
// Mock configContext
|
||||
@@ -83,8 +81,14 @@ describe("ScenarioMonitor", () => {
|
||||
// Let timeout fire - should emit healthy because of expected failure
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: true,
|
||||
hasExpectedFailure: true,
|
||||
timedOut: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets flag on multiple active scenarios", () => {
|
||||
@@ -98,8 +102,8 @@ describe("ScenarioMonitor", () => {
|
||||
// Let timeouts fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(2);
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).toHaveBeenCalledTimes(2);
|
||||
expect(reportMetric).toHaveBeenCalledWith(expect.objectContaining({ healthy: true, hasExpectedFailure: true }));
|
||||
});
|
||||
|
||||
it("does not affect already emitted scenarios", () => {
|
||||
@@ -113,8 +117,9 @@ describe("ScenarioMonitor", () => {
|
||||
// Now mark expected failure - should not change anything
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Healthy was called when phases completed
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(1);
|
||||
// reportMetric was called when phases completed
|
||||
expect(reportMetric).toHaveBeenCalledTimes(1);
|
||||
expect(reportMetric).toHaveBeenCalledWith(expect.objectContaining({ healthy: true }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,8 +130,13 @@ describe("ScenarioMonitor", () => {
|
||||
// Let timeout fire without marking expected failure
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportUnhealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportHealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: false,
|
||||
timedOut: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("emits healthy on timeout with expected failure", () => {
|
||||
@@ -138,8 +148,14 @@ describe("ScenarioMonitor", () => {
|
||||
// Let timeout fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: true,
|
||||
timedOut: true,
|
||||
hasExpectedFailure: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("emits healthy even with partial phase completion and expected failure", () => {
|
||||
@@ -154,8 +170,14 @@ describe("ScenarioMonitor", () => {
|
||||
// Let timeout fire (Interactive phase not completed)
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalled();
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
healthy: true,
|
||||
timedOut: true,
|
||||
hasExpectedFailure: true,
|
||||
completedPhases: expect.arrayContaining(["ExplorerInitialized"]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,7 +189,13 @@ describe("ScenarioMonitor", () => {
|
||||
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
|
||||
|
||||
// Should emit unhealthy immediately, not wait for timeout
|
||||
expect(reportUnhealthy).toHaveBeenCalledWith(MetricScenario.DatabaseLoad, configContext.platform, "SQL");
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scenario: MetricScenario.DatabaseLoad,
|
||||
healthy: false,
|
||||
timedOut: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not emit twice after failPhase and timeout", () => {
|
||||
@@ -180,7 +208,7 @@ describe("ScenarioMonitor", () => {
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
// Should only have emitted once (from failPhase)
|
||||
expect(reportUnhealthy).toHaveBeenCalledTimes(1);
|
||||
expect(reportMetric).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,7 +220,14 @@ describe("ScenarioMonitor", () => {
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportMetric).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scenario: MetricScenario.ApplicationLoad,
|
||||
healthy: true,
|
||||
timedOut: false,
|
||||
completedPhases: expect.arrayContaining(["ExplorerInitialized", "Interactive"]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not emit until all phases complete", () => {
|
||||
@@ -201,8 +236,7 @@ describe("ScenarioMonitor", () => {
|
||||
// Complete only one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
|
||||
expect(reportHealthy).not.toHaveBeenCalled();
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -224,8 +258,11 @@ describe("ScenarioMonitor", () => {
|
||||
|
||||
// ApplicationLoad emitted healthy on completion
|
||||
// DatabaseLoad emits healthy on timeout (expected failure)
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(2);
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
expect(reportMetric).toHaveBeenCalledTimes(2);
|
||||
// Both should be healthy
|
||||
const calls = (reportMetric as jest.Mock).mock.calls;
|
||||
expect(calls[0][0].healthy).toBe(true);
|
||||
expect(calls[1][0].healthy).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { PhaseTimings, WebVitals } from "Metrics/Constants";
|
||||
import { Metric, onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceMark, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../UserContext";
|
||||
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
||||
import MetricScenario, { reportMetric } from "./MetricEvents";
|
||||
import { scenarioConfigs } from "./MetricScenarioConfigs";
|
||||
import { MetricPhase, PhaseTimings, ScenarioConfig, ScenarioContextSnapshot, WebVitals } from "./ScenarioConfig";
|
||||
import { MetricPhase, ScenarioConfig, ScenarioContextSnapshot } from "./ScenarioConfig";
|
||||
|
||||
interface PhaseContext {
|
||||
startMarkName: string; // Performance mark name for phase start
|
||||
@@ -331,13 +332,23 @@ class ScenarioMonitor {
|
||||
})}`,
|
||||
);
|
||||
|
||||
// Call portal backend health metrics endpoint
|
||||
// If healthy is true (either completed successfully or timeout with expected failure), report healthy
|
||||
if (healthy) {
|
||||
reportHealthy(ctx.scenario, platform, api);
|
||||
} else {
|
||||
reportUnhealthy(ctx.scenario, platform, api);
|
||||
}
|
||||
// Call portal backend health metrics endpoint with enriched payload
|
||||
reportMetric({
|
||||
platform,
|
||||
api,
|
||||
scenario: ctx.scenario,
|
||||
healthy,
|
||||
durationMs: finalSnapshot.durationMs,
|
||||
timedOut,
|
||||
documentHidden: document.hidden,
|
||||
hasExpectedFailure: ctx.hasExpectedFailure,
|
||||
completedPhases: finalSnapshot.completed,
|
||||
failedPhases: finalSnapshot.failedPhases ?? [],
|
||||
phaseTimings: finalSnapshot.phaseTimings,
|
||||
startTimeISO: finalSnapshot.startTimeISO,
|
||||
endTimeISO: finalSnapshot.endTimeISO,
|
||||
vitals: finalSnapshot.vitals,
|
||||
});
|
||||
|
||||
// Cleanup performance entries
|
||||
this.cleanupPerformanceEntries(ctx);
|
||||
|
||||
Reference in New Issue
Block a user