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