diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 353530638c..bb1c8f4e5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,9 +53,9 @@ repos: rev: v8.25.0 hooks: - id: eslint - entry: bash -c 'cd grafana-plugin && eslint --max-warnings=0 --fix ${@/grafana-plugin\//}' -- + entry: bash -c "cd grafana-plugin && eslint --max-warnings=0 --fix ${@/grafana-plugin\//}" -- types: [file] - files: ^grafana-plugin/src/.*\.(js|jsx|ts|tsx)$ + files: ^grafana-plugin/src/(?:(?!autogenerated).)*\.(js|jsx|ts|tsx)$ additional_dependencies: - eslint@^8.25.0 - eslint-plugin-import@^2.25.4 @@ -81,7 +81,7 @@ repos: rev: v13.13.1 hooks: - id: stylelint - entry: bash -c 'cd grafana-plugin && stylelint --fix ${@/grafana-plugin\//}' -- + entry: bash -c "cd grafana-plugin && stylelint --fix ${@/grafana-plugin\//}" -- types: [file] files: ^grafana-plugin/src/.*\.css$ additional_dependencies: diff --git a/dev/.env.dev.example b/dev/.env.dev.example index 1a37d99b7d..d7d6f7fc37 100644 --- a/dev/.env.dev.example +++ b/dev/.env.dev.example @@ -41,3 +41,5 @@ RABBITMQ_PORT=5672 RABBITMQ_DEFAULT_VHOST="/" REDIS_URI=redis://redis:6379/0 + +DRF_SPECTACULAR_ENABLED=True diff --git a/dev/README.md b/dev/README.md index 77b4c67273..8b193e5cf4 100644 --- a/dev/README.md +++ b/dev/README.md @@ -43,15 +43,23 @@ Related: [How to develop integrations](/engine/config_integrations/README.md) 1. Create local k8s cluster: - ```bash - make cluster/up - ``` + ```bash + make cluster/up + ``` 2. Deploy the project: - ```bash - tilt up - ``` + ```bash + tilt up + ``` + + You can set local environment variables using `dev/helm-local.dev.yml` file, e.g.: + + ```yaml + env: + - name: FEATURE_LABELS_ENABLED_FOR_ALL + value: 'True' + ``` 3. Wait until all resources are green and open (user: oncall, password: oncall) @@ -59,9 +67,9 @@ Related: [How to develop integrations](/engine/config_integrations/README.md) 5. Clean up the project by deleting the local k8s cluster: - ```bash - make cluster/down - ``` + ```bash + make cluster/down + ``` ## Running the project with docker-compose diff --git a/engine/engine/urls.py b/engine/engine/urls.py index ff2d1c6d55..8a59d8c1e2 100644 --- a/engine/engine/urls.py +++ b/engine/engine/urls.py @@ -84,9 +84,9 @@ ] if settings.DRF_SPECTACULAR_ENABLED: - from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + from drf_spectacular.views import SpectacularSwaggerView, SpectacularYAMLAPIView urlpatterns += [ - path("internal/schema/", SpectacularAPIView.as_view(api_version="internal/v1"), name="schema"), + path("internal/schema/", SpectacularYAMLAPIView.as_view(api_version="internal/v1"), name="schema"), path("internal/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), ] diff --git a/engine/settings/base.py b/engine/settings/base.py index 50eee03ddd..2ce633d134 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -290,7 +290,7 @@ class DatabaseTypes: } -DRF_SPECTACULAR_ENABLED = getenv_boolean("DRF_SPECTACULAR_ENABLED", default=False) +DRF_SPECTACULAR_ENABLED = getenv_boolean("DRF_SPECTACULAR_ENABLED", default=True) SPECTACULAR_SETTINGS = { "TITLE": "Grafana OnCall Private API", diff --git a/grafana-plugin/.eslintignore b/grafana-plugin/.eslintignore index 1783d4b6e4..7eeb9c040a 100644 --- a/grafana-plugin/.eslintignore +++ b/grafana-plugin/.eslintignore @@ -1 +1,2 @@ -/src/assets \ No newline at end of file +/src/assets +/src/network/oncall-api/autogenerated-api.types.d.ts \ No newline at end of file diff --git a/grafana-plugin/README.md b/grafana-plugin/README.md new file mode 100644 index 0000000000..dafb08b0b9 --- /dev/null +++ b/grafana-plugin/README.md @@ -0,0 +1,89 @@ +# Grafana OnCall + +Developer-Friendly +Alert Management +with Brilliant Slack Integration + +- Connect monitoring systems +- Collect and analyze data +- On-call rotation +- Automatic escalation +- Never miss alerts with calls and SMS + +## Documentation + +- [On Github](http://github.com/grafana/oncall) +- [Grafana OnCall](https://grafana.com/docs/oncall/latest/) + +## Development + +### Autogenerating TS types based on OpenAPI schema + +| :warning: WARNING | +| :------------------------------------------------------------------------------------------ | +| Transition to this approach is [in progress](https://github.com/grafana/oncall/issues/3338) | + +#### Overview + +In order to automate types creation and prevent API usage pitfalls, OnCall project is using the following approach: + +1. OnCall Engine (backend) exposes OpenAPI schema +2. OnCall Grafana Plugin (frontend) autogenerates TS type definitions based on it +3. OnCall Grafana Plugin (frontend) uses autogenerated types as a single source of truth for + any backend-related interactions (url paths, request bodies, params, response payloads) + +#### Instruction + +1. Whenever API contract changes, run `yarn generate-types` from `grafana-plugin` directory +2. Then you can start consuming types and you can use fully typed http client: + + ```ts + import { ApiSchemas } from 'network/oncall-api/api.types'; + import onCallApi from 'network/oncall-api/http-client'; + + const { + data: { results }, + } = await onCallApi.GET('/alertgroups/'); + const alertGroups: Array = results; + ``` + +3. [Optional] If there is any property that is not yet exposed in OpenAPI schema and you already want to use it, + you can append missing properties to particular schemas by editing + `grafana-plugin/src/network/oncall-api/types-generator/custom-schemas.ts` file: + + ```ts + export type CustomApiSchemas = { + Alert: { + propertyMissingInOpenAPI: string; + }; + AlertGroup: { + anotherPropertyMissingInOpenAPI: number[]; + }; + }; + ``` + + Then add their names to `CUSTOMIZED_SCHEMAS` array in `grafana-plugin/src/network/oncall-api/types-generator/generate-types.ts`: + + ```ts + const CUSTOMIZED_SCHEMAS = ['Alert', 'AlertGroup']; + ``` + + The outcome is that autogenerated schemas will be modified as follows: + + ```ts + import type { CustomApiSchemas } from './types-generator/custom-schemas'; + + export interface components { + schemas: { + Alert: CustomApiSchemas['Alert'] & { + readonly id: string; + ... + }; + AlertGroup: CustomApiSchemas['AlertGroup'] & { + readonly pk: string; + ... + }, + ... + } + } + ``` diff --git a/grafana-plugin/jest.config.js b/grafana-plugin/jest.config.js index e100b0c3b7..735e149694 100644 --- a/grafana-plugin/jest.config.js +++ b/grafana-plugin/jest.config.js @@ -1,15 +1,16 @@ -const esModules = ['@grafana', 'uplot', 'ol', 'd3', 'react-colorful', 'uuid'].join('|'); +const esModules = ['@grafana', 'uplot', 'ol', 'd3', 'react-colorful', 'uuid', 'openapi-fetch'].join('|'); module.exports = { testEnvironment: 'jsdom', moduleDirectories: ['node_modules', 'src'], - moduleFileExtensions: ['ts', 'tsx', 'js'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'd.ts', 'cjs'], transformIgnorePatterns: [`/node_modules/(?!${esModules})`], moduleNameMapper: { 'grafana/app/(.*)': '/src/jest/grafanaMock.ts', + 'openapi-fetch': '/src/jest/openapiFetchMock.ts', 'jest/matchMedia': '/src/jest/matchMedia.ts', '^jest$': '/src/jest', '^.+\\.(css|scss)$': '/src/jest/styleMock.ts', diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 0178bc5792..4298095942 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -15,8 +15,8 @@ "test:e2e-expensive": "yarn playwright test --grep @expensive", "test:e2e:watch": "yarn test:e2e --ui", "test:e2e:gen": "yarn playwright codegen http://localhost:3000", - "cleanup-e2e-results": "rm -rf playwright-report && rm -rf test-results", "e2e-show-report": "yarn playwright show-report", + "generate-types": "cd ./src/network/oncall-api/types-generator && yarn generate", "dev": "grafana-toolkit plugin:dev", "watch": "grafana-toolkit plugin:dev --watch", "sign": "grafana-toolkit plugin:sign", @@ -96,6 +96,7 @@ "lodash-es": "^4.17.21", "mailslurp-client": "^15.14.1", "moment-timezone": "^0.5.35", + "openapi-typescript": "^7.0.0-next.4", "plop": "^2.7.4", "postcss-loader": "^7.0.1", "react": "17.0.2", @@ -105,6 +106,7 @@ "stylelint-prettier": "^2.0.0", "ts-jest": "29.0.3", "ts-loader": "^9.3.1", + "ts-node": "^10.9.1", "typescript": "4.6.4", "webpack-bundle-analyzer": "^4.6.1" }, @@ -127,6 +129,7 @@ "mobx": "5.13.0", "mobx-react": "6.1.1", "object-hash": "^3.0.0", + "openapi-fetch": "^0.8.1", "prettier": "^2.8.2", "qrcode.react": "^3.1.0", "raw-loader": "^4.0.2", diff --git a/grafana-plugin/src/README.md b/grafana-plugin/src/README.md deleted file mode 100644 index 27ee49bc53..0000000000 --- a/grafana-plugin/src/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Grafana OnCall - -Developer-Friendly -Alert Management -with Brilliant Slack Integration - -- Connect monitoring systems -- Collect and analyze data -- On-call rotation -- Automatic escalation -- Never miss alerts with calls and SMS - -## Documentation - -- [On Github](http://github.com/grafana/oncall) -- [Grafana OnCall](https://grafana.com/docs/oncall/latest/) diff --git a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx index 651331c305..61d18c2dfc 100644 --- a/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx +++ b/grafana-plugin/src/containers/IntegrationLabelsForm/IntegrationLabelsForm.tsx @@ -22,7 +22,7 @@ import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEdi import Text from 'components/Text/Text'; import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; -import { LabelKey } from 'models/label/label.types'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; @@ -68,7 +68,7 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => { onOpenIntegraionSettings(id); }; - const getInheritanceChangeHandler = (keyId: LabelKey['id']) => { + const getInheritanceChangeHandler = (keyId: ApiSchemas['LabelKey']['id']) => { return (event: React.ChangeEvent) => { setAlertGroupLabels((alertGroupLabels) => ({ ...alertGroupLabels, diff --git a/grafana-plugin/src/jest/openapiFetchMock.ts b/grafana-plugin/src/jest/openapiFetchMock.ts new file mode 100644 index 0000000000..56bf55ff18 --- /dev/null +++ b/grafana-plugin/src/jest/openapiFetchMock.ts @@ -0,0 +1 @@ +export default () => ({}); diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index e2fecd3c9c..4e7069e36c 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -3,9 +3,9 @@ import qs from 'query-string'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import BaseStore from 'models/base_store'; -import { LabelKey } from 'models/label/label.types'; import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; +import { ApiSchemas } from 'network/oncall-api/api.types'; import { Mixpanel } from 'services/mixpanel'; import { RootStore } from 'state'; import { SelectOption } from 'state/types'; @@ -63,9 +63,6 @@ export class AlertGroupStore extends BaseStore { @observable silencedIncidents: any = {}; - @observable - alertGroupStats: any = {}; - @observable liveUpdatesEnabled = false; @@ -358,11 +355,6 @@ export class AlertGroupStore extends BaseStore { this.silencedIncidents = result; } - @action - async getAlertGroupsStats() { - this.alertGroupStats = await makeRequest('/alertgroups/stats/', {}); - } - @action async doIncidentAction(alertId: Alert['pk'], action: AlertAction, isUndo = false, data?: any) { this.updateAlert(alertId, { loading: true }); @@ -442,7 +434,7 @@ export class AlertGroupStore extends BaseStore { } @action - public async loadValuesForLabelKey(key: LabelKey['id'], search = '') { + public async loadValuesForLabelKey(key: ApiSchemas['LabelKey']['id'], search = '') { if (!key) { return []; } diff --git a/grafana-plugin/src/models/label/label.ts b/grafana-plugin/src/models/label/label.ts index 83387c1ab9..de8277e20a 100644 --- a/grafana-plugin/src/models/label/label.ts +++ b/grafana-plugin/src/models/label/label.ts @@ -1,18 +1,18 @@ -import { action, observable } from 'mobx'; +import { action, observable, runInAction } from 'mobx'; import BaseStore from 'models/base_store'; import { makeRequest } from 'network'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import onCallApi from 'network/oncall-api/http-client'; import { RootStore } from 'state'; import { openNotification } from 'utils'; -import { LabelKey, LabelValue } from './label.types'; - export class LabelStore extends BaseStore { @observable.shallow - public keys: LabelKey[] = []; + public keys: Array = []; @observable.shallow - public values: { [key: string]: LabelValue[] } = {}; + public values: { [key: string]: Array } = {}; constructor(rootStore: RootStore) { super(rootStore); @@ -22,15 +22,17 @@ export class LabelStore extends BaseStore { @action public async loadKeys() { - const result = await makeRequest(`${this.path}keys/`, {}); + const { data } = await onCallApi.GET('/labels/keys/', undefined); - this.keys = result; + runInAction(() => { + this.keys = data; + }); - return result; + return data; } @action - public async loadValuesForKey(key: LabelKey['id'], search = '') { + public async loadValuesForKey(key: ApiSchemas['LabelKey']['id'], search = '') { if (!key) { return []; } @@ -62,7 +64,7 @@ export class LabelStore extends BaseStore { return key; } - public async createValue(keyId: LabelKey['id'], value: string) { + public async createValue(keyId: ApiSchemas['LabelKey']['id'], value: string) { const result = await makeRequest(`${this.path}id/${keyId}/values`, { method: 'POST', data: { name: value }, @@ -76,7 +78,7 @@ export class LabelStore extends BaseStore { } @action - public async updateKey(keyId: LabelKey['id'], name: string) { + public async updateKey(keyId: ApiSchemas['LabelKey']['id'], name: string) { const result = await makeRequest(`${this.path}id/${keyId}`, { method: 'PUT', data: { name }, @@ -90,7 +92,11 @@ export class LabelStore extends BaseStore { } @action - public async updateKeyValue(keyId: LabelKey['id'], valueId: LabelValue['id'], name: string) { + public async updateKeyValue( + keyId: ApiSchemas['LabelKey']['id'], + valueId: ApiSchemas['LabelValue']['id'], + name: string + ) { const result = await makeRequest(`${this.path}id/${keyId}/values/${valueId}`, { method: 'PUT', data: { name }, diff --git a/grafana-plugin/src/models/label/label.types.ts b/grafana-plugin/src/models/label/label.types.ts index a15a0478cc..44cbbcd001 100644 --- a/grafana-plugin/src/models/label/label.types.ts +++ b/grafana-plugin/src/models/label/label.types.ts @@ -1,12 +1,6 @@ -export interface Label { - id: string; - name: string; -} - -export interface LabelKey extends Label {} -export interface LabelValue extends Label {} +import { ApiSchemas } from 'network/oncall-api/api.types'; export interface LabelKeyValue { - key: LabelKey; - value: LabelValue; + key: ApiSchemas['LabelKey']; + value: ApiSchemas['LabelValue']; } diff --git a/grafana-plugin/src/network/index.ts b/grafana-plugin/src/network/index.ts index 8224dbc09c..6fc5fcf708 100644 --- a/grafana-plugin/src/network/index.ts +++ b/grafana-plugin/src/network/index.ts @@ -5,7 +5,6 @@ import qs from 'query-string'; import FaroHelper from 'utils/faro'; -export const API_HOST = `${window.location.protocol}//${window.location.host}/`; export const API_PROXY_PREFIX = 'api/plugin-proxy/grafana-oncall-app'; export const API_PATH_PREFIX = '/api/internal/v1'; diff --git a/grafana-plugin/src/network/oncall-api/api.types.d.ts b/grafana-plugin/src/network/oncall-api/api.types.d.ts new file mode 100644 index 0000000000..7d98964289 --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/api.types.d.ts @@ -0,0 +1,3 @@ +import { components } from './autogenerated-api.types'; + +export type ApiSchemas = components['schemas']; diff --git a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts new file mode 100644 index 0000000000..966546a08a --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts @@ -0,0 +1,1318 @@ +import type { CustomApiSchemas } from './types-generator/custom-schemas'; + +export interface paths { + '/alertgroups/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Fetch a list of alert groups */ + get: operations['alertgroups_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Fetch a single alert group */ + get: operations['alertgroups_retrieve']; + put?: never; + post?: never; + /** @description Delete an alert group */ + delete: operations['alertgroups_destroy']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/acknowledge/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Acknowledge an alert group */ + post: operations['alertgroups_acknowledge_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/attach/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Attach alert group to another alert group */ + post: operations['alertgroups_attach_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/preview_template/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Preview a template for an alert group */ + post: operations['alertgroups_preview_template_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/resolve/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Resolve an alert group */ + post: operations['alertgroups_resolve_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/silence/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Silence an alert group for a specified delay */ + post: operations['alertgroups_silence_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/unacknowledge/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Unacknowledge an alert group */ + post: operations['alertgroups_unacknowledge_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/unattach/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Unattach an alert group that is already attached to another alert group */ + post: operations['alertgroups_unattach_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/unpage_user/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Remove a user that was directly paged for the alert group */ + post: operations['alertgroups_unpage_user_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/unresolve/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Unresolve an alert group */ + post: operations['alertgroups_unresolve_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/{id}/unsilence/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Unsilence a silenced alert group */ + post: operations['alertgroups_unsilence_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/bulk_action/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Perform a bulk action on a list of alert groups */ + post: operations['alertgroups_bulk_action_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/bulk_action_options/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Retrieve a list of valid bulk action options */ + get: operations['alertgroups_bulk_action_options_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/filters/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Retrieve a list of valid filter options that can be used to filter alert groups */ + get: operations['alertgroups_filters_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/labels/id/{key_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Key with the list of values. IDs and names are interchangeable (see get_keys() for more details). */ + get: operations['alertgroups_labels_id_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/labels/keys/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List of alert group label keys. + * IDs are the same as names to keep the response format consistent with LabelsViewSet.get_keys(). */ + get: operations['alertgroups_labels_keys_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/silence_options/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Retrieve a list of valid silence options */ + get: operations['alertgroups_silence_options_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/alertgroups/stats/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Return number of alert groups capped at 100001 */ + get: operations['alertgroups_stats_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/features/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Return whitelist of enabled features. + * It is needed to disable features for On-prem installations. */ + get: operations['features_retrieve']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/labels/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Create a new label key with values(Optional) */ + post: operations['labels_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/labels/id/{key_id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Key with the list of values */ + get: operations['labels_id_retrieve']; + /** @description Rename the key */ + put: operations['labels_id_update']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/labels/id/{key_id}/values/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Add a new value to the key */ + post: operations['labels_id_values_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/labels/id/{key_id}/values/{value_id}/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Value name */ + get: operations['labels_id_values_retrieve']; + /** @description Rename the value */ + put: operations['labels_id_values_update']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/labels/keys/': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description List of labels keys */ + get: operations['labels_keys_list']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Alert: { + readonly id: string; + /** Format: uri */ + link_to_upstream_details?: string | null; + readonly render_for_web: string; + /** Format: date-time */ + readonly created_at: string; + }; + AlertGroup: { + readonly pk: string; + readonly alerts_count: number; + inside_organization_number?: number; + alert_receive_channel: components['schemas']['FastAlertReceiveChannel']; + resolved?: boolean; + resolved_by?: components['schemas']['ResolvedByEnum']; + resolved_by_user?: components['schemas']['FastUser']; + /** Format: date-time */ + resolved_at?: string | null; + /** Format: date-time */ + acknowledged_at?: string | null; + acknowledged?: boolean; + acknowledged_on_source?: boolean; + acknowledged_by_user?: components['schemas']['FastUser']; + silenced?: boolean; + silenced_by_user?: components['schemas']['FastUser']; + /** Format: date-time */ + silenced_at?: string | null; + /** Format: date-time */ + silenced_until?: string | null; + /** Format: date-time */ + readonly started_at: string; + readonly related_users: components['schemas']['UserShort'][]; + readonly render_for_web: components['schemas']['render_for_web']; + dependent_alert_groups: components['schemas']['ShortAlertGroup'][]; + root_alert_group: components['schemas']['ShortAlertGroup']; + readonly status: string; + /** @description Generate a link for AlertGroup to declare Grafana Incident by click */ + readonly declare_incident_link: string; + team: string | null; + grafana_incident_id?: string | null; + readonly labels: components['schemas']['AlertGroupLabel'][]; + readonly alerts: components['schemas']['Alert'][]; + readonly render_after_resolve_report_json: { + time: string; + action: string; + /** @enum {string} */ + realm: 'user_notification' | 'alert_group' | 'resolution_note'; + type: number; + created_at: string; + author: { + username: string; + pk: string; + avatar: string; + avatar_full: string; + }; + }[]; + readonly slack_permalink: string | null; + readonly permalinks: { + slack: string | null; + telegram: string | null; + web: string; + }; + /** Format: date-time */ + readonly last_alert_at: string; + readonly paged_users: { + id: number; + username: string; + name: string; + pk: string; + avatar: string; + avatar_full: string; + important: boolean; + }[]; + }; + AlertGroupLabel: { + key: components['schemas']['Key']; + value: components['schemas']['Value']; + }; + AlertGroupList: { + readonly pk: string; + readonly alerts_count: number; + inside_organization_number?: number; + alert_receive_channel: components['schemas']['FastAlertReceiveChannel']; + resolved?: boolean; + resolved_by?: components['schemas']['ResolvedByEnum']; + resolved_by_user?: components['schemas']['FastUser']; + /** Format: date-time */ + resolved_at?: string | null; + /** Format: date-time */ + acknowledged_at?: string | null; + acknowledged?: boolean; + acknowledged_on_source?: boolean; + acknowledged_by_user?: components['schemas']['FastUser']; + silenced?: boolean; + silenced_by_user?: components['schemas']['FastUser']; + /** Format: date-time */ + silenced_at?: string | null; + /** Format: date-time */ + silenced_until?: string | null; + /** Format: date-time */ + readonly started_at: string; + readonly related_users: components['schemas']['UserShort'][]; + readonly render_for_web: components['schemas']['render_for_web']; + dependent_alert_groups: components['schemas']['ShortAlertGroup'][]; + root_alert_group: components['schemas']['ShortAlertGroup']; + readonly status: string; + /** @description Generate a link for AlertGroup to declare Grafana Incident by click */ + readonly declare_incident_link: string; + team: string | null; + grafana_incident_id?: string | null; + readonly labels: components['schemas']['AlertGroupLabel'][]; + }; + AlertGroupStats: { + count: number; + }; + FastAlertReceiveChannel: { + readonly id: string; + readonly integration: string; + verbal_name?: string | null; + readonly deleted: boolean; + }; + FastUser: { + pk: string; + readonly username: string; + }; + Key: { + id: string; + name: string; + }; + LabelCreate: { + key: components['schemas']['LabelRepr']; + values: components['schemas']['LabelRepr'][]; + }; + LabelKey: { + id: string; + name: string; + }; + LabelKeyValues: { + key: components['schemas']['LabelKey']; + values: components['schemas']['LabelValue'][]; + }; + LabelRepr: { + name: string; + }; + LabelValue: { + id: string; + name: string; + }; + PaginatedAlertGroupListList: { + next?: string | null; + previous?: string | null; + results?: components['schemas']['AlertGroupList'][]; + }; + Paginatedsilence_optionsList: { + next?: string | null; + previous?: string | null; + results?: components['schemas']['silence_options'][]; + }; + /** + * @description * `0` - source + * * `1` - user + * * `2` - not yet + * * `3` - last escalation step + * * `4` - archived + * * `5` - wiped + * * `6` - stop maintenance + * * `7` - not yet, autoresolve disabled + * @enum {integer} + */ + ResolvedByEnum: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; + ShortAlertGroup: { + readonly pk: string; + readonly render_for_web: components['schemas']['render_for_web']; + alert_receive_channel: components['schemas']['FastAlertReceiveChannel']; + readonly inside_organization_number: number; + }; + UserShort: { + username: string; + pk: string; + avatar: string; + avatar_full: string; + }; + Value: { + id: string; + name: string; + }; + render_for_web: { + title: string; + message: string; + image_url: string; + source_link: string; + }; + silence_options: { + value: string; + display_name: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + alertgroups_list: { + parameters: { + query?: { + /** @description The pagination cursor value. */ + cursor?: string; + /** @description Number of results to return per page. */ + perpage?: number; + /** @description A search term. */ + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PaginatedAlertGroupListList']; + }; + }; + }; + }; + alertgroups_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_destroy: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + alertgroups_acknowledge_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_attach_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_preview_template_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_resolve_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_silence_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_unacknowledge_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_unattach_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_unpage_user_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_unresolve_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_unsilence_create: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_bulk_action_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AlertGroup']; + 'application/x-www-form-urlencoded': components['schemas']['AlertGroup']; + 'multipart/form-data': components['schemas']['AlertGroup']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_bulk_action_options_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_filters_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroup']; + }; + }; + }; + }; + alertgroups_labels_id_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKeyValues']; + }; + }; + }; + }; + alertgroups_labels_keys_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKey'][]; + }; + }; + }; + }; + alertgroups_silence_options_list: { + parameters: { + query?: { + /** @description The pagination cursor value. */ + cursor?: string; + /** @description Number of results to return per page. */ + perpage?: number; + /** @description A search term. */ + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Paginatedsilence_optionsList']; + }; + }; + }; + }; + alertgroups_stats_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AlertGroupStats']; + }; + }; + }; + }; + features_retrieve: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: unknown; + }; + }; + }; + }; + }; + labels_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LabelCreate'][]; + 'application/x-www-form-urlencoded': components['schemas']['LabelCreate'][]; + 'multipart/form-data': components['schemas']['LabelCreate'][]; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKeyValues']; + }; + }; + }; + }; + labels_id_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKeyValues']; + }; + }; + }; + }; + labels_id_update: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LabelRepr']; + 'application/x-www-form-urlencoded': components['schemas']['LabelRepr']; + 'multipart/form-data': components['schemas']['LabelRepr']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKeyValues']; + }; + }; + }; + }; + labels_id_values_create: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LabelRepr']; + 'application/x-www-form-urlencoded': components['schemas']['LabelRepr']; + 'multipart/form-data': components['schemas']['LabelRepr']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKeyValues']; + }; + }; + }; + }; + labels_id_values_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + value_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelValue']; + }; + }; + }; + }; + labels_id_values_update: { + parameters: { + query?: never; + header?: never; + path: { + key_id: string; + value_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LabelRepr']; + 'application/x-www-form-urlencoded': components['schemas']['LabelRepr']; + 'multipart/form-data': components['schemas']['LabelRepr']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKeyValues']; + }; + }; + }; + }; + labels_keys_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LabelKey'][]; + }; + }; + }; + }; +} diff --git a/grafana-plugin/src/network/oncall-api/http-client.test.ts b/grafana-plugin/src/network/oncall-api/http-client.test.ts new file mode 100644 index 0000000000..7c4b0c4be1 --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/http-client.test.ts @@ -0,0 +1,126 @@ +import { SpanStatusCode } from '@opentelemetry/api'; + +import FaroHelper from 'utils/faro'; + +import { customFetch } from './http-client'; + +jest.mock('utils/faro', () => ({ + __esModule: true, + default: { + faro: { + api: { + getOTEL: jest.fn(() => undefined), + pushEvent: jest.fn(), + pushError: jest.fn(), + }, + }, + }, +})); +jest.mock('openapi-fetch', () => ({ + __esModule: true, + default: () => {}, +})); + +const fetchMock = jest.fn().mockResolvedValue(true); + +const REQUEST_CONFIG = { + headers: { + 'Content-Type': 'application/json', + }, +}; +const URL = 'https://someurl.com'; +const SUCCESSFUL_RESPONSE_MOCK = { ok: true }; +const ERROR_MOCK = 'error'; + +describe('customFetch', () => { + beforeAll(() => { + Object.defineProperty(global, 'fetch', { + writable: true, + value: fetchMock, + }); + }); + afterEach(jest.clearAllMocks); + + describe('if there is no otel', () => { + describe('if response is successful', () => { + it('should push event to faro and return response', async () => { + fetchMock.mockResolvedValue(SUCCESSFUL_RESPONSE_MOCK); + const response = await customFetch(URL, REQUEST_CONFIG); + expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request completed', { url: URL }); + expect(response).toEqual(SUCCESSFUL_RESPONSE_MOCK); + }); + }); + + describe('if response is not successful', () => { + it('should push event and error to faro', async () => { + (FaroHelper.faro.api.getOTEL as unknown as jest.Mock).mockReturnValueOnce(undefined); + fetchMock.mockRejectedValueOnce(ERROR_MOCK); + await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(Error(ERROR_MOCK)); + expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL }); + expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK); + }); + }); + }); + + describe('if there is otel', () => { + const spanEndMock = jest.fn(); + const setStatusMock = jest.fn(); + const setAttributeMock = jest.fn(); + const spanStartMock = jest.fn(() => ({ + setAttribute: setAttributeMock, + end: spanEndMock, + setStatus: setStatusMock, + })); + const otel = { + trace: { + getTracer: () => ({ + startSpan: spanStartMock, + }), + getActiveSpan: jest.fn(), + setSpan: jest.fn(), + }, + context: { + active: jest.fn(), + with: (_ctx, fn) => { + fn(); + }, + }, + }; + (FaroHelper.faro.api.getOTEL as unknown as jest.Mock).mockReturnValue(otel); + + it(`starts span if it doesn't exist`, async () => { + otel.trace.getActiveSpan.mockReturnValueOnce(undefined); + await customFetch(URL, REQUEST_CONFIG); + expect(spanStartMock).toHaveBeenCalledTimes(1); + }); + + it(`adds 'X-Idempotency-Key' header`, async () => { + await customFetch(URL, REQUEST_CONFIG); + expect(fetchMock).toHaveBeenCalledWith(expect.any(String), { + headers: { ...REQUEST_CONFIG.headers, 'X-Idempotency-Key': expect.any(String) }, + }); + }); + + describe('if response is successful', () => { + it('should push event to faro, end span and return response', async () => { + fetchMock.mockResolvedValue(SUCCESSFUL_RESPONSE_MOCK); + const response = await customFetch(URL, REQUEST_CONFIG); + expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request completed', { url: URL }); + expect(spanEndMock).toHaveBeenCalledTimes(1); + expect(response).toEqual(SUCCESSFUL_RESPONSE_MOCK); + }); + }); + + describe('if response is not successful', () => { + it('should reject Promise, push event to faro, set span status to error and end span', async () => { + fetchMock.mockRejectedValueOnce(ERROR_MOCK); + await expect(customFetch(URL, REQUEST_CONFIG)).rejects.toEqual(ERROR_MOCK); + expect(FaroHelper.faro.api.pushEvent).toHaveBeenCalledWith('Request failed', { url: URL }); + expect(FaroHelper.faro.api.pushError).toHaveBeenCalledWith(ERROR_MOCK); + expect(setStatusMock).toHaveBeenCalledTimes(1); + expect(setStatusMock).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR }); + expect(spanEndMock).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/grafana-plugin/src/network/oncall-api/http-client.ts b/grafana-plugin/src/network/oncall-api/http-client.ts new file mode 100644 index 0000000000..5dd37c363a --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/http-client.ts @@ -0,0 +1,78 @@ +import { SpanStatusCode } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import createClient from 'openapi-fetch'; +import qs from 'query-string'; + +import FaroHelper from 'utils/faro'; + +import { paths } from './autogenerated-api.types'; + +export const API_PROXY_PREFIX = 'api/plugin-proxy/grafana-oncall-app'; +export const API_PATH_PREFIX = '/api/internal/v1'; + +export const customFetch = async (url: string, requestConfig: Parameters[1] = {}): Promise => { + const { faro } = FaroHelper; + const otel = faro?.api?.getOTEL(); + + if (faro && otel) { + const tracer = otel.trace.getTracer('default'); + let span = otel.trace.getActiveSpan(); + + if (!span) { + span = tracer.startSpan('http-request'); + span.setAttribute('page_url', document.URL.split('//')[1]); + span.setAttribute(SemanticAttributes.HTTP_URL, url); + span.setAttribute(SemanticAttributes.HTTP_METHOD, requestConfig.method); + } + + return new Promise((resolve, reject) => { + otel.context.with(otel.trace.setSpan(otel.context.active(), span), async () => { + faro.api.pushEvent('Sending request', { url }); + + try { + const response = await fetch(url, { + ...requestConfig, + headers: { + ...requestConfig.headers, + /** + * In short, this header will tell the Grafana plugin proxy, a Go service which use Go's HTTP Transport, + * to retry POST requests (and other non-idempotent requests). This doesn't necessarily make these requests + * idempotent, but it will make them retry-able from Go's (read: net/http) perspective. + * + * https://stackoverflow.com/questions/42847294/how-to-catch-http-server-closed-idle-connection-error/62292758#62292758 + * https://raintank-corp.slack.com/archives/C01C4K8DETW/p1692280544382739?thread_ts=1692279329.797149&cid=C01C4K8DETW + */ 'X-Idempotency-Key': `${Date.now()}-${Math.random()}`, + }, + }); + faro.api.pushEvent('Request completed', { url }); + span.end(); + resolve(response); + } catch (error) { + faro.api.pushEvent('Request failed', { url }); + faro.api.pushError(error); + span.setStatus({ code: SpanStatusCode.ERROR }); + span.end(); + reject(error); + } + }); + }); + } else { + try { + const response = await fetch(url, requestConfig); + faro?.api.pushEvent('Request completed', { url }); + return response; + } catch (error) { + faro?.api.pushEvent('Request failed', { url }); + faro?.api.pushError(error); + throw new Error(error); + } + } +}; + +const onCallApi = createClient({ + baseUrl: `${API_PROXY_PREFIX}${API_PATH_PREFIX}`, + querySerializer: (params: unknown) => qs.stringify(params, { arrayFormat: 'none' }), + fetch: customFetch, +}); + +export default onCallApi; diff --git a/grafana-plugin/src/network/oncall-api/types-generator/custom-schemas.ts b/grafana-plugin/src/network/oncall-api/types-generator/custom-schemas.ts new file mode 100644 index 0000000000..3c011d251b --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/types-generator/custom-schemas.ts @@ -0,0 +1,3 @@ +// Custom properties not exposed by OpenAPI schema should be defined here + +export type CustomApiSchemas = {}; diff --git a/grafana-plugin/src/network/oncall-api/types-generator/generate-types.ts b/grafana-plugin/src/network/oncall-api/types-generator/generate-types.ts new file mode 100644 index 0000000000..da1d7b4d27 --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/types-generator/generate-types.ts @@ -0,0 +1,34 @@ +import openapiTS, { astToString } from 'openapi-typescript'; + +import fs from 'fs'; + +// They need to match with any custom schema added to CustomApiSchemas keys +const CUSTOMIZED_SCHEMAS = []; + +const addCustomSchemasToAutogeneratedOutput = ( + originalOutput: string, + customizedSchemas: string[] = CUSTOMIZED_SCHEMAS +) => { + const REGEX = new RegExp(`\\s(${customizedSchemas.join('|')})\\s*:`, 'g'); + + let newOutput = ` + import type { CustomApiSchemas } from './types-generator/custom-schemas'; + + ${originalOutput} + `; + newOutput = newOutput.replace(REGEX, (match) => + match.concat(` CustomApiSchemas["${match.split(':')[0].trim()}"] & `) + ); + + return newOutput; +}; + +(async () => { + const ast = await openapiTS(new URL('http://localhost:8080/internal/schema/')); + const output = astToString(ast); + + fs.writeFileSync( + '../autogenerated-api.types.d.ts', + addCustomSchemasToAutogeneratedOutput(output, CUSTOMIZED_SCHEMAS) + ); +})(); diff --git a/grafana-plugin/src/network/oncall-api/types-generator/package.json b/grafana-plugin/src/network/oncall-api/types-generator/package.json new file mode 100644 index 0000000000..a31b8f4393 --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/types-generator/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "scripts": { + "generate": "npx ts-node-esm -P ./types-generator.tsconfig.json ./generate-types.ts && prettier --write ../autogenerated-api.types.d.ts" + } +} diff --git a/grafana-plugin/src/network/oncall-api/types-generator/types-generator.tsconfig.json b/grafana-plugin/src/network/oncall-api/types-generator/types-generator.tsconfig.json new file mode 100644 index 0000000000..cc9765a65b --- /dev/null +++ b/grafana-plugin/src/network/oncall-api/types-generator/types-generator.tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true + } +} diff --git a/grafana-plugin/tsconfig.json b/grafana-plugin/tsconfig.json index 71c71bb482..676efe28b4 100644 --- a/grafana-plugin/tsconfig.json +++ b/grafana-plugin/tsconfig.json @@ -10,6 +10,7 @@ "noUnusedParameters": true, "strict": false, "resolveJsonModule": true, - "noImplicitAny": false + "noImplicitAny": false, + "skipLibCheck": true } } diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 3f55fb20f3..75c6b94f5f 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -1310,6 +1310,13 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@csstools/postcss-color-function@^1.0.3": version "1.1.1" resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz#2bd36ab34f82d0497cfacdc9b18d34b5e6f64b6b" @@ -2726,6 +2733,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -2744,6 +2756,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -3833,6 +3853,32 @@ resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.21.0.tgz#1af41fdf7dfbdbd33bbc1210617c43ed0d4ef20c" integrity sha512-wJA2cUF8dP4LkuNUt9Vh2kkfiQb2NLnV2pPXxVnKJZ7d4x2/7VPccN+LYPnH8m0X3+rt50cxWuPKQmjxSsCFOg== +"@redocly/ajv@^8.11.0": + version "8.11.0" + resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.11.0.tgz#2fad322888dc0113af026e08fceb3e71aae495ae" + integrity sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +"@redocly/openapi-core@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.4.1.tgz#0620a5e204159626a1d99b88f758e23ef0cb5740" + integrity sha512-oAhnG8MKocM9LuP++NGFxdniNKWSLA7hzHPQoOK92LIP/DdvXx8pEeZ68UTNxIXhKonoUcO6s86I3L0zj143zg== + dependencies: + "@redocly/ajv" "^8.11.0" + "@types/node" "^14.11.8" + colorette "^1.2.0" + js-levenshtein "^1.1.6" + js-yaml "^4.1.0" + lodash.isequal "^4.5.0" + minimatch "^5.0.1" + node-fetch "^2.6.1" + pluralize "^8.0.0" + yaml-ast-parser "0.0.43" + "@sentry/browser@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.7.tgz#a40b6b72d911b5f1ed70ed3b4e7d4d4e625c0b5f" @@ -3979,6 +4025,26 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/aria-query@^4.2.0": version "4.2.2" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" @@ -4360,6 +4426,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.26.tgz#63d204d136c9916fb4dcd1b50f9740fe86884e47" integrity sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ== +"@types/node@^14.11.8": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -5064,6 +5135,11 @@ acorn-walk@^8.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== +acorn-walk@^8.1.1: + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== + acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" @@ -5074,6 +5150,11 @@ acorn@^8.0.4, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +acorn@^8.4.1: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + add-dom-event-listener@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310" @@ -5145,7 +5226,7 @@ ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@^4.1.1: +ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== @@ -5602,6 +5683,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -6034,6 +6122,11 @@ colord@^2.9.1: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== +colorette@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + colorette@^2.0.16: version "2.0.19" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" @@ -10146,6 +10239,11 @@ js-cookie@^2.2.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + js-sdsl@^4.1.4: version "4.1.5" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a" @@ -10487,6 +10585,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isstring@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" @@ -10874,6 +10977,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -11085,6 +11195,13 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -11407,6 +11524,29 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-fetch@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/openapi-fetch/-/openapi-fetch-0.8.1.tgz#a2bda1f72a8311e92cc789d1c8fec7b2d8ca28b6" + integrity sha512-xmzMaBCydPTMd0TKy4P2DYx/JOe9yjXtPIky1n1GV7nJJdZ3IZgSHvAWVbe06WsPD8EreR7E97IAiskPr6sa2g== + dependencies: + openapi-typescript-helpers "^0.0.4" + +openapi-typescript-helpers@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.4.tgz#ffe7c4868f094fcc8502dbdcddc6c32ce8011aee" + integrity sha512-Q0MTapapFAG993+dx8lNw33X6P/6EbFr31yNymJHq56fNc6dODyRm8tWyRnGxuC74lyl1iCRMV6nQCGQsfVNKg== + +openapi-typescript@^7.0.0-next.4: + version "7.0.0-next.4" + resolved "https://registry.yarnpkg.com/openapi-typescript/-/openapi-typescript-7.0.0-next.4.tgz#62117ed4c5dd3b09e344ec10838a576bc60acad9" + integrity sha512-mm6cpWnvYZgaTTfMnA/z+NkbmjzWNX5FeoW7+ZBAfI9tBhb8JlNf/HZfXueV1Eq6ZOWKQ1doyjTxJNgmsF6mNQ== + dependencies: + "@redocly/openapi-core" "^1.4.1" + ansi-colors "^4.1.3" + supports-color "^9.4.0" + typescript "^5.3.2" + yargs-parser "^21.1.1" + opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -11789,6 +11929,11 @@ plop@^2.7.4: ora "^3.4.0" v8flags "^2.0.10" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + pngjs@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" @@ -14712,6 +14857,11 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +supports-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + supports-hyperlinks@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" @@ -15052,6 +15202,25 @@ ts-loader@^9.3.1: micromatch "^4.0.0" semver "^7.3.4" +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + ts-node@^9.1.0: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" @@ -15177,6 +15346,11 @@ typescript@4.8.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +typescript@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" + integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== + ua-parser-js@^1.0.32: version "1.0.33" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4" @@ -15396,6 +15570,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -15746,6 +15925,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml-ast-parser@0.0.43: + version "0.0.43" + resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" + integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== + yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" @@ -15756,7 +15940,7 @@ yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.0.1: +yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==