Skip to content

Commit dd72371

Browse files
♻️ [RUMF-1529] use an enum for experimental features (#2113)
* ♻️ [RUMF-1529] use an enum for experimental features * 👌 rename updateExperimentalFeatures * 👌 simplify experimentalFeatures implementation Let's define the `enabledExperimentalFeature` during module evaluation. This way, we don't need to worry about it being undefined. Also, we are now validating `enabledExperimentalFeature` configuration in `configuration.ts`, so move validation tests there. * 👌 correctly reset experimentalFeatures
1 parent 84e1ff5 commit dd72371

28 files changed

+174
-111
lines changed

packages/core/src/browser/pageExitObservable.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createNewEvent, restorePageVisibility, setPageVisibility } from '../../test'
2-
import { resetExperimentalFeatures, updateExperimentalFeatures } from '../domain/configuration'
2+
import { resetExperimentalFeatures, addExperimentalFeatures, ExperimentalFeature } from '../domain/configuration'
33
import type { Subscription } from '../tools/observable'
44
import type { PageExitEvent } from './pageExitObservable'
55
import { PageExitReason, createPageExitObservable } from './pageExitObservable'
@@ -20,7 +20,7 @@ describe('createPageExitObservable', () => {
2020
})
2121

2222
it('notifies when the page fires pagehide if ff pagehide is enabled', () => {
23-
updateExperimentalFeatures(['pagehide'])
23+
addExperimentalFeatures([ExperimentalFeature.PAGEHIDE])
2424
onExitSpy = jasmine.createSpy()
2525
pageExitSubscription = createPageExitObservable().subscribe(onExitSpy)
2626

packages/core/src/browser/pageExitObservable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isExperimentalFeatureEnabled } from '../domain/configuration'
1+
import { isExperimentalFeatureEnabled, ExperimentalFeature } from '../domain/configuration'
22
import { Observable } from '../tools/observable'
33
import { includes, noop, objectValues } from '../tools/utils'
44
import { addEventListeners, addEventListener, DOM_EVENT } from './addEventListener'
@@ -18,7 +18,7 @@ export interface PageExitEvent {
1818

1919
export function createPageExitObservable(): Observable<PageExitEvent> {
2020
const observable = new Observable<PageExitEvent>(() => {
21-
const pagehideEnabled = isExperimentalFeatureEnabled('pagehide')
21+
const pagehideEnabled = isExperimentalFeatureEnabled(ExperimentalFeature.PAGEHIDE)
2222
const { stop: stopListeners } = addEventListeners(
2323
window,
2424
[DOM_EVENT.VISIBILITY_CHANGE, DOM_EVENT.FREEZE, DOM_EVENT.PAGE_HIDE],

packages/core/src/domain/configuration/configuration.spec.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,41 @@ import type { RumEvent } from '../../../../rum-core/src'
22
import { display } from '../../tools/display'
33
import type { InitConfiguration } from './configuration'
44
import { validateAndBuildConfiguration } from './configuration'
5-
import { isExperimentalFeatureEnabled, updateExperimentalFeatures } from './experimentalFeatures'
5+
import { ExperimentalFeature, isExperimentalFeatureEnabled, resetExperimentalFeatures } from './experimentalFeatures'
66

77
describe('validateAndBuildConfiguration', () => {
88
const clientToken = 'some_client_token'
99

10-
beforeEach(() => {
11-
updateExperimentalFeatures([])
10+
afterEach(() => {
11+
resetExperimentalFeatures()
1212
})
1313

14-
it('updates experimental feature flags', () => {
15-
validateAndBuildConfiguration({ clientToken, enableExperimentalFeatures: ['foo'] })
16-
expect(isExperimentalFeatureEnabled('foo')).toBeTrue()
14+
describe('experimentalFeatures', () => {
15+
const TEST_FEATURE_FLAG = 'foo' as ExperimentalFeature
16+
17+
beforeEach(() => {
18+
;(ExperimentalFeature as any).FOO = TEST_FEATURE_FLAG
19+
})
20+
21+
afterEach(() => {
22+
delete (ExperimentalFeature as any).FOO
23+
})
24+
25+
it('updates experimental feature flags', () => {
26+
validateAndBuildConfiguration({ clientToken, enableExperimentalFeatures: ['foo'] })
27+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG)).toBeTrue()
28+
})
29+
30+
it('ignores unknown experimental features', () => {
31+
validateAndBuildConfiguration({
32+
clientToken,
33+
enableExperimentalFeatures: ['bar', undefined as any, null as any, 11 as any],
34+
})
35+
expect(isExperimentalFeatureEnabled('bar' as any)).toBeFalse()
36+
expect(isExperimentalFeatureEnabled(undefined as any)).toBeFalse()
37+
expect(isExperimentalFeatureEnabled(null as any)).toBeFalse()
38+
expect(isExperimentalFeatureEnabled(11 as any)).toBeFalse()
39+
})
1740
})
1841

1942
describe('validate init configuration', () => {

packages/core/src/domain/configuration/configuration.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { CookieOptions } from '../../browser/cookie'
22
import { getCurrentSite } from '../../browser/cookie'
33
import { catchUserErrors } from '../../tools/catchUserErrors'
44
import { display } from '../../tools/display'
5-
import { assign, isPercentage, ONE_KIBI_BYTE, ONE_SECOND } from '../../tools/utils'
5+
import { assign, isPercentage, objectHasValue, ONE_KIBI_BYTE, ONE_SECOND } from '../../tools/utils'
66
import type { RawTelemetryConfiguration } from '../telemetry'
7-
import { updateExperimentalFeatures } from './experimentalFeatures'
7+
import { ExperimentalFeature, addExperimentalFeatures } from './experimentalFeatures'
88
import type { TransportConfiguration } from './transportConfiguration'
99
import { computeTransportConfiguration } from './transportConfiguration'
1010

@@ -112,7 +112,13 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati
112112
}
113113

114114
// Set the experimental feature flags as early as possible, so we can use them in most places
115-
updateExperimentalFeatures(initConfiguration.enableExperimentalFeatures)
115+
if (Array.isArray(initConfiguration.enableExperimentalFeatures)) {
116+
addExperimentalFeatures(
117+
initConfiguration.enableExperimentalFeatures.filter((flag): flag is ExperimentalFeature =>
118+
objectHasValue(ExperimentalFeature, flag)
119+
)
120+
)
121+
}
116122

117123
return assign(
118124
{

packages/core/src/domain/configuration/endpointBuilder.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { BuildEnvWindow } from '../../../test'
22
import { startsWith } from '../../tools/utils'
33
import type { InitConfiguration } from './configuration'
44
import { createEndpointBuilder } from './endpointBuilder'
5-
import { resetExperimentalFeatures, updateExperimentalFeatures } from './experimentalFeatures'
5+
import { ExperimentalFeature, resetExperimentalFeatures, addExperimentalFeatures } from './experimentalFeatures'
66

77
describe('endpointBuilder', () => {
88
const clientToken = 'some_client_token'
@@ -126,7 +126,7 @@ describe('endpointBuilder', () => {
126126
})
127127

128128
it('should contain flush reason when ff collect_flush_reason is enabled', () => {
129-
updateExperimentalFeatures(['collect_flush_reason'])
129+
addExperimentalFeatures([ExperimentalFeature.COLLECT_FLUSH_REASON])
130130
expect(createEndpointBuilder(initConfiguration, 'rum', []).build('xhr', 'batch_bytes_limit')).toContain(
131131
'flush_reason%3Abatch_bytes_limit'
132132
)

packages/core/src/domain/configuration/endpointBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { normalizeUrl } from '../../tools/urlPolyfill'
44
import { generateUUID } from '../../tools/utils'
55
import type { InitConfiguration } from './configuration'
66
import { INTAKE_SITE_AP1, INTAKE_SITE_US1 } from './intakeSites'
7-
import { isExperimentalFeatureEnabled } from './experimentalFeatures'
7+
import { ExperimentalFeature, isExperimentalFeatureEnabled } from './experimentalFeatures'
88

99
// replaced at build time
1010
declare const __BUILD_ENV__SDK_VERSION__: string
@@ -104,7 +104,7 @@ function buildEndpointParameters(
104104
retry: RetryInfo | undefined
105105
) {
106106
const tags = [`sdk_version:${__BUILD_ENV__SDK_VERSION__}`, `api:${api}`].concat(configurationTags)
107-
if (flushReason && isExperimentalFeatureEnabled('collect_flush_reason')) {
107+
if (flushReason && isExperimentalFeatureEnabled(ExperimentalFeature.COLLECT_FLUSH_REASON)) {
108108
tags.push(`flush_reason:${flushReason}`)
109109
}
110110
if (retry) {
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,34 @@
1+
import type { ExperimentalFeature } from './experimentalFeatures'
12
import {
2-
updateExperimentalFeatures,
3+
addExperimentalFeatures,
34
isExperimentalFeatureEnabled,
45
resetExperimentalFeatures,
56
} from './experimentalFeatures'
67

8+
const TEST_FEATURE_FLAG_ONE = 'foo' as ExperimentalFeature
9+
const TEST_FEATURE_FLAG_TWO = 'bar' as ExperimentalFeature
10+
711
describe('experimentalFeatures', () => {
812
afterEach(() => {
913
resetExperimentalFeatures()
1014
})
1115

1216
it('initial state is empty', () => {
13-
expect(isExperimentalFeatureEnabled('foo')).toBeFalse()
14-
expect(isExperimentalFeatureEnabled('bar')).toBeFalse()
17+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeFalse()
18+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeFalse()
1519
})
1620

1721
it('should define enabled experimental features', () => {
18-
updateExperimentalFeatures(['foo'])
19-
expect(isExperimentalFeatureEnabled('foo')).toBeTrue()
20-
expect(isExperimentalFeatureEnabled('bar')).toBeFalse()
22+
addExperimentalFeatures([TEST_FEATURE_FLAG_ONE])
23+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue()
24+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeFalse()
2125
})
2226

2327
it('should allow to be shared between products', () => {
24-
updateExperimentalFeatures(['foo'])
25-
updateExperimentalFeatures(['bar'])
26-
27-
expect(isExperimentalFeatureEnabled('foo')).toBeTrue()
28-
expect(isExperimentalFeatureEnabled('bar')).toBeTrue()
29-
})
30-
31-
it('should support some edge cases', () => {
32-
updateExperimentalFeatures(['foo'])
33-
updateExperimentalFeatures(undefined)
34-
updateExperimentalFeatures([])
35-
updateExperimentalFeatures([11 as any])
28+
addExperimentalFeatures([TEST_FEATURE_FLAG_ONE])
29+
addExperimentalFeatures([TEST_FEATURE_FLAG_TWO])
3630

37-
expect(isExperimentalFeatureEnabled('foo')).toBeTrue()
31+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_ONE)).toBeTrue()
32+
expect(isExperimentalFeatureEnabled(TEST_FEATURE_FLAG_TWO)).toBeTrue()
3833
})
3934
})

packages/core/src/domain/configuration/experimentalFeatures.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,40 @@
33
* For NPM setup, this feature flag singleton is shared between RUM and Logs product.
44
* This means that an experimental flag set on the RUM product will be set on the Logs product.
55
* So keep in mind that in certain configurations, your experimental feature flag may affect other products.
6+
*
7+
* FORMAT:
8+
* All feature flags should be snake_cased
69
*/
7-
import { includes } from '../../tools/utils'
8-
import { display } from '../../tools/display'
9-
10-
let enabledExperimentalFeatures: Set<string> | undefined
11-
12-
export function updateExperimentalFeatures(enabledFeatures: string[] | undefined): void {
13-
// Safely handle external data
14-
if (!Array.isArray(enabledFeatures)) {
15-
return
16-
}
10+
// We want to use a real enum (i.e. not a const enum) here, to be able to check whether an arbitrary
11+
// string is an expected feature flag
12+
// eslint-disable-next-line no-restricted-syntax
13+
export enum ExperimentalFeature {
14+
PAGEHIDE = 'pagehide',
15+
SHADOW_DOM_DEBUG = 'shadow_dom_debug',
16+
FEATURE_FLAGS = 'feature_flags',
17+
RESOURCE_DURATIONS = 'resource_durations',
18+
RESOURCE_PAGE_STATES = 'resource_page_states',
19+
CLICKMAP = 'clickmap',
20+
COLLECT_FLUSH_REASON = 'collect_flush_reason',
21+
SANITIZE_INPUTS = 'sanitize_inputs',
22+
}
1723

18-
if (!enabledExperimentalFeatures) {
19-
enabledExperimentalFeatures = new Set(enabledFeatures)
20-
}
24+
const enabledExperimentalFeatures: Set<ExperimentalFeature> = new Set()
2125

22-
enabledFeatures
23-
.filter((flag) => typeof flag === 'string')
24-
.forEach((flag: string) => {
25-
if (includes(flag, '-')) {
26-
display.warn(`please use snake case for '${flag}'`)
27-
}
28-
enabledExperimentalFeatures!.add(flag)
29-
})
26+
export function addExperimentalFeatures(enabledFeatures: ExperimentalFeature[]): void {
27+
enabledFeatures.forEach((flag) => {
28+
enabledExperimentalFeatures.add(flag)
29+
})
3030
}
3131

32-
export function isExperimentalFeatureEnabled(featureName: string): boolean {
33-
return !!enabledExperimentalFeatures && enabledExperimentalFeatures.has(featureName)
32+
export function isExperimentalFeatureEnabled(featureName: ExperimentalFeature): boolean {
33+
return enabledExperimentalFeatures.has(featureName)
3434
}
3535

3636
export function resetExperimentalFeatures(): void {
37-
enabledExperimentalFeatures = new Set()
37+
enabledExperimentalFeatures.clear()
3838
}
3939

40-
export function getExperimentalFeatures(): Set<string> {
41-
return enabledExperimentalFeatures || new Set()
40+
export function getExperimentalFeatures(): Set<ExperimentalFeature> {
41+
return enabledExperimentalFeatures
4242
}

packages/core/src/domain/configuration/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ export {
99
export { createEndpointBuilder, EndpointBuilder, EndpointType } from './endpointBuilder'
1010
export {
1111
isExperimentalFeatureEnabled,
12-
updateExperimentalFeatures,
12+
addExperimentalFeatures,
1313
resetExperimentalFeatures,
1414
getExperimentalFeatures,
15+
ExperimentalFeature,
1516
} from './experimentalFeatures'
1617
export * from './intakeSites'

packages/core/src/domain/console/consoleObservable.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { find, jsonStringify } from '../../tools/utils'
55
import { ConsoleApiName } from '../../tools/display'
66
import { callMonitored } from '../../tools/monitor'
77
import { sanitize } from '../../tools/sanitize'
8-
import { isExperimentalFeatureEnabled } from '../configuration'
8+
import { ExperimentalFeature, isExperimentalFeatureEnabled } from '../configuration'
99

1010
export interface ConsoleLog {
1111
message: string
@@ -70,10 +70,14 @@ function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack:
7070

7171
function formatConsoleParameters(param: unknown) {
7272
if (typeof param === 'string') {
73-
return isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(param) : param
73+
return isExperimentalFeatureEnabled(ExperimentalFeature.SANITIZE_INPUTS) ? sanitize(param) : param
7474
}
7575
if (param instanceof Error) {
7676
return formatErrorMessage(computeStackTrace(param))
7777
}
78-
return jsonStringify(isExperimentalFeatureEnabled('sanitize_inputs') ? sanitize(param) : param, undefined, 2)
78+
return jsonStringify(
79+
isExperimentalFeatureEnabled(ExperimentalFeature.SANITIZE_INPUTS) ? sanitize(param) : param,
80+
undefined,
81+
2
82+
)
7983
}

0 commit comments

Comments
 (0)