Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/api-review/remote-config.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export function fetchConfig(remoteConfig: RemoteConfig): Promise<void>;
export interface FetchResponse {
config?: FirebaseRemoteConfigObject;
eTag?: string;
// Warning: (ae-forgotten-export) The symbol "FirebaseExperimentDescription" needs to be exported by the entry point index.d.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't look far enough up and see that this is part of FetchResponse, which is already public. In that case I guess we need to export FirebaseExperimentDescription as well. You can keep the documentation on it minimal if you don't expect users to have to populate it. I was trying to see why FetchResponse needs to be public and the only place I see it exposed to users is that they can specify a "initialFetchResponse" in RemoteConfigOptions when they initialize remote config but I don't know where they get that object from. I assume from server-side RC somewhere, and they just pass it in. So in that case they probably don't inspect it and probably don't need much documentation about what's in it, but since they're handling it and passing it, I guess they need a somewhat accurate typing for it.

I'll leave it up to you and your tech writer how much or how little documentation you think it needs but if FetchResponse needs to stay public, then we need to at least export the types of everything in it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

experiments?: FirebaseExperimentDescription[];
status: number;
templateVersion?: number;
}
Expand Down
13 changes: 13 additions & 0 deletions docs-devsite/remote-config.fetchresponse.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface FetchResponse
| --- | --- | --- |
| [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. |
| [eTag](./remote-config.fetchresponse.md#fetchresponseetag) | string | Defines the ETag response header value.<p>Only defined for 200 and 304 responses. |
| [experiments](./remote-config.fetchresponse.md#fetchresponseexperiments) | FirebaseExperimentDescription\[\] | A/B Test and Rollout experiment metadata. |
| [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. |
| [templateVersion](./remote-config.fetchresponse.md#fetchresponsetemplateversion) | number | The version number of the config template fetched from the server. |

Expand Down Expand Up @@ -53,6 +54,18 @@ Defines the ETag response header value.
eTag?: string;
```

## FetchResponse.experiments

A/B Test and Rollout experiment metadata.

Only defined for 200 responses.

<b>Signature:</b>

```typescript
experiments?: FirebaseExperimentDescription[];
```

## FetchResponse.status

The HTTP status, which is useful for differentiating success responses with data from those without.
Expand Down
31 changes: 30 additions & 1 deletion packages/remote-config/src/client/rest_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ interface FetchRequestBody {
/* eslint-enable camelcase */
}

/**
* Defines experiment and variant attached to a config parameter.
*/
export interface FirebaseExperimentDescription {
// A string of max length 22 characters and of format: _exp_<experiment_id>
experimentId: string;

// The variant of the experiment assigned to the app instance.
variantId: string;

// When the experiment was started.
experimentStartTime: string;

// How long the experiment can remain in STANDBY state. Valid range from 1 ms
// to 6 months.
triggerTimeoutMillis: string;

// How long the experiment can remain in ON state. Valid range from 1 ms to 6
// months.
timeToLiveMillis: string;

// A repeated of Remote Config parameter keys that this experiment is
// affecting the value of.
affectedParameterKeys?: string[];
}

/**
* Implements the Client abstraction for the Remote Config REST API.
*/
Expand Down Expand Up @@ -143,6 +169,7 @@ export class RestClient implements RemoteConfigFetchClient {
let config: FirebaseRemoteConfigObject | undefined;
let state: string | undefined;
let templateVersion: number | undefined;
let experiments: FirebaseExperimentDescription[] | undefined;

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

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

// Normalize to exception-based control flow for non-success cases.
Expand All @@ -180,6 +209,6 @@ export class RestClient implements RemoteConfigFetchClient {
});
}

return { status, eTag: responseEtag, config, templateVersion };
return { status, eTag: responseEtag, config, templateVersion, experiments };
}
}
9 changes: 7 additions & 2 deletions packages/remote-config/src/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { FirebaseApp, FirebaseError } from '@firebase/app';
import { FirebaseExperimentDescription } from './client/rest_client';

/**
* The Firebase Remote Config service interface.
Expand Down Expand Up @@ -99,8 +100,12 @@ export interface FetchResponse {
*/
templateVersion?: number;

// Note: we're not extracting experiment metadata until
// ABT and Analytics have Web SDKs.
/**
* A/B Test and Rollout experiment metadata.
*
* @remarks Only defined for 200 responses.
*/
experiments?: FirebaseExperimentDescription[];
}

/**
Expand Down
14 changes: 12 additions & 2 deletions packages/remote-config/test/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,16 @@ describe('Remote Config API', () => {
status: 200,
eTag: 'asdf',
config: { 'foobar': 'hello world' },
templateVersion: 1
templateVersion: 1,
experiments: [
{
experimentId: '_exp_1',
variantId: '1',
experimentStartTime: '2025-04-06T14:13:57.597Z',
triggerTimeoutMillis: '15552000000',
timeToLiveMillis: '15552000000'
}
]
};
let fetchStub: sinon.SinonStub;

Expand Down Expand Up @@ -106,7 +115,8 @@ describe('Remote Config API', () => {
Promise.resolve({
entries: response.config,
state: 'OK',
templateVersion: response.templateVersion
templateVersion: response.templateVersion,
experimentDescriptions: response.experiments
})
} as Response)
);
Expand Down
26 changes: 20 additions & 6 deletions packages/remote-config/test/client/rest_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ describe('RestClient', () => {
eTag: 'etag',
state: 'UPDATE',
entries: { color: 'sparkling' },
templateVersion: 1
templateVersion: 1,
experimentDescriptions: [
{
experimentId: '_exp_1',
variantId: '1',
experimentStartTime: '2025-04-06T14:13:57.597Z',
triggerTimeoutMillis: '15552000000',
timeToLiveMillis: '15552000000'
}
]
};

fetchStub.returns(
Expand All @@ -90,7 +99,8 @@ describe('RestClient', () => {
Promise.resolve({
entries: expectedResponse.entries,
state: expectedResponse.state,
templateVersion: expectedResponse.templateVersion
templateVersion: expectedResponse.templateVersion,
experimentDescriptions: expectedResponse.experimentDescriptions
})
} as Response)
);
Expand All @@ -101,7 +111,8 @@ describe('RestClient', () => {
status: expectedResponse.status,
eTag: expectedResponse.eTag,
config: expectedResponse.entries,
templateVersion: expectedResponse.templateVersion
templateVersion: expectedResponse.templateVersion,
experiments: expectedResponse.experimentDescriptions
});
});

Expand Down Expand Up @@ -191,7 +202,8 @@ describe('RestClient', () => {
status: 304,
eTag: 'response-etag',
config: undefined,
templateVersion: undefined
templateVersion: undefined,
experiments: undefined
});
});

Expand Down Expand Up @@ -230,7 +242,8 @@ describe('RestClient', () => {
status: 304,
eTag: 'etag',
config: undefined,
templateVersion: undefined
templateVersion: undefined,
experiments: undefined
});
});

Expand All @@ -248,7 +261,8 @@ describe('RestClient', () => {
status: 200,
eTag: 'etag',
config: {},
templateVersion: undefined
templateVersion: undefined,
experiments: []
});
}
});
Expand Down
15 changes: 13 additions & 2 deletions packages/remote-config/test/remote_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,15 @@ describe('RemoteConfig', () => {
const CONFIG = { key: 'val' };
const NEW_ETAG = 'new_etag';
const TEMPLATE_VERSION = 1;
const EXPERIMENTS = [
{
'experimentId': '_exp_1',
'variantId': '1',
'experimentStartTime': '2025-04-06T14:13:57.597Z',
'triggerTimeoutMillis': '15552000000',
'timeToLiveMillis': '15552000000'
}
];

let getLastSuccessfulFetchResponseStub: sinon.SinonStub;
let getActiveConfigEtagStub: sinon.SinonStub;
Expand Down Expand Up @@ -456,7 +465,8 @@ describe('RemoteConfig', () => {
Promise.resolve({
config: CONFIG,
eTag: NEW_ETAG,
templateVersion: TEMPLATE_VERSION
templateVersion: TEMPLATE_VERSION,
experiments: EXPERIMENTS
})
);
getActiveConfigEtagStub.returns(Promise.resolve(ETAG));
Expand All @@ -476,7 +486,8 @@ describe('RemoteConfig', () => {
Promise.resolve({
config: CONFIG,
eTag: NEW_ETAG,
templateVersion: TEMPLATE_VERSION
templateVersion: TEMPLATE_VERSION,
experiments: EXPERIMENTS
})
);
getActiveConfigEtagStub.returns(Promise.resolve());
Expand Down
Loading