Skip to content

Commit 8ee4b72

Browse files
athiramanuAthira M
andauthored
Process experiment metadata in RC fetch response (#9276)
* feat: Process experiment metadata in RC fetch response * [Fix] Storage cache is not updating when there are no experiments in response * Add result of running yarn docgen:all * Address review comments * Fix yarn format failures * yarn docgen changes added * Export firebaseExperimentDescription --------- Co-authored-by: Athira M <athiramanu@google.com>
1 parent 1bcf83d commit 8ee4b72

File tree

10 files changed

+196
-14
lines changed

10 files changed

+196
-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)

docs-devsite/_toc.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,8 @@ toc:
649649
path: /docs/reference/js/remote-config.customsignals.md
650650
- title: FetchResponse
651651
path: /docs/reference/js/remote-config.fetchresponse.md
652+
- title: FirebaseExperimentDescription
653+
path: /docs/reference/js/remote-config.firebaseexperimentdescription.md
652654
- title: FirebaseRemoteConfigObject
653655
path: /docs/reference/js/remote-config.firebaseremoteconfigobject.md
654656
- title: RemoteConfig

docs-devsite/remote-config.fetchresponse.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface FetchResponse
2626
| --- | --- | --- |
2727
| [config](./remote-config.fetchresponse.md#fetchresponseconfig) | [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines the map of parameters returned as "entries" in the fetch response body.<p>Only defined for 200 responses. |
2828
| [eTag](./remote-config.fetchresponse.md#fetchresponseetag) | string | Defines the ETag response header value.<p>Only defined for 200 and 304 responses. |
29+
| [experiments](./remote-config.fetchresponse.md#fetchresponseexperiments) | [FirebaseExperimentDescription](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescription_interface)<!-- -->\[\] | A/B Test and Rollout experiment metadata. |
2930
| [status](./remote-config.fetchresponse.md#fetchresponsestatus) | number | The HTTP status, which is useful for differentiating success responses with data from those without.<p>The Remote Config client is modeled after the native <code>Fetch</code> interface, so HTTP status is first-class.<p>Disambiguation: the fetch response returns a legacy "state" value that is redundant with the HTTP status code. The former is normalized into the latter. |
3031
| [templateVersion](./remote-config.fetchresponse.md#fetchresponsetemplateversion) | number | The version number of the config template fetched from the server. |
3132

@@ -53,6 +54,18 @@ Defines the ETag response header value.
5354
eTag?: string;
5455
```
5556

57+
## FetchResponse.experiments
58+
59+
A/B Test and Rollout experiment metadata.
60+
61+
Only defined for 200 responses.
62+
63+
<b>Signature:</b>
64+
65+
```typescript
66+
experiments?: FirebaseExperimentDescription[];
67+
```
68+
5669
## FetchResponse.status
5770

5871
The HTTP status, which is useful for differentiating success responses with data from those without.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
Project: /docs/reference/js/_project.yaml
2+
Book: /docs/reference/_book.yaml
3+
page_type: reference
4+
5+
{% comment %}
6+
DO NOT EDIT THIS FILE!
7+
This is generated by the JS SDK team, and any local changes will be
8+
overwritten. Changes should be made in the source code at
9+
https://github.com/firebase/firebase-js-sdk
10+
{% endcomment %}
11+
12+
# FirebaseExperimentDescription interface
13+
Defines experiment and variant attached to a config parameter.
14+
15+
<b>Signature:</b>
16+
17+
```typescript
18+
export interface FirebaseExperimentDescription
19+
```
20+
21+
## Properties
22+
23+
| Property | Type | Description |
24+
| --- | --- | --- |
25+
| [affectedParameterKeys](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionaffectedparameterkeys) | string\[\] | |
26+
| [experimentId](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionexperimentid) | string | |
27+
| [experimentStartTime](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionexperimentstarttime) | string | |
28+
| [timeToLiveMillis](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptiontimetolivemillis) | string | |
29+
| [triggerTimeoutMillis](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptiontriggertimeoutmillis) | string | |
30+
| [variantId](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionvariantid) | string | |
31+
32+
## FirebaseExperimentDescription.affectedParameterKeys
33+
34+
<b>Signature:</b>
35+
36+
```typescript
37+
affectedParameterKeys?: string[];
38+
```
39+
40+
## FirebaseExperimentDescription.experimentId
41+
42+
<b>Signature:</b>
43+
44+
```typescript
45+
experimentId: string;
46+
```
47+
48+
## FirebaseExperimentDescription.experimentStartTime
49+
50+
<b>Signature:</b>
51+
52+
```typescript
53+
experimentStartTime: string;
54+
```
55+
56+
## FirebaseExperimentDescription.timeToLiveMillis
57+
58+
<b>Signature:</b>
59+
60+
```typescript
61+
timeToLiveMillis: string;
62+
```
63+
64+
## FirebaseExperimentDescription.triggerTimeoutMillis
65+
66+
<b>Signature:</b>
67+
68+
```typescript
69+
triggerTimeoutMillis: string;
70+
```
71+
72+
## FirebaseExperimentDescription.variantId
73+
74+
<b>Signature:</b>
75+
76+
```typescript
77+
variantId: string;
78+
```

docs-devsite/remote-config.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
4242
| [ConfigUpdateObserver](./remote-config.configupdateobserver.md#configupdateobserver_interface) | Observer interface for receiving real-time Remote Config update notifications.<!-- -->NOTE: Although an <code>complete</code> callback can be provided, it will never be called because the ConfigUpdate stream is never-ending. |
4343
| [CustomSignals](./remote-config.customsignals.md#customsignals_interface) | Defines the type for representing custom signals and their values.<p>The values in CustomSignals must be one of the following types:<ul> <li><code>string</code> <li><code>number</code> <li><code>null</code> </ul> |
4444
| [FetchResponse](./remote-config.fetchresponse.md#fetchresponse_interface) | Defines a successful response (200 or 304).<p>Modeled after the native <code>Response</code> interface, but simplified for Remote Config's use case. |
45+
| [FirebaseExperimentDescription](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescription_interface) | Defines experiment and variant attached to a config parameter. |
4546
| [FirebaseRemoteConfigObject](./remote-config.firebaseremoteconfigobject.md#firebaseremoteconfigobject_interface) | Defines a self-descriptive reference for config key-value pairs. |
4647
| [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The Firebase Remote Config service interface. |
4748
| [RemoteConfigOptions](./remote-config.remoteconfigoptions.md#remoteconfigoptions_interface) | Options for Remote Config initialization. |

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: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,34 @@ export interface FirebaseRemoteConfigObject {
5959
[key: string]: string;
6060
}
6161

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

102-
// Note: we're not extracting experiment metadata until
103-
// ABT and Analytics have Web SDKs.
130+
/**
131+
* A/B Test and Rollout experiment metadata.
132+
*
133+
* @remarks Only defined for 200 responses.
134+
*/
135+
experiments?: FirebaseExperimentDescription[];
104136
}
105137

106138
/**

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

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

@@ -106,7 +115,8 @@ describe('Remote Config API', () => {
106115
Promise.resolve({
107116
entries: response.config,
108117
state: 'OK',
109-
templateVersion: response.templateVersion
118+
templateVersion: response.templateVersion,
119+
experimentDescriptions: response.experiments
110120
})
111121
} as Response)
112122
);

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

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

8493
fetchStub.returns(
@@ -90,7 +99,8 @@ describe('RestClient', () => {
9099
Promise.resolve({
91100
entries: expectedResponse.entries,
92101
state: expectedResponse.state,
93-
templateVersion: expectedResponse.templateVersion
102+
templateVersion: expectedResponse.templateVersion,
103+
experimentDescriptions: expectedResponse.experimentDescriptions
94104
})
95105
} as Response)
96106
);
@@ -101,7 +111,8 @@ describe('RestClient', () => {
101111
status: expectedResponse.status,
102112
eTag: expectedResponse.eTag,
103113
config: expectedResponse.entries,
104-
templateVersion: expectedResponse.templateVersion
114+
templateVersion: expectedResponse.templateVersion,
115+
experiments: expectedResponse.experimentDescriptions
105116
});
106117
});
107118

@@ -191,7 +202,8 @@ describe('RestClient', () => {
191202
status: 304,
192203
eTag: 'response-etag',
193204
config: undefined,
194-
templateVersion: undefined
205+
templateVersion: undefined,
206+
experiments: undefined
195207
});
196208
});
197209

@@ -230,7 +242,8 @@ describe('RestClient', () => {
230242
status: 304,
231243
eTag: 'etag',
232244
config: undefined,
233-
templateVersion: undefined
245+
templateVersion: undefined,
246+
experiments: undefined
234247
});
235248
});
236249

@@ -248,7 +261,8 @@ describe('RestClient', () => {
248261
status: 200,
249262
eTag: 'etag',
250263
config: {},
251-
templateVersion: undefined
264+
templateVersion: undefined,
265+
experiments: []
252266
});
253267
}
254268
});

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

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

395404
let getLastSuccessfulFetchResponseStub: sinon.SinonStub;
396405
let getActiveConfigEtagStub: sinon.SinonStub;
@@ -456,7 +465,8 @@ describe('RemoteConfig', () => {
456465
Promise.resolve({
457466
config: CONFIG,
458467
eTag: NEW_ETAG,
459-
templateVersion: TEMPLATE_VERSION
468+
templateVersion: TEMPLATE_VERSION,
469+
experiments: EXPERIMENTS
460470
})
461471
);
462472
getActiveConfigEtagStub.returns(Promise.resolve(ETAG));
@@ -476,7 +486,8 @@ describe('RemoteConfig', () => {
476486
Promise.resolve({
477487
config: CONFIG,
478488
eTag: NEW_ETAG,
479-
templateVersion: TEMPLATE_VERSION
489+
templateVersion: TEMPLATE_VERSION,
490+
experiments: EXPERIMENTS
480491
})
481492
);
482493
getActiveConfigEtagStub.returns(Promise.resolve());

0 commit comments

Comments
 (0)