mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: productivity report trends visualization (#8956)
This commit is contained in:
		
							parent
							
								
									311df82d37
								
							
						
					
					
						commit
						48b21591f6
					
				@ -126,6 +126,11 @@ test('Can send productivity report email', async () => {
 | 
			
		||||
            flagsCreated: 1,
 | 
			
		||||
            productionUpdates: 2,
 | 
			
		||||
            health: 99,
 | 
			
		||||
            previousMonth: {
 | 
			
		||||
                health: 89,
 | 
			
		||||
                flagsCreated: 1,
 | 
			
		||||
                productionUpdates: 3,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
    expect(content.from).toBe('noreply@getunleash.ai');
 | 
			
		||||
@ -133,9 +138,11 @@ test('Can send productivity report email', async () => {
 | 
			
		||||
    expect(content.html.includes(`Productivity Report`)).toBe(true);
 | 
			
		||||
    expect(content.html.includes(`localhost/insights`)).toBe(true);
 | 
			
		||||
    expect(content.html.includes(`localhost/profile`)).toBe(true);
 | 
			
		||||
    expect(content.html.includes(`#b0d182`)).toBe(true);
 | 
			
		||||
    expect(content.html.includes('#68a611')).toBe(true);
 | 
			
		||||
    expect(content.html.includes(`10% more than previous month`)).toBe(true);
 | 
			
		||||
    expect(content.text.includes(`localhost/insights`)).toBe(true);
 | 
			
		||||
    expect(content.text.includes(`localhost/profile`)).toBe(true);
 | 
			
		||||
    expect(content.text.includes(`localhost/profile`)).toBe(true);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Should add optional headers to productivity email', async () => {
 | 
			
		||||
@ -170,6 +177,7 @@ test('Should add optional headers to productivity email', async () => {
 | 
			
		||||
            flagsCreated: 1,
 | 
			
		||||
            productionUpdates: 2,
 | 
			
		||||
            health: 99,
 | 
			
		||||
            previousMonth: null,
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,10 @@ import { existsSync, readFileSync } from 'fs';
 | 
			
		||||
import type { Logger } from '../logger';
 | 
			
		||||
import NotFoundError from '../error/notfound-error';
 | 
			
		||||
import type { IUnleashConfig } from '../types/option';
 | 
			
		||||
import {
 | 
			
		||||
    type ProductivityReportMetrics,
 | 
			
		||||
    productivityReportViewModel,
 | 
			
		||||
} from '../../mailtemplates/productivity-report/productivity-report-view-model';
 | 
			
		||||
 | 
			
		||||
export interface IAuthOptions {
 | 
			
		||||
    user: string;
 | 
			
		||||
@ -460,29 +464,15 @@ export class EmailService {
 | 
			
		||||
    async sendProductivityReportEmail(
 | 
			
		||||
        userEmail: string,
 | 
			
		||||
        userName: string,
 | 
			
		||||
        metrics: {
 | 
			
		||||
            health: number;
 | 
			
		||||
            flagsCreated: number;
 | 
			
		||||
            productionUpdates: number;
 | 
			
		||||
        },
 | 
			
		||||
        metrics: ProductivityReportMetrics,
 | 
			
		||||
    ): Promise<IEmailEnvelope> {
 | 
			
		||||
        if (this.configured()) {
 | 
			
		||||
            const context = {
 | 
			
		||||
                userName,
 | 
			
		||||
            const context = productivityReportViewModel({
 | 
			
		||||
                metrics,
 | 
			
		||||
                userEmail,
 | 
			
		||||
                ...metrics,
 | 
			
		||||
                userName,
 | 
			
		||||
                unleashUrl: this.config.server.unleashUrl,
 | 
			
		||||
                healthColor() {
 | 
			
		||||
                    const healthRating = this.health;
 | 
			
		||||
                    const healthColor =
 | 
			
		||||
                        healthRating >= 0 && healthRating <= 24
 | 
			
		||||
                            ? '#d93644'
 | 
			
		||||
                            : healthRating >= 25 && healthRating <= 74
 | 
			
		||||
                              ? '#ffc46f'
 | 
			
		||||
                              : '#b0d182';
 | 
			
		||||
                    return healthColor;
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const template = 'productivity-report';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,181 @@
 | 
			
		||||
import {
 | 
			
		||||
    productivityReportViewModel,
 | 
			
		||||
    type ProductivityReportMetrics,
 | 
			
		||||
} from './productivity-report-view-model';
 | 
			
		||||
 | 
			
		||||
const mockData = {
 | 
			
		||||
    unleashUrl: 'http://example.com',
 | 
			
		||||
    userEmail: 'user@example.com',
 | 
			
		||||
    userName: 'Test User',
 | 
			
		||||
};
 | 
			
		||||
const mockMetrics = {
 | 
			
		||||
    health: 0,
 | 
			
		||||
    flagsCreated: 0,
 | 
			
		||||
    productionUpdates: 0,
 | 
			
		||||
    previousMonth: {
 | 
			
		||||
        health: 0,
 | 
			
		||||
        flagsCreated: 0,
 | 
			
		||||
        productionUpdates: 0,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('productivityReportViewModel', () => {
 | 
			
		||||
    describe('healthColor', () => {
 | 
			
		||||
        it('returns RED for health between 0 and 24', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                health: 20,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthColor()).toBe('#d93644');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('returns ORANGE for health between 25 and 74', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                health: 50,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthColor()).toBe('#d76500');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('returns GREEN for health 75 or above', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                health: 80,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthColor()).toBe('#68a611');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('healthTrendMessage', () => {
 | 
			
		||||
        it('returns correct trend message when health increased', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                health: 80,
 | 
			
		||||
                previousMonth: { ...mockMetrics.previousMonth, health: 70 },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthTrendMessage()).toBe(
 | 
			
		||||
                "<span style='color: #68a611'>▲</span> 10% more than previous month",
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('returns correct trend message when health decreased', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                health: 60,
 | 
			
		||||
                previousMonth: { ...mockMetrics.previousMonth, health: 70 },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthTrendMessage()).toBe(
 | 
			
		||||
                "<span style='color: #d93644'>▼</span> 10% less than previous month",
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('returns correct message when health is the same', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                health: 70,
 | 
			
		||||
                previousMonth: { ...mockMetrics.previousMonth, health: 70 },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthTrendMessage()).toBe('Same as last month');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('flagsCreatedTrendMessage', () => {
 | 
			
		||||
        it('returns correct trend message for flagsCreated increase', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                flagsCreated: 10,
 | 
			
		||||
                previousMonth: {
 | 
			
		||||
                    ...mockMetrics.previousMonth,
 | 
			
		||||
                    flagsCreated: 8,
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.flagsCreatedTrendMessage()).toBe(
 | 
			
		||||
                "<span style='color: #68a611'>▲</span> 2 more than previous month",
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('productionUpdatedTrendMessage', () => {
 | 
			
		||||
        it('returns correct trend message for productionUpdates decrease', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                ...mockMetrics,
 | 
			
		||||
                productionUpdates: 5,
 | 
			
		||||
                previousMonth: {
 | 
			
		||||
                    ...mockMetrics.previousMonth,
 | 
			
		||||
                    productionUpdates: 8,
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.productionUpdatedTrendMessage()).toBe(
 | 
			
		||||
                "<span style='color: #d93644'>▼</span> 3 less than previous month",
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Missing previous month data', () => {
 | 
			
		||||
        it('returns no trends messages', () => {
 | 
			
		||||
            const metrics: ProductivityReportMetrics = {
 | 
			
		||||
                health: 100,
 | 
			
		||||
                flagsCreated: 10,
 | 
			
		||||
                productionUpdates: 5,
 | 
			
		||||
                previousMonth: null,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const viewModel = productivityReportViewModel({
 | 
			
		||||
                ...mockData,
 | 
			
		||||
                metrics,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            expect(viewModel.healthTrendMessage()).toBe(null);
 | 
			
		||||
            expect(viewModel.flagsCreatedTrendMessage()).toBe(null);
 | 
			
		||||
            expect(viewModel.productionUpdatedTrendMessage()).toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
export type ProductivityReportMetrics = {
 | 
			
		||||
    health: number;
 | 
			
		||||
    flagsCreated: number;
 | 
			
		||||
    productionUpdates: number;
 | 
			
		||||
    previousMonth: {
 | 
			
		||||
        health: number;
 | 
			
		||||
        flagsCreated: number;
 | 
			
		||||
        productionUpdates: number;
 | 
			
		||||
    } | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const RED = '#d93644';
 | 
			
		||||
const GREEN = '#68a611';
 | 
			
		||||
const ORANGE = '#d76500';
 | 
			
		||||
 | 
			
		||||
export const productivityReportViewModel = ({
 | 
			
		||||
    unleashUrl,
 | 
			
		||||
    userEmail,
 | 
			
		||||
    userName,
 | 
			
		||||
    metrics,
 | 
			
		||||
}: {
 | 
			
		||||
    unleashUrl: string;
 | 
			
		||||
    userEmail: string;
 | 
			
		||||
    userName: string;
 | 
			
		||||
    metrics: ProductivityReportMetrics;
 | 
			
		||||
}) => ({
 | 
			
		||||
    userName,
 | 
			
		||||
    userEmail,
 | 
			
		||||
    ...metrics,
 | 
			
		||||
    unleashUrl,
 | 
			
		||||
    healthColor() {
 | 
			
		||||
        const healthRating = this.health;
 | 
			
		||||
        const healthColor =
 | 
			
		||||
            healthRating >= 0 && healthRating <= 24
 | 
			
		||||
                ? RED
 | 
			
		||||
                : healthRating >= 25 && healthRating <= 74
 | 
			
		||||
                  ? ORANGE
 | 
			
		||||
                  : GREEN;
 | 
			
		||||
        return healthColor;
 | 
			
		||||
    },
 | 
			
		||||
    healthTrendMessage() {
 | 
			
		||||
        return this.previousMonthText(
 | 
			
		||||
            '%',
 | 
			
		||||
            this.health,
 | 
			
		||||
            this.previousMonth?.health,
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
    flagsCreatedTrendMessage() {
 | 
			
		||||
        return this.previousMonthText(
 | 
			
		||||
            '',
 | 
			
		||||
            this.flagsCreated,
 | 
			
		||||
            this.previousMonth?.flagsCreated,
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
    productionUpdatedTrendMessage() {
 | 
			
		||||
        return this.previousMonthText(
 | 
			
		||||
            '',
 | 
			
		||||
            this.productionUpdates,
 | 
			
		||||
            this.previousMonth?.productionUpdates,
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
    previousMonthText(
 | 
			
		||||
        unit: '' | '%',
 | 
			
		||||
        currentValue: number,
 | 
			
		||||
        previousValue?: number,
 | 
			
		||||
    ) {
 | 
			
		||||
        if (previousValue == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        if (currentValue > previousValue) {
 | 
			
		||||
            return `<span style='color: ${GREEN}'>▲</span> ${currentValue - previousValue}${unit} more than previous month`;
 | 
			
		||||
        }
 | 
			
		||||
        if (previousValue > currentValue) {
 | 
			
		||||
            return `<span style='color: ${RED}'>▼</span> ${previousValue - currentValue}${unit} less than previous month`;
 | 
			
		||||
        }
 | 
			
		||||
        return `Same as last month`;
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
@ -28,7 +28,8 @@
 | 
			
		||||
                    style="margin: 0;padding: 36px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
 | 
			
		||||
                    <div style="padding-top: 12px;">
 | 
			
		||||
                        <span style="color: {{healthColor}};">{{health}}%</span><br>
 | 
			
		||||
                        <span style="font-size: 16px; color: #888;">your instance health</span>
 | 
			
		||||
                        <span style="font-size: 16px; color: #1A4049; font-weight: 700">your instance health</span><br>
 | 
			
		||||
                        <span style="font-size: 12px; color: #6E6E70; font-weight: 400; line-height: 14px">{{{healthTrendMessage}}}</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -40,18 +41,20 @@
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td style="text-align: center;">
 | 
			
		||||
                        <div class="shaded"
 | 
			
		||||
                            style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
 | 
			
		||||
                            <span
 | 
			
		||||
                                style="font-size: 24px; font-weight: bold; color: #1a4049;">{{flagsCreated}}</span><br>
 | 
			
		||||
                            <span style="color: #888;display: block;height: 20px;">flags created last month</span>
 | 
			
		||||
                             style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
 | 
			
		||||
                                <span
 | 
			
		||||
                                    style="font-size: 24px; font-weight: bold; color: #1a4049;">{{flagsCreated}}</span><br>
 | 
			
		||||
                            <span style="color: #1A4049;display: block;height: 16px;">flags created</span><br>
 | 
			
		||||
                            <span style="font-size: 12px; color: #6E6E70; font-weight: 400;">{{{flagsCreatedTrendMessage}}}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td style="text-align: center;">
 | 
			
		||||
                        <div class="shaded"
 | 
			
		||||
                            style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
 | 
			
		||||
                            <span
 | 
			
		||||
                                style="font-size: 24px; font-weight: bold; color: #1a4049;">{{productionUpdates}}</span><br>
 | 
			
		||||
                            <span style="color: #888;display: block;height: 20px;">production updates last month</span>
 | 
			
		||||
                             style="margin: 0;padding: 42px 8px;background: #f0f0f5;border-width: 3px;border-color: #ffffff;border-style: solid;">
 | 
			
		||||
                                <span
 | 
			
		||||
                                    style="font-size: 24px; font-weight: bold; color: #1a4049;">{{productionUpdates}}</span><br>
 | 
			
		||||
                            <span style="color: #1A4049;display: block;height: 16px;">production updates</span><br>
 | 
			
		||||
                            <span style="font-size: 12px; color: #6E6E70; font-weight: 400;">{{{productionUpdatedTrendMessage}}}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user