Skip to content

Commit 7bf2aec

Browse files
feat(rc): Add custom signals support (#8602)
Add support for custom signal targeting in Remote Config. Using this feature, developers can set custom signals (key/value pairs) in their apps and use them for building custom targeting conditions in their templates. Design doc (internal): [go/rc-custom-targeting-dd](http://goto.google.com/rc-custom-targeting-dd) API Proposal (internal): [go/remote-config-custom-targeting-signals-api-review](https://goto.google.com/remote-config-custom-targeting-signals-api-review)
1 parent f3a8df7 commit 7bf2aec

18 files changed

+411
-14
lines changed

.changeset/hip-apricots-end.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@firebase/remote-config-types': minor
3+
'@firebase/remote-config': minor
4+
'firebase': minor
5+
---
6+
7+
Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config.

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

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { FirebaseApp } from '@firebase/app';
99
// @public
1010
export function activate(remoteConfig: RemoteConfig): Promise<boolean>;
1111

12+
// @public
13+
export interface CustomSignals {
14+
// (undocumented)
15+
[key: string]: string | number | null;
16+
}
17+
1218
// @public
1319
export function ensureInitialized(remoteConfig: RemoteConfig): Promise<void>;
1420

@@ -62,6 +68,9 @@ export interface RemoteConfigSettings {
6268
minimumFetchIntervalMillis: number;
6369
}
6470

71+
// @public
72+
export function setCustomSignals(remoteConfig: RemoteConfig, customSignals: CustomSignals): Promise<void>;
73+
6574
// @public
6675
export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void;
6776

docs-devsite/_toc.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ toc:
428428
- title: remote-config
429429
path: /docs/reference/js/remote-config.md
430430
section:
431+
- title: CustomSignals
432+
path: /docs/reference/js/remote-config.customsignals.md
431433
- title: RemoteConfig
432434
path: /docs/reference/js/remote-config.remoteconfig.md
433435
- title: RemoteConfigSettings
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
# CustomSignals interface
13+
Defines the type for representing custom signals and their values.
14+
15+
<p>The values in CustomSignals must be one of the following types:
16+
17+
<ul> <li><code>string</code> <li><code>number</code> <li><code>null</code> </ul>
18+
19+
<b>Signature:</b>
20+
21+
```typescript
22+
export interface CustomSignals
23+
```

docs-devsite/remote-config.md

+23
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
2828
| [getNumber(remoteConfig, key)](./remote-config.md#getnumber_476c09f) | Gets the value for the given key as a number.<!-- -->Convenience method for calling <code>remoteConfig.getValue(key).asNumber()</code>. |
2929
| [getString(remoteConfig, key)](./remote-config.md#getstring_476c09f) | Gets the value for the given key as a string. Convenience method for calling <code>remoteConfig.getValue(key).asString()</code>. |
3030
| [getValue(remoteConfig, key)](./remote-config.md#getvalue_476c09f) | Gets the [Value](./remote-config.value.md#value_interface) for the given key. |
31+
| [setCustomSignals(remoteConfig, customSignals)](./remote-config.md#setcustomsignals_aeeb95e) | Sets the custom signals for the app instance. |
3132
| [setLogLevel(remoteConfig, logLevel)](./remote-config.md#setloglevel_039a45b) | Defines the log level to use. |
3233
| <b>function()</b> |
3334
| [isSupported()](./remote-config.md#issupported) | This method provides two different checks:<!-- -->1. Check if IndexedDB exists in the browser environment. 2. Check if the current browser context allows IndexedDB <code>open()</code> calls. |
@@ -36,6 +37,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
3637

3738
| Interface | Description |
3839
| --- | --- |
40+
| [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> |
3941
| [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The Firebase Remote Config service interface. |
4042
| [RemoteConfigSettings](./remote-config.remoteconfigsettings.md#remoteconfigsettings_interface) | Defines configuration options for the Remote Config SDK. |
4143
| [Value](./remote-config.value.md#value_interface) | Wraps a value with metadata and type-safe getters. |
@@ -276,6 +278,27 @@ export declare function getValue(remoteConfig: RemoteConfig, key: string): Value
276278

277279
The value for the given key.
278280

281+
### setCustomSignals(remoteConfig, customSignals) {:#setcustomsignals_aeeb95e}
282+
283+
Sets the custom signals for the app instance.
284+
285+
<b>Signature:</b>
286+
287+
```typescript
288+
export declare function setCustomSignals(remoteConfig: RemoteConfig, customSignals: CustomSignals): Promise<void>;
289+
```
290+
291+
#### Parameters
292+
293+
| Parameter | Type | Description |
294+
| --- | --- | --- |
295+
| remoteConfig | [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance. |
296+
| customSignals | [CustomSignals](./remote-config.customsignals.md#customsignals_interface) | Map (key, value) of the custom signals to be set for the app instance. If a key already exists, the value is overwritten. Setting the value of a custom signal to null unsets the signal. The signals will be persisted locally on the client. |
297+
298+
<b>Returns:</b>
299+
300+
Promise&lt;void&gt;
301+
279302
### setLogLevel(remoteConfig, logLevel) {:#setloglevel_039a45b}
280303

281304
Defines the log level to use.

packages/firebase/compat/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,7 @@ declare namespace firebase.remoteConfig {
20462046
* Defines levels of Remote Config logging.
20472047
*/
20482048
export type LogLevel = 'debug' | 'error' | 'silent';
2049+
20492050
/**
20502051
* This method provides two different checks:
20512052
*

packages/remote-config-types/index.d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';
173173
*/
174174
export type LogLevel = 'debug' | 'error' | 'silent';
175175

176+
/**
177+
* Defines the type for representing custom signals and their values.
178+
*
179+
* <p>The values in CustomSignals must be one of the following types:
180+
*
181+
* <ul>
182+
* <li><code>string</code>
183+
* <li><code>number</code>
184+
* <li><code>null</code>
185+
* </ul>
186+
*/
187+
export type CustomSignals = { [key: string]: string | number | null };
188+
176189
declare module '@firebase/component' {
177190
interface NameServiceMapping {
178191
'remoteConfig-compat': RemoteConfig;

packages/remote-config/src/api.ts

+62-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717

1818
import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
1919
import {
20+
CustomSignals,
2021
LogLevel as RemoteConfigLogLevel,
2122
RemoteConfig,
2223
Value
2324
} from './public_types';
2425
import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client';
25-
import { RC_COMPONENT_NAME } from './constants';
26+
import {
27+
RC_COMPONENT_NAME,
28+
RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH,
29+
RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH
30+
} from './constants';
2631
import { ErrorCode, hasErrorCode } from './errors';
2732
import { RemoteConfig as RemoteConfigImpl } from './remote_config';
2833
import { Value as ValueImpl } from './value';
@@ -114,11 +119,18 @@ export async function fetchConfig(remoteConfig: RemoteConfig): Promise<void> {
114119
abortSignal.abort();
115120
}, rc.settings.fetchTimeoutMillis);
116121

122+
const customSignals = rc._storageCache.getCustomSignals();
123+
if (customSignals) {
124+
rc._logger.debug(
125+
`Fetching config with custom signals: ${JSON.stringify(customSignals)}`
126+
);
127+
}
117128
// Catches *all* errors thrown by client so status can be set consistently.
118129
try {
119130
await rc._client.fetch({
120131
cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis,
121-
signal: abortSignal
132+
signal: abortSignal,
133+
customSignals
122134
});
123135

124136
await rc._storageCache.setLastFetchStatus('success');
@@ -258,3 +270,51 @@ export function setLogLevel(
258270
function getAllKeys(obj1: {} = {}, obj2: {} = {}): string[] {
259271
return Object.keys({ ...obj1, ...obj2 });
260272
}
273+
274+
/**
275+
* Sets the custom signals for the app instance.
276+
*
277+
* @param remoteConfig - The {@link RemoteConfig} instance.
278+
* @param customSignals - Map (key, value) of the custom signals to be set for the app instance. If
279+
* a key already exists, the value is overwritten. Setting the value of a custom signal to null
280+
* unsets the signal. The signals will be persisted locally on the client.
281+
*
282+
* @public
283+
*/
284+
export async function setCustomSignals(
285+
remoteConfig: RemoteConfig,
286+
customSignals: CustomSignals
287+
): Promise<void> {
288+
const rc = getModularInstance(remoteConfig) as RemoteConfigImpl;
289+
if (Object.keys(customSignals).length === 0) {
290+
return;
291+
}
292+
293+
// eslint-disable-next-line guard-for-in
294+
for (const key in customSignals) {
295+
if (key.length > RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH) {
296+
rc._logger.error(
297+
`Custom signal key ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH}.`
298+
);
299+
return;
300+
}
301+
const value = customSignals[key];
302+
if (
303+
typeof value === 'string' &&
304+
value.length > RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH
305+
) {
306+
rc._logger.error(
307+
`Value supplied for custom signal ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH}.`
308+
);
309+
return;
310+
}
311+
}
312+
313+
try {
314+
await rc._storageCache.setCustomSignals(customSignals);
315+
} catch (error) {
316+
rc._logger.error(
317+
`Error encountered while setting custom signals: ${error}`
318+
);
319+
}
320+
}

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

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { CustomSignals } from '../public_types';
19+
1820
/**
1921
* Defines a client, as in https://en.wikipedia.org/wiki/Client%E2%80%93server_model, for the
2022
* Remote Config server (https://firebase.google.com/docs/reference/remote-config/rest).
@@ -99,6 +101,12 @@ export interface FetchRequest {
99101
* <p>Comparable to passing `headers = { 'If-None-Match': <eTag> }` to the native Fetch API.
100102
*/
101103
eTag?: string;
104+
105+
/** The custom signals stored for the app instance.
106+
*
107+
* <p>Optional in case no custom signals are set for the instance.
108+
*/
109+
customSignals?: CustomSignals;
102110
}
103111

104112
/**

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { CustomSignals } from '../public_types';
1819
import {
1920
FetchResponse,
2021
RemoteConfigFetchClient,
@@ -41,6 +42,7 @@ interface FetchRequestBody {
4142
app_instance_id_token: string;
4243
app_id: string;
4344
language_code: string;
45+
custom_signals?: CustomSignals;
4446
/* eslint-enable camelcase */
4547
}
4648

@@ -92,7 +94,8 @@ export class RestClient implements RemoteConfigFetchClient {
9294
app_instance_id: installationId,
9395
app_instance_id_token: installationToken,
9496
app_id: this.appId,
95-
language_code: getUserLanguage()
97+
language_code: getUserLanguage(),
98+
custom_signals: request.customSignals
9699
/* eslint-enable camelcase */
97100
};
98101

packages/remote-config/src/constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@
1616
*/
1717

1818
export const RC_COMPONENT_NAME = 'remote-config';
19+
export const RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 100;
20+
export const RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH = 250;
21+
export const RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH = 500;

packages/remote-config/src/errors.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export const enum ErrorCode {
3131
FETCH_THROTTLE = 'fetch-throttle',
3232
FETCH_PARSE = 'fetch-client-parse',
3333
FETCH_STATUS = 'fetch-status',
34-
INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable'
34+
INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable',
35+
CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 'custom-signal-max-allowed-signals'
3536
}
3637

3738
const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
@@ -67,7 +68,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
6768
[ErrorCode.FETCH_STATUS]:
6869
'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
6970
[ErrorCode.INDEXED_DB_UNAVAILABLE]:
70-
'Indexed DB is not supported by current browser'
71+
'Indexed DB is not supported by current browser',
72+
[ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]:
73+
'Setting more than {$maxSignals} custom signals is not supported.'
7174
};
7275

7376
// Note this is effectively a type system binding a code to params. This approach overlaps with the
@@ -86,6 +89,7 @@ interface ErrorParams {
8689
[ErrorCode.FETCH_THROTTLE]: { throttleEndTimeMillis: number };
8790
[ErrorCode.FETCH_PARSE]: { originalErrorMessage: string };
8891
[ErrorCode.FETCH_STATUS]: { httpStatus: number };
92+
[ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]: { maxSignals: number };
8993
}
9094

9195
export const ERROR_FACTORY = new ErrorFactory<ErrorCode, ErrorParams>(

packages/remote-config/src/public_types.ts

+17
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,23 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';
134134
*/
135135
export type LogLevel = 'debug' | 'error' | 'silent';
136136

137+
/**
138+
* Defines the type for representing custom signals and their values.
139+
*
140+
* <p>The values in CustomSignals must be one of the following types:
141+
*
142+
* <ul>
143+
* <li><code>string</code>
144+
* <li><code>number</code>
145+
* <li><code>null</code>
146+
* </ul>
147+
*
148+
* @public
149+
*/
150+
export interface CustomSignals {
151+
[key: string]: string | number | null;
152+
}
153+
137154
declare module '@firebase/component' {
138155
interface NameServiceMapping {
139156
'remote-config': RemoteConfig;

0 commit comments

Comments
 (0)