diff --git a/apps/api/src/app/integrations/dtos/credentials.dto.ts b/apps/api/src/app/integrations/dtos/credentials.dto.ts
index 969525bd7f1..95049a0a749 100644
--- a/apps/api/src/app/integrations/dtos/credentials.dto.ts
+++ b/apps/api/src/app/integrations/dtos/credentials.dto.ts
@@ -171,4 +171,29 @@ export class CredentialsDto implements ICredentials {
@IsString()
@IsOptional()
instanceId?: string;
+
+ @ApiPropertyOptional()
+ @IsString()
+ @IsOptional()
+ alertUid?: string;
+
+ @ApiPropertyOptional()
+ @IsString()
+ @IsOptional()
+ title?: string;
+
+ @ApiPropertyOptional()
+ @IsString()
+ @IsOptional()
+ imageUrl?: string;
+
+ @ApiPropertyOptional()
+ @IsString()
+ @IsOptional()
+ state?: string;
+
+ @ApiPropertyOptional()
+ @IsString()
+ @IsOptional()
+ externalLink?: string;
}
diff --git a/apps/api/src/app/shared/dtos/subscriber-channel.ts b/apps/api/src/app/shared/dtos/subscriber-channel.ts
index cb223671a7b..2c14d71907f 100644
--- a/apps/api/src/app/shared/dtos/subscriber-channel.ts
+++ b/apps/api/src/app/shared/dtos/subscriber-channel.ts
@@ -15,6 +15,31 @@ export class ChannelCredentials {
description: 'Contains an array of the subscriber device tokens for a given provider. Used on Push integrations',
})
deviceTokens?: string[];
+
+ @ApiPropertyOptional({
+ description: 'alert_uid for grafana on-call webhook payload',
+ })
+ alertUid?: string;
+
+ @ApiPropertyOptional({
+ description: 'title to be used with grafana on call webhook',
+ })
+ title?: string;
+
+ @ApiPropertyOptional({
+ description: 'image_url property fo grafana on call webhook',
+ })
+ imageUrl?: string;
+
+ @ApiPropertyOptional({
+ description: 'state property fo grafana on call webhook',
+ })
+ state?: string;
+
+ @ApiPropertyOptional({
+ description: 'link_to_upstream_details property fo grafana on call webhook',
+ })
+ externalUrl?: string;
}
export class SubscriberChannel {
diff --git a/apps/web/public/static/images/providers/dark/grafana-on-call.png b/apps/web/public/static/images/providers/dark/grafana-on-call.png
new file mode 100644
index 00000000000..caccf8b4aaf
Binary files /dev/null and b/apps/web/public/static/images/providers/dark/grafana-on-call.png differ
diff --git a/apps/web/public/static/images/providers/dark/square/grafana-on-call.svg b/apps/web/public/static/images/providers/dark/square/grafana-on-call.svg
new file mode 100644
index 00000000000..09579340d1d
--- /dev/null
+++ b/apps/web/public/static/images/providers/dark/square/grafana-on-call.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/apps/web/public/static/images/providers/light/grafana-on-call.png b/apps/web/public/static/images/providers/light/grafana-on-call.png
new file mode 100644
index 00000000000..caccf8b4aaf
Binary files /dev/null and b/apps/web/public/static/images/providers/light/grafana-on-call.png differ
diff --git a/apps/web/public/static/images/providers/light/square/grafana-on-call.svg b/apps/web/public/static/images/providers/light/square/grafana-on-call.svg
new file mode 100644
index 00000000000..09579340d1d
--- /dev/null
+++ b/apps/web/public/static/images/providers/light/square/grafana-on-call.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts b/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts
index 26e3184cff5..0aa914f9839 100644
--- a/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts
+++ b/apps/web/src/pages/integrations/components/multi-provider/sort-providers.ts
@@ -16,6 +16,7 @@ const providers: Record = {
ChatProviderIdEnum.MsTeams,
ChatProviderIdEnum.Discord,
ChatProviderIdEnum.Mattermost,
+ ChatProviderIdEnum.GrafanaOnCall,
],
[ChannelTypeEnum.EMAIL]: [
EmailProviderIdEnum.SendGrid,
diff --git a/libs/dal/src/repositories/integration/integration.schema.ts b/libs/dal/src/repositories/integration/integration.schema.ts
index 5f42f9f89dc..492956caa67 100644
--- a/libs/dal/src/repositories/integration/integration.schema.ts
+++ b/libs/dal/src/repositories/integration/integration.schema.ts
@@ -53,6 +53,11 @@ const integrationSchema = new Schema(
authenticateByToken: Schema.Types.Boolean,
authenticationTokenKey: Schema.Types.String,
instanceId: Schema.Types.String,
+ alertUid: Schema.Types.String,
+ title: Schema.Types.String,
+ imageUrl: Schema.Types.String,
+ state: Schema.Types.String,
+ externalLink: Schema.Types.String,
},
active: {
type: Schema.Types.Boolean,
diff --git a/libs/shared/src/consts/providers/channels/chat.ts b/libs/shared/src/consts/providers/channels/chat.ts
index 0d504bf8943..ee294985140 100644
--- a/libs/shared/src/consts/providers/channels/chat.ts
+++ b/libs/shared/src/consts/providers/channels/chat.ts
@@ -1,5 +1,5 @@
import { IConfigCredentials, IProviderConfig } from '../provider.interface';
-import { slackConfig } from '../credentials';
+import { grafanaOnCallConfig, slackConfig } from '../credentials';
import { ChatProviderIdEnum } from '../provider.enum';
import { ChannelTypeEnum } from '../../../types';
@@ -21,6 +21,14 @@ export const chatProviders: IProviderConfig[] = [
docReference: 'https://docs.novu.co/channels-and-providers/chat/discord',
logoFileName: { light: 'discord.svg', dark: 'discord.svg' },
},
+ {
+ id: ChatProviderIdEnum.GrafanaOnCall,
+ displayName: 'Grafana On Call Webhook',
+ channel: ChannelTypeEnum.CHAT,
+ credentials: grafanaOnCallConfig,
+ docReference: 'https://grafana.com/docs/oncall/latest/integrations/webhook/',
+ logoFileName: { light: 'grafana-on-call.png', dark: 'grafana-on-call.png' },
+ },
{
id: ChatProviderIdEnum.MsTeams,
displayName: 'MSTeams',
diff --git a/libs/shared/src/consts/providers/credentials/provider-credentials.ts b/libs/shared/src/consts/providers/credentials/provider-credentials.ts
index 1ed1561e628..d3c36b32a00 100644
--- a/libs/shared/src/consts/providers/credentials/provider-credentials.ts
+++ b/libs/shared/src/consts/providers/credentials/provider-credentials.ts
@@ -475,6 +475,45 @@ export const slackConfig: IConfigCredentials[] = [
},
];
+export const grafanaOnCallConfig: IConfigCredentials[] = [
+ {
+ key: CredentialsKeyEnum.alertUid,
+ displayName: 'Alert UID',
+ type: 'string',
+ description: 'a unique alert ID for grouping, maps to alert_uid of grafana webhook body content',
+ required: false,
+ },
+ {
+ key: CredentialsKeyEnum.title,
+ displayName: 'Title.',
+ type: 'string',
+ description: 'title for the alert',
+ required: false,
+ },
+ {
+ key: CredentialsKeyEnum.imageUrl,
+ displayName: 'Image URL',
+ type: 'string',
+ description: 'a URL for an image attached to alert, maps to image_url of grafana webhook body content',
+ required: false,
+ },
+ {
+ key: CredentialsKeyEnum.state,
+ displayName: 'Alert State',
+ type: 'string',
+ description: 'either "ok" or "alerting". Helpful for auto-resolving',
+ required: false,
+ },
+ {
+ key: CredentialsKeyEnum.externalLink,
+ displayName: 'External Link',
+ type: 'string',
+ description:
+ 'link back to your monitoring system, maps to "link_to_upstream_details" of grafana webhook body content',
+ required: false,
+ },
+];
+
export const fcmConfig: IConfigCredentials[] = [
{
key: CredentialsKeyEnum.ServiceAccount,
diff --git a/libs/shared/src/consts/providers/provider.enum.ts b/libs/shared/src/consts/providers/provider.enum.ts
index e260d3c4051..c443248a28b 100644
--- a/libs/shared/src/consts/providers/provider.enum.ts
+++ b/libs/shared/src/consts/providers/provider.enum.ts
@@ -38,6 +38,11 @@ export enum CredentialsKeyEnum {
ApiToken = 'apiToken',
ApiURL = 'apiURL',
AppID = 'appID',
+ alertUid = 'alertUid',
+ title = 'title',
+ imageUrl = 'imageUrl',
+ state = 'state',
+ externalLink = 'externalLink',
}
export enum EmailProviderIdEnum {
@@ -100,6 +105,7 @@ export enum ChatProviderIdEnum {
Mattermost = 'mattermost',
Ryver = 'ryver',
Zulip = 'zulip',
+ GrafanaOnCall = 'grafana-on-call',
}
export enum PushProviderIdEnum {
diff --git a/libs/shared/src/entities/integration/credential.interface.ts b/libs/shared/src/entities/integration/credential.interface.ts
index 903aa4e60fd..43461059350 100644
--- a/libs/shared/src/entities/integration/credential.interface.ts
+++ b/libs/shared/src/entities/integration/credential.interface.ts
@@ -36,4 +36,9 @@ export interface ICredentials {
apiToken?: string;
apiURL?: string;
appID?: string;
+ alertUid?: string;
+ title?: string;
+ imageUrl?: string;
+ state?: string;
+ externalLink?: string;
}
diff --git a/packages/application-generic/package.json b/packages/application-generic/package.json
index 4c0c7585778..e328c167e26 100644
--- a/packages/application-generic/package.json
+++ b/packages/application-generic/package.json
@@ -77,6 +77,7 @@
"@novu/mattermost": "^0.21.0",
"@novu/messagebird": "^0.21.0",
"@novu/ms-teams": "^0.21.0",
+ "@novu/grafana-on-call": "^0.21.0",
"@novu/netcore": "^0.21.0",
"@novu/nodemailer": "^0.21.0",
"@novu/one-signal": "^0.21.0",
diff --git a/packages/application-generic/src/factories/chat/chat.factory.ts b/packages/application-generic/src/factories/chat/chat.factory.ts
index 8ff9beb5ba3..f132f73a2d3 100644
--- a/packages/application-generic/src/factories/chat/chat.factory.ts
+++ b/packages/application-generic/src/factories/chat/chat.factory.ts
@@ -4,6 +4,7 @@ import { IntegrationEntity } from '@novu/dal';
import { DiscordHandler } from './handlers/discord.handler';
import { MSTeamsHandler } from './handlers/msteams.handler';
import { MattermostHandler } from './handlers/mattermost.handler';
+import { GrafanaOnCallHandler } from './handlers/grafana-on-call.handler';
import { RyverHandler } from './handlers/ryver.handler';
import { ZulipHandler } from './handlers/zulip.handler';
@@ -15,6 +16,7 @@ export class ChatFactory implements IChatFactory {
new MattermostHandler(),
new RyverHandler(),
new ZulipHandler(),
+ new GrafanaOnCallHandler(),
];
getHandler(integration: IntegrationEntity) {
diff --git a/packages/application-generic/src/factories/chat/handlers/grafana-on-call.handler.ts b/packages/application-generic/src/factories/chat/handlers/grafana-on-call.handler.ts
new file mode 100644
index 00000000000..571f2f1111e
--- /dev/null
+++ b/packages/application-generic/src/factories/chat/handlers/grafana-on-call.handler.ts
@@ -0,0 +1,15 @@
+import { ICredentials } from '@novu/shared';
+import { ChannelTypeEnum } from '@novu/stateless';
+import { GrafanaOnCallChatProvider } from '@novu/grafana-on-call';
+
+import { BaseChatHandler } from './base.handler';
+
+export class GrafanaOnCallHandler extends BaseChatHandler {
+ constructor() {
+ super('grafana-on-call', ChannelTypeEnum.CHAT);
+ }
+
+ buildProvider(credentials: ICredentials) {
+ this.provider = new GrafanaOnCallChatProvider(credentials);
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3a2edb0fb20..757d838c8ee 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2149,6 +2149,9 @@ importers:
'@novu/generic-sms':
specifier: ^0.21.0
version: link:../../providers/generic-sms
+ '@novu/grafana-on-call':
+ specifier: ^0.21.0
+ version: link:../../providers/grafana-on-call
'@novu/gupshup':
specifier: ^0.21.0
version: link:../../providers/gupshup
@@ -3885,6 +3888,52 @@ importers:
specifier: 4.9.5
version: 4.9.5
+ providers/grafana-on-call:
+ dependencies:
+ '@novu/stateless':
+ specifier: 0.16.3
+ version: 0.16.3
+ axios:
+ specifier: ^1.3.3
+ version: 1.6.0
+ uuid:
+ specifier: ^9.0.0
+ version: 9.0.0
+ devDependencies:
+ '@istanbuljs/nyc-config-typescript':
+ specifier: ~1.0.1
+ version: 1.0.2(nyc@15.1.0)
+ '@types/jest':
+ specifier: ~27.5.2
+ version: 27.5.2
+ cspell:
+ specifier: ~6.19.2
+ version: 6.19.2
+ jest:
+ specifier: ~27.5.1
+ version: 27.5.1(ts-node@10.9.1)
+ npm-run-all:
+ specifier: ^4.1.5
+ version: 4.1.5
+ nyc:
+ specifier: ~15.1.0
+ version: 15.1.0
+ prettier:
+ specifier: ~2.8.0
+ version: 2.8.7
+ rimraf:
+ specifier: ~3.0.2
+ version: 3.0.2
+ ts-jest:
+ specifier: ~27.1.5
+ version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5)
+ ts-node:
+ specifier: ~10.9.1
+ version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5)
+ typescript:
+ specifier: 4.9.5
+ version: 4.9.5
+
providers/gupshup:
dependencies:
'@novu/stateless':
diff --git a/providers/grafana-on-call/.czrc b/providers/grafana-on-call/.czrc
new file mode 100644
index 00000000000..d1bcc209ca1
--- /dev/null
+++ b/providers/grafana-on-call/.czrc
@@ -0,0 +1,3 @@
+{
+ "path": "cz-conventional-changelog"
+}
diff --git a/providers/grafana-on-call/.eslintrc.json b/providers/grafana-on-call/.eslintrc.json
new file mode 100644
index 00000000000..ec40100be69
--- /dev/null
+++ b/providers/grafana-on-call/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../.eslintrc.js"
+}
diff --git a/providers/grafana-on-call/.gitignore b/providers/grafana-on-call/.gitignore
new file mode 100644
index 00000000000..963d5292865
--- /dev/null
+++ b/providers/grafana-on-call/.gitignore
@@ -0,0 +1,9 @@
+.idea/*
+.nyc_output
+build
+node_modules
+test
+src/**.js
+coverage
+*.log
+package-lock.json
diff --git a/providers/grafana-on-call/README.md b/providers/grafana-on-call/README.md
new file mode 100644
index 00000000000..d92f85768c3
--- /dev/null
+++ b/providers/grafana-on-call/README.md
@@ -0,0 +1,11 @@
+# Novu GrafanaOnCall Provider
+
+A GrafanaOnCall chat provider library for [@novu/node](https://github.com/novuhq/novu)
+
+## Usage
+
+```javascript
+ import { GrafanaOnCallChatProvider } from '@novu/grafana-on-call';
+
+ const provider = new GrafanaOnCallChatProvider({ alertUid: "123", externalLink: "link", imageUrl: "url", state: "ok", title: "title" });
+```
diff --git a/providers/grafana-on-call/jest.config.js b/providers/grafana-on-call/jest.config.js
new file mode 100644
index 00000000000..ac8bee2b638
--- /dev/null
+++ b/providers/grafana-on-call/jest.config.js
@@ -0,0 +1,8 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ "moduleNameMapper": {
+ "axios": "axios/dist/node/axios.cjs"
+ }
+};
diff --git a/providers/grafana-on-call/package.json b/providers/grafana-on-call/package.json
new file mode 100644
index 00000000000..b286f0a121c
--- /dev/null
+++ b/providers/grafana-on-call/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "@novu/grafana-on-call",
+ "version": "0.21.0",
+ "description": "A grafana-on-call wrapper for novu",
+ "main": "build/main/index.js",
+ "typings": "build/main/index.d.ts",
+ "module": "build/module/index.js",
+ "private": false,
+ "repository": "https://github.com/novuhq/novu",
+ "license": "MIT",
+ "keywords": [],
+ "scripts": {
+ "prebuild": "rimraf build",
+ "build": "run-p build:*",
+ "build:main": "tsc -p tsconfig.json",
+ "build:module": "tsc -p tsconfig.module.json",
+ "fix": "run-s fix:*",
+ "fix:prettier": "prettier \"src/**/*.ts\" --write",
+ "fix:lint": "eslint src --ext .ts --fix",
+ "test": "run-s test:*",
+ "lint": "eslint src --ext .ts",
+ "test:unit": "jest src",
+ "watch:build": "tsc -p tsconfig.json -w",
+ "watch:test": "jest src --watch",
+ "reset-hard": "git clean -dfx && git reset --hard && yarn",
+ "prepare-release": "run-s reset-hard test"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@novu/stateless": "0.16.3",
+ "axios": "^1.3.3",
+ "uuid": "^9.0.0"
+ },
+ "devDependencies": {
+ "@istanbuljs/nyc-config-typescript": "~1.0.1",
+ "@types/jest": "~27.5.2",
+ "cspell": "~6.19.2",
+ "jest": "~27.5.1",
+ "npm-run-all": "^4.1.5",
+ "nyc": "~15.1.0",
+ "prettier": "~2.8.0",
+ "rimraf": "~3.0.2",
+ "ts-jest": "~27.1.5",
+ "ts-node": "~10.9.1",
+ "typescript": "4.9.5"
+ },
+ "files": [
+ "build/main",
+ "build/module",
+ "!**/*.spec.*",
+ "!**/*.json",
+ "CHANGELOG.md",
+ "LICENSE",
+ "README.md"
+ ],
+ "ava": {
+ "failFast": true,
+ "timeout": "60s",
+ "typescript": {
+ "rewritePaths": {
+ "src/": "build/main/"
+ }
+ },
+ "files": [
+ "!build/module/**"
+ ]
+ },
+ "prettier": {
+ "singleQuote": true
+ },
+ "nyc": {
+ "extends": "@istanbuljs/nyc-config-typescript",
+ "exclude": [
+ "**/*.spec.js"
+ ]
+ }
+}
diff --git a/providers/grafana-on-call/src/index.ts b/providers/grafana-on-call/src/index.ts
new file mode 100644
index 00000000000..d4f06b04ae6
--- /dev/null
+++ b/providers/grafana-on-call/src/index.ts
@@ -0,0 +1 @@
+export * from './lib/grafana-on-call.provider';
diff --git a/providers/grafana-on-call/src/lib/grafana-on-call.provider.spec.ts b/providers/grafana-on-call/src/lib/grafana-on-call.provider.spec.ts
new file mode 100644
index 00000000000..2ed2f235029
--- /dev/null
+++ b/providers/grafana-on-call/src/lib/grafana-on-call.provider.spec.ts
@@ -0,0 +1,42 @@
+import { GrafanaOnCallChatProvider } from './grafana-on-call.provider';
+import axios from 'axios';
+
+test('should trigger grafana-on-call library correctly', async () => {
+ const date = new Date();
+ const fakePost = jest.fn(() => {
+ return { headers: { ['Date']: date } };
+ });
+
+ jest.spyOn(axios, 'create').mockImplementation(() => {
+ return {
+ post: fakePost,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } as any;
+ });
+
+ const provider = new GrafanaOnCallChatProvider({
+ alertUid: '123',
+ externalLink: 'link',
+ imageUrl: 'url',
+ state: 'ok',
+ title: 'title',
+ });
+
+ const testWebhookUrl = 'https://mycompany.webhook.grafana.com/';
+ const testContent = 'warning!!';
+ const res = await provider.sendMessage({
+ webhookUrl: testWebhookUrl,
+ content: testContent,
+ });
+
+ expect(fakePost).toHaveBeenCalled();
+ expect(fakePost).toHaveBeenCalledWith(testWebhookUrl, {
+ alert_uid: '123',
+ link_to_upstream_details: 'link',
+ image_url: 'url',
+ state: 'ok',
+ title: 'title',
+ message: testContent,
+ });
+ expect(res).toEqual({ id: expect.any(String), date: date.toISOString() });
+});
diff --git a/providers/grafana-on-call/src/lib/grafana-on-call.provider.ts b/providers/grafana-on-call/src/lib/grafana-on-call.provider.ts
new file mode 100644
index 00000000000..f21d66efcad
--- /dev/null
+++ b/providers/grafana-on-call/src/lib/grafana-on-call.provider.ts
@@ -0,0 +1,44 @@
+import {
+ ChannelTypeEnum,
+ ISendMessageSuccessResponse,
+ IChatOptions,
+ IChatProvider,
+} from '@novu/stateless';
+import axios from 'axios';
+import { v4 as uuid } from 'uuid';
+
+export class GrafanaOnCallChatProvider implements IChatProvider {
+ id = 'grafana-on-call';
+ channelType = ChannelTypeEnum.CHAT as ChannelTypeEnum.CHAT;
+ private axiosInstance = axios.create();
+ constructor(
+ private config: {
+ alertUid?: string;
+ title?: string;
+ imageUrl?: string;
+ state?: string;
+ externalLink?: string;
+ }
+ ) {}
+
+ async sendMessage(
+ options: IChatOptions
+ ): Promise {
+ const url = new URL(options.webhookUrl);
+ const body = {
+ alert_uid: this.config.alertUid,
+ title: this.config.title,
+ image_url: this.config.imageUrl,
+ state: this.config.state,
+ link_to_upstream_details: this.config.externalLink,
+ message: options.content,
+ };
+ //response is just string "Ok."
+ const { headers } = await this.axiosInstance.post(url.toString(), body);
+
+ return {
+ id: uuid(),
+ date: (headers.Date ? new Date(headers.Date) : new Date()).toISOString(),
+ };
+ }
+}
diff --git a/providers/grafana-on-call/tsconfig.json b/providers/grafana-on-call/tsconfig.json
new file mode 100644
index 00000000000..5b8120fea36
--- /dev/null
+++ b/providers/grafana-on-call/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "build/main",
+ "rootDir": "src",
+ "types": ["node", "jest"]
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules/**"]
+}
diff --git a/providers/grafana-on-call/tsconfig.module.json b/providers/grafana-on-call/tsconfig.module.json
new file mode 100644
index 00000000000..79be3a5c40b
--- /dev/null
+++ b/providers/grafana-on-call/tsconfig.module.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig",
+ "compilerOptions": {
+ "target": "esnext",
+ "outDir": "build/module",
+ "module": "esnext"
+ },
+ "exclude": ["node_modules/**"]
+}