Skip to content

Commit

Permalink
feat: New Relic integration #878
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrusavin committed Jun 29, 2024
1 parent 94a7179 commit 7e67484
Show file tree
Hide file tree
Showing 7 changed files with 425 additions and 0 deletions.
5 changes: 5 additions & 0 deletions frontend/src/assets/icons/new-relic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { formatAssetPath } from 'utils/formatPath';
import { capitalizeFirst } from 'utils/capitalizeFirst';

import dataDogIcon from 'assets/icons/datadog.svg';
import newRelicIcon from 'assets/icons/new-relic.svg';
import jiraIcon from 'assets/icons/jira.svg';
import jiraCommentIcon from 'assets/icons/jira-comment.svg';
import signals from 'assets/icons/signals.svg';
Expand Down Expand Up @@ -50,6 +51,7 @@ const integrations: Record<
}
> = {
datadog: { title: 'Datadog', icon: dataDogIcon },
'new-relic': { title: 'New Relic', icon: newRelicIcon },
jira: { title: 'Jira', icon: jiraIcon },
'jira-comment': { title: 'Jira', icon: jiraCommentIcon },
signals: { title: 'Signals', icon: signals },
Expand Down
62 changes: 62 additions & 0 deletions src/lib/addons/__snapshots__/new-relic.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should call New Relic Event API for $type toggle 1`] = `
{
"Api-Key": "fakeLicenseKey",
"Content-Encoding": "gzip",
"Content-Type": "application/json",
}
`;

exports[`Should call New Relic Event API for $type toggle 2`] = `"{"eventType":"Unleash Service Event","unleashEventType":"feature-created","featureName":"some-toggle","createdBy":"[email protected]","createdByUserId":-1337,"createdAt":1719580982209,"name":"some-toggle","enabled":false,"strategies":[{"name":"default"}]}"`;

exports[`Should call New Relic Event API for FEATURE_ARCHIVED toggle with project info 1`] = `
{
"Api-Key": "fakeLicenseKey",
"Content-Encoding": "gzip",
"Content-Type": "application/json",
}
`;

exports[`Should call New Relic Event API for FEATURE_ARCHIVED toggle with project info 2`] = `"{"eventType":"Unleash Service Event","unleashEventType":"feature-archived","featureName":"some-toggle","createdBy":"[email protected]","createdByUserId":-1337,"createdAt":1719580982209,"name":"some-toggle"}"`;

exports[`Should call New Relic Event API for FEATURE_ARCHIVED with project info 1`] = `
{
"Api-Key": "fakeLicenseKey",
"Content-Encoding": "gzip",
"Content-Type": "application/json",
}
`;

exports[`Should call New Relic Event API for FEATURE_ARCHIVED with project info 2`] = `"{"eventType":"Unleash Service Event","unleashEventType":"feature-archived","featureName":"some-toggle","createdBy":"[email protected]","createdByUserId":-1337,"createdAt":1719580982209,"name":"some-toggle"}"`;

exports[`Should call New Relic Event API for custom body template 1`] = `
{
"Api-Key": "fakeLicenseKey",
"Content-Encoding": "gzip",
"Content-Type": "application/json",
}
`;

exports[`Should call New Relic Event API for custom body template 2`] = `"{"eventType":"Unleash Service Event","unleashEventType":"feature-environment-disabled","featureName":"some-toggle","environment":"development","createdBy":"[email protected]","createdByUserId":-1337,"createdAt":1719580982209,"name":"some-toggle","enabled":false,"strategies":[{"name":"default"}]}"`;

exports[`Should call New Relic Event API for customHeaders in headers when calling service 1`] = `
{
"Api-Key": "fakeLicenseKey",
"Content-Encoding": "gzip",
"Content-Type": "application/json",
"MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE",
}
`;

exports[`Should call New Relic Event API for customHeaders in headers when calling service 2`] = `"{"eventType":"Unleash Service Event","unleashEventType":"feature-environment-disabled","featureName":"some-toggle","environment":"development","createdBy":"[email protected]","createdByUserId":-1337,"createdAt":1719580982209,"name":"some-toggle","enabled":false,"strategies":[{"name":"default"}]}"`;

exports[`Should call New Relic Event API for toggled environment 1`] = `
{
"Api-Key": "fakeLicenseKey",
"Content-Encoding": "gzip",
"Content-Type": "application/json",
}
`;

exports[`Should call New Relic Event API for toggled environment 2`] = `"{"eventType":"Unleash Service Event","unleashEventType":"feature-environment-disabled","featureName":"some-toggle","environment":"development","createdBy":"[email protected]","createdByUserId":-1337,"createdAt":1719580982209,"name":"some-toggle","enabled":false,"strategies":[{"name":"default"}]}"`;
2 changes: 2 additions & 0 deletions src/lib/addons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Webhook from './webhook';
import SlackAddon from './slack';
import TeamsAddon from './teams';
import DatadogAddon from './datadog';
import NewRelicAddon from './new-relic';
import type Addon from './addon';
import type { LogProvider } from '../logger';
import SlackAppAddon from './slack-app';
Expand All @@ -22,6 +23,7 @@ export const getAddons: (args: {
new SlackAppAddon({ getLogger, unleashUrl }),
new TeamsAddon({ getLogger, unleashUrl }),
new DatadogAddon({ getLogger, unleashUrl }),
new NewRelicAddon({ getLogger, unleashUrl }),
];

return addons.reduce((map, addon) => {
Expand Down
102 changes: 102 additions & 0 deletions src/lib/addons/new-relic-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
FEATURE_CREATED,
FEATURE_UPDATED,
FEATURE_ARCHIVED,
FEATURE_REVIVED,
FEATURE_STALE_ON,
FEATURE_STALE_OFF,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_ENVIRONMENT_DISABLED,
FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE,
FEATURE_STRATEGY_ADD,
FEATURE_METADATA_UPDATED,
FEATURE_PROJECT_CHANGE,
FEATURE_POTENTIALLY_STALE_ON,
FEATURE_ENVIRONMENT_VARIANTS_UPDATED,
} from '../types/events';
import type { IAddonDefinition } from '../types/model';

const newRelicDefinition: IAddonDefinition = {
name: 'new-relic',
displayName: 'New Relic',
description: 'Allows Unleash to post updates to New Relic Event API.',
documentationUrl: 'https://docs.getunleash.io/docs/addons/new-relic',
howTo: 'The New Relic integration allows Unleash to post Updates to New Relic Event API when a feature flag is updated.',
parameters: [
{
name: 'url',
displayName: 'New Relic Event URL',
description:
"(Required) If data is hosted in EU then use the EU region endpoints (https://docs.newrelic.com/docs/using-new-relic/welcome-new-relic/getting-started/introduction-eu-region-data-center/#endpoints). Otherwise, it should be something like this: https://insights-collector.newrelic.com/v1/accounts/YOUR_ACCOUNT_ID/events",
type: 'url',
required: true,
sensitive: false,
},
{
name: 'licenseKey',
displayName: 'New Relic License Key',
placeholder: 'eu01xx0f12a6b3434a8d710110bd862',
description: '(Required) License key to connect to New Relic',
type: 'text',
required: true,
sensitive: true,
},
{
name: 'customHeaders',
displayName: 'Extra HTTP Headers',
placeholder: `{
"SOME_CUSTOM_HTTP_HEADER": "SOME_VALUE",
"SOME_OTHER_CUSTOM_HTTP_HEADER": "SOME_OTHER_VALUE"
}`,
description:
'(Optional) Used to add extra HTTP Headers to the request the plugin fires off. This must be a valid json object of key-value pairs where both the key and the value are strings',
required: false,
sensitive: true,
type: 'textfield',
},
{
name: 'bodyTemplate',
displayName: 'Body template',
placeholder: `{
"event": "{{event.type}}",
"eventType": "unleash",
"createdBy": "{{event.createdBy}}",
"featureToggle": "{{event.data.name}}",
"timestamp": "{{event.data.createdAt}}"
}`,
description:
'(Optional) The default format is a markdown string formatted by Unleash. You may override the format of the body using a mustache template.',
required: false,
sensitive: false,
type: 'textfield',
},
],
events: [
FEATURE_CREATED,
FEATURE_UPDATED,
FEATURE_ARCHIVED,
FEATURE_REVIVED,
FEATURE_STALE_ON,
FEATURE_STALE_OFF,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_ENVIRONMENT_DISABLED,
FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE,
FEATURE_STRATEGY_ADD,
FEATURE_METADATA_UPDATED,
FEATURE_PROJECT_CHANGE,
FEATURE_ENVIRONMENT_VARIANTS_UPDATED,
FEATURE_POTENTIALLY_STALE_ON,
],
tagTypes: [
{
name: 'new-relic',
description:
'All New Relic tags added to a specific feature are sent to New Relic Event API.',
icon: 'D',
},
],
};

export default newRelicDefinition;
152 changes: 152 additions & 0 deletions src/lib/addons/new-relic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {FEATURE_ARCHIVED, FEATURE_CREATED, FEATURE_ENVIRONMENT_DISABLED, type IEvent,} from '../types/events';
import type {Logger} from '../logger';

import NewRelicAddon, {INewRelicParameters} from './new-relic';

import noLogger from '../../test/fixtures/no-logger';

let fetchRetryCalls: any[] = [];

jest.mock(
'./addon',
() =>
class Addon {
logger: Logger;

constructor(definition, {getLogger}) {
this.logger = getLogger('addon/test');
fetchRetryCalls = [];
}

async fetchRetry(url, options, retries, backoff) {
fetchRetryCalls.push({
url,
options,
retries,
backoff,
});
return Promise.resolve({status: 200});
}
},
);

const defaultParameters = {
url: 'fakeUrl',
licenseKey: 'fakeLicenseKey',
} as INewRelicParameters;

const defaultEvent = {
id: 1,
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: '[email protected]',
createdByUserId: -1337,
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
strategies: [{name: 'default'}],
},
} as IEvent;

const makeAddHandleEvent = (event: IEvent, parameters: INewRelicParameters,) => {
const addon = new NewRelicAddon({
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
});

return () => addon.handleEvent(event, parameters);
}


test.each([
{
partialEvent: {type: FEATURE_CREATED},
test: '$type toggle',
},
{
partialEvent: {
type: FEATURE_ARCHIVED,
data: {
name: 'some-toggle',
},
},
test: 'FEATURE_ARCHIVED toggle with project info'
},
{
partialEvent: {
type: FEATURE_ARCHIVED,
project: 'some-project',
data: {
name: 'some-toggle',
},
},
test: 'FEATURE_ARCHIVED with project info'
},
{
partialEvent: {
type: FEATURE_ENVIRONMENT_DISABLED,
environment: 'development',
},
test: 'toggled environment'
},
{
partialEvent: {
type: FEATURE_ENVIRONMENT_DISABLED,
environment: 'development',
},
partialParameters: {
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
},
test: 'customHeaders in headers when calling service'
},
{
partialEvent: {
type: FEATURE_ENVIRONMENT_DISABLED,
environment: 'development',
},
partialParameters: {
bodyTemplate:
'{\n "eventType": "{{event.type}}",\n "createdBy": "{{event.createdBy}}"\n}',
},
test: 'custom body template'
}
] as Array<{
partialEvent: Partial<IEvent>,
partialParameters?: Partial<INewRelicParameters>,
test: String
}>)('Should call New Relic Event API for $test', async ({partialEvent, partialParameters}) => {
const event = {
...defaultEvent,
...partialEvent,
}

const parameters = {
...defaultParameters,
...partialParameters,
}

const handleEvent = makeAddHandleEvent(event, parameters);

await handleEvent();
expect(fetchRetryCalls.length).toBe(1);

const {url, options} = fetchRetryCalls[0];
const jsonBody = JSON.parse(options.body);
expect(url).toBe(parameters.url);
expect(options.method).toBe('POST');
expect(options.headers['Api-Key']).toBe(parameters.licenseKey);
expect(options.headers['Content-Type']).toBe('application/json');
expect(options.headers['Content-Encoding']).toBe('gzip');
expect(options.headers).toMatchSnapshot();

expect(jsonBody.eventType).toBe('Unleash Service Event');
expect(jsonBody.unleashEventType).toBe(event.type);
expect(jsonBody.featureName).toBe(event.data.name);
expect(jsonBody.environment).toBe(event.environment);
expect(jsonBody.createdBy).toBe(event.createdBy);
expect(jsonBody.createdByUserId).toBe(event.createdByUserId);
expect(jsonBody.createdAt).toBe(event.createdAt.getTime());

expect(options.body).toMatchSnapshot();
});
Loading

0 comments on commit 7e67484

Please sign in to comment.