Skip to content

Commit bec6e56

Browse files
author
Athira M
committed
feat: Process experiment metadata in RC fetch response
1 parent 1bcf83d commit bec6e56

File tree

7 files changed

+95
-14
lines changed

7 files changed

+95
-14
lines changed

common/api-review/remote-config.api.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function fetchConfig(remoteConfig: RemoteConfig): Promise<void>;
4141
export interface FetchResponse {
4242
config?: FirebaseRemoteConfigObject;
4343
eTag?: string;
44+
experiments?: FirebaseExperimentDescription[];
4445
status: number;
4546
templateVersion?: number;
4647
}
@@ -51,6 +52,22 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';
5152
// @public
5253
export type FetchType = 'BASE' | 'REALTIME';
5354

55+
// @public
56+
export interface FirebaseExperimentDescription {
57+
// (undocumented)
58+
affectedParameterKeys?: string[];
59+
// (undocumented)
60+
experimentId: string;
61+
// (undocumented)
62+
experimentStartTime: string;
63+
// (undocumented)
64+
timeToLiveMillis: string;
65+
// (undocumented)
66+
triggerTimeoutMillis: string;
67+
// (undocumented)
68+
variantId: string;
69+
}
70+
5471
// @public
5572
export interface FirebaseRemoteConfigObject {
5673
// (undocumented)

packages/remote-config/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export async function activate(remoteConfig: RemoteConfig): Promise<boolean> {
102102
if (
103103
!lastSuccessfulFetchResponse ||
104104
!lastSuccessfulFetchResponse.config ||
105+
!lastSuccessfulFetchResponse.experiments ||
105106
!lastSuccessfulFetchResponse.eTag ||
106107
!lastSuccessfulFetchResponse.templateVersion ||
107108
lastSuccessfulFetchResponse.eTag === activeConfigEtag

packages/remote-config/src/client/rest_client.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
import {
1919
CustomSignals,
2020
FetchResponse,
21-
FirebaseRemoteConfigObject
21+
FirebaseRemoteConfigObject,
22+
FirebaseExperimentDescription
2223
} from '../public_types';
2324
import {
2425
RemoteConfigFetchClient,
@@ -143,6 +144,7 @@ export class RestClient implements RemoteConfigFetchClient {
143144
let config: FirebaseRemoteConfigObject | undefined;
144145
let state: string | undefined;
145146
let templateVersion: number | undefined;
147+
let experiments: FirebaseExperimentDescription[] | undefined;
146148

147149
// JSON parsing throws SyntaxError if the response body isn't a JSON string.
148150
// Requesting application/json and checking for a 200 ensures there's JSON data.
@@ -158,6 +160,7 @@ export class RestClient implements RemoteConfigFetchClient {
158160
config = responseBody['entries'];
159161
state = responseBody['state'];
160162
templateVersion = responseBody['templateVersion'];
163+
experiments = responseBody['experimentDescriptions'];
161164
}
162165

163166
// Normalizes based on legacy state.
@@ -168,6 +171,7 @@ export class RestClient implements RemoteConfigFetchClient {
168171
} else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') {
169172
// These cases can be fixed remotely, so normalize to safe value.
170173
config = {};
174+
experiments = [];
171175
}
172176

173177
// Normalize to exception-based control flow for non-success cases.
@@ -180,6 +184,6 @@ export class RestClient implements RemoteConfigFetchClient {
180184
});
181185
}
182186

183-
return { status, eTag: responseEtag, config, templateVersion };
187+
return { status, eTag: responseEtag, config, templateVersion, experiments };
184188
}
185189
}

packages/remote-config/src/public_types.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,32 @@ export interface FirebaseRemoteConfigObject {
5959
[key: string]: string;
6060
}
6161

62+
/**
63+
* Defines experiment and variant attached to a config parameter.
64+
*/
65+
export interface FirebaseExperimentDescription {
66+
// A string of max length 22 characters and of format: _exp_<experiment_id>
67+
experimentId: string;
68+
69+
// The variant of the experiment assigned to the app instance.
70+
variantId: string;
71+
72+
// When the experiment was started.
73+
experimentStartTime: string;
74+
75+
// How long the experiment can remain in STANDBY state. Valid range from 1 ms
76+
// to 6 months.
77+
triggerTimeoutMillis: string;
78+
79+
// How long the experiment can remain in ON state. Valid range from 1 ms to 6
80+
// months.
81+
timeToLiveMillis: string;
82+
83+
// A repeated of Remote Config parameter keys that this experiment is
84+
// affecting the value of.
85+
affectedParameterKeys?: string[];
86+
}
87+
6288
/**
6389
* Defines a successful response (200 or 304).
6490
*
@@ -99,8 +125,12 @@ export interface FetchResponse {
99125
*/
100126
templateVersion?: number;
101127

102-
// Note: we're not extracting experiment metadata until
103-
// ABT and Analytics have Web SDKs.
128+
/**
129+
* A/B Test and Rollout experiment metadata.
130+
*
131+
* <p>Only defined for 200 responses.
132+
*/
133+
experiments?: FirebaseExperimentDescription[];
104134
}
105135

106136
/**

packages/remote-config/test/api.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@ describe('Remote Config API', () => {
6868
status: 200,
6969
eTag: 'asdf',
7070
config: { 'foobar': 'hello world' },
71-
templateVersion: 1
71+
templateVersion: 1,
72+
experiments: [{
73+
experimentId: "_exp_1",
74+
variantId : "1",
75+
experimentStartTime : "2025-04-06T14:13:57.597Z",
76+
triggerTimeoutMillis : "15552000000",
77+
timeToLiveMillis : "15552000000"
78+
}]
7279
};
7380
let fetchStub: sinon.SinonStub;
7481

@@ -106,7 +113,8 @@ describe('Remote Config API', () => {
106113
Promise.resolve({
107114
entries: response.config,
108115
state: 'OK',
109-
templateVersion: response.templateVersion
116+
templateVersion: response.templateVersion,
117+
experimentDescriptions: response.experiments,
110118
})
111119
} as Response)
112120
);

packages/remote-config/test/client/rest_client.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ describe('RestClient', () => {
7878
eTag: 'etag',
7979
state: 'UPDATE',
8080
entries: { color: 'sparkling' },
81-
templateVersion: 1
81+
templateVersion: 1,
82+
experimentDescriptions: [{
83+
experimentId: "_exp_1",
84+
variantId : "1",
85+
experimentStartTime : "2025-04-06T14:13:57.597Z",
86+
triggerTimeoutMillis : "15552000000",
87+
timeToLiveMillis : "15552000000"
88+
}]
8289
};
8390

8491
fetchStub.returns(
@@ -90,7 +97,8 @@ describe('RestClient', () => {
9097
Promise.resolve({
9198
entries: expectedResponse.entries,
9299
state: expectedResponse.state,
93-
templateVersion: expectedResponse.templateVersion
100+
templateVersion: expectedResponse.templateVersion,
101+
experimentDescriptions: expectedResponse.experimentDescriptions
94102
})
95103
} as Response)
96104
);
@@ -101,7 +109,8 @@ describe('RestClient', () => {
101109
status: expectedResponse.status,
102110
eTag: expectedResponse.eTag,
103111
config: expectedResponse.entries,
104-
templateVersion: expectedResponse.templateVersion
112+
templateVersion: expectedResponse.templateVersion,
113+
experiments: expectedResponse.experimentDescriptions
105114
});
106115
});
107116

@@ -191,7 +200,8 @@ describe('RestClient', () => {
191200
status: 304,
192201
eTag: 'response-etag',
193202
config: undefined,
194-
templateVersion: undefined
203+
templateVersion: undefined,
204+
experiments: undefined
195205
});
196206
});
197207

@@ -230,7 +240,8 @@ describe('RestClient', () => {
230240
status: 304,
231241
eTag: 'etag',
232242
config: undefined,
233-
templateVersion: undefined
243+
templateVersion: undefined,
244+
experiments: undefined
234245
});
235246
});
236247

@@ -248,7 +259,8 @@ describe('RestClient', () => {
248259
status: 200,
249260
eTag: 'etag',
250261
config: {},
251-
templateVersion: undefined
262+
templateVersion: undefined,
263+
experiments: []
252264
});
253265
}
254266
});

packages/remote-config/test/remote_config.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,13 @@ describe('RemoteConfig', () => {
391391
const CONFIG = { key: 'val' };
392392
const NEW_ETAG = 'new_etag';
393393
const TEMPLATE_VERSION = 1;
394+
const EXPERIMENTS = [{
395+
"experimentId" : "_exp_1",
396+
"variantId" : "1",
397+
"experimentStartTime" : "2025-04-06T14:13:57.597Z",
398+
"triggerTimeoutMillis" : "15552000000",
399+
"timeToLiveMillis" : "15552000000"
400+
}];
394401

395402
let getLastSuccessfulFetchResponseStub: sinon.SinonStub;
396403
let getActiveConfigEtagStub: sinon.SinonStub;
@@ -456,7 +463,8 @@ describe('RemoteConfig', () => {
456463
Promise.resolve({
457464
config: CONFIG,
458465
eTag: NEW_ETAG,
459-
templateVersion: TEMPLATE_VERSION
466+
templateVersion: TEMPLATE_VERSION,
467+
experiments: EXPERIMENTS
460468
})
461469
);
462470
getActiveConfigEtagStub.returns(Promise.resolve(ETAG));
@@ -476,7 +484,8 @@ describe('RemoteConfig', () => {
476484
Promise.resolve({
477485
config: CONFIG,
478486
eTag: NEW_ETAG,
479-
templateVersion: TEMPLATE_VERSION
487+
templateVersion: TEMPLATE_VERSION,
488+
experiments: EXPERIMENTS
480489
})
481490
);
482491
getActiveConfigEtagStub.returns(Promise.resolve());

0 commit comments

Comments
 (0)