Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arc-2308 add backend ffs #183

Merged
merged 12 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator Author

@rachellerathbone rachellerathbone Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forge cli was complaining I was using an old version and was having issues running forge commands. Had to bump, which came parcelled with a need to change this. Will need to merge this PR straight after this one.

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 || '',
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These have been set for the appId using some forge cli commands. For more info see https://developer.atlassian.com/platform/forge/environments-and-versions/

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 });
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using fetch to call the LD API as we currently can't use the @launchdarkly/node-server-sdk due to Forge's lack of support for the Node.js runtime. https://hello.atlassian.net/wiki/spaces/OTFS/pages/2803689031/Forge+Limitations Work is being done to add this support and we can opt in by being onboarded to a feature flag but doing some removed the ability to run forge tunnel locally... Figured this was our next best option.


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}`);
}
};