mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Datadog integration (#820)
fixes: #815 Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
This commit is contained in:
		
							parent
							
								
									0bed8f605e
								
							
						
					
					
						commit
						d61c7242d8
					
				
							
								
								
									
										38
									
								
								docs/addons/datadog.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								docs/addons/datadog.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
---
 | 
			
		||||
id: datadog
 | 
			
		||||
title: Datadog
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
> This feature was introduced in \_Unleash v4.0.x.
 | 
			
		||||
 | 
			
		||||
The Datadog addon allows Unleash to post Updates when a feature toggle is updated. To set up this addon, you need to set up a webhook connector for your channel. You can follow [Submitting events to Datadog](https://docs.datadoghq.com/api/latest/events/#post-an-event) on how to do that.
 | 
			
		||||
 | 
			
		||||
The Datadog addon will perform a single retry if the HTTP POST against the Datadog Webhook URL fails (either a 50x or network error). Duplicate events may happen, and you should never assume events always comes in order.
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
#### Events
 | 
			
		||||
 | 
			
		||||
You can choose to trigger updates for the following events (we might add more event types in the future):
 | 
			
		||||
 | 
			
		||||
- feature-created
 | 
			
		||||
- feature-updated
 | 
			
		||||
- feature-archived
 | 
			
		||||
- feature-revived
 | 
			
		||||
- feature-stale-on
 | 
			
		||||
- feature-stale-off
 | 
			
		||||
 | 
			
		||||
#### Parameters
 | 
			
		||||
 | 
			
		||||
Unleash Datadog addon takes the following parameters.
 | 
			
		||||
 | 
			
		||||
- **Datadog Events URL** - This property is optional. The default url is https://api.datadoghq.com/api/v1/events. Needs to be changed if you are not not on the US1 [Datadog site](https://docs.datadoghq.com/getting_started/site/). Possible alternatives:
 | 
			
		||||
  - EU: https://app.datadoghq.eu/api/v1/events
 | 
			
		||||
  - US1: https://app.datadoghq.com/api/v1/events
 | 
			
		||||
  - US3: https://us3.datadoghq.com/api/v1/events
 | 
			
		||||
  - US1-FED: https://app.ddog-gov.com/api/v1/events
 | 
			
		||||
- **DD API KEY** - This is a required property.
 | 
			
		||||
 | 
			
		||||
#### Tags
 | 
			
		||||
 | 
			
		||||
Datadog's incoming webhooks are app specific. You will be able to create multiple addons to support messaging on different apps.
 | 
			
		||||
@ -30,4 +30,4 @@ Unleash Microsoft Teams addon takes the following parameters.
 | 
			
		||||
 | 
			
		||||
#### Tags
 | 
			
		||||
 | 
			
		||||
Microsoft teams's income webhooks are channel specific. You will be able to create multiple addons to support messaging on multiple channels.
 | 
			
		||||
Microsoft teams's incoming webhooks are channel specific. You will be able to create multiple addons to support messaging on multiple channels.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								snapshots/src/lib/addons/datadog.test.js.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								snapshots/src/lib/addons/datadog.test.js.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
# Snapshot report for `src/lib/addons/datadog.test.js`
 | 
			
		||||
 | 
			
		||||
The actual snapshot is saved in `datadog.test.js.snap`.
 | 
			
		||||
 | 
			
		||||
Generated by [AVA](https://avajs.dev).
 | 
			
		||||
 | 
			
		||||
## Should call datadog webhook
 | 
			
		||||
 | 
			
		||||
> Snapshot 1
 | 
			
		||||
 | 
			
		||||
    '{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/#/features/strategies/some-toggle)\\n**Enabled**: no | **Type**: undefined | **Project**: undefined\\n**Activation strategies**: ```- name: default\\n``` \\n %%% ","title":"Unleash notification update"}'
 | 
			
		||||
 | 
			
		||||
## Should call datadog webhook for archived toggle
 | 
			
		||||
 | 
			
		||||
> Snapshot 1
 | 
			
		||||
 | 
			
		||||
    '{"text":"%%% \\n The feature toggle *[some-toggle](http://some-url.com/#/archive/strategies/some-toggle)* was *archived* by some@user.com. \\n %%% ","title":"Unleash notification update"}'
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								snapshots/src/lib/addons/datadog.test.js.snap
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								snapshots/src/lib/addons/datadog.test.js.snap
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										52
									
								
								src/lib/addons/datadog-definition.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/lib/addons/datadog-definition.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
    FEATURE_CREATED,
 | 
			
		||||
    FEATURE_UPDATED,
 | 
			
		||||
    FEATURE_ARCHIVED,
 | 
			
		||||
    FEATURE_REVIVED,
 | 
			
		||||
    FEATURE_STALE_ON,
 | 
			
		||||
    FEATURE_STALE_OFF,
 | 
			
		||||
} = require('../types/events');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    name: 'datadog',
 | 
			
		||||
    displayName: 'Datadog',
 | 
			
		||||
    description: 'Allows Unleash to post updates to Datadog.',
 | 
			
		||||
    documentationUrl: 'https://docs.getunleash.io/docs/addons/datadog',
 | 
			
		||||
    parameters: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'url',
 | 
			
		||||
            displayName: 'Datadog Events URL',
 | 
			
		||||
            description:
 | 
			
		||||
                'Default url: https://api.datadoghq.com/api/v1/events. Needs to be changed if your not using the US1 site.',
 | 
			
		||||
            type: 'url',
 | 
			
		||||
            required: false,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'apiKey',
 | 
			
		||||
            displayName: 'DD API KEY',
 | 
			
		||||
            placeholder: 'j96c23b0f12a6b3434a8d710110bd862',
 | 
			
		||||
            description: 'Api key from datadog',
 | 
			
		||||
            type: 'text',
 | 
			
		||||
            required: true,
 | 
			
		||||
            sensitive: true,
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    events: [
 | 
			
		||||
        FEATURE_CREATED,
 | 
			
		||||
        FEATURE_UPDATED,
 | 
			
		||||
        FEATURE_ARCHIVED,
 | 
			
		||||
        FEATURE_REVIVED,
 | 
			
		||||
        FEATURE_STALE_ON,
 | 
			
		||||
        FEATURE_STALE_OFF,
 | 
			
		||||
    ],
 | 
			
		||||
    tagTypes: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'datadog',
 | 
			
		||||
            description:
 | 
			
		||||
                'All Datadog tags added to a specific feature are sent to datadog event stream.',
 | 
			
		||||
            icon: 'D',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										114
									
								
								src/lib/addons/datadog.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/lib/addons/datadog.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,114 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const YAML = require('js-yaml');
 | 
			
		||||
const Addon = require('./addon');
 | 
			
		||||
 | 
			
		||||
const {
 | 
			
		||||
    FEATURE_CREATED,
 | 
			
		||||
    FEATURE_UPDATED,
 | 
			
		||||
    FEATURE_ARCHIVED,
 | 
			
		||||
    FEATURE_REVIVED,
 | 
			
		||||
    FEATURE_STALE_ON,
 | 
			
		||||
    FEATURE_STALE_OFF,
 | 
			
		||||
} = require('../types/events');
 | 
			
		||||
 | 
			
		||||
const definition = require('./datadog-definition');
 | 
			
		||||
 | 
			
		||||
class DatadogAddon extends Addon {
 | 
			
		||||
    constructor(args) {
 | 
			
		||||
        super(definition, args);
 | 
			
		||||
        this.unleashUrl = args.unleashUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleEvent(event, parameters) {
 | 
			
		||||
        const {
 | 
			
		||||
            url = 'https://api.datadoghq.com/api/v1/events',
 | 
			
		||||
            apiKey,
 | 
			
		||||
        } = parameters;
 | 
			
		||||
        let text;
 | 
			
		||||
 | 
			
		||||
        if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) {
 | 
			
		||||
            text = this.generateArchivedText(event);
 | 
			
		||||
        } else if ([FEATURE_STALE_ON, FEATURE_STALE_OFF].includes(event.type)) {
 | 
			
		||||
            text = this.generateStaleText(event);
 | 
			
		||||
        } else {
 | 
			
		||||
            text = this.generateText(event);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { tags: eventTags } = event;
 | 
			
		||||
        const tags =
 | 
			
		||||
            eventTags && eventTags.map(tag => `${tag.value}:${tag.type}`);
 | 
			
		||||
        const body = {
 | 
			
		||||
            text: `%%% \n ${text} \n %%% `,
 | 
			
		||||
            title: 'Unleash notification update',
 | 
			
		||||
            tags,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const requestOpts = {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            headers: {
 | 
			
		||||
                'Content-Type': 'application/json',
 | 
			
		||||
                'DD-API-KEY': apiKey,
 | 
			
		||||
            },
 | 
			
		||||
            body: JSON.stringify(body),
 | 
			
		||||
        };
 | 
			
		||||
        const res = await this.fetchRetry(url, requestOpts);
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
            `Handled event ${event.type}. Status codes=${res.status}`,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    featureLink(event) {
 | 
			
		||||
        const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features';
 | 
			
		||||
        return `${this.unleashUrl}/#/${path}/strategies/${event.data.name}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    generateStaleText(event) {
 | 
			
		||||
        const { createdBy, data, type } = event;
 | 
			
		||||
        const isStale = type === FEATURE_STALE_ON;
 | 
			
		||||
        const feature = `[${data.name}](${this.featureLink(event)})`;
 | 
			
		||||
 | 
			
		||||
        if (isStale) {
 | 
			
		||||
            return `The feature toggle *${feature}* is now *ready to be removed* from the code. 
 | 
			
		||||
This was changed by ${createdBy}.`;
 | 
			
		||||
        }
 | 
			
		||||
        return `The feature toggle *${feature}* was *unmarked as stale* by ${createdBy}.`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    generateArchivedText(event) {
 | 
			
		||||
        const { createdBy, data, type } = event;
 | 
			
		||||
        const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived';
 | 
			
		||||
        const feature = `[${data.name}](${this.featureLink(event)})`;
 | 
			
		||||
        return `The feature toggle *${feature}* was *${action}* by ${createdBy}.`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    generateText(event) {
 | 
			
		||||
        const { createdBy, data, type } = event;
 | 
			
		||||
        const action = this.getAction(type);
 | 
			
		||||
        const feature = `[${data.name}](${this.featureLink(event)})`;
 | 
			
		||||
        const enabled = `**Enabled**: ${data.enabled ? 'yes' : 'no'}`;
 | 
			
		||||
        const stale = data.stale ? '("stale")' : '';
 | 
			
		||||
        const typeStr = `**Type**: ${data.type}`;
 | 
			
		||||
        const project = `**Project**: ${data.project}`;
 | 
			
		||||
        const strategies = `**Activation strategies**: \`\`\`${YAML.safeDump(
 | 
			
		||||
            data.strategies,
 | 
			
		||||
            { skipInvalid: true },
 | 
			
		||||
        )}\`\`\``;
 | 
			
		||||
        return `${createdBy} ${action} feature toggle ${feature}
 | 
			
		||||
${enabled}${stale} | ${typeStr} | ${project}
 | 
			
		||||
${strategies}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAction(type) {
 | 
			
		||||
        switch (type) {
 | 
			
		||||
            case FEATURE_CREATED:
 | 
			
		||||
                return 'created';
 | 
			
		||||
            case FEATURE_UPDATED:
 | 
			
		||||
                return 'updated';
 | 
			
		||||
            default:
 | 
			
		||||
                return type;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = DatadogAddon;
 | 
			
		||||
							
								
								
									
										67
									
								
								src/lib/addons/datadog.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/lib/addons/datadog.test.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
const test = require('ava');
 | 
			
		||||
const proxyquire = require('proxyquire').noCallThru();
 | 
			
		||||
const { FEATURE_CREATED, FEATURE_ARCHIVED } = require('../types/events');
 | 
			
		||||
 | 
			
		||||
const DatadogAddon = proxyquire.load('./datadog', {
 | 
			
		||||
    './addon': class Addon {
 | 
			
		||||
        constructor(definition, { getLogger }) {
 | 
			
		||||
            this.logger = getLogger('addon/test');
 | 
			
		||||
            this.fetchRetryCalls = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        async fetchRetry(url, options, retries, backoff) {
 | 
			
		||||
            this.fetchRetryCalls.push({ url, options, retries, backoff });
 | 
			
		||||
            return Promise.resolve({ status: 200 });
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const noLogger = require('../../test/fixtures/no-logger');
 | 
			
		||||
 | 
			
		||||
test('Should call datadog webhook', async t => {
 | 
			
		||||
    const addon = new DatadogAddon({
 | 
			
		||||
        getLogger: noLogger,
 | 
			
		||||
        unleashUrl: 'http://some-url.com',
 | 
			
		||||
    });
 | 
			
		||||
    const event = {
 | 
			
		||||
        type: FEATURE_CREATED,
 | 
			
		||||
        createdBy: 'some@user.com',
 | 
			
		||||
        data: {
 | 
			
		||||
            name: 'some-toggle',
 | 
			
		||||
            enabled: false,
 | 
			
		||||
            strategies: [{ name: 'default' }],
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const parameters = {
 | 
			
		||||
        url: 'http://api.datadoghq.com/api/v1/events',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await addon.handleEvent(event, parameters);
 | 
			
		||||
    t.is(addon.fetchRetryCalls.length, 1);
 | 
			
		||||
    t.is(addon.fetchRetryCalls[0].url, parameters.url);
 | 
			
		||||
    t.snapshot(addon.fetchRetryCalls[0].options.body);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Should call datadog webhook for archived toggle', async t => {
 | 
			
		||||
    const addon = new DatadogAddon({
 | 
			
		||||
        getLogger: noLogger,
 | 
			
		||||
        unleashUrl: 'http://some-url.com',
 | 
			
		||||
    });
 | 
			
		||||
    const event = {
 | 
			
		||||
        type: FEATURE_ARCHIVED,
 | 
			
		||||
        createdBy: 'some@user.com',
 | 
			
		||||
        data: {
 | 
			
		||||
            name: 'some-toggle',
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const parameters = {
 | 
			
		||||
        url: 'http://api.datadoghq.com/api/v1/events',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await addon.handleEvent(event, parameters);
 | 
			
		||||
    t.is(addon.fetchRetryCalls.length, 1);
 | 
			
		||||
    t.is(addon.fetchRetryCalls[0].url, parameters.url);
 | 
			
		||||
    t.snapshot(addon.fetchRetryCalls[0].options.body);
 | 
			
		||||
});
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
const webhook = require('./webhook');
 | 
			
		||||
const slackAddon = require('./slack');
 | 
			
		||||
const teamsAddon = require('./teams');
 | 
			
		||||
const datadogAddon = require('./datadog');
 | 
			
		||||
 | 
			
		||||
const addons = [webhook, slackAddon, teamsAddon];
 | 
			
		||||
const addons = [webhook, slackAddon, teamsAddon, datadogAddon];
 | 
			
		||||
 | 
			
		||||
module.exports = addons;
 | 
			
		||||
 | 
			
		||||
@ -76,8 +76,10 @@ class TeamsAddon extends Addon {
 | 
			
		||||
            headers: { 'Content-Type': 'application/json' },
 | 
			
		||||
            body: JSON.stringify(body),
 | 
			
		||||
        };
 | 
			
		||||
        const result = await this.fetchRetry(url, requestOpts);
 | 
			
		||||
        this.logger.info(`Handled event ${event.type}. Status codes=${result}`);
 | 
			
		||||
        const res = await this.fetchRetry(url, requestOpts);
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
            `Handled event ${event.type}. Status codes=${res.status}`,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    featureLink(event) {
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ test.serial('gets all addons', async t => {
 | 
			
		||||
        .expect(200)
 | 
			
		||||
        .expect(res => {
 | 
			
		||||
            t.is(res.body.addons.length, 0, 'expected 0 configured addons');
 | 
			
		||||
            t.is(res.body.providers.length, 3, 'expected 3 addon providers');
 | 
			
		||||
            t.is(res.body.providers.length, 4, 'expected 4 addon providers');
 | 
			
		||||
            t.is(res.body.providers[0].name, 'webhook');
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user