Repository navigation
fix: record analytics against the canonical feature name #428
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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/expose-feature-name
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -704,7 +704,7 @@ const Flagsmith = class { | |
| } | ||
|
|
||
| if (!options?.skipAnalytics && !skipAnalytics) { | ||
| this.evaluateFlag(key, "VALUE"); | ||
| this.evaluateFlag(key, "VALUE", flag); | ||
| } | ||
|
|
||
| if (res === null && typeof options?.fallback !== 'undefined') { | ||
|
|
@@ -829,13 +829,13 @@ const Flagsmith = class { | |
| res = true; | ||
| } | ||
| if ((usingNewOptions && !options.skipAnalytics) || !options) { | ||
| this.evaluateFlag(key, "ENABLED"); | ||
| this.evaluateFlag(key, "ENABLED", flag); | ||
| } | ||
| if(this.sentryClient) { | ||
| try { | ||
| this.sentryClient.getIntegrationByName( | ||
| "FeatureFlags", | ||
| )?.addFeatureFlag?.(key, res); | ||
| )?.addFeatureFlag?.(flag?.name || normalizeFlagKey(key), res); | ||
| } catch (e) { | ||
| console.error(e) | ||
| } | ||
|
|
@@ -950,15 +950,20 @@ const Flagsmith = class { | |
| } | ||
| } | ||
|
|
||
| private evaluateFlag =(key: string, method: 'VALUE' | 'ENABLED') => { | ||
| private evaluateFlag =(key: string, method: 'VALUE' | 'ENABLED', flag?: IFlagsmithFeature | null) => { | ||
| // The API resolves analytics by exact feature name, so always report the | ||
| // feature's own name rather than whatever casing the caller happened to use. | ||
| // Without this, getValue('My Flag') and getValue('my_flag') report as two | ||
| // separate features and neither resolves unless it matches the name exactly. | ||
| const analyticsKey = flag?.name || normalizeFlagKey(key); | ||
| if (this.datadogRum) { | ||
| if (!this.datadogRum!.client!.addFeatureFlagEvaluation) { | ||
| console.error('Flagsmith: Your datadog RUM client does not support the function addFeatureFlagEvaluation, please update it.'); | ||
| } else { | ||
| if (method === 'VALUE') { | ||
| this.datadogRum!.client!.addFeatureFlagEvaluation(FLAGSMITH_CONFIG_ANALYTICS_KEY + key, this.getValue(key, {}, true)); | ||
| this.datadogRum!.client!.addFeatureFlagEvaluation(FLAGSMITH_CONFIG_ANALYTICS_KEY + analyticsKey, this.getValue(key, {}, true)); | ||
| } else { | ||
| this.datadogRum!.client!.addFeatureFlagEvaluation(FLAGSMITH_FLAG_ANALYTICS_KEY + key, this.hasFeature(key, true)); | ||
| this.datadogRum!.client!.addFeatureFlagEvaluation(FLAGSMITH_FLAG_ANALYTICS_KEY + analyticsKey, this.hasFeature(key, true)); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -968,10 +973,10 @@ const Flagsmith = class { | |
| if (!this.evaluationEvent[this.evaluationContext.environment.apiKey]) { | ||
| this.evaluationEvent[this.evaluationContext.environment.apiKey] = {}; | ||
| } | ||
| if (this.evaluationEvent[this.evaluationContext.environment.apiKey][key] === undefined) { | ||
| this.evaluationEvent[this.evaluationContext.environment.apiKey][key] = 0; | ||
| if (this.evaluationEvent[this.evaluationContext.environment.apiKey][analyticsKey] === undefined) { | ||
| this.evaluationEvent[this.evaluationContext.environment.apiKey][analyticsKey] = 0; | ||
| } | ||
| this.evaluationEvent[this.evaluationContext.environment.apiKey][key] += 1; | ||
| this.evaluationEvent[this.evaluationContext.environment.apiKey][analyticsKey] += 1; | ||
| } | ||
|
|
||
| this.updateEventStorage(); | ||
|
|
@@ -1010,8 +1015,11 @@ const Flagsmith = class { | |
| this.log(`Flagsmith: trackExposureEvent called for "${featureName}" without an identity; call identify() (optionally with transient: true) or pass opts.identifier. No exposure recorded.`); | ||
| return; | ||
| } | ||
| // As with evaluateFlag, report the feature's own name so that exposures for the | ||
| // same flag aggregate regardless of the casing the caller used. | ||
| const key = normalizeFlagKey(featureName); | ||
|
Comment on lines
+1018
to
+1020
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Zaimwa9 thoughts on this? |
||
| this.eventProcessor.trackExposureEvent({ | ||
| featureName, | ||
| featureName: this.flags?.[key]?.name || key, | ||
| identifier, | ||
| value: opts?.value ?? null, | ||
| traits: resolveTraitValues(opts?.traits ?? this.evaluationContext.identity?.traits), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||
| import { IInitConfig } from '../types'; | ||||||||||||||||||||||||
| import { getFlagsmith } from './test-constants'; | ||||||||||||||||||||||||
| import { delay, getFlagsmith } from './test-constants'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const mixedCaseFlags = [ | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
|
|
@@ -14,6 +14,14 @@ const mixedCaseFlags = [ | |||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const instances: any[] = []; | ||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Can we not be more specific with the typing here too? Something like:
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| afterEach(() => { | ||||||||||||||||||||||||
| // enableAnalytics starts an interval that nothing public tears down. | ||||||||||||||||||||||||
| instances.forEach((flagsmith) => clearInterval(flagsmith.analyticsInterval)); | ||||||||||||||||||||||||
| instances.length = 0; | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async function initWithMixedCaseFlags(config: Partial<IInitConfig> = {}) { | ||||||||||||||||||||||||
| const { flagsmith, initConfig, mockFetch } = getFlagsmith(config); | ||||||||||||||||||||||||
| mockFetch.mockImplementation(async (url: string) => { | ||||||||||||||||||||||||
|
|
@@ -26,9 +34,15 @@ async function initWithMixedCaseFlags(config: Partial<IInitConfig> = {}) { | |||||||||||||||||||||||
| throw new Error('Please mock the call to ' + url); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| await flagsmith.init(initConfig); | ||||||||||||||||||||||||
| instances.push(flagsmith); | ||||||||||||||||||||||||
| return { flagsmith, mockFetch }; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const postedAnalytics = (mockFetch: jest.Mock) => { | ||||||||||||||||||||||||
| const call = mockFetch.mock.calls.find(([url]: [string]) => url.includes('analytics/flags')); | ||||||||||||||||||||||||
| return call ? JSON.parse(call[1].body) : null; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| describe('feature name casing', () => { | ||||||||||||||||||||||||
| test('exposes the original feature name on each flag', async () => { | ||||||||||||||||||||||||
| const { flagsmith } = await initWithMixedCaseFlags(); | ||||||||||||||||||||||||
|
|
@@ -44,4 +58,29 @@ describe('feature name casing', () => { | |||||||||||||||||||||||
| expect(flagsmith.getValue('myfeatureflag')).toBe('on'); | ||||||||||||||||||||||||
| expect(flagsmith.hasFeature('MYFEATUREFLAG')).toBe(true); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| test('records analytics against the canonical feature name, whatever casing the caller used', async () => { | ||||||||||||||||||||||||
| const { flagsmith, mockFetch } = await initWithMixedCaseFlags({ enableAnalytics: true }); | ||||||||||||||||||||||||
| await delay(1); // evaluationEvent is restored from storage asynchronously | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| flagsmith.getValue('myfeatureflag'); | ||||||||||||||||||||||||
| flagsmith.getValue('MyFeatureFlag'); | ||||||||||||||||||||||||
| flagsmith.hasFeature('MYFEATUREFLAG'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // @ts-ignore internal, normally driven by an interval | ||||||||||||||||||||||||
| await flagsmith.analyticsFlags(); | ||||||||||||||||||||||||
| expect(postedAnalytics(mockFetch)).toEqual({ MyFeatureFlag: 3 }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| test('buckets unknown flags under the normalised key rather than the caller casing', async () => { | ||||||||||||||||||||||||
| const { flagsmith, mockFetch } = await initWithMixedCaseFlags({ enableAnalytics: true }); | ||||||||||||||||||||||||
| await delay(1); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| flagsmith.getValue('Does Not Exist'); | ||||||||||||||||||||||||
| flagsmith.getValue('does_not_exist'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // @ts-ignore internal, normally driven by an interval | ||||||||||||||||||||||||
| await flagsmith.analyticsFlags(); | ||||||||||||||||||||||||
| expect(postedAnalytics(mockFetch)).toEqual({ does_not_exist: 2 }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
Comment on lines
+75
to
+85
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description of the PR mentions nothing about Sentry... why are we changing this too?