Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Seb Duggan
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Seb Duggan@sebduggan )

Locating LaunchDarkly Feature Flag References In Your Application Code In Lucee 5.3.2.77

By Ben Nadel on
Tags: ColdFusion, Work

At InVision, we've been using - and loving - LaunchDarkly feature flags for the last 3-years. Feature flags have completely changed the way that we approach application development. But, it's not all roses and unicorns. Feature flags also introduce a new type of technical debt which, left unchecked, will lead to critical code rot within the application. Ideally, all non-operational feature flags should be removed from the code once a feature is rolled-out. But, this rarely happens. Which means, the application becomes littered with obfuscated, misleading, and out-dated control-flows. As such, I wanted to put together a small Lucee 5.3.2.77 utility that would help me analyze my ColdFUsion and JavaScript code, shinning a light on which feature flags might be removed from the application.

The underpinnings of this Lucee CFML utility entail a fairly straight-forward, brute-force workflow:

  1. Look up feature flags in the remote LaunchDarkly API.
  2. Find reference to those feature flags in the local application code.
  3. Present the feature flag references to the user in a sortable manner.

At first, this seems like a ridiculous approach. But, then one remembers that computers are hella fast. And, we can sprinkle in caching to make the consumption of the data more enjoyable. With that said, this demo has only two files: the locator and the renderer. Let's look at the renderer first so we can see how this demo works.

The renderer for the demo takes the results of the feature flag locator and presents them to the user. It's unclear how the data will be interpreted; so, we're going to provide some sorting options:

  • Sort by date of feature flag creation.
  • Sort by count of references in the application code.

With these options, the user (ie, the developer) can examine the collection of feature flags using different perspectives. And then, hopefully, formulate a plan for removing rotting feature flags from the application.

Here's the renderer. Note that the actual search is being performed inside a CFML closure that uses the cachedWithin function memoization feature. This way, the user can re-render the page using different sorting options without having to incur the relatively high cost of the search algorithm on every page refresh.

NOTE: My LaunchDarkly credentials are being read from a different file for security purposes. They live in a simple JSON file for this demo.

<cfscript>

	param name="url.sortOn" type="string" default="createdAt";
	param name="url.sortDir" type="numeric" default="-1";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	credentials = deserializeJson( fileRead( "./credentials.json" ) );

	// NOTE: Since scouring the file system is a SLOW PROCESS, we're going to wrap the
	// call in a Closure that uses process caching. This way, we can refresh this page
	// to examine the feature flag usage without having to re-process the search each and
	// every time.
	// --
	// This Immediately Invoked Function Expression (IIFE) uses a static string argument
	// in order to bust the cache as needed during the algorithm development process.
	results = (
		function() cachedWithin = createTimeSpan( 0, 1, 0, 0 ) {

			var locator = new FeatureFlagLocator(
				launchDarklyAccessToken = credentials.accessToken,
				launchDarklyProjectKey = credentials.projectKey,
				launchDarklyEnvironmentKey = credentials.environmentKey
			);

			var results = locator.searchForFeatureFlags(
				directories = [
					expandPath( "../../assets/apps/" ),
					expandPath( "../../cflibs/" ),
					expandPath( "../../subsystems/" )
				],
				fileExtensions = "cfc,cfm,js"
			);

			return( results );

		}
	)( "cache-version: 1" );

	sortedResults = results.sort(
		( a, b ) => {

			var aValue = a[ url.sortOn ];
			var bValue = b[ url.sortOn ];

			if ( aValue == bValue ) {

				return( 0 );

			}

			return( ( aValue < bValue ) ? url.sortDir : -url.sortDir );

		}
	);

</cfscript>
<cfoutput>

	<!doctype html>
	<html lang="en">
	<head>
		<meta charset="utf-8" />

		<title>
			LaunchDarkly Feature Flag Locator
		</title>

		<style type="text/css">
			body {
				font-size: 18px ;
			}
		</style>
	</head>
	<body>

		<h1>
			LaunchDarkly Feature Flag Locator
		</h1>

		<p>
			<strong>Sort By:</strong>
			<a href="#cgi.script_name#?sortOn=createdAt&sortDir=-1">Date-ASC</a>
			<a href="#cgi.script_name#?sortOn=createdAt&sortDir=1">Date-DESC</a>
			&mdash;
			<a href="#cgi.script_name#?sortOn=codeCount&sortDir=-1">References-ASC</a>
			<a href="#cgi.script_name#?sortOn=codeCount&sortDir=1">References-DESC</a>
		</p>

		<h2>
			Results
		</h2>

		<ul>
			<cfloop index="item" array="#sortedResults#">

				<!---
					Since feature flag keys represent some degree of internal information,
					let's obfuscate the key for the demo.
				--->
				<cfset keyish = ( item.key.left( 5 ) & "*".repeatString( item.key.len() ) ) />

				<li>
					<strong>#encodeForHtml( keyish )#</strong>
					created on
					#item.createdAt.dateFormat( "yyyy-mm-dd" )#<br />

					<cfloop index="filePath" array="#item.codeReferences#">
						- #encodeForHtml( getFileFromPath( filePath ) )#<br />
					</cfloop>
				</li>

			</cfloop>
		</ul>

	</body>
	</html>	

</cfoutput>

The renderer doesn't really do very much. It invokes the search; then, it lists each LaunchDarkly feature flag - by the desired sort - along with its date of creation and the collection of files that reference the feature flag key. If we run this Lucee CFML code, we get the following output:

Results of the LaunchDarkly feature flag locator in Lucee 5.3.2.77.

As you can see, there are a number of LaunchDarkly feature flags that aren't referenced in the application code at all. Those should be removed / archived within LaunchDarkly in order to reduce noise. But, for those LaunchDarkly feature flags that are referenced, we can quickly see how many times they are references in the code. The fewer the references, the easier it should be (theoretically) to remove them. And, we can cross-reference those counts with the creation-date to help determine which feature flags are more likely to be irrelevant in the current application state.

ASIDE: In our case, some of those non-referenced feature flags are owned by different applications. That was a mistake that we made as a company - commingling feature flags from different applications in the same LaunchDarkly project context.

Now, let's look at the powerhouse of this demo - the feature flag locator. As I outlined above, the locator is just a brute-force iteration over the file-system looking for feature flag references. It exposes one public method, searchForFeatureFlags(), which takes a list of directories and file-extensions to search. Internally, it's quite procedural in nature:

component
	output = false
	hint = "I help find LaunchDarkly feature flag references in the code."
	{

	/**
	* I initialize the feature flag locator with the given LaunchDarkly credentials.
	* 
	* @launchDarklyAccessToken I am the provisioned API key for this task (should be READ ONLY).
	* @launchDarklyProjectKey I am the project key in which to search.
	* @launchDarklyEnvironmentKey I am the environment in which to search.
	*/
	public any function init(
		required string launchDarklyAccessToken,
		required string launchDarklyProjectKey,
		required string launchDarklyEnvironmentKey
		) {

		variables.launchDarklyAccessToken = arguments.launchDarklyAccessToken;
		variables.launchDarklyProjectKey = arguments.launchDarklyProjectKey;
		variables.launchDarklyEnvironmentKey = arguments.launchDarklyEnvironmentKey;

		return( this );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I search for LaunchDarkly feature flag references in the given directory, filtering
	* on the given file extensions list. Returns an array of structs with the following
	* keys:
	* 
	* - name
	* - key
	* - createdAt
	* - codeReferences
	* - codeCount
	* 
	* @directories I am the directories in which to search files.
	* @fileExtensions I am the comma-delimited list of extensions on which to filter (inclusive).
	*/
	public array function searchForFeatureFlags(
		required array directories,
		required string fileExtensions
		) {

		var flags = getLaunchDarklyFeatureFlags();
		var pattern = createRegexPattern( flags );
		var filePaths = getFilePaths( directories, fileExtensions );

		// The results for this method is an augmented collection of feature flags that
		// also contains the list of files in which each feature flag was referenced.
		var results = flags.map(
			( flag ) => {

				return({
					name: flag.name,
					key: flag.key,
					createdAt: flag.createdAt,
					// This will represent the places within the code that the feature
					// flag was referenced (file paths).
					codeReferences: [],
					codeCount: 0 // Easier for sorting in the calling context.
				});

			}
		);
		// As we match the RegEx pattern, we need a way to quickly look up a result based
		// on the matching feature flag key. As such, let's index the results by key.
		// This will allow us to locate the desired result item without having to search
		// through the entire collection.
		var resultsIndex = groupBy( results, "key" );

		// Now that we have our feature flag pattern and our file paths, we're just going
		// to brute-force iterate over the file system, reading in files, and trying to
		// locate key-references.
		for ( var filePath in filePaths ) {

			// Some directories are named like file-paths. Skip those devious wonks.
			if ( directoryExists( filePath ) ) {

				continue;

			}

			var matcher = pattern.matcher( fileRead( filePath ) );

			// Iterate over each key-match in the given file.
			while ( matcher.find() ) {

				var matchedKey = matcher.group( 2 );

				// Record the matching file path in the results item.
				resultsIndex[ matchedKey ].codeReferences.append( filePath );
				resultsIndex[ matchedKey ].codeCount++;

			}

		}

		return( results );	

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I return a Java RegEx Pattern based on the given set of Feature Flags. The pattern
	* requires the feature flag keys to be quoted (either double or single quotes).
	* 
	* @featureFlags I am the remote LaunchDarkly feature flags.
	*/
	private any function createRegexPattern( required array featureFlags ) {

		var keyList = featureFlags
			.map(
				( flag ) => {

					return( flag.key );

				}
			)
			.toList( "|" )
		;

		var patternText = "(['""])(#keyList#)\1";

		return( createObject( "java", "java.util.regex.Pattern" ).compile( patternText ) );

	}


	/**
	* I return a comprehensive list of files that live below the given directory. Only
	* includes files with the matching extensions.
	* 
	* @directories I am the directories to iterate.
	* @fileExtensions I am a comma-delimited list of file extensions to filter on.
	*/
	private array function getFilePaths(
		required string directories,
		required string fileExtensions
		) {

		// Convert list of extensions to filter list, ex: "*.cfm|*.txt|*.js".
		var filter = fileExtensions
			.listToArray( "," )
			.map(
				( fileExtension ) => {

					return( "*.#fileExtension#" );

				}
			)
			.toList( "|" )
		;

		var results = [];

		for ( var directory in directories ) {

			var fileList = directoryList(
				path = directory,
				recurse = true,
				listInfo = "path",
				filter = filter
			);

			results.append( fileList, true );

		}

		return( results );

	}


	/**
	* I retrieve the active (ie, non-archived) feature flags from the given LaunchDarkly
	* environment. Returns an array of structs with the following keys:
	* 
	* - name
	* - key
	* - createdAt
	*/
	private array function getLaunchDarklyFeatureFlags()
		// NOTE: Since this is performing an HTTP call for data that is unlikely to
		// change, we're going to cache it for an hour. This way, we can refresh the
		// page over-and-over while developing this algorithm without incurring the
		// network latency.
		cachedWithin = createTimeSpan( 0, 1, 0, 0 )
		{

		var apiRequest = new Http(
			method = "GET",
			url = "https://app.launchdarkly.com/api/v2/flags/#launchDarklyProjectKey#/",
			getAsBinary = "yes",
			charset = "utf-8"
		);

		apiRequest.addParam(
			type = "header",
			name = "Authorization",
			value = launchDarklyAccessToken
		);

		apiRequest.addParam(
			type = "header",
			name = "Content-Type",
			value = "application/json"
		);

		apiRequest.addParam(
			type = "url",
			name = "env",
			value = launchDarklyEnvironmentKey
		);

		var apiResponse = apiRequest.send().getPrefix();

		var fileContent = isBinary( apiResponse.fileContent )
			? charsetEncode( apiResponse.fileContent, "utf-8" )
			: apiResponse.fileContent
		;

		// Validate a successful response code.
		if ( ! reFind( "2\d\d", apiResponse.statusCode ) ) {

			throw( type = "HttpRequestFailure" );

		}

		// Let's filter out any archived flags and normalize the response.
		var flags = deserializeJson( fileContent )
			.items
			.filter(
				( flag ) => {

					// NOTE: Someone created some very poorly named flags.
					if ( "test".listFind( flag.key ) ) {

						return( false );

					}

					return( ! flag.archived );

				}
			)
			.map(
				( flag ) => {

					return({
						name: flag.name,
						key: flag.key,
						createdAt: createObject( "java", "java.util.Date" ).init( javaCast( "long", flag.creationDate ) )
					});

				}
			)
		;

		return( flags );

	}


	/**
	* I group the given collection by the given key / property.
	* 
	* @collection I am the collection being grouped.
	* @key I am the item key on which to base the grouping.
	*/
	private struct function groupBy(
		required array collection,
		required string key
		) {

		var index = {};

		for ( var item in collection ) {

			index[ item[ key ] ] = item;

		}

		return( index );

	}

}

Like I said, very brute-force. We're literally gathering a list of files; then, for each file, we're using a Regular Expression Pattern to see which feature flags are referenced in the file content. This algorithm assumes that all references to the feature flag keys will live within a quoted-string. In our application, this makes sense because of our naming conventions; but, it may not be a viable solution for your application. You may need to tweak as needed.

We currently have over 600 feature flags in LaunchDarkly - like I said, it revolutionized the way we approach application development. But, of those 600-plus feature flags, a large portion of them are rotting away unnecessarily in our application code. And I, for one, can't stand it. So, I'm hoping that with this utility, I'll be able to start going through my ColdFusion application and removing LaunchDarkly feature flags that are no longer relevant.



Reader Comments

@All,

One of my fellow InVisioneers, Jason LeMoine, pointed out that LaunchDarkly does have a code-integration feature:

https://docs.launchdarkly.com/docs/git-code-references

From the docs:

The Git code references feature allows you to find source code references to your feature flags within LaunchDarkly. This makes it easy to determine which projects reference your feature flags, and makes cleanup and removal of technical debt easy.

We've designed this feature so that LaunchDarkly does not need any direct access to your source code. It's also agnostic to which git hosting provider you use-- our approach makes it possible to push references to LaunchDarkly whether you're using GitHub, GitHub Enterprise, Bitbucket, Bitbucket Enterprise, Azure DevOps, GitLab, or any other Git code hosting tool.

It sounds really cool. But, we've never set it up, so I can't say whether it work well or not :D But, the idea is groovy.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.