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

Changes 06-07 – Feature Toggles using fflip #6

Open
wants to merge 6 commits into
base: 06-web-security-and-rate-limiting
from

Some generated files are not rendered by default. Learn more.

@@ -29,6 +29,8 @@
"csurf": "^1.10.0",
"express": "^4.16.2",
"express-rate-limit": "^3.5.1",
"fflip": "^4.0.0",
"fflip-express": "^1.0.2",
"fs-extra": "^7.0.1",
"helmet": "^3.18.0",
"hot-shots": "^4.7.0",
@@ -17,6 +17,7 @@ import { RequestServices } from './types/CustomRequest'
import { addServicesToRequest } from './middlewares/ServiceDependenciesMiddleware'
import { Environment } from './Environment'
import { FrontendContext } from '../shared/FrontendContext'
import { applyFeatureToggles } from './middlewares/feature-toggles/setupFeatureToggles'

/**
* Abstraction around the raw Express.js server and Nodes' HTTP server.
@@ -34,6 +35,7 @@ export class ExpressServer {
const server = express()
this.setupStandardMiddlewares(server)
this.setupSecurityMiddlewares(server)
this.setupFeatureToggles(server)
this.applyWebpackDevMiddleware(server)
this.setupTelemetry(server)
this.setupServiceDependencies(server)
@@ -82,6 +84,10 @@ export class ExpressServer {
server.use('/api/', new RateLimit(baseRateLimitingOptions))
}

private setupFeatureToggles(server: Express) {
applyFeatureToggles(server)
}

private configureEjsTemplates(server: Express) {
server.set('views', [ 'resources/views' ])
server.set('view engine', 'ejs')
@@ -1,5 +1,6 @@
import { NextFunction, Request, Response } from 'express'
import * as HttpStatus from 'http-status-codes'
import { FeatureToggles } from '../middlewares/feature-toggles/features'

export class CatEndpoints {
public getCatDetails = async (req: Request, res: Response, next: NextFunction) => {
@@ -29,7 +30,11 @@ export class CatEndpoints {

public getCatsStatistics = async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(req.services.catService.getCatsStatistics())
if (req.fflip.has(FeatureToggles.WITH_CAT_STATISTICS)) {
res.json(req.services.catService.getCatsStatistics())
} else {
res.sendStatus(HttpStatus.NOT_FOUND)
}
} catch (err) {
next(err)
}
@@ -20,7 +20,10 @@ describe('CatEndpoints', () => {
}
sampleRequest = {
services: { catService },
params: { catId: 1 }
params: { catId: 1 },
fflip: {
has: sandbox.stub().returns(true)
}
}
})

@@ -81,6 +84,14 @@ describe('CatEndpoints', () => {
.expectJson({ amount: 30, averageAge: 50 })
})

it('should send status 404 if the feature toggle is deactivated', () => {
sampleRequest.fflip.has.returns(false)

return ExpressMocks.create(sampleRequest)
.test(endpoints.getCatsStatistics)
.expectSendStatus(HttpStatus.NOT_FOUND)
})

it('should handle thrown errors by passing them to NextFunction', () => {
const thrownError = new Error('Some problem with accessing the data')
catService.getCatsStatistics.throws(thrownError)
@@ -0,0 +1,12 @@
import * as fflip from 'fflip'

export const criteria: fflip.Criteria[] = [
{
id: 'isPaidUser',
check: (user: any, needsToBePaid: boolean) => user && user.isPaid === needsToBePaid
},
{
id: 'shareOfUsers',
check: (user: any, share: number) => user && user.id % 100 < share * 100
}
]
@@ -0,0 +1,17 @@
import * as fflip from 'fflip'

export const FeatureToggles: { [ key: string ]: string } = {
CLOSED_BETA: 'CLOSED_BETA',
WITH_CAT_STATISTICS: 'WITH_CAT_STATISTICS'
}

export const features: fflip.Feature[] = [
{
id: FeatureToggles.CLOSED_BETA,
criteria: { isPaidUser: true, shareOfUsers: 0.5 }
},
{
id: FeatureToggles.WITH_CAT_STATISTICS,
enabled: true
}
]
@@ -0,0 +1,23 @@
import { Express, NextFunction, Response, Request } from 'express'
import * as fflip from 'fflip'
import * as FFlipExpressIntegration from 'fflip-express'
import { criteria } from './criteria'
import { features } from './features'

export const applyFeatureToggles = (server: Express) => {
fflip.config({ criteria, features })
const fflipExpressIntegration = new FFlipExpressIntegration(fflip, {
cookieName: 'fflip',
manualRoutePath: '/api/toggles/local/:name/:action'
})

server.use(fflipExpressIntegration.middleware)
server.use((req: Request, _: Response, next: NextFunction) => {
try {
req.fflip.setForUser(req.user)
} catch (err) {
console.error('Error while binding feature toggles to req.user')
}
next()
})
}
@@ -0,0 +1,14 @@
// tslint:disable no-implicit-dependencies

import 'express-serve-static-core'

declare module 'express-serve-static-core' {
interface Request {
fflip: {
features: { [s: string]: boolean }
setForUser(user: any): void
has(featureName: string): boolean
}
user?: any
}
}
@@ -0,0 +1,22 @@
/* tslint:disable no-namespace */

declare module 'fflip-express' {
import * as FFlip from 'fflip'
import { CookieOptions, Handler } from 'express'

class FFlipExpressIntegration {
public middleware: Handler
public manualRoute: Handler
constructor(fflip: FFlip, options: FFlipExpressIntegration.Options)
}

namespace FFlipExpressIntegration {
export interface Options {
cookieName?: string
cookieOptions?: CookieOptions
manualRoutePath?: string
}
}

export = FFlipExpressIntegration
}
@@ -0,0 +1,45 @@
/* tslint:disable no-namespace */
declare module 'fflip' {

class FFlip { }

namespace FFlip {
type GetFeaturesSync = () => Feature[]
type GetFeaturesAsync = (callback: (features: Feature[]) => void) => void
type CriteriaConfig = StringMap | StringMap[]

export interface Config {
criteria: Criteria[]
features: Feature[] | GetFeaturesSync | GetFeaturesAsync
reload?: number
}

export interface Criteria {
id: string
check(user: any, config: any): boolean
}

export interface Feature {
id: string
criteria?: CriteriaConfig
enabled?: boolean
[s: string]: any
}

export interface Features {
[featureName: string]: boolean
}

export interface StringMap {
$veto?: boolean
[s: string]: any
}

export function config(config: Config): void
export function isFeatureEnabledForUser(featureName: string, user: any): boolean
export function getFeaturesForUser(user: any): Features
export function reload(): void
}

export = FFlip
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.