From 9ff393b3d7a5f9e23e92f221aaeb0a909d09af4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 23 Jul 2024 10:07:31 +0100 Subject: [PATCH] chore: register integration events in New Relic integration (#7636) https://linear.app/unleash/issue/2-2462/register-integration-events-new-relic Registers integration events in the **New Relic** integration. Similar to: - #7635 - #7634 - #7631 - #7626 - #7621 --- .../__snapshots__/new-relic.test.ts.snap | 12 +- src/lib/addons/new-relic.test.ts | 222 ++++++++++-------- src/lib/addons/new-relic.ts | 46 +++- 3 files changed, 174 insertions(+), 106 deletions(-) diff --git a/src/lib/addons/__snapshots__/new-relic.test.ts.snap b/src/lib/addons/__snapshots__/new-relic.test.ts.snap index e37c7d99e1..710d3ff262 100644 --- a/src/lib/addons/__snapshots__/new-relic.test.ts.snap +++ b/src/lib/addons/__snapshots__/new-relic.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should call New Relic Event API for $type toggle 1`] = ` +exports[`New Relic integration Should call New Relic Event API for $type toggle 1`] = ` { "Api-Key": "fakeLicenseKey", "Content-Encoding": "gzip", @@ -8,7 +8,7 @@ exports[`Should call New Relic Event API for $type toggle 1`] = ` } `; -exports[`Should call New Relic Event API for FEATURE_ARCHIVED toggle with project info 1`] = ` +exports[`New Relic integration Should call New Relic Event API for FEATURE_ARCHIVED toggle with project info 1`] = ` { "Api-Key": "fakeLicenseKey", "Content-Encoding": "gzip", @@ -16,7 +16,7 @@ exports[`Should call New Relic Event API for FEATURE_ARCHIVED toggle with projec } `; -exports[`Should call New Relic Event API for FEATURE_ARCHIVED with project info 1`] = ` +exports[`New Relic integration Should call New Relic Event API for FEATURE_ARCHIVED with project info 1`] = ` { "Api-Key": "fakeLicenseKey", "Content-Encoding": "gzip", @@ -24,7 +24,7 @@ exports[`Should call New Relic Event API for FEATURE_ARCHIVED with project info } `; -exports[`Should call New Relic Event API for custom body template 1`] = ` +exports[`New Relic integration Should call New Relic Event API for custom body template 1`] = ` { "Api-Key": "fakeLicenseKey", "Content-Encoding": "gzip", @@ -32,7 +32,7 @@ exports[`Should call New Relic Event API for custom body template 1`] = ` } `; -exports[`Should call New Relic Event API for customHeaders in headers when calling service 1`] = ` +exports[`New Relic integration Should call New Relic Event API for customHeaders in headers when calling service 1`] = ` { "Api-Key": "fakeLicenseKey", "Content-Encoding": "gzip", @@ -41,7 +41,7 @@ exports[`Should call New Relic Event API for customHeaders in headers when calli } `; -exports[`Should call New Relic Event API for toggled environment 1`] = ` +exports[`New Relic integration Should call New Relic Event API for toggled environment 1`] = ` { "Api-Key": "fakeLicenseKey", "Content-Encoding": "gzip", diff --git a/src/lib/addons/new-relic.test.ts b/src/lib/addons/new-relic.test.ts index 99e696c474..cc0d9b9ade 100644 --- a/src/lib/addons/new-relic.test.ts +++ b/src/lib/addons/new-relic.test.ts @@ -5,6 +5,7 @@ import { type IFlagResolver, type IAddonConfig, type IEvent, + serializeDates, } from '../types'; import type { Logger } from '../logger'; @@ -18,6 +19,7 @@ import type { IntegrationEventsService } from '../services'; const asyncGunzip = promisify(gunzip); let fetchRetryCalls: any[] = []; +const registerEventMock = jest.fn(); const INTEGRATION_ID = 1337; const ARGS: IAddonConfig = { @@ -45,11 +47,11 @@ 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); } }, ); @@ -79,98 +81,132 @@ const makeAddHandleEvent = (event: IEvent, parameters: INewRelicParameters) => { return () => addon.handleEvent(event, parameters, INTEGRATION_ID); }; -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; - partialParameters?: Partial; - test: String; -}>)( - 'Should call New Relic Event API for $test', - async ({ partialEvent, partialParameters }) => { - const event = { - ...defaultEvent, - ...partialEvent, - }; +describe('New Relic integration', () => { + beforeEach(() => { + registerEventMock.mockClear(); + }); - const parameters = { - ...defaultParameters, - ...partialParameters, - }; + 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; + partialParameters?: Partial; + test: String; + }>)( + 'Should call New Relic Event API for $test', + async ({ partialEvent, partialParameters }) => { + const event = { + ...defaultEvent, + ...partialEvent, + }; - const handleEvent = makeAddHandleEvent(event, parameters); + 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( + (await asyncGunzip(options.body)).toString(), + ); + + 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('UnleashServiceEvent'); + 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()); + }, + ); + + test('Should call registerEvent', async () => { + const handleEvent = makeAddHandleEvent(defaultEvent, defaultParameters); await handleEvent(); - expect(fetchRetryCalls.length).toBe(1); - const { url, options } = fetchRetryCalls[0]; - const jsonBody = JSON.parse( - (await asyncGunzip(options.body)).toString(), - ); - - 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('UnleashServiceEvent'); - 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(registerEventMock).toHaveBeenCalledTimes(1); + expect(registerEventMock).toHaveBeenCalledWith({ + integrationId: INTEGRATION_ID, + state: 'success', + stateDetails: + 'New Relic Events API request was successful with status code: 200.', + event: serializeDates(defaultEvent), + details: { + url: defaultParameters.url, + body: { + eventType: 'UnleashServiceEvent', + unleashEventType: defaultEvent.type, + featureName: defaultEvent.featureName, + environment: defaultEvent.environment, + createdBy: defaultEvent.createdBy, + createdByUserId: defaultEvent.createdByUserId, + createdAt: defaultEvent.createdAt.getTime(), + ...defaultEvent.data, + }, + }, + }); + }); +}); diff --git a/src/lib/addons/new-relic.ts b/src/lib/addons/new-relic.ts index 40357f5924..4dcc69beee 100644 --- a/src/lib/addons/new-relic.ts +++ b/src/lib/addons/new-relic.ts @@ -2,7 +2,12 @@ import Addon from './addon'; import definition from './new-relic-definition'; import Mustache from 'mustache'; -import type { IAddonConfig, IEvent, IEventType } from '../types'; +import { + type IAddonConfig, + type IEvent, + type IEventType, + serializeDates, +} from '../types'; import { type FeatureEventFormatter, FeatureEventFormatterMd, @@ -10,6 +15,7 @@ import { } from './feature-event-formatter-md'; import { gzip } from 'node:zlib'; import { promisify } from 'util'; +import type { IntegrationEventState } from '../features/integration-events/integration-events-store'; const asyncGzip = promisify(gzip); @@ -46,6 +52,9 @@ export default class NewRelicAddon extends Addon { parameters: INewRelicParameters, integrationId: number, ): Promise { + let state: IntegrationEventState = 'success'; + const stateDetails: string[] = []; + const { url, licenseKey, customHeaders, bodyTemplate } = parameters; const context = { event, @@ -74,9 +83,11 @@ export default class NewRelicAddon 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); } } @@ -92,8 +103,29 @@ export default class NewRelicAddon extends Addon { }; 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 = `New Relic Events API request was successful with status code: ${res.status}.`; + stateDetails.push(successMessage); + this.logger.info(successMessage); + } else { + state = 'failed'; + const failedMessage = `New Relic 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, + }, + }); } }