-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #183 from atlassian/ARC-2308-add-backend-ffs
Arc-2308 add backend ffs
- Loading branch information
Showing
9 changed files
with
272 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
16.20.2 | ||
18.17.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
16.20.2 | ||
18.17.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const fetch = jest.fn(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
}; |