-
Notifications
You must be signed in to change notification settings - Fork 167
Add initial support for feature flags #244
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
Merged
Merged
Changes from 10 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
1f85e6a
Add initial support for feature flags
efahk 3352258
Updating types
efahk 9b4b447
Test updates
efahk e9bf8f4
update version
efahk d6661d3
update track
efahk dc28dd5
Test updates
efahk 756aeef
addressing pr comments
efahk d6b2594
Addressing PR feedback
efahk fea580f
Refactor common code into base class and add more tests
6ce48c9
minor updates
b743ca5
refactor exposure tracking into base class & revert version update
7c2a1d1
remove bad expect chai import
b2874d3
removing version bump from packages lock file
34df629
Merge remote-tracking branch 'origin/master' into efahk_flags
tdumitrescu ca44f61
install deps after merge
tdumitrescu 27942b9
updating deceiving comment
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,38 @@ | ||
| /** | ||
| * TypeScript type definitions for Base Feature Flags Provider | ||
| */ | ||
|
|
||
| import { CustomLogger } from '../mixpanel-node'; | ||
|
|
||
| /** | ||
| * Configuration for feature flags API requests | ||
| */ | ||
| export interface FeatureFlagsConfig { | ||
| token: string; | ||
| api_host: string; | ||
| request_timeout_in_seconds: number; | ||
| } | ||
|
|
||
| /** | ||
| * Base Feature Flags Provider | ||
| * Contains common methods for feature flag evaluation | ||
| */ | ||
| export class FeatureFlagsProvider { | ||
| providerConfig: FeatureFlagsConfig; | ||
| endpoint: string; | ||
| logger: CustomLogger | null; | ||
|
|
||
| /** | ||
| * @param config - Common configuration for feature flag providers | ||
| * @param endpoint - API endpoint path (i.e., '/flags' or '/flags/definitions') | ||
| * @param logger - Logger instance | ||
| */ | ||
| constructor(config: FeatureFlagsConfig, endpoint: string, logger: CustomLogger | null); | ||
|
|
||
| /** | ||
| * Common HTTP request handler for flags API endpoints | ||
| * @param additionalParams - Additional query parameters to append | ||
| * @returns Parsed JSON response | ||
| */ | ||
| callFlagsEndpoint(additionalParams?: Record<string, any> | null): Promise<any>; | ||
| } |
This file contains hidden or 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,94 @@ | ||
| /** | ||
| * Base Feature Flags Provider | ||
| * Contains common methods for feature flag evaluation | ||
| */ | ||
|
|
||
| const https = require('https'); | ||
| const packageInfo = require('../../package.json'); | ||
| const { prepareCommonQueryParams, generateTraceparent, REQUEST_HEADERS } = require('./utils'); | ||
|
|
||
| class FeatureFlagsProvider { | ||
| /** | ||
| * @param {Object} providerConfig - Configuration object with token, api_host, request_timeout_in_seconds | ||
| * @param {string} endpoint - API endpoint path (e.g., '/flags' or '/flags/definitions') | ||
| * @param {Function} tracker - Function to track events (signature: track(distinct_id, event, properties, callback)) | ||
| * @param {CustomLogger} logger - Logger instance | ||
| */ | ||
| constructor(providerConfig, endpoint, tracker, logger) { | ||
| this.providerConfig = providerConfig; | ||
| this.endpoint = endpoint; | ||
| this.tracker = tracker; | ||
| this.logger = logger; | ||
| } | ||
|
|
||
| /** | ||
| * Common HTTP request handler for flags API endpoints | ||
| * @param {Object} additionalParams - Additional query parameters to append | ||
| * @returns {Promise<Object>} - Parsed JSON response | ||
| */ | ||
| async callFlagsEndpoint(additionalParams = null) { | ||
| return new Promise((resolve, reject) => { | ||
| const commonParams = prepareCommonQueryParams(this.providerConfig.token, packageInfo.version); | ||
| const params = new URLSearchParams(commonParams); | ||
|
|
||
| if (additionalParams) { | ||
| for (const [key, value] of Object.entries(additionalParams)) { | ||
| params.append(key, value); | ||
| } | ||
| } | ||
|
|
||
| const path = `${this.endpoint}?${params.toString()}`; | ||
|
|
||
| const requestOptions = { | ||
| host: this.providerConfig.api_host, | ||
| port: 443, | ||
| path: path, | ||
| method: 'GET', | ||
| headers: { | ||
| ...REQUEST_HEADERS, | ||
| 'Authorization': 'Basic ' + Buffer.from(this.providerConfig.token + ':').toString('base64'), | ||
| 'traceparent': generateTraceparent(), | ||
| }, | ||
| timeout: this.providerConfig.request_timeout_in_seconds * 1000, | ||
| }; | ||
|
|
||
| const request = https.request(requestOptions, (res) => { | ||
| let data = ''; | ||
|
|
||
| res.on('data', (chunk) => { | ||
| data += chunk; | ||
| }); | ||
|
|
||
| res.on('end', () => { | ||
| if (res.statusCode !== 200) { | ||
| this.logger?.error(`HTTP ${res.statusCode} error calling flags endpoint: ${data}`); | ||
| return reject(new Error(`HTTP ${res.statusCode}: ${data}`)); | ||
| } | ||
|
|
||
| try { | ||
| const result = JSON.parse(data); | ||
| resolve(result); | ||
| } catch (parseErr) { | ||
| this.logger?.error(`Failed to parse JSON response: ${parseErr.message}`); | ||
| reject(parseErr); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| request.on('error', (err) => { | ||
| this.logger?.error(`Network error calling flags endpoint: ${err.message}`); | ||
| reject(err); | ||
| }); | ||
|
|
||
| request.on('timeout', () => { | ||
| this.logger?.error(`Request timeout calling flags endpoint`); | ||
| request.destroy(); | ||
| reject(new Error('Request timeout')); | ||
| }); | ||
|
|
||
| request.end(); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| module.exports = FeatureFlagsProvider; |
This file contains hidden or 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,12 @@ | ||
| /** | ||
| * Mixpanel Feature Flags | ||
| * Exports for local and remote feature flag evaluation | ||
| */ | ||
|
|
||
| const LocalFeatureFlagsProvider = require('./local_flags'); | ||
| const RemoteFeatureFlagsProvider = require('./remote_flags'); | ||
|
|
||
| module.exports = { | ||
| LocalFeatureFlagsProvider, | ||
| RemoteFeatureFlagsProvider, | ||
| }; |
This file contains hidden or 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,93 @@ | ||
| /** | ||
| * TypeScript definitions for Local Feature Flags Provider | ||
| */ | ||
|
|
||
| import { LocalFlagsConfig, FlagContext, SelectedVariant } from './types'; | ||
| import { CustomLogger } from '../mixpanel-node'; | ||
|
|
||
| /** | ||
| * Local Feature Flags Provider | ||
| * Evaluates feature flags client-side using locally cached definitions | ||
| */ | ||
| export default class LocalFeatureFlagsProvider { | ||
| constructor( | ||
| token: string, | ||
| config: LocalFlagsConfig, | ||
| tracker: (distinct_id: string, event: string, properties: object, callback: (err?: Error) => void) => void, | ||
| logger: CustomLogger | ||
| ); | ||
|
|
||
| /** | ||
| * Start polling for flag definitions | ||
| * Fetches immediately and then at regular intervals if polling is enabled | ||
| */ | ||
| startPollingForDefinitions(): Promise<void>; | ||
|
|
||
| /** | ||
| * Stop polling for flag definitions | ||
| */ | ||
| stopPollingForDefinitions(): void; | ||
|
|
||
| /** | ||
| * Get the variant value for a feature flag | ||
| * @param flagKey - Feature flag key | ||
| * @param fallbackValue - Value to return if flag evaluation fails | ||
| * @param context - Evaluation context (must include distinct_id) | ||
| * @param reportExposure - Whether to track exposure event (default: true) | ||
| */ | ||
| getVariantValue<T>( | ||
| flagKey: string, | ||
| fallbackValue: T, | ||
| context: FlagContext, | ||
| reportExposure?: boolean | ||
| ): T; | ||
|
|
||
| /** | ||
| * Get the complete variant information for a feature flag | ||
| * @param flagKey - Feature flag key | ||
| * @param fallbackVariant - Variant to return if flag evaluation fails | ||
| * @param context - Evaluation context (must include distinct_id) | ||
| * @param reportExposure - Whether to track exposure event (default: true) | ||
| */ | ||
| getVariant( | ||
| flagKey: string, | ||
| fallbackVariant: SelectedVariant, | ||
| context: FlagContext, | ||
| reportExposure?: boolean | ||
| ): SelectedVariant; | ||
|
|
||
| /** | ||
| * Check if a feature flag is enabled | ||
| * This method is intended only for flags defined as Mixpanel Feature Gates (boolean flags) | ||
| * This checks that the variant value of a selected variant is concretely the boolean 'true' | ||
| * It does not coerce other truthy values. | ||
| * @param flagKey - Feature flag key | ||
| * @param context - Evaluation context (must include distinct_id) | ||
| */ | ||
| isEnabled( | ||
| flagKey: string, | ||
| context: FlagContext | ||
| ): boolean; | ||
|
|
||
| /** | ||
| * Get all feature flag variants for the current user context | ||
| * Exposure events are not automatically tracked when this method is used | ||
| * @param context - Evaluation context (must include distinct_id) | ||
| */ | ||
| getAllVariants( | ||
| context: FlagContext | ||
| ): {[key: string]: SelectedVariant}; | ||
|
|
||
| /** | ||
| * Manually tracks a feature flag exposure event to Mixpanel | ||
| * This provides flexibility for reporting individual exposure events when using getAllVariants | ||
| * @param flagKey - The key of the feature flag | ||
| * @param variant - The selected variant for the feature flag | ||
| * @param context - The user context used to evaluate the feature flag | ||
| */ | ||
| trackExposureEvent( | ||
| flagKey: string, | ||
| variant: SelectedVariant, | ||
| context: FlagContext | ||
| ): void; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.