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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jest.mock('react-redux', () => ({
useDispatch: jest.fn(),
}));

jest.mock('../../../contexts', () => ({
useSyntheticsRefreshContext: jest.fn().mockReturnValue({ lastRefresh: 0 }),
}));

jest.mock('../../../state/monitor_management/api', () => ({
resetMonitorAPI: jest.fn(),
resetMonitorBulkAPI: jest.fn(),
Expand Down Expand Up @@ -91,6 +95,23 @@ describe('useMonitorIntegrationHealth', () => {
(reactRedux.useDispatch as jest.Mock).mockReturnValue(dispatchSpy);
});

it('does not re-fetch health when configIds is a new array reference with the same ids', () => {
setupSelectors({ monitors: [unhealthyMonitor], errors: [] });

const { rerender } = renderHook(
({ configIds }: { configIds: string[] }) => useMonitorIntegrationHealth({ configIds }),
{ initialProps: { configIds: ['mon-2'] } }
);

rerender({ configIds: ['mon-2'] });
rerender({ configIds: ['mon-2'] });

const healthDispatches = dispatchSpy.mock.calls.filter(
([action]: [{ type: string }]) => action.type === '[MONITOR HEALTH] GET'
);
expect(healthDispatches).toHaveLength(1);
});

describe('status helpers', () => {
it('isUnhealthy returns true for unhealthy monitors', () => {
setupSelectors({ monitors: [healthyMonitor, unhealthyMonitor], errors: [] });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
ConfigKey,
PrivateLocationHealthStatusValue,
type EncryptedSyntheticsSavedMonitor,
} from '../../../../../../common/runtime_types';
import { fetchMonitorHealthAction, selectMonitorHealth } from '../../../state/monitor_health';
import { resetMonitorAPI, resetMonitorBulkAPI } from '../../../state/monitor_management/api';
Expand All @@ -35,6 +36,29 @@ interface UseMonitorIntegrationHealthOptions {
configIds?: string[];
}

const getPrivateLocationMonitorIds = (monitors: EncryptedSyntheticsSavedMonitor[]): string[] =>
monitors
.filter((m) => (m[ConfigKey.LOCATIONS] ?? []).some((loc) => !loc.isServiceManaged))
.map((m) => m[ConfigKey.CONFIG_ID]);

const getMonitorIdsFetchKey = ({
explicitConfigIdsKey,
listLoaded,
listMonitors,
}: {
explicitConfigIdsKey: string | undefined;
listLoaded: boolean;
listMonitors: EncryptedSyntheticsSavedMonitor[];
}): string => {
if (explicitConfigIdsKey !== undefined) {
return explicitConfigIdsKey;
}
if (!listLoaded) {
return '';
}
return getPrivateLocationMonitorIds(listMonitors).join(',');
};

interface UseMonitorIntegrationHealthReturn {
statuses: Map<string, MonitorIntegrationStatus[]>;
loading: boolean;
Expand Down Expand Up @@ -73,22 +97,28 @@ export const useMonitorIntegrationHealth = (
}
}, [dispatch, configIds, listLoaded, listLoading]);

const monitorIdsToFetch = useMemo(() => {
if (configIds) {
return configIds;
}
if (!listLoaded) {
return [];
}
return listMonitors
.filter((m) => (m[ConfigKey.LOCATIONS] ?? []).some((loc) => !loc.isServiceManaged))
.map((m) => m[ConfigKey.CONFIG_ID]);
}, [configIds, listLoaded, listMonitors]);
// Compare by joined ids — callers often pass inline arrays (e.g. [configId]).
const explicitConfigIdsKey = configIds?.join(',');

const monitorIdsFetchKey = useMemo(
() =>
getMonitorIdsFetchKey({
explicitConfigIdsKey,
listLoaded,
listMonitors,
}),
[explicitConfigIdsKey, listLoaded, listMonitors]
);

const monitorIdsToFetch = useMemo(
() => (monitorIdsFetchKey ? monitorIdsFetchKey.split(',') : []),
[monitorIdsFetchKey]
);

useEffect(() => {
if (monitorIdsToFetch.length === 0) return;
dispatch(fetchMonitorHealthAction.get(monitorIdsToFetch));
}, [dispatch, monitorIdsToFetch, lastRefresh]);
}, [dispatch, lastRefresh, monitorIdsToFetch]);

const statuses = useMemo(() => {
const map = new Map<string, MonitorIntegrationStatus[]>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const createMonitorSO = (
opts: {
name?: string;
origin?: string;
monitorQueryId?: string;
locations?: Array<{ id: string; label?: string; isServiceManaged: boolean }>;
} = {}
): SavedObject<EncryptedSyntheticsMonitorAttributes> =>
Expand All @@ -47,6 +48,7 @@ const createMonitorSO = (
attributes: {
[ConfigKey.NAME]: opts.name ?? `Monitor ${id}`,
[ConfigKey.MONITOR_SOURCE_TYPE]: opts.origin ?? SourceType.UI,
[ConfigKey.MONITOR_QUERY_ID]: opts.monitorQueryId ?? id,
[ConfigKey.LOCATIONS]: opts.locations ?? [],
},
} as unknown as SavedObject<EncryptedSyntheticsMonitorAttributes>);
Expand Down Expand Up @@ -469,6 +471,45 @@ describe('MonitorIntegrationHealthApi', () => {
);
expect(result.monitors[0].privateLocations[0].packagePolicyId).toBe(expectedPolicyId);
});

it('uses MONITOR_QUERY_ID when it differs from the saved object id', async () => {
const privateLoc = createPrivateLocation('priv-loc-1', 'agent-policy-1');
const monitorQueryId = 'journey-project-default';
const so = createMonitorSO('so-uuid', {
origin: SourceType.PROJECT,
monitorQueryId,
locations: [{ id: 'priv-loc-1', label: 'Private Loc 1', isServiceManaged: false }],
});

mockedGetPrivateLocations.mockResolvedValue([privateLoc]);

const expectedPolicyId = `${monitorQueryId}-priv-loc-1`;
const wrongPolicyId = `so-uuid-priv-loc-1`;
const packagePolicy = createPackagePolicy(expectedPolicyId, ['agent-policy-1']);
const fleetGetByIDs = jest.fn().mockResolvedValue([packagePolicy]);

const api = buildApi({
monitorConfigRepository: { get: jest.fn().mockResolvedValue(so) },
fleetGetByIDs,
});

const result = await api.getHealth(['so-uuid']);

expect(fleetGetByIDs).toHaveBeenCalledWith(
expect.anything(),
expect.arrayContaining([expectedPolicyId]),
expect.anything()
);
expect(fleetGetByIDs).not.toHaveBeenCalledWith(
expect.anything(),
expect.arrayContaining([wrongPolicyId]),
expect.anything()
);
expect(result.monitors[0].privateLocations[0].status).toBe(
PrivateLocationHealthStatusValue.Healthy
);
expect(result.monitors[0].privateLocations[0].packagePolicyId).toBe(expectedPolicyId);
});
});

describe('multiple monitors and locations', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ export class MonitorIntegrationHealthApi {
private readonly spaceId: string
) {}

/**
* Returns the monitor id used to look up Fleet package policies for this monitor.
* Prefers `MONITOR_QUERY_ID` when present; otherwise falls back to the saved object id.
* Project monitors store a journey-based id in `MONITOR_QUERY_ID` (e.g. `journey-project-namespace`),
* which differs from `so.id`.
*
* @param so Saved object for the synthetics monitor.
*/
private static getMonitorPolicyId(so: SavedObject<EncryptedSyntheticsMonitorAttributes>): string {
return so.attributes[ConfigKey.MONITOR_QUERY_ID] || so.id;
}

async getHealth(monitorIds: string[]): Promise<MonitorsHealthResponse> {
const { foundMonitors, errors } = await this.fetchMonitors(monitorIds);

Expand Down Expand Up @@ -86,6 +98,11 @@ export class MonitorIntegrationHealthApi {
const monitors: MonitorHealthStatus[] = foundMonitors.map(({ so }) => {
const locations = so.attributes[ConfigKey.LOCATIONS] ?? [];
const privateLocations = locations.filter((loc) => !loc.isServiceManaged);
const monitorPolicyId = MonitorIntegrationHealthApi.getMonitorPolicyId(so);
const policyConfig = {
origin: so.attributes[ConfigKey.MONITOR_SOURCE_TYPE],
id: monitorPolicyId,
};

// Status checks are ordered by root-cause severity (most fundamental first).
// Only the first matching status is returned per location — downstream issues
Expand All @@ -94,10 +111,7 @@ export class MonitorIntegrationHealthApi {
// Priority: missing_location > missing_agent_policy > missing_package_policy > missing_agents > unhealthy_agent > healthy
const locationStatuses: PrivateLocationHealthStatus[] = privateLocations.map((loc) => {
const existingPrivateLocation = allPrivateLocationsMap.get(loc.id);
const newFormatPolicyId = privateLocationAPI.getPolicyId(
{ origin: so.attributes[ConfigKey.MONITOR_SOURCE_TYPE], id: so.id },
loc.id
);
const newFormatPolicyId = privateLocationAPI.getPolicyId(policyConfig, loc.id);

if (!existingPrivateLocation) {
return MonitorIntegrationHealthApi.buildLocationStatus(
Expand All @@ -120,7 +134,7 @@ export class MonitorIntegrationHealthApi {

const { hasNewFormatPolicyId, hasAnyLegacyPolicyId, legacyPolicyIds } =
privateLocationAPI.getPolicyIdFormatInfo(
{ id: so.id },
{ id: monitorPolicyId },
loc.id,
existingPoliciesArray,
allSpaces
Expand Down Expand Up @@ -218,14 +232,14 @@ export class MonitorIntegrationHealthApi {
for (const { so } of foundMonitors) {
const locations = so.attributes[ConfigKey.LOCATIONS] ?? [];
const privateLocations = locations.filter((loc) => !loc.isServiceManaged);
const monitorPolicyId = MonitorIntegrationHealthApi.getMonitorPolicyId(so);
const policyConfig = {
origin: so.attributes[ConfigKey.MONITOR_SOURCE_TYPE],
id: monitorPolicyId,
};

for (const loc of privateLocations) {
ids.add(
privateLocationAPI.getPolicyId(
{ origin: so.attributes[ConfigKey.MONITOR_SOURCE_TYPE], id: so.id },
loc.id
)
);
ids.add(privateLocationAPI.getPolicyId(policyConfig, loc.id));
for (const legacyId of privateLocationAPI.getLegacyPolicyIdsForAllSpaces(
so.id,
loc.id,
Expand Down
Loading