Skip to content

Commit

Permalink
Chore: introduce Fishery as object factory and yarn dev:msw (#1023)
Browse files Browse the repository at this point in the history
* feat: define types for CheckAlert

* feat: add AlertsPerCheck and AlertCard components in Check form

* feat: add schema for new alert fields

* feat: add useCheckAlert hook and datasource methods to fetch and update alerts

* feat: fetch alerts when creating/updating checks and format payload for api

* feat: fetch alerts when editing a check, and format response for form

* chore: mock API response (only for testing purposes)

* fix: filter alerts according to the supported checktypes

* fix: make current tests pass

- adjust schema validation to make it optional
- add test handlers for new alerts requests

* fix: remove mocked response for testing with dev API

* fix: set correct alert id to payload

- Also, invalidate cache for fetching fresh check alerts

* fix: remove alert mocks as the API is available in dev

* test: add tests for creating a check with alerts per check

* fix: remove percentiles dropdown

- Instead of grouping by alert type, I'm displaying all alerts as separate ones
- Grouping by type didn't allow to specify threshold for different percentiles

* refactor: introduce new CheckAlert types: Base, Draft and Published

* test: add mocks back temporarily

- since the alerts API is no longer in dev, I'm adding the mocks back to be able to test it
- This should be improved by #850

* refactor: avoid passing unneeded props to AlertsPerCheck

- Also, adding loading and error states

* fix: remove emtpy response from listAlertsforCheck

* refactor: move predefined alerts to external file

- Add specific predefined alerts according to the check type in a single constants file
- Display threshold units

* fix: tests

* fix: rebase conflicts

* refactor: display new alerts in list format

* chore: remove unneeded AlertCard component

* fix: tests

* feat: add AlertsPerCheck and AlertCard components in Check form

* refactor: avoid passing unneeded props to AlertsPerCheck

- Also, adding loading and error states

* chore: introduce fishery and faker

* feat: define Fishery factories for creating objects

- Addresses all check types, probes and alerts

* chore: create fixtures by consuming the object factories

* fix: filter by probe on data transferred panels for browser checks

* feat: introduce yarn dev:msw to start the app mocking the api using msw handlers

* fix: move worker setup under useEffect

* fix: conflicts after rebase

* fix: humanize alert labels

* fix: tests

* feat: add feature flag

- To enable the feature, set sm-alerts-per-check=true

* fix: address review comments

- move worker initialization to module.ts
- use sequence for Fishery ids
- simplify probes and checks mocks creation

* fix: remove unneeded properties from check fixtures

* fix: remove type assertion

* fix: review comments

* fix: add comment on query key

* fix: change layout

* feat: add default values for check alerts

* fix: address review comments

* fix: setting submitting form state when alerts request fails

* fix: allow to delete input values

* fix: target definition for http checks

* fix: set default value when generating form values

* fix: tests

* chore: remove checkAlert id concept (#1041)

The API is removing the id property for check alerts as they can be identified just by the name

* fix: remove check alert id from factory

* fix: center header and tweak description text

* chore: remove mocked data as API is available in dev

* fix: move serviceWorker setup to external file and load async
  • Loading branch information
VikaCep authored Jan 31, 2025
1 parent ef8c3f3 commit 0253f91
Show file tree
Hide file tree
Showing 15 changed files with 507 additions and 412 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"build": "webpack -c webpack.config.ts --env production",
"dev": "webpack -w -c webpack.config.ts --env development",
"dev:msw": "REACT_APP_MSW=true webpack -w -c webpack.config.ts --env development",
"e2e": "yarn exec cypress install && yarn exec grafana-e2e run",
"e2e:update": "yarn exec cypress install && yarn exec grafana-e2e run --update-screenshots",
"lint": "eslint --cache --ignore-path ./.gitignore --ext .js,.jsx,.ts,.tsx .",
Expand All @@ -27,6 +28,7 @@
"@babel/core": "^7.21.4",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@faker-js/faker": "^9.3.0",
"@grafana/eslint-config": "^6.0.0",
"@grafana/tsconfig": "^2.0.0",
"@release-it/conventional-changelog": "^3.0.0",
Expand Down Expand Up @@ -65,6 +67,7 @@
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-testing-library": "^4.10.1",
"eslint-webpack-plugin": "^4.0.1",
"fishery": "^2.2.2",
"fork-ts-checker-webpack-plugin": "^8.0.0",
"glob": "^10.2.7",
"husky": "^6.0.0",
Expand Down Expand Up @@ -128,8 +131,8 @@
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "^6.27.0",
"rxjs": "7.8.1",
"usehooks-ts": "^3.1.0",
"tslib": "2.5.3",
"usehooks-ts": "^3.1.0",
"valid-url": "^1.0.9",
"yaml": "^2.2.2",
"zod": "3.23.6"
Expand Down
2 changes: 2 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const App = (props: AppRootProps<ProvisioningJsonData>) => {
const { meta } = props;

useEffect(() => {


return () => {
// we have a dependency on alerts to display our alerting correctly
// so we are invalidating the alerts list on the assumption the user might change their alerting options when they leave SM
Expand Down
4 changes: 4 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ if (window.location.hostname !== 'localhost') {
});
}

if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_MSW) {
await import('./startServerWorker');
}

export const plugin = new AppPlugin<ProvisioningJsonData>().setRootPage(App).addConfigPage({
title: 'Config',
icon: 'cog',
Expand Down
2 changes: 1 addition & 1 deletion src/page/ConfigPageLayout/tabs/TerraformTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('TerraformTab', () => {
const { getAllByTestId } = await renderTerraformTab();
const preformatted = getAllByTestId(DataTestIds.PREFORMATTED);
expect(preformatted[2]).toHaveTextContent(
'terraform import grafana_synthetic_monitoring_probe.tacos 1:<PROBE_ACCESS_TOKEN>'
`terraform import grafana_synthetic_monitoring_probe.${PRIVATE_PROBE.name} 1:<PROBE_ACCESS_TOKEN>`
);
});
});
Expand Down
3 changes: 2 additions & 1 deletion src/routing/InitialisedRouter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { BASIC_HTTP_CHECK } from 'test/fixtures/checks';
import { SM_DATASOURCE } from 'test/fixtures/datasources';
import { type CustomRenderOptions, render } from 'test/render';

Expand Down Expand Up @@ -70,7 +71,7 @@ describe('Routes to pages correctly', () => {

test('Redirect old scenes URLS to new scenes URL', async () => {
renderInitialisedRouting({
path: `${PLUGIN_URL_PATH}${ROUTES.Scene}?var-job=Job name for http&var-instance=https://http.com`,
path: `${PLUGIN_URL_PATH}${ROUTES.Scene}?var-job=${BASIC_HTTP_CHECK.job}&var-instance=${BASIC_HTTP_CHECK.target}`,
});
const sceneText = await screen.findByText('Dashboard page');
expect(sceneText).toBeInTheDocument();
Expand Down
4 changes: 4 additions & 0 deletions src/startServerWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { setupWorker } from 'msw';
import { handlers } from 'test/handlers';

setupWorker(...handlers).start();
262 changes: 262 additions & 0 deletions src/test/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { faker } from '@faker-js/faker';
import { Factory } from 'fishery';

import {
AlertSensitivity,
Check,
CheckAlertPublished,
CheckAlertType,
CheckType,
DnsProtocol,
DnsRecordType,
DnsResponseCodes,
HTTPCompressionAlgo,
HttpMethod,
HttpVersion,
IpVersion,
Probe,
} from 'types';

const baseCheckModel = ({ sequence }: { sequence: number }) => ({
id: sequence,
job: faker.lorem.word(),
target: faker.internet.domainName(),
frequency: faker.number.int({ min: 1, max: 60 * 1000 }),
timeout: faker.number.int({ min: 30, max: 60 * 1000 }),
enabled: true,
alertSensitivity: faker.helpers.arrayElement(Object.values(AlertSensitivity)),
basicMetricsOnly: faker.datatype.boolean(),
labels: [{ name: faker.animal.petName(), value: faker.color.human() }],
probes: [],
modified: Math.floor(faker.date.recent().getTime() / 1000),
created: Math.floor(faker.date.past().getTime() / 1000),
});

const baseProbeModel = ({ sequence }: { sequence: number }) => ({
id: sequence,
name: faker.lorem.word(),
public: faker.datatype.boolean(),
latitude: faker.location.latitude(),
longitude: faker.location.longitude(),
region: faker.helpers.arrayElement(['EMEA', 'AMER', 'APAC']),
labels: [{ name: faker.animal.petName(), value: faker.color.human() }],
online: true,
onlineChange: Math.floor(faker.date.past().getTime() / 1000),
version: faker.system.semver(),
deprecated: false,
modified: Math.floor(faker.date.recent().getTime() / 1000),
created: Math.floor(faker.date.past().getTime() / 1000),
capabilities: {
disableScriptedChecks: false,
disableBrowserChecks: false,
},
});

const tlsConfig = () => ({
caCert: faker.helpers.maybe(() => faker.string.uuid()),
clientCert: faker.helpers.maybe(() => faker.string.uuid()),
clientKey: faker.helpers.maybe(() => faker.string.uuid()),
insecureSkipVerify: faker.helpers.maybe(() => faker.datatype.boolean()),
serverName: faker.helpers.maybe(() => faker.lorem.word()),
});

type CheckTransientParams = {
type: CheckType;
};

export const db = {
check: Factory.define<Check, CheckTransientParams>(({ transientParams, sequence }) => {
const { type } = transientParams;

switch (type) {
case CheckType.HTTP: {
return {
...baseCheckModel({ sequence }),
target: faker.internet.url(),
settings: {
http: {
method: faker.helpers.arrayElement(Object.values(HttpMethod)),
headers: faker.helpers.maybe(() => []),
body: faker.helpers.maybe(() => faker.lorem.text()),
ipVersion: faker.helpers.arrayElement(Object.values(IpVersion)),
noFollowRedirects: faker.datatype.boolean(),
tlsConfig: faker.helpers.maybe(() => tlsConfig()),
compression: faker.helpers.maybe(() => faker.helpers.arrayElement(Object.values(HTTPCompressionAlgo))),
proxyURL: faker.helpers.maybe(() => faker.internet.url()),
proxyConnectHeaders: faker.helpers.maybe(() => []),
bearerToken: faker.helpers.maybe(() => faker.lorem.word()),
basicAuth: faker.helpers.maybe(() => ({
username: faker.internet.username(),
password: faker.internet.password(),
})),
failIfSSL: faker.helpers.maybe(() => faker.datatype.boolean()),
failIfNotSSL: faker.helpers.maybe(() => faker.datatype.boolean()),
validStatusCodes: faker.helpers.maybe(() => []),
validHTTPVersions: faker.helpers.maybe(() => faker.helpers.arrayElements(Object.values(HttpVersion))),
failIfBodyMatchesRegexp: faker.helpers.maybe(() => []),
failIfBodyNotMatchesRegexp: faker.helpers.maybe(() => []),
failIfHeaderMatchesRegexp: faker.helpers.maybe(() => []),
failIfHeaderNotMatchesRegexp: faker.helpers.maybe(() => []),
cacheBustingQueryParamName: faker.helpers.maybe(() => faker.lorem.word()),
},
},
};
}

case CheckType.PING: {
return {
...baseCheckModel({ sequence }),
settings: {
ping: {
ipVersion: faker.helpers.arrayElement(Object.values(IpVersion)),
dontFragment: faker.datatype.boolean(),
},
},
};
}

case CheckType.DNS: {
return {
...baseCheckModel({ sequence }),
settings: {
dns: {
recordType: faker.helpers.arrayElement(Object.values(DnsRecordType)),
server: faker.internet.domainName(),
ipVersion: faker.helpers.arrayElement(Object.values(IpVersion)),
protocol: faker.helpers.arrayElement(Object.values(DnsProtocol)),
port: faker.number.int({ min: 1, max: 65535 }),
validRCodes: faker.helpers.maybe(() => faker.helpers.arrayElements(Object.values(DnsResponseCodes))),
validateAnswerRRS: faker.helpers.maybe(() => ({
failIfMatchesRegexp: [],
failIfNotMatchesRegexp: [],
})),
validateAuthorityRRS: faker.helpers.maybe(() => ({
failIfMatchesRegexp: [],
failIfNotMatchesRegexp: [],
})),
validateAdditionalRRS: faker.helpers.maybe(() => ({
failIfMatchesRegexp: [],
failIfNotMatchesRegexp: [],
})),
},
},
};
}

case CheckType.TCP: {
return {
...baseCheckModel({ sequence }),
settings: {
tcp: {
ipVersion: faker.helpers.arrayElement(Object.values(IpVersion)),
tls: faker.helpers.maybe(() => faker.datatype.boolean()),
tlsConfig: faker.helpers.maybe(() => tlsConfig()),
queryResponse: faker.helpers.maybe(() => []),
},
},
};
}

case CheckType.Traceroute: {
return {
...baseCheckModel({ sequence }),
settings: {
traceroute: {
maxHops: faker.number.int({ min: 1, max: 10 }),
maxUnknownHops: faker.number.int({ min: 1, max: 10 }),
ptrLookup: faker.datatype.boolean(),
hopTimeout: faker.number.int({ min: 1, max: 120 }),
},
},
};
}

case CheckType.MULTI_HTTP: {
return {
...baseCheckModel({ sequence }),
settings: {
multihttp: {
entries: [
{
variables: faker.helpers.maybe(() => []),
request: {
method: faker.helpers.arrayElement(Object.values(HttpMethod)),
url: faker.internet.url(),
body: faker.helpers.maybe(() => ({
contentType: faker.helpers.arrayElement([
'application/json',
'application/xml',
'application/x-www-form-urlencoded',
]),
contentEncoding: faker.helpers.maybe(() => faker.helpers.arrayElement(['gzip', 'deflate'])),
payload: faker.lorem.sentence(),
})),
headers: faker.helpers.maybe(() => []),
queryFields: faker.helpers.maybe(() => []),
postData: faker.helpers.maybe(() => ({
mimeType: 'text/plain',
text: '',
})),
},
checks: faker.helpers.maybe(() => []),
},
],
},
},
};
}

case CheckType.Scripted: {
return {
...baseCheckModel({ sequence }),
settings: {
scripted: {
script: faker.lorem.text(),
},
},
};
}

case CheckType.Browser: {
return {
...baseCheckModel({ sequence }),
settings: {
browser: {
script: faker.lorem.text(),
},
},
};
}

case CheckType.GRPC: {
return {
...baseCheckModel({ sequence }),
settings: {
grpc: {
ipVersion: faker.helpers.arrayElement(Object.values(IpVersion)),
service: faker.helpers.maybe(() => faker.lorem.word()),
tls: faker.helpers.maybe(() => faker.datatype.boolean()),
tlsConfig: faker.helpers.maybe(() => tlsConfig()),
},
},
};
}

default: {
throw new Error(`Unsupported check type: ${type}`);
}
}
}),

probe: Factory.define<Probe>(({ sequence }) => ({
...baseProbeModel({ sequence }),
public: false,
})),

alert: Factory.define<CheckAlertPublished>(() => ({
name: faker.helpers.arrayElement(Object.values(CheckAlertType)),
threshold: faker.number.int({ min: 50, max: 500 }),
created: Math.floor(faker.date.past().getTime() / 1000),
modified: Math.floor(faker.date.recent().getTime() / 1000),
})),
};
32 changes: 7 additions & 25 deletions src/test/fixtures/checkAlerts.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import { db } from 'test/db';

import { CheckAlertType } from 'types';
import { CheckAlertsResponse } from 'datasource/responses.types';

export const BASIC_CHECK_ALERTS: CheckAlertsResponse = {
alerts: [
{
name: CheckAlertType['HTTPRequestDurationTooHighP90'],
threshold: 350,
created: 1724854935,
modified: 1724854935,
},
{
name: CheckAlertType['HTTPRequestDurationTooHighP95'],
threshold: 100,
created: 1724854935,
modified: 1724854935,
},
{
name: CheckAlertType['HTTPTargetCertificateCloseToExpiring'],
threshold: 90,
created: 1724854935,
modified: 1724854935,
},
{
name: CheckAlertType['ProbeFailedExecutionsTooHigh'],
threshold: 20,
created: 1724854935,
modified: 1724854935,
},
],
CheckAlertType.HTTPRequestDurationTooHighP90,
CheckAlertType.HTTPRequestDurationTooHighP95,
CheckAlertType.HTTPTargetCertificateCloseToExpiring,
CheckAlertType.ProbeFailedExecutionsTooHigh,
].map((name) => db.alert.build({ name })),
};
Loading

0 comments on commit 0253f91

Please sign in to comment.