diff --git a/src/lib/addons/__snapshots__/datadog.test.ts.snap b/src/lib/addons/__snapshots__/datadog.test.ts.snap index c24851de7f49..239587319710 100644 --- a/src/lib/addons/__snapshots__/datadog.test.ts.snap +++ b/src/lib/addons/__snapshots__/datadog.test.ts.snap @@ -1,18 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project ** \\n %%% ","title":"Unleash notification update"}"`; +exports[`Datadog integration Should call datadog webhook for archived toggle 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project ** \\n %%% ","title":"Unleash notification update"}"`; -exports[`Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)* \\n %%% ","title":"Unleash notification update"}"`; +exports[`Datadog integration Should call datadog webhook for archived toggle with project info 1`] = `"{"text":"%%% \\n *some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)* \\n %%% ","title":"Unleash notification update"}"`; -exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n *some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project ** \\n %%% ","title":"Unleash notification update"}"`; +exports[`Datadog integration Should call datadog webhook 1`] = `"{"text":"%%% \\n *some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project ** \\n %%% ","title":"Unleash notification update"}"`; -exports[`Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; +exports[`Datadog integration Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; -exports[`Should call datadog webhook with JSON when template set 1`] = `"{"text":"{\\n \\"event\\": \\"feature-created\\",\\n \\"createdBy\\": \\"some@user.com\\"\\n}","title":"Unleash notification update"}"`; +exports[`Datadog integration Should call datadog webhook with JSON when template set 1`] = `"{"text":"{\\n \\"event\\": \\"feature-created\\",\\n \\"createdBy\\": \\"some@user.com\\"\\n}","title":"Unleash notification update"}"`; -exports[`Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; +exports[`Datadog integration Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update"}"`; -exports[`Should include customHeaders in headers when calling service 2`] = ` +exports[`Datadog integration Should include customHeaders in headers when calling service 2`] = ` { "Content-Type": "application/json", "DD-API-KEY": "fakeKey", @@ -20,9 +20,9 @@ exports[`Should include customHeaders in headers when calling service 2`] = ` } `; -exports[`Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update","source_type_name":"my-custom-source-type"}"`; +exports[`Datadog integration Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n *some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)* \\n %%% ","title":"Unleash notification update","source_type_name":"my-custom-source-type"}"`; -exports[`Should not include source_type_name when included in the config 2`] = ` +exports[`Datadog integration Should not include source_type_name when included in the config 2`] = ` { "Content-Type": "application/json", "DD-API-KEY": "fakeKey", diff --git a/src/lib/addons/datadog.test.ts b/src/lib/addons/datadog.test.ts index 2a8c9045b80d..76c06a3c84e9 100644 --- a/src/lib/addons/datadog.test.ts +++ b/src/lib/addons/datadog.test.ts @@ -9,10 +9,15 @@ import type { Logger } from '../logger'; import DatadogAddon from './datadog'; import noLogger from '../../test/fixtures/no-logger'; -import type { IAddonConfig, IFlagResolver } from '../types'; +import { + serializeDates, + type IAddonConfig, + type IFlagResolver, +} from '../types'; import type { IntegrationEventsService } from '../services'; let fetchRetryCalls: any[] = []; +const registerEventMock = jest.fn(); const INTEGRATION_ID = 1337; const ARGS: IAddonConfig = { @@ -40,207 +45,266 @@ jest.mock( retries, backoff, }); - return Promise.resolve({ status: 200 }); + return Promise.resolve({ ok: true, status: 200 }); } - async registerEvent(_) { - return Promise.resolve(); + async registerEvent(event) { + return registerEventMock(event); } }, ); -test('Should call datadog webhook', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 1, - createdAt: new Date(), - type: FEATURE_CREATED, - createdBy: 'some@user.com', - createdByUserId: -1337, - featureName: 'some-toggle', - data: { - name: 'some-toggle', - enabled: false, - strategies: [{ name: 'default' }], - }, - }; +describe('Datadog integration', () => { + beforeEach(() => { + registerEventMock.mockClear(); + }); - const parameters = { - url: 'http://api.datadoghq.com/api/v1/events', - apiKey: 'fakeKey', - }; + test('Should call datadog webhook', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 1, + createdAt: new Date(), + type: FEATURE_CREATED, + createdBy: 'some@user.com', + createdByUserId: -1337, + featureName: 'some-toggle', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }; - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', + }; -test('Should call datadog webhook for archived toggle', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - type: FEATURE_ARCHIVED, - createdBy: 'some@user.com', - createdByUserId: -1337, - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); - const parameters = { - url: 'http://api.datadoghq.com/api/v1/events', - apiKey: 'fakeKey', - }; + test('Should call datadog webhook for archived toggle', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ARCHIVED, + createdBy: 'some@user.com', + createdByUserId: -1337, + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', + }; -test('Should call datadog webhook for archived toggle with project info', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - type: FEATURE_ARCHIVED, - createdBy: 'some@user.com', - featureName: 'some-toggle', - createdByUserId: -1337, - project: 'some-project', - data: { - name: 'some-toggle', - }, - }; + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); - const parameters = { - url: 'http://api.datadoghq.com/api/v1/events', - apiKey: 'fakeKey', - }; + test('Should call datadog webhook for archived toggle with project info', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ARCHIVED, + createdBy: 'some@user.com', + featureName: 'some-toggle', + createdByUserId: -1337, + project: 'some-project', + data: { + name: 'some-toggle', + }, + }; - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', + }; -test('Should call datadog webhook for toggled environment', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - type: FEATURE_ENVIRONMENT_DISABLED, - createdBy: 'some@user.com', - createdByUserId: -1337, - environment: 'development', - project: 'default', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.slack.com', - apiKey: 'fakeKey', - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls).toHaveLength(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); -test('Should include customHeaders in headers when calling service', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - type: FEATURE_ENVIRONMENT_DISABLED, - createdBy: 'some@user.com', - environment: 'development', - createdByUserId: -1337, - project: 'default', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.slack.com', - apiKey: 'fakeKey', - customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, - }; - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls).toHaveLength(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); - expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); -}); + test('Should call datadog webhook for toggled environment', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + createdByUserId: -1337, + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; -test('Should not include source_type_name when included in the config', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - type: FEATURE_ENVIRONMENT_DISABLED, - createdBy: 'some@user.com', - createdByUserId: -1337, - environment: 'development', - project: 'default', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.slack.com', - apiKey: 'fakeKey', - sourceTypeName: 'my-custom-source-type', - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls).toHaveLength(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatch( - /"source_type_name":"my-custom-source-type"/, - ); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); - expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); -}); + const parameters = { + url: 'http://hooks.slack.com', + apiKey: 'fakeKey', + }; -test('Should call datadog webhook with JSON when template set', async () => { - const addon = new DatadogAddon(ARGS); - const event: IEvent = { - id: 1, - createdAt: new Date(), - type: FEATURE_CREATED, - createdBy: 'some@user.com', - createdByUserId: -1337, - featureName: 'some-toggle', - data: { - name: 'some-toggle', - enabled: false, - strategies: [{ name: 'default' }], - }, - }; - - const parameters = { - url: 'http://api.datadoghq.com/api/v1/events', - apiKey: 'fakeKey', - bodyTemplate: - '{\n "event": "{{event.type}}",\n "createdBy": "{{event.createdBy}}"\n}', - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); + + test('Should include customHeaders in headers when calling service', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + createdByUserId: -1337, + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + apiKey: 'fakeKey', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); + }); + + test('Should not include source_type_name when included in the config', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + createdByUserId: -1337, + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + apiKey: 'fakeKey', + sourceTypeName: 'my-custom-source-type', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch( + /"source_type_name":"my-custom-source-type"/, + ); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); + }); + + test('Should call datadog webhook with JSON when template set', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 1, + createdAt: new Date(), + type: FEATURE_CREATED, + createdBy: 'some@user.com', + createdByUserId: -1337, + featureName: 'some-toggle', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }; + + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', + bodyTemplate: + '{\n "event": "{{event.type}}",\n "createdBy": "{{event.createdBy}}"\n}', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); + + test('Should call registerEvent', async () => { + const addon = new DatadogAddon(ARGS); + const event: IEvent = { + id: 1, + createdAt: new Date(), + type: FEATURE_CREATED, + createdBy: 'some@user.com', + createdByUserId: -1337, + featureName: 'some-toggle', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + tags: [ + { + type: 'test', + value: '1', + }, + { + type: 'test', + value: '2', + }, + ], + }; + + const parameters = { + url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', + bodyTemplate: + '{\n "event": "{{event.type}}",\n "createdBy": "{{event.createdBy}}"\n}', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + + expect(registerEventMock).toHaveBeenCalledTimes(1); + expect(registerEventMock).toHaveBeenCalledWith({ + integrationId: INTEGRATION_ID, + state: 'success', + stateDetails: + 'Datadog Events API request was successful with status code: 200.', + event: serializeDates(event), + details: { + url: parameters.url, + body: { + text: `{\n "event": "${event.type}",\n "createdBy": "${event.createdBy}"\n}`, + title: 'Unleash notification update', + tags: ['test:1', 'test:2'], + }, + }, + }); + }); }); diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index 2d39614266fd..1af3ab600bae 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -2,13 +2,14 @@ import Addon from './addon'; import definition from './datadog-definition'; import Mustache from 'mustache'; -import type { IAddonConfig } from '../types/model'; +import { type IAddonConfig, serializeDates } from '../types'; import { type FeatureEventFormatter, FeatureEventFormatterMd, LinkStyle, } from './feature-event-formatter-md'; import type { IEvent } from '../types/events'; +import type { IntegrationEventState } from '../features/integration-events/integration-events-store'; interface IDatadogParameters { url: string; @@ -41,6 +42,9 @@ export default class DatadogAddon extends Addon { parameters: IDatadogParameters, integrationId: number, ): Promise { + let state: IntegrationEventState = 'success'; + const stateDetails: string[] = []; + const { url = 'https://api.datadoghq.com/api/v1/events', apiKey, @@ -75,9 +79,11 @@ export default class DatadogAddon extends Addon { try { extraHeaders = JSON.parse(customHeaders); } catch (e) { - this.logger.warn( - `Could not parse the json in the customHeaders parameter. [${customHeaders}]`, - ); + state = 'successWithErrors'; + const badHeadersMessage = + 'Could not parse the JSON in the customHeaders parameter.'; + stateDetails.push(badHeadersMessage); + this.logger.warn(badHeadersMessage); } } const requestOpts = { @@ -90,8 +96,29 @@ export default class DatadogAddon extends Addon { body: JSON.stringify(body), }; const res = await this.fetchRetry(url, requestOpts); - this.logger.info( - `Handled event ${event.type}. Status codes=${res.status}`, - ); + + this.logger.info(`Handled event "${event.type}".`); + + if (res.ok) { + const successMessage = `Datadog Events API request was successful with status code: ${res.status}.`; + stateDetails.push(successMessage); + this.logger.info(successMessage); + } else { + state = 'failed'; + const failedMessage = `Datadog Events API request failed with status code: ${res.status}.`; + stateDetails.push(failedMessage); + this.logger.warn(failedMessage); + } + + this.registerEvent({ + integrationId, + state, + stateDetails: stateDetails.join('\n'), + event: serializeDates(event), + details: { + url, + body, + }, + }); } }