From 72bdce98de1d16505c91fe1ef138f727f0b047a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 24 Sep 2024 10:25:12 +0100 Subject: [PATCH] chore: feature event formatter md format style (#8222) https://linear.app/unleash/issue/2-2697/implement-proper-markdown-bold-format-in-feature-event-formatter-md This is a follow up to https://github.com/Unleash/unleash/pull/8205, specifically [this comment](https://github.com/Unleash/unleash/pull/8205#issuecomment-2368207656) from @gastonfournier Implements an easy way to switch between formatting styles in our event formatter. This enhancement will allow us to generate fully markdown-formatted event summaries when needed, while preserving the simplistic markdown formatting currently supported by platforms like Slack. Also includes some slight scouting. See comments for details. --- src/lib/addons/datadog.ts | 8 +- .../feature-event-formatter-md-events.ts | 359 +++++++++++++++ .../addons/feature-event-formatter-md.test.ts | 4 +- src/lib/addons/feature-event-formatter-md.ts | 429 +++--------------- src/lib/addons/new-relic.ts | 8 +- src/lib/addons/slack-app.ts | 8 +- src/lib/addons/slack.ts | 8 +- src/lib/addons/teams.ts | 4 +- src/lib/addons/webhook.ts | 8 +- .../events/event-search-controller.ts | 7 +- 10 files changed, 440 insertions(+), 403 deletions(-) create mode 100644 src/lib/addons/feature-event-formatter-md-events.ts diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index 8efad9df7cb7..2f1516467be1 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -10,7 +10,6 @@ import { 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'; @@ -38,10 +37,9 @@ export default class DatadogAddon extends Addon { constructor(config: IAddonConfig) { super(definition, config); - this.msgFormatter = new FeatureEventFormatterMd( - config.unleashUrl, - LinkStyle.MD, - ); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: config.unleashUrl, + }); this.flagResolver = config.flagResolver; } diff --git a/src/lib/addons/feature-event-formatter-md-events.ts b/src/lib/addons/feature-event-formatter-md-events.ts new file mode 100644 index 000000000000..fc1633e92ec9 --- /dev/null +++ b/src/lib/addons/feature-event-formatter-md-events.ts @@ -0,0 +1,359 @@ +import { + ADDON_CONFIG_CREATED, + ADDON_CONFIG_DELETED, + ADDON_CONFIG_UPDATED, + API_TOKEN_CREATED, + API_TOKEN_DELETED, + CHANGE_ADDED, + CHANGE_DISCARDED, + CHANGE_EDITED, + CHANGE_REQUEST_APPLIED, + CHANGE_REQUEST_APPROVAL_ADDED, + CHANGE_REQUEST_APPROVED, + CHANGE_REQUEST_CANCELLED, + CHANGE_REQUEST_CREATED, + CHANGE_REQUEST_DISCARDED, + CHANGE_REQUEST_REJECTED, + CHANGE_REQUEST_SENT_TO_REVIEW, + CONTEXT_FIELD_CREATED, + CONTEXT_FIELD_DELETED, + CONTEXT_FIELD_UPDATED, + FEATURE_ARCHIVED, + FEATURE_CREATED, + FEATURE_DELETED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_VARIANTS_UPDATED, + FEATURE_METADATA_UPDATED, + FEATURE_POTENTIALLY_STALE_ON, + FEATURE_PROJECT_CHANGE, + FEATURE_REVIVED, + FEATURE_STALE_OFF, + FEATURE_STALE_ON, + FEATURE_STRATEGY_ADD, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_TAGGED, + FEATURE_UNTAGGED, + GROUP_CREATED, + GROUP_DELETED, + GROUP_UPDATED, + BANNER_CREATED, + BANNER_DELETED, + BANNER_UPDATED, + PROJECT_CREATED, + PROJECT_DELETED, + SEGMENT_CREATED, + SEGMENT_DELETED, + SEGMENT_UPDATED, + SERVICE_ACCOUNT_CREATED, + SERVICE_ACCOUNT_DELETED, + SERVICE_ACCOUNT_UPDATED, + USER_CREATED, + USER_DELETED, + USER_UPDATED, + CHANGE_REQUEST_SCHEDULED, + CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS, + CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE, + CHANGE_REQUEST_SCHEDULE_SUSPENDED, + FEATURE_COMPLETED, +} from '../types'; + +interface IEventData { + label: string; + action: string; + path?: string; +} + +export const EVENT_MAP: Record = { + [ADDON_CONFIG_CREATED]: { + label: 'Integration configuration created', + action: '{{b}}{{user}}{{b}} created a new {{b}}{{event.data.provider}}{{b}} integration configuration', + path: '/integrations', + }, + [ADDON_CONFIG_DELETED]: { + label: 'Integration configuration deleted', + action: '{{b}}{{user}}{{b}} deleted a {{b}}{{event.preData.provider}}{{b}} integration configuration', + path: '/integrations', + }, + [ADDON_CONFIG_UPDATED]: { + label: 'Integration configuration updated', + action: '{{b}}{{user}}{{b}} updated a {{b}}{{event.preData.provider}}{{b}} integration configuration', + path: '/integrations', + }, + [API_TOKEN_CREATED]: { + label: 'API token created', + action: '{{b}}{{user}}{{b}} created API token {{b}}{{event.data.username}}{{b}}', + path: '/admin/api', + }, + [API_TOKEN_DELETED]: { + label: 'API token deleted', + action: '{{b}}{{user}}{{b}} deleted API token {{b}}{{event.preData.username}}{{b}}', + path: '/admin/api', + }, + [CHANGE_ADDED]: { + label: 'Change added', + action: '{{b}}{{user}}{{b}} added a change to change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_DISCARDED]: { + label: 'Change discarded', + action: '{{b}}{{user}}{{b}} discarded a change in change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_EDITED]: { + label: 'Change edited', + action: '{{b}}{{user}}{{b}} edited a change in change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_APPLIED]: { + label: 'Change request applied', + action: '{{b}}{{user}}{{b}} applied change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_APPROVAL_ADDED]: { + label: 'Change request approval added', + action: '{{b}}{{user}}{{b}} added an approval to change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_APPROVED]: { + label: 'Change request approved', + action: '{{b}}{{user}}{{b}} approved change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_CANCELLED]: { + label: 'Change request cancelled', + action: '{{b}}{{user}}{{b}} cancelled change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_CREATED]: { + label: 'Change request created', + action: '{{b}}{{user}}{{b}} created change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_DISCARDED]: { + label: 'Change request discarded', + action: '{{b}}{{user}}{{b}} discarded change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_REJECTED]: { + label: 'Change request rejected', + action: '{{b}}{{user}}{{b}} rejected change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_SENT_TO_REVIEW]: { + label: 'Change request sent to review', + action: '{{b}}{{user}}{{b}} sent to review change request {{changeRequest}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_SCHEDULED]: { + label: 'Change request scheduled', + action: '{{b}}{{user}}{{b}} scheduled change request {{changeRequest}} to be applied at {{event.data.scheduledDate}} in project {{b}}{{event.project}}{{b}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS]: { + label: 'Scheduled change request applied successfully', + action: '{{b}}Successfully{{b}} applied the scheduled change request {{changeRequest}} by {{b}}{{user}}{{b}} in project {{b}}{{event.project}}{{b}}.', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE]: { + label: 'Scheduled change request failed', + action: '{{b}}Failed{{b}} to apply the scheduled change request {{changeRequest}} by {{b}}{{user}}{{b}} in project {{b}}{{event.project}}{{b}}.', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CHANGE_REQUEST_SCHEDULE_SUSPENDED]: { + label: 'Change request suspended', + action: 'Change request {{changeRequest}} was suspended for the following reason: {{event.data.reason}}', + path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', + }, + [CONTEXT_FIELD_CREATED]: { + label: 'Context field created', + action: '{{b}}{{user}}{{b}} created context field {{b}}{{event.data.name}}{{b}}', + path: '/context', + }, + [CONTEXT_FIELD_DELETED]: { + label: 'Context field deleted', + action: '{{b}}{{user}}{{b}} deleted context field {{b}}{{event.preData.name}}{{b}}', + path: '/context', + }, + [CONTEXT_FIELD_UPDATED]: { + label: 'Context field updated', + action: '{{b}}{{user}}{{b}} updated context field {{b}}{{event.preData.name}}{{b}}', + path: '/context', + }, + [FEATURE_ARCHIVED]: { + label: 'Flag archived', + action: '{{b}}{{user}}{{b}} archived {{b}}{{event.featureName}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/archive', + }, + [FEATURE_CREATED]: { + label: 'Flag created', + action: '{{b}}{{user}}{{b}} created {{b}}{{feature}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_DELETED]: { + label: 'Flag deleted', + action: '{{b}}{{user}}{{b}} deleted {{b}}{{event.featureName}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}', + }, + [FEATURE_ENVIRONMENT_DISABLED]: { + label: 'Flag disabled', + action: '{{b}}{{user}}{{b}} disabled {{b}}{{feature}}{{b}} for the {{b}}{{event.environment}}{{b}} environment in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_ENVIRONMENT_ENABLED]: { + label: 'Flag enabled', + action: '{{b}}{{user}}{{b}} enabled {{b}}{{feature}}{{b}} for the {{b}}{{event.environment}}{{b}} environment in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: { + label: 'Flag variants updated', + action: '{{b}}{{user}}{{b}} updated variants for {{b}}{{feature}}{{b}} for the {{b}}{{event.environment}}{{b}} environment in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}/variants', + }, + [FEATURE_METADATA_UPDATED]: { + label: 'Flag metadata updated', + action: '{{b}}{{user}}{{b}} updated {{b}}{{feature}}{{b}} metadata in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_COMPLETED]: { + label: 'Flag marked as completed', + action: '{{b}}{{feature}}{{b}} was marked as completed in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_POTENTIALLY_STALE_ON]: { + label: 'Flag potentially stale', + action: '{{b}}{{feature}}{{b}} was marked as potentially stale in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_PROJECT_CHANGE]: { + label: 'Flag moved to a new project', + action: '{{b}}{{user}}{{b}} moved {{b}}{{feature}}{{b}} from {{b}}{{event.data.oldProject}}{{b}} to {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_REVIVED]: { + label: 'Flag revived', + action: '{{b}}{{user}}{{b}} revived {{b}}{{feature}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STALE_OFF]: { + label: 'Flag stale marking removed', + action: '{{b}}{{user}}{{b}} removed the stale marking on {{b}}{{feature}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STALE_ON]: { + label: 'Flag marked as stale', + action: '{{b}}{{user}}{{b}} marked {{b}}{{feature}}{{b}} as stale in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STRATEGY_ADD]: { + label: 'Flag strategy added', + action: '{{b}}{{user}}{{b}} added strategy {{b}}{{strategyTitle}}{{b}} to {{b}}{{feature}}{{b}} for the {{b}}{{event.environment}}{{b}} environment in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STRATEGY_REMOVE]: { + label: 'Flag strategy removed', + action: '{{b}}{{user}}{{b}} removed strategy {{b}}{{strategyTitle}}{{b}} from {{b}}{{feature}}{{b}} for the {{b}}{{event.environment}}{{b}} environment in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_STRATEGY_UPDATE]: { + label: 'Flag strategy updated', + action: '{{b}}{{user}}{{b}} updated {{b}}{{feature}}{{b}} in project {{b}}{{project}}{{b}} {{strategyChangeText}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_TAGGED]: { + label: 'Flag tagged', + action: '{{b}}{{user}}{{b}} tagged {{b}}{{feature}}{{b}} with {{b}}{{event.data.type}}:{{event.data.value}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [FEATURE_UNTAGGED]: { + label: 'Flag untagged', + action: '{{b}}{{user}}{{b}} untagged {{b}}{{feature}}{{b}} with {{b}}{{event.preData.type}}:{{event.preData.value}}{{b}} in project {{b}}{{project}}{{b}}', + path: '/projects/{{event.project}}/features/{{event.featureName}}', + }, + [GROUP_CREATED]: { + label: 'Group created', + action: '{{b}}{{user}}{{b}} created group {{b}}{{event.data.name}}{{b}}', + path: '/admin/groups', + }, + [GROUP_DELETED]: { + label: 'Group deleted', + action: '{{b}}{{user}}{{b}} deleted group {{b}}{{event.preData.name}}{{b}}', + path: '/admin/groups', + }, + [GROUP_UPDATED]: { + label: 'Group updated', + action: '{{b}}{{user}}{{b}} updated group {{b}}{{event.preData.name}}{{b}}', + path: '/admin/groups', + }, + [BANNER_CREATED]: { + label: 'Banner created', + action: '{{b}}{{user}}{{b}} created banner {{b}}{{event.data.message}}{{b}}', + path: '/admin/message-banners', + }, + [BANNER_DELETED]: { + label: 'Banner deleted', + action: '{{b}}{{user}}{{b}} deleted banner {{b}}{{event.preData.message}}{{b}}', + path: '/admin/message-banners', + }, + [BANNER_UPDATED]: { + label: 'Banner updated', + action: '{{b}}{{user}}{{b}} updated banner {{b}}{{event.preData.message}}{{b}}', + path: '/admin/message-banners', + }, + [PROJECT_CREATED]: { + label: 'Project created', + action: '{{b}}{{user}}{{b}} created project {{b}}{{project}}{{b}}', + path: '/projects', + }, + [PROJECT_DELETED]: { + label: 'Project deleted', + action: '{{b}}{{user}}{{b}} deleted project {{b}}{{event.project}}{{b}}', + path: '/projects', + }, + [SEGMENT_CREATED]: { + label: 'Segment created', + action: '{{b}}{{user}}{{b}} created segment {{b}}{{event.data.name}}{{b}}', + path: '/segments', + }, + [SEGMENT_DELETED]: { + label: 'Segment deleted', + action: '{{b}}{{user}}{{b}} deleted segment {{b}}{{event.preData.name}}{{b}}', + path: '/segments', + }, + [SEGMENT_UPDATED]: { + label: 'Segment updated', + action: '{{b}}{{user}}{{b}} updated segment {{b}}{{event.preData.name}}{{b}}', + path: '/segments', + }, + [SERVICE_ACCOUNT_CREATED]: { + label: 'Service account created', + action: '{{b}}{{user}}{{b}} created service account {{b}}{{event.data.name}}{{b}}', + path: '/admin/service-accounts', + }, + [SERVICE_ACCOUNT_DELETED]: { + label: 'Service account deleted', + action: '{{b}}{{user}}{{b}} deleted service account {{b}}{{event.preData.name}}{{b}}', + path: '/admin/service-accounts', + }, + [SERVICE_ACCOUNT_UPDATED]: { + label: 'Service account updated', + action: '{{b}}{{user}}{{b}} updated service account {{b}}{{event.preData.name}}{{b}}', + path: '/admin/service-accounts', + }, + [USER_CREATED]: { + label: 'User created', + action: '{{b}}{{user}}{{b}} created user {{b}}{{event.data.name}}{{b}}', + path: '/admin/users', + }, + [USER_DELETED]: { + label: 'User deleted', + action: '{{b}}{{user}}{{b}} deleted user {{b}}{{event.preData.name}}{{b}}', + path: '/admin/users', + }, + [USER_UPDATED]: { + label: 'User updated', + action: '{{b}}{{user}}{{b}} updated user {{b}}{{event.preData.name}}{{b}}', + path: '/admin/users', + }, +}; diff --git a/src/lib/addons/feature-event-formatter-md.test.ts b/src/lib/addons/feature-event-formatter-md.test.ts index 0dfe90fb8fa7..74370455176f 100644 --- a/src/lib/addons/feature-event-formatter-md.test.ts +++ b/src/lib/addons/feature-event-formatter-md.test.ts @@ -575,7 +575,9 @@ const testCases: [string, IEvent][] = [ testCases.forEach(([description, event]) => test(`Should format specialised text for events ${description}`, () => { - const formatter = new FeatureEventFormatterMd('unleashUrl'); + const formatter = new FeatureEventFormatterMd({ + unleashUrl: 'unleashUrl', + }); const formattedEvent = formatter.format(event); expect(formattedEvent).toMatchSnapshot(); }), diff --git a/src/lib/addons/feature-event-formatter-md.ts b/src/lib/addons/feature-event-formatter-md.ts index 09015e2df516..d07beb16ad0a 100644 --- a/src/lib/addons/feature-event-formatter-md.ts +++ b/src/lib/addons/feature-event-formatter-md.ts @@ -1,72 +1,11 @@ import Mustache from 'mustache'; import { - ADDON_CONFIG_CREATED, - ADDON_CONFIG_DELETED, - ADDON_CONFIG_UPDATED, - API_TOKEN_CREATED, - API_TOKEN_DELETED, - CHANGE_ADDED, - CHANGE_DISCARDED, - CHANGE_EDITED, - CHANGE_REQUEST_APPLIED, - CHANGE_REQUEST_APPROVAL_ADDED, - CHANGE_REQUEST_APPROVED, - CHANGE_REQUEST_CANCELLED, - CHANGE_REQUEST_CREATED, - CHANGE_REQUEST_DISCARDED, - CHANGE_REQUEST_REJECTED, - CHANGE_REQUEST_SENT_TO_REVIEW, - CONTEXT_FIELD_CREATED, - CONTEXT_FIELD_DELETED, - CONTEXT_FIELD_UPDATED, FEATURE_ARCHIVED, - FEATURE_CREATED, - FEATURE_DELETED, - FEATURE_ENVIRONMENT_DISABLED, - FEATURE_ENVIRONMENT_ENABLED, - FEATURE_ENVIRONMENT_VARIANTS_UPDATED, - FEATURE_METADATA_UPDATED, - FEATURE_POTENTIALLY_STALE_ON, - FEATURE_PROJECT_CHANGE, - FEATURE_REVIVED, - FEATURE_STALE_OFF, - FEATURE_STALE_ON, - FEATURE_STRATEGY_ADD, - FEATURE_STRATEGY_REMOVE, FEATURE_STRATEGY_UPDATE, - FEATURE_TAGGED, - FEATURE_UNTAGGED, - GROUP_CREATED, - GROUP_DELETED, - GROUP_UPDATED, type IConstraint, type IEvent, - BANNER_CREATED, - BANNER_DELETED, - BANNER_UPDATED, - PROJECT_CREATED, - PROJECT_DELETED, - SEGMENT_CREATED, - SEGMENT_DELETED, - SEGMENT_UPDATED, - SERVICE_ACCOUNT_CREATED, - SERVICE_ACCOUNT_DELETED, - SERVICE_ACCOUNT_UPDATED, - USER_CREATED, - USER_DELETED, - USER_UPDATED, - CHANGE_REQUEST_SCHEDULED, - CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS, - CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE, - CHANGE_REQUEST_SCHEDULE_SUSPENDED, - FEATURE_COMPLETED, } from '../types'; - -interface IEventData { - label: string; - action: string; - path?: string; -} +import { EVENT_MAP } from './feature-event-formatter-md-events'; interface IFormattedEventData { label: string; @@ -82,307 +21,40 @@ export enum LinkStyle { MD = 1, } -const EVENT_MAP: Record = { - [ADDON_CONFIG_CREATED]: { - label: 'Integration configuration created', - action: '*{{user}}* created a new *{{event.data.provider}}* integration configuration', - path: '/integrations', - }, - [ADDON_CONFIG_DELETED]: { - label: 'Integration configuration deleted', - action: '*{{user}}* deleted a *{{event.preData.provider}}* integration configuration', - path: '/integrations', - }, - [ADDON_CONFIG_UPDATED]: { - label: 'Integration configuration updated', - action: '*{{user}}* updated a *{{event.preData.provider}}* integration configuration', - path: '/integrations', - }, - [API_TOKEN_CREATED]: { - label: 'API token created', - action: '*{{user}}* created API token *{{event.data.username}}*', - path: '/admin/api', - }, - [API_TOKEN_DELETED]: { - label: 'API token deleted', - action: '*{{user}}* deleted API token *{{event.preData.username}}*', - path: '/admin/api', - }, - [CHANGE_ADDED]: { - label: 'Change added', - action: '*{{user}}* added a change to change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_DISCARDED]: { - label: 'Change discarded', - action: '*{{user}}* discarded a change in change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_EDITED]: { - label: 'Change edited', - action: '*{{user}}* edited a change in change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_APPLIED]: { - label: 'Change request applied', - action: '*{{user}}* applied change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_APPROVAL_ADDED]: { - label: 'Change request approval added', - action: '*{{user}}* added an approval to change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_APPROVED]: { - label: 'Change request approved', - action: '*{{user}}* approved change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_CANCELLED]: { - label: 'Change request cancelled', - action: '*{{user}}* cancelled change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_CREATED]: { - label: 'Change request created', - action: '*{{user}}* created change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_DISCARDED]: { - label: 'Change request discarded', - action: '*{{user}}* discarded change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_REJECTED]: { - label: 'Change request rejected', - action: '*{{user}}* rejected change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_SENT_TO_REVIEW]: { - label: 'Change request sent to review', - action: '*{{user}}* sent to review change request {{changeRequest}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_SCHEDULED]: { - label: 'Change request scheduled', - action: '*{{user}}* scheduled change request {{changeRequest}} to be applied at {{event.data.scheduledDate}} in project *{{event.project}}*', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS]: { - label: 'Scheduled change request applied successfully', - action: '*Successfully* applied the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE]: { - label: 'Scheduled change request failed', - action: '*Failed* to apply the scheduled change request {{changeRequest}} by *{{user}}* in project *{{event.project}}*.', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CHANGE_REQUEST_SCHEDULE_SUSPENDED]: { - label: 'Change request suspended', - action: 'Change request {{changeRequest}} was suspended for the following reason: {{event.data.reason}}', - path: '/projects/{{event.project}}/change-requests/{{event.data.changeRequestId}}', - }, - [CONTEXT_FIELD_CREATED]: { - label: 'Context field created', - action: '*{{user}}* created context field *{{event.data.name}}*', - path: '/context', - }, - [CONTEXT_FIELD_DELETED]: { - label: 'Context field deleted', - action: '*{{user}}* deleted context field *{{event.preData.name}}*', - path: '/context', - }, - [CONTEXT_FIELD_UPDATED]: { - label: 'Context field updated', - action: '*{{user}}* updated context field *{{event.preData.name}}*', - path: '/context', - }, - [FEATURE_ARCHIVED]: { - label: 'Flag archived', - action: '*{{user}}* archived *{{event.featureName}}* in project *{{project}}*', - path: '/projects/{{event.project}}/archive', - }, - [FEATURE_CREATED]: { - label: 'Flag created', - action: '*{{user}}* created *{{feature}}* in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_DELETED]: { - label: 'Flag deleted', - action: '*{{user}}* deleted *{{event.featureName}}* in project *{{project}}*', - path: '/projects/{{event.project}}', - }, - [FEATURE_ENVIRONMENT_DISABLED]: { - label: 'Flag disabled', - action: '*{{user}}* disabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_ENVIRONMENT_ENABLED]: { - label: 'Flag enabled', - action: '*{{user}}* enabled *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_ENVIRONMENT_VARIANTS_UPDATED]: { - label: 'Flag variants updated', - action: '*{{user}}* updated variants for *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}/variants', - }, - [FEATURE_METADATA_UPDATED]: { - label: 'Flag metadata updated', - action: '*{{user}}* updated *{{feature}}* metadata in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_COMPLETED]: { - label: 'Flag marked as completed', - action: '*{{feature}}* was marked as completed in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_POTENTIALLY_STALE_ON]: { - label: 'Flag potentially stale', - action: '*{{feature}}* was marked as potentially stale in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_PROJECT_CHANGE]: { - label: 'Flag moved to a new project', - action: '*{{user}}* moved *{{feature}}* from *{{event.data.oldProject}}* to *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_REVIVED]: { - label: 'Flag revived', - action: '*{{user}}* revived *{{feature}}* in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_STALE_OFF]: { - label: 'Flag stale marking removed', - action: '*{{user}}* removed the stale marking on *{{feature}}* in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_STALE_ON]: { - label: 'Flag marked as stale', - action: '*{{user}}* marked *{{feature}}* as stale in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_STRATEGY_ADD]: { - label: 'Flag strategy added', - action: '*{{user}}* added strategy *{{strategyTitle}}* to *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_STRATEGY_REMOVE]: { - label: 'Flag strategy removed', - action: '*{{user}}* removed strategy *{{strategyTitle}}* from *{{feature}}* for the *{{event.environment}}* environment in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_STRATEGY_UPDATE]: { - label: 'Flag strategy updated', - action: '*{{user}}* updated *{{feature}}* in project *{{project}}* {{strategyChangeText}}', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_TAGGED]: { - label: 'Flag tagged', - action: '*{{user}}* tagged *{{feature}}* with *{{event.data.type}}:{{event.data.value}}* in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [FEATURE_UNTAGGED]: { - label: 'Flag untagged', - action: '*{{user}}* untagged *{{feature}}* with *{{event.preData.type}}:{{event.preData.value}}* in project *{{project}}*', - path: '/projects/{{event.project}}/features/{{event.featureName}}', - }, - [GROUP_CREATED]: { - label: 'Group created', - action: '*{{user}}* created group *{{event.data.name}}*', - path: '/admin/groups', - }, - [GROUP_DELETED]: { - label: 'Group deleted', - action: '*{{user}}* deleted group *{{event.preData.name}}*', - path: '/admin/groups', - }, - [GROUP_UPDATED]: { - label: 'Group updated', - action: '*{{user}}* updated group *{{event.preData.name}}*', - path: '/admin/groups', - }, - [BANNER_CREATED]: { - label: 'Banner created', - action: '*{{user}}* created banner *{{event.data.message}}*', - path: '/admin/message-banners', - }, - [BANNER_DELETED]: { - label: 'Banner deleted', - action: '*{{user}}* deleted banner *{{event.preData.message}}*', - path: '/admin/message-banners', - }, - [BANNER_UPDATED]: { - label: 'Banner updated', - action: '*{{user}}* updated banner *{{event.preData.message}}*', - path: '/admin/message-banners', - }, - [PROJECT_CREATED]: { - label: 'Project created', - action: '*{{user}}* created project *{{project}}*', - path: '/projects', - }, - [PROJECT_DELETED]: { - label: 'Project deleted', - action: '*{{user}}* deleted project *{{event.project}}*', - path: '/projects', - }, - [SEGMENT_CREATED]: { - label: 'Segment created', - action: '*{{user}}* created segment *{{event.data.name}}*', - path: '/segments', - }, - [SEGMENT_DELETED]: { - label: 'Segment deleted', - action: '*{{user}}* deleted segment *{{event.preData.name}}*', - path: '/segments', - }, - [SEGMENT_UPDATED]: { - label: 'Segment updated', - action: '*{{user}}* updated segment *{{event.preData.name}}*', - path: '/segments', - }, - [SERVICE_ACCOUNT_CREATED]: { - label: 'Service account created', - action: '*{{user}}* created service account *{{event.data.name}}*', - path: '/admin/service-accounts', - }, - [SERVICE_ACCOUNT_DELETED]: { - label: 'Service account deleted', - action: '*{{user}}* deleted service account *{{event.preData.name}}*', - path: '/admin/service-accounts', - }, - [SERVICE_ACCOUNT_UPDATED]: { - label: 'Service account updated', - action: '*{{user}}* updated service account *{{event.preData.name}}*', - path: '/admin/service-accounts', - }, - [USER_CREATED]: { - label: 'User created', - action: '*{{user}}* created user *{{event.data.name}}*', - path: '/admin/users', - }, - [USER_DELETED]: { - label: 'User deleted', - action: '*{{user}}* deleted user *{{event.preData.name}}*', - path: '/admin/users', - }, - [USER_UPDATED]: { - label: 'User updated', - action: '*{{user}}* updated user *{{event.preData.name}}*', - path: '/admin/users', - }, -}; +type FormatStyle = 'simple' | 'markdown'; + +interface IFeatureEventFormatterMdArgs { + unleashUrl: string; + linkStyle?: LinkStyle; + formatStyle?: FormatStyle; +} +// This is not only formatting feature events. And it's also not only for (proper) markdown. We should probably revisit this sometime in the future and try to split it / refactor it. export class FeatureEventFormatterMd implements FeatureEventFormatter { private readonly unleashUrl: string; private readonly linkStyle: LinkStyle; - constructor(unleashUrl: string, linkStyle: LinkStyle = LinkStyle.MD) { + private readonly formatStyle: FormatStyle; + + constructor({ + unleashUrl, + linkStyle = LinkStyle.MD, + formatStyle = 'simple', + }: IFeatureEventFormatterMdArgs) { this.unleashUrl = unleashUrl; this.linkStyle = linkStyle; + this.formatStyle = formatStyle; + } + + /** + * Returns the bold marker based on formatStyle, or wraps text with bold markers. + * @param text Optional text to wrap with bold markers. + * @returns Bold marker or bolded text. + */ + bold(text?: string): string { + const boldChar = this.formatStyle === 'simple' ? '*' : '**'; + return text ? `${boldChar}${text}${boldChar}` : boldChar; } generateChangeRequestLink(event: IEvent): string | undefined { @@ -394,17 +66,19 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const text = `#${changeRequestId}`; const featureLink = this.generateFeatureLink(event); const featureText = featureLink - ? ` for feature flag *${featureLink}*` + ? ` for feature flag ${this.bold(featureLink)}` : ''; const environmentText = environment - ? ` in the *${environment}* environment` + ? ` in the ${this.bold(environment)} environment` : ''; const projectLink = this.generateProjectLink(event); - const projectText = project ? ` in project *${projectLink}*` : ''; + const projectText = project + ? ` in project ${this.bold(projectLink)}` + : ''; if (this.linkStyle === LinkStyle.SLACK) { - return `*<${url}|${text}>*${featureText}${environmentText}${projectText}`; + return `${this.bold(`<${url}|${text}>`)}${featureText}${environmentText}${projectText}`; } else { - return `*[${text}](${url})*${featureText}${environmentText}${projectText}`; + return `${this.bold(`[${text}](${url})`)}${featureText}${environmentText}${projectText}`; } } } @@ -470,9 +144,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { event, ); default: - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*`; + return `by updating strategy ${this.bold( + this.getStrategyTitle(event), + )} in ${this.bold(environment)}`; } }; @@ -522,9 +196,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const strategySpecificText = [usersText, constraintText, segmentsText] .filter((x) => x.length) .join(';'); - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*${strategySpecificText}`; + return `by updating strategy ${this.bold( + this.getStrategyTitle(event), + )} in ${this.bold(environment)}${strategySpecificText}`; } private flexibleRolloutStrategyChangeText(event: IEvent) { @@ -570,9 +244,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { ] .filter((txt) => txt.length) .join(';'); - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*${strategySpecificText}`; + return `by updating strategy ${this.bold( + this.getStrategyTitle(event), + )} in ${this.bold(environment)}${strategySpecificText}`; } private defaultStrategyChangeText(event: IEvent) { @@ -588,9 +262,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { const strategySpecificText = [constraintText, segmentsText] .filter((txt) => txt.length) .join(';'); - return `by updating strategy *${this.getStrategyTitle( - event, - )}* in *${environment}*${strategySpecificText}`; + return `by updating strategy ${this.bold( + this.getStrategyTitle(event), + )} in ${this.bold(environment)}${strategySpecificText}`; } private constraintChangeText( @@ -660,8 +334,13 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { format(event: IEvent): IFormattedEventData { const { createdBy, type } = event; - const { action, path } = EVENT_MAP[type] || { - action: `triggered *${type}*`, + const { label, action, path } = EVENT_MAP[type] || { + label: type, + action: `${this.bold(createdBy)} triggered ${this.bold(type)}`, + }; + + const formatting = { + b: this.bold(), }; const context = { @@ -672,11 +351,11 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter { changeRequest: this.generateChangeRequestLink(event), feature: this.generateFeatureLink(event), project: this.generateProjectLink(event), + ...formatting, }; Mustache.escape = (text) => text; - const label = EVENT_MAP[type]?.label || type; const text = Mustache.render(action, context); const url = path ? `${this.unleashUrl}${Mustache.render(path, context)}` diff --git a/src/lib/addons/new-relic.ts b/src/lib/addons/new-relic.ts index d9dcf44c5b36..eeebe3fcbe5c 100644 --- a/src/lib/addons/new-relic.ts +++ b/src/lib/addons/new-relic.ts @@ -12,7 +12,6 @@ import { import { type FeatureEventFormatter, FeatureEventFormatterMd, - LinkStyle, } from './feature-event-formatter-md'; import { gzip } from 'node:zlib'; import { promisify } from 'util'; @@ -45,10 +44,9 @@ export default class NewRelicAddon extends Addon { constructor(config: IAddonConfig) { super(definition, config); - this.msgFormatter = new FeatureEventFormatterMd( - config.unleashUrl, - LinkStyle.MD, - ); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: config.unleashUrl, + }); this.flagResolver = config.flagResolver; } diff --git a/src/lib/addons/slack-app.ts b/src/lib/addons/slack-app.ts index e3c7dbdee72c..4d4ef0fed7a5 100644 --- a/src/lib/addons/slack-app.ts +++ b/src/lib/addons/slack-app.ts @@ -43,10 +43,10 @@ export default class SlackAppAddon extends Addon { constructor(args: IAddonConfig) { super(slackAppDefinition, args); - this.msgFormatter = new FeatureEventFormatterMd( - args.unleashUrl, - LinkStyle.SLACK, - ); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: args.unleashUrl, + linkStyle: LinkStyle.SLACK, + }); this.flagResolver = args.flagResolver; } diff --git a/src/lib/addons/slack.ts b/src/lib/addons/slack.ts index ba8c3c336815..c2699fd3b036 100644 --- a/src/lib/addons/slack.ts +++ b/src/lib/addons/slack.ts @@ -30,10 +30,10 @@ export default class SlackAddon extends Addon { constructor(args: IAddonConfig) { super(slackDefinition, args); - this.msgFormatter = new FeatureEventFormatterMd( - args.unleashUrl, - LinkStyle.SLACK, - ); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: args.unleashUrl, + linkStyle: LinkStyle.SLACK, + }); this.flagResolver = args.flagResolver; } diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index 1366b4987603..52dda9127b3b 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -25,7 +25,9 @@ export default class TeamsAddon extends Addon { constructor(args: IAddonConfig) { super(teamsDefinition, args); - this.msgFormatter = new FeatureEventFormatterMd(args.unleashUrl); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: args.unleashUrl, + }); this.flagResolver = args.flagResolver; } diff --git a/src/lib/addons/webhook.ts b/src/lib/addons/webhook.ts index d64c4f1ac069..ff8460704142 100644 --- a/src/lib/addons/webhook.ts +++ b/src/lib/addons/webhook.ts @@ -11,7 +11,6 @@ import type { IntegrationEventState } from '../features/integration-events/integ import { type FeatureEventFormatter, FeatureEventFormatterMd, - LinkStyle, } from './feature-event-formatter-md'; import { ADDON_EVENTS_HANDLED } from '../metric-events'; @@ -31,10 +30,9 @@ export default class Webhook extends Addon { constructor(args: IAddonConfig) { super(definition, args); - this.msgFormatter = new FeatureEventFormatterMd( - args.unleashUrl, - LinkStyle.MD, - ); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: args.unleashUrl, + }); this.flagResolver = args.flagResolver; } diff --git a/src/lib/features/events/event-search-controller.ts b/src/lib/features/events/event-search-controller.ts index ee005a837b43..1872471cd4cd 100644 --- a/src/lib/features/events/event-search-controller.ts +++ b/src/lib/features/events/event-search-controller.ts @@ -47,9 +47,10 @@ export default class EventSearchController extends Controller { this.eventService = eventService; this.flagResolver = config.flagResolver; this.openApiService = openApiService; - this.msgFormatter = new FeatureEventFormatterMd( - config.server.unleashUrl, - ); + this.msgFormatter = new FeatureEventFormatterMd({ + unleashUrl: config.server.unleashUrl, + formatStyle: 'markdown', + }); this.route({ method: 'get',