UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

253 lines • 11.6 kB
import Mustache from 'mustache'; import { FEATURE_ARCHIVED, FEATURE_STRATEGY_UPDATE, } from '../events/index.js'; import { EVENT_MAP } from './feature-event-formatter-md-events.js'; export var LinkStyle; (function (LinkStyle) { LinkStyle[LinkStyle["SLACK"] = 0] = "SLACK"; LinkStyle[LinkStyle["MD"] = 1] = "MD"; })(LinkStyle || (LinkStyle = {})); // 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 { constructor({ unleashUrl, linkStyle = LinkStyle.MD, formatStyle = 'simple', }) { 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) { const boldChar = this.formatStyle === 'simple' ? '*' : '**'; return text ? `${boldChar}${text}${boldChar}` : boldChar; } generateChangeRequestLink(event) { const { preData, data, project, environment } = event; const changeRequestId = data?.changeRequestId || preData?.changeRequestId; if (project && changeRequestId) { const url = `${this.unleashUrl}/projects/${project}/change-requests/${changeRequestId}`; const text = `#${changeRequestId}`; const featureLink = this.generateFeatureLink(event); const featureText = featureLink ? ` for feature flag ${this.bold(featureLink)}` : ''; const environmentText = environment ? ` in the ${this.bold(environment)} environment` : ''; const projectLink = this.generateProjectLink(event); const projectText = project ? ` in project ${this.bold(projectLink)}` : ''; if (this.linkStyle === LinkStyle.SLACK) { return `${this.bold(`<${url}|${text}>`)}${featureText}${environmentText}${projectText}`; } else { return `${this.bold(`[${text}](${url})`)}${featureText}${environmentText}${projectText}`; } } } featureLink(event) { const { type, project = '', featureName } = event; if (type === FEATURE_ARCHIVED) { if (project) { return `${this.unleashUrl}/projects/${project}/archive`; } return `${this.unleashUrl}/archive`; } if (featureName) { return `${this.unleashUrl}/projects/${project}/features/${featureName}`; } } generateFeatureLink(event) { if (event.featureName) { if (this.linkStyle === LinkStyle.SLACK) { return `<${this.featureLink(event)}|${event.featureName}>`; } else { return `[${event.featureName}](${this.featureLink(event)})`; } } } generateProjectLink(event) { if (event.project) { if (this.linkStyle === LinkStyle.SLACK) { return `<${this.unleashUrl}/projects/${event.project}|${event.project}>`; } else { return `[${event.project}](${this.unleashUrl}/projects/${event.project})`; } } } getStrategyTitle(event) { return (event.preData?.title || event.data?.title || event.preData?.name || event.data?.name); } generateFeatureStrategyChangeText(event) { const { environment, data, preData, type } = event; if (type === FEATURE_STRATEGY_UPDATE && (data || preData)) { const strategyText = () => { switch ((data || preData).name) { case 'flexibleRollout': return this.flexibleRolloutStrategyChangeText(event); case 'default': return this.defaultStrategyChangeText(event); case 'remoteAddress': return this.remoteAddressStrategyChangeText(event); case 'applicationHostname': return this.applicationHostnameStrategyChangeText(event); default: return `by updating strategy ${this.bold(this.getStrategyTitle(event))} in ${this.bold(environment)}`; } }; return strategyText(); } } applicationHostnameStrategyChangeText(event) { return this.listOfValuesStrategyChangeText(event, 'hostNames'); } remoteAddressStrategyChangeText(event) { return this.listOfValuesStrategyChangeText(event, 'IPs'); } listOfValuesStrategyChangeText(event, propertyName) { const { preData, data, environment } = event; const userIdText = (values) => values.length === 0 ? `empty set of ${propertyName}` : `[${values}]`; const usersText = preData?.parameters[propertyName] === data?.parameters[propertyName] ? '' : !preData ? ` ${propertyName} to ${userIdText(data?.parameters[propertyName])}` : ` ${propertyName} from ${userIdText(preData.parameters[propertyName])} to ${userIdText(data?.parameters[propertyName])}`; const constraintText = this.constraintChangeText(preData?.constraints, data?.constraints); const segmentsText = this.segmentsChangeText(preData?.segments, data?.segments); const strategySpecificText = [usersText, constraintText, segmentsText] .filter((x) => x.length) .join(';'); return `by updating strategy ${this.bold(this.getStrategyTitle(event))} in ${this.bold(environment)}${strategySpecificText}`; } flexibleRolloutStrategyChangeText(event) { const { preData, data, environment } = event; const { rollout: oldRollout, stickiness: oldStickiness, groupId: oldGroupId, } = preData?.parameters || {}; const { rollout, stickiness, groupId } = data?.parameters || {}; const stickinessText = oldStickiness === stickiness ? '' : !oldStickiness ? ` stickiness to ${stickiness}` : ` stickiness from ${oldStickiness} to ${stickiness}`; const rolloutText = oldRollout === rollout ? '' : !oldRollout ? ` rollout to ${rollout}%` : ` rollout from ${oldRollout}% to ${rollout}%`; const groupIdText = oldGroupId === groupId ? '' : !oldGroupId ? ` groupId to ${groupId}` : ` groupId from ${oldGroupId} to ${groupId}`; const constraintText = this.constraintChangeText(preData?.constraints, data?.constraints); const segmentsText = this.segmentsChangeText(preData?.segments, data?.segments); const strategySpecificText = [ stickinessText, rolloutText, groupIdText, constraintText, segmentsText, ] .filter((txt) => txt.length) .join(';'); return `by updating strategy ${this.bold(this.getStrategyTitle(event))} in ${this.bold(environment)}${strategySpecificText}`; } defaultStrategyChangeText(event) { const { preData, data, environment } = event; const constraintText = this.constraintChangeText(preData?.constraints, data?.constraints); const segmentsText = this.segmentsChangeText(preData?.segments, data?.segments); const strategySpecificText = [constraintText, segmentsText] .filter((txt) => txt.length) .join(';'); return `by updating strategy ${this.bold(this.getStrategyTitle(event))} in ${this.bold(environment)}${strategySpecificText}`; } constraintChangeText(oldConstraints = [], newConstraints = []) { const formatConstraints = (constraints) => { const constraintOperatorDescriptions = { IN: 'is one of', NOT_IN: 'is not one of', STR_CONTAINS: 'is a string that contains', STR_STARTS_WITH: 'is a string that starts with', STR_ENDS_WITH: 'is a string that ends with', NUM_EQ: 'is a number equal to', NUM_GT: 'is a number greater than', NUM_GTE: 'is a number greater than or equal to', NUM_LT: 'is a number less than', NUM_LTE: 'is a number less than or equal to', DATE_BEFORE: 'is a date before', DATE_AFTER: 'is a date after', SEMVER_EQ: 'is a SemVer equal to', SEMVER_GT: 'is a SemVer greater than', SEMVER_LT: 'is a SemVer less than', }; const formatConstraint = (constraint) => { const val = constraint.hasOwnProperty('value') ? constraint.value : `(${constraint.values?.join(',')})`; const operator = constraintOperatorDescriptions.hasOwnProperty(constraint.operator) ? constraintOperatorDescriptions[constraint.operator] : constraint.operator; return `${constraint.contextName} ${constraint.inverted ? 'not ' : ''}${operator} ${val}`; }; return constraints.length === 0 ? 'empty set of constraints' : `[${constraints.map(formatConstraint).join(', ')}]`; }; const oldConstraintText = formatConstraints(oldConstraints); const newConstraintText = formatConstraints(newConstraints); return oldConstraintText === newConstraintText ? '' : ` constraints from ${oldConstraintText} to ${newConstraintText}`; } segmentsChangeText(oldSegments = [], newSegments = []) { const formatSegments = (segments) => { return segments.length === 0 ? 'empty set of segments' : `(${segments.join(',')})`; }; const oldSegmentsText = formatSegments(oldSegments); const newSegmentsText = formatSegments(newSegments); return oldSegmentsText === newSegmentsText ? '' : ` segments from ${oldSegmentsText} to ${newSegmentsText}`; } format(event) { const { createdBy, type } = event; const { label, action, path } = EVENT_MAP[type] || { label: type, action: `${this.bold(createdBy)} triggered ${this.bold(type)}`, }; const formatting = { b: this.bold(), }; const context = { user: createdBy, event, strategyTitle: this.getStrategyTitle(event), strategyChangeText: this.generateFeatureStrategyChangeText(event), changeRequest: this.generateChangeRequestLink(event), feature: this.generateFeatureLink(event), project: this.generateProjectLink(event), ...formatting, }; Mustache.escape = (text) => text; const text = Mustache.render(action, context); const url = path ? `${this.unleashUrl}${Mustache.render(path, context)}` : undefined; return { label, text, url, }; } } //# sourceMappingURL=feature-event-formatter-md.js.map