Skip to content

Commit

Permalink
Merge pull request #183 from atlassian/ARC-2308-add-backend-ffs
Browse files Browse the repository at this point in the history
Arc-2308 add backend ffs
  • Loading branch information
rachellerathbone authored Oct 9, 2023
2 parents 97a98c9 + 95c9bda commit 8341f23
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 4 deletions.
2 changes: 1 addition & 1 deletion app/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.20.2
18.17.0
2 changes: 1 addition & 1 deletion app/jenkins-for-jira-ui/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.20.2
18.17.0
2 changes: 1 addition & 1 deletion app/jenkins-for-jira-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "jenkins-for-jira-ui",
"version": "0.1.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
"node": ">=18.0.0 <19.0.0"
},
"private": true,
"homepage": ".",
Expand Down
2 changes: 2 additions & 0 deletions app/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ permissions:
- 'https://app.launchdarkly.com'
- 'https://events.launchdarkly.com'
- 'https://clientstream.launchdarkly.com'
backend:
- 'https://app.launchdarkly.com/api/v2/flags/Jenkins'

content:
styles:
Expand Down
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"devDependencies": {
"@types/jest": "27.5.2",
"@types/jest-when": "3.5.2",
"@types/lodash": "^4.14.197",
"@types/jsonwebtoken": "8.5.9",
"@types/lodash": "^4.14.197",
"@typescript-eslint/eslint-plugin": "5.61.0",
"@typescript-eslint/parser": "5.61.0",
"eslint": "8.50.0",
Expand Down
1 change: 1 addition & 0 deletions app/src/__mocks__/@forge/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const fetch = jest.fn();
13 changes: 13 additions & 0 deletions app/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface EnvVars {
LAUNCHDARKLY_API_KEY: string;
LAUNCHDARKLY_APP_NAME: string;
}

const envVars: EnvVars = {
LAUNCHDARKLY_API_KEY: process.env.LAUNCHDARKLY_API_KEY || '',
LAUNCHDARKLY_APP_NAME: process.env.LAUNCHDARKLY_APP_NAME || 'jenkins-for-jira'
};

export type Environment = 'test' | 'development' | 'staging' | 'production';

export default envVars;
111 changes: 111 additions & 0 deletions app/src/config/feature-flag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { fetch as forgeFetch } from '@forge/api';
import { fetchFeatureFlag, LAUNCH_DARKLY_URL, launchDarklyService } from './feature-flags';
import envVars from './env';

jest.mock('@forge/api', () => ({
fetch: jest.fn(),
}));

const fetch = forgeFetch as jest.Mock;

describe('fetchFeatureFlag', () => {
afterEach(() => {
fetch.mockClear();
});

it('should return true when flag is on', async () => {
const mockFeatureFlagData = {
name: 'test-flag',
kind: 'boolean',
environments: {
test: {
on: true,
archived: false,
}
},
};

fetch.mockResolvedValueOnce({
status: 200,
json: async () => mockFeatureFlagData,
});

const result = await fetchFeatureFlag('test-flag', 'test');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('test-flag'), expect.anything());
expect(result).toEqual(true);
});

it('should return false when flag is off', async () => {
const mockFeatureFlagData = {
name: 'test-flag',
kind: 'boolean',
environments: {
test: {
on: false,
archived: false,
}
},
};

fetch.mockResolvedValueOnce({
status: 200,
json: async () => mockFeatureFlagData,
});

const result = await fetchFeatureFlag('test-flag', 'test');
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('test-flag'), expect.anything());
expect(result).toEqual(false);
});

it('handles fetch errors', async () => {
fetch.mockRejectedValueOnce(new Error('Fetch error'));
await expect(fetchFeatureFlag('test-flag', 'test')).rejects.toThrow('Fetch error');
});
});

describe('launchDarklyService', () => {
it('returns the correct product app name', () => {
expect(launchDarklyService.getProductAppName()).toEqual(envVars.LAUNCHDARKLY_APP_NAME);
});

it('returns the correct product app page URL', () => {
expect(launchDarklyService.getProductAppPageUrl())
.toEqual(`https://app.launchdarkly.com/${envVars.LAUNCHDARKLY_APP_NAME}`);
});

it('returns the correct feature flag page URL for testing environment', () => {
const featureFlagKey = 'yourFeatureFlagKey';
const environment = 'testing';

const expectedUrl =
`${LAUNCH_DARKLY_URL}/${envVars.LAUNCHDARKLY_APP_NAME}/${environment}/features/${featureFlagKey}`;
expect(launchDarklyService.getFeatureFlagPageUrl(featureFlagKey, environment)).toEqual(expectedUrl);
});

it('returns the correct feature flag page URL for development environment', () => {
const featureFlagKey = 'yourFeatureFlagKey';
const environment = 'development';

const expectedUrl =
`${LAUNCH_DARKLY_URL}/${envVars.LAUNCHDARKLY_APP_NAME}/${environment}/features/${featureFlagKey}`;
expect(launchDarklyService.getFeatureFlagPageUrl(featureFlagKey, environment)).toEqual(expectedUrl);
});

it('returns the correct feature flag page URL for staging environment', () => {
const featureFlagKey = 'yourFeatureFlagKey';
const environment = 'staging';

const expectedUrl =
`${LAUNCH_DARKLY_URL}/${envVars.LAUNCHDARKLY_APP_NAME}/${environment}/features/${featureFlagKey}`;
expect(launchDarklyService.getFeatureFlagPageUrl(featureFlagKey, environment)).toEqual(expectedUrl);
});

it('returns the correct feature flag page URL for production environment', () => {
const featureFlagKey = 'yourFeatureFlagKey';
const environment = 'production';

const expectedUrl =
`${LAUNCH_DARKLY_URL}/${envVars.LAUNCHDARKLY_APP_NAME}/${environment}/features/${featureFlagKey}`;
expect(launchDarklyService.getFeatureFlagPageUrl(featureFlagKey, environment)).toEqual(expectedUrl);
});
});
141 changes: 141 additions & 0 deletions app/src/config/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { fetch } from '@forge/api';
import { Logger } from './logger';
import envVars, { Environment } from './env';

interface Variation {
_id: string;
value: boolean;
}

interface EnvironmentData {
on: boolean;
archived: boolean;
salt: string;
sel: string;
lastModified: number;
version: number;
targets: any[];
contextTargets: any[];
rules: any[];
fallthrough: any;
offVariation: number;
prerequisites: any[];
_site: any;
_environmentName: string;
trackEvents: boolean;
trackEventsFallthrough: boolean;
_summary: any;
}

interface FeatureFlag {
name: string;
kind: string;
description: string;
key: string;
_version: number;
creationDate: number;
includeInSnippet: boolean;
clientSideAvailability: {
usingMobileKey: boolean;
usingEnvironmentId: boolean;
};
variations: Variation[];
variationJsonSchema: null | any;
temporary: boolean;
tags: string[];
_links: {
parent: {
href: string;
type: string;
};
self: {
href: string;
type: string;
};
};
maintainerId: string;
_maintainer: {
_links: {
self: any;
};
_id: string;
firstName: string;
lastName: string;
role: string;
email: string;
};
goalIds: string[];
experiments: {
baselineIdx: number;
items: any[];
};
customProperties: any;
archived: boolean;
defaults: {
onVariation: number;
offVariation: number;
};
environments: {
development: EnvironmentData;
production: EnvironmentData;
staging: EnvironmentData;
test: EnvironmentData;
};
}

export const LAUNCH_DARKLY_URL = `https://app.launchdarkly.com`;
const BASE_URL = `${LAUNCH_DARKLY_URL}/api/v2/flags/${envVars.LAUNCHDARKLY_APP_NAME}`;

const baseHeaders = {
headers: {
Authorization: envVars.LAUNCHDARKLY_API_KEY
}
};

const logger = Logger.getInstance('featureFlags');

async function getFeatureFlag(featureFlagKey: string): Promise<FeatureFlag> {
const eventType = 'retrievingFeatureFlag';
const errorMsg = 'fetching feature flag unexpected status';

try {
const response = await fetch(`${BASE_URL}/${featureFlagKey}`, { ...baseHeaders });

if (response.status === 200) {
logger.logInfo({ eventType, data: { message: `Successfully retrieved ${featureFlagKey}` } });
return await response.json();
}

logger.logWarn({ eventType: `${eventType}Error`, errorMsg });
throw new Error(errorMsg);
} catch (error) {
logger.logWarn({ eventType: `${eventType}Error`, errorMsg, error });
throw error;
}
}

export const launchDarklyService = {
getProductAppName: () => envVars.LAUNCHDARKLY_APP_NAME,
getProductAppPageUrl: () => `${LAUNCH_DARKLY_URL}/${envVars.LAUNCHDARKLY_APP_NAME}`,
getFeatureFlag,
getFeatureFlagPageUrl: (featureFlagKey: string, environment: string) =>
`${LAUNCH_DARKLY_URL}/${envVars.LAUNCHDARKLY_APP_NAME}/${environment}/features/${featureFlagKey}`
};

export const fetchFeatureFlag =
async (featureFlagKey: string, env: Environment): Promise<boolean | null> => {
try {
const environment: Environment = env?.toLowerCase() as Environment;
const featureFlag = await launchDarklyService.getFeatureFlag(featureFlagKey);
const envData = featureFlag.environments[environment];
return envData?.on || false;
} catch (error) {
logger.logError({
eventType: 'fetchFeatureFlagError',
errorMsg: 'Error fetching feature flag:',
error
});

throw new Error(`Failed to retrieve feature flag: ${error}`);
}
};

0 comments on commit 8341f23

Please sign in to comment.