Skip to content

Commit 2ec305d

Browse files
author
Athira M
committed
feat: Process experiment metadata in RC fetch response
1 parent 30de503 commit 2ec305d

File tree

7 files changed

+92
-13
lines changed

7 files changed

+92
-13
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,29 @@ export function fetchConfig(remoteConfig: RemoteConfig): Promise<void>;
2828
export interface FetchResponse {
2929
config?: FirebaseRemoteConfigObject;
3030
eTag?: string;
31+
experiments?: FirebaseExperimentDescription[];
3132
status: number;
3233
}
3334

3435
// @public
3536
export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';
3637

38+
// @public
39+
export interface FirebaseExperimentDescription {
40+
// (undocumented)
41+
affectedParameterKeys?: string[];
42+
// (undocumented)
43+
experimentId: string;
44+
// (undocumented)
45+
experimentStartTime: string;
46+
// (undocumented)
47+
timeToLiveMillis: string;
48+
// (undocumented)
49+
triggerTimeoutMillis: string;
50+
// (undocumented)
51+
variantId: string;
52+
}
53+
3754
// @public
3855
export interface FirebaseRemoteConfigObject {
3956
// (undocumented)

packages/remote-config/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export async function activate(remoteConfig: RemoteConfig): Promise<boolean> {
9797
if (
9898
!lastSuccessfulFetchResponse ||
9999
!lastSuccessfulFetchResponse.config ||
100+
!lastSuccessfulFetchResponse.experiments ||
100101
!lastSuccessfulFetchResponse.eTag ||
101102
lastSuccessfulFetchResponse.eTag === activeConfigEtag
102103
) {

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,
@@ -140,6 +141,7 @@ export class RestClient implements RemoteConfigFetchClient {
140141

141142
let config: FirebaseRemoteConfigObject | undefined;
142143
let state: string | undefined;
144+
let experiments: FirebaseExperimentDescription[] | undefined;
143145

144146
// JSON parsing throws SyntaxError if the response body isn't a JSON string.
145147
// Requesting application/json and checking for a 200 ensures there's JSON data.
@@ -154,6 +156,7 @@ export class RestClient implements RemoteConfigFetchClient {
154156
}
155157
config = responseBody['entries'];
156158
state = responseBody['state'];
159+
experiments = responseBody['experimentDescriptions'];
157160
}
158161

159162
// Normalizes based on legacy state.
@@ -164,6 +167,7 @@ export class RestClient implements RemoteConfigFetchClient {
164167
} else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') {
165168
// These cases can be fixed remotely, so normalize to safe value.
166169
config = {};
170+
experiments = [];
167171
}
168172

169173
// Normalize to exception-based control flow for non-success cases.
@@ -176,6 +180,6 @@ export class RestClient implements RemoteConfigFetchClient {
176180
});
177181
}
178182

179-
return { status, eTag: responseEtag, config };
183+
return { status, eTag: responseEtag, config, experiments };
180184
}
181185
}

packages/remote-config/src/public_types.ts

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

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

93-
// Note: we're not extracting experiment metadata until
94-
// ABT and Analytics have Web SDKs.
119+
/**
120+
* A/B Test and Rollout experiment metadata.
121+
*
122+
* <p>Only defined for 200 responses.
123+
*/
124+
experiments?: FirebaseExperimentDescription[];
95125
}
96126

97127
/**

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ describe('Remote Config API', () => {
5757
const STUB_FETCH_RESPONSE: FetchResponse = {
5858
status: 200,
5959
eTag: 'asdf',
60-
config: { 'foobar': 'hello world' }
60+
config: { 'foobar': 'hello world' },
61+
experiments: [{
62+
experimentId: "_exp_1",
63+
variantId : "1",
64+
experimentStartTime : "2025-04-06T14:13:57.597Z",
65+
triggerTimeoutMillis : "15552000000",
66+
timeToLiveMillis : "15552000000"
67+
}]
6168
};
6269
let fetchStub: sinon.SinonStub;
6370

@@ -94,6 +101,7 @@ describe('Remote Config API', () => {
94101
json: () =>
95102
Promise.resolve({
96103
entries: response.config,
104+
experimentDescriptions: response.experiments,
97105
state: 'OK'
98106
})
99107
} as Response)

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ describe('RestClient', () => {
7474
status: 200,
7575
eTag: 'etag',
7676
state: 'UPDATE',
77-
entries: { color: 'sparkling' }
77+
entries: { color: 'sparkling' },
78+
experimentDescriptions: [{
79+
experimentId: "_exp_1",
80+
variantId : "1",
81+
experimentStartTime : "2025-04-06T14:13:57.597Z",
82+
triggerTimeoutMillis : "15552000000",
83+
timeToLiveMillis : "15552000000"
84+
}]
7885
};
7986

8087
fetchStub.returns(
@@ -85,7 +92,8 @@ describe('RestClient', () => {
8592
json: () =>
8693
Promise.resolve({
8794
entries: expectedResponse.entries,
88-
state: expectedResponse.state
95+
state: expectedResponse.state,
96+
experimentDescriptions: expectedResponse.experimentDescriptions
8997
})
9098
} as Response)
9199
);
@@ -95,7 +103,8 @@ describe('RestClient', () => {
95103
expect(response).to.deep.eq({
96104
status: expectedResponse.status,
97105
eTag: expectedResponse.eTag,
98-
config: expectedResponse.entries
106+
config: expectedResponse.entries,
107+
experiments: expectedResponse.experimentDescriptions
99108
});
100109
});
101110

@@ -184,7 +193,8 @@ describe('RestClient', () => {
184193
expect(response).to.deep.eq({
185194
status: 304,
186195
eTag: 'response-etag',
187-
config: undefined
196+
config: undefined,
197+
experiments: undefined
188198
});
189199
});
190200

@@ -222,7 +232,8 @@ describe('RestClient', () => {
222232
expect(response).to.deep.eq({
223233
status: 304,
224234
eTag: 'etag',
225-
config: undefined
235+
config: undefined,
236+
experiments: undefined
226237
});
227238
});
228239

@@ -239,7 +250,8 @@ describe('RestClient', () => {
239250
await expect(client.fetch(DEFAULT_REQUEST)).to.eventually.be.deep.eq({
240251
status: 200,
241252
eTag: 'etag',
242-
config: {}
253+
config: {},
254+
experiments: []
243255
});
244256
}
245257
});

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ describe('RemoteConfig', () => {
380380
const ETAG = 'etag';
381381
const CONFIG = { key: 'val' };
382382
const NEW_ETAG = 'new_etag';
383+
const EXPERIMENTS = [{
384+
"experimentId" : "_exp_1",
385+
"variantId" : "1",
386+
"experimentStartTime" : "2025-04-06T14:13:57.597Z",
387+
"triggerTimeoutMillis" : "15552000000",
388+
"timeToLiveMillis" : "15552000000"
389+
}];
383390

384391
let getLastSuccessfulFetchResponseStub: sinon.SinonStub;
385392
let getActiveConfigEtagStub: sinon.SinonStub;
@@ -425,7 +432,7 @@ describe('RemoteConfig', () => {
425432

426433
it('activates if fetched and active etags are different', async () => {
427434
getLastSuccessfulFetchResponseStub.returns(
428-
Promise.resolve({ config: CONFIG, eTag: NEW_ETAG })
435+
Promise.resolve({ config: CONFIG, experiments: EXPERIMENTS, eTag: NEW_ETAG })
429436
);
430437
getActiveConfigEtagStub.returns(Promise.resolve(ETAG));
431438

@@ -438,7 +445,7 @@ describe('RemoteConfig', () => {
438445

439446
it('activates if fetched is defined but active config is not', async () => {
440447
getLastSuccessfulFetchResponseStub.returns(
441-
Promise.resolve({ config: CONFIG, eTag: NEW_ETAG })
448+
Promise.resolve({ config: CONFIG, experiments: EXPERIMENTS, eTag: NEW_ETAG })
442449
);
443450
getActiveConfigEtagStub.returns(Promise.resolve());
444451

0 commit comments

Comments
 (0)