Skip to content

Commit

Permalink
feat(rc): Add custom signals support (#8602)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
ashish-kothari authored Jan 8, 2025
1 parent f3a8df7 commit 7bf2aec
Show file tree
Hide file tree
Showing 18 changed files with 411 additions and 14 deletions.
7 changes: 7 additions & 0 deletions .changeset/hip-apricots-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@firebase/remote-config-types': minor
'@firebase/remote-config': minor
'firebase': minor
---

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.
9 changes: 9 additions & 0 deletions common/api-review/remote-config.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { FirebaseApp } from '@firebase/app';
// @public
export function activate(remoteConfig: RemoteConfig): Promise<boolean>;

// @public
export interface CustomSignals {
// (undocumented)
[key: string]: string | number | null;
}

// @public
export function ensureInitialized(remoteConfig: RemoteConfig): Promise<void>;

Expand Down Expand Up @@ -62,6 +68,9 @@ export interface RemoteConfigSettings {
minimumFetchIntervalMillis: number;
}

// @public
export function setCustomSignals(remoteConfig: RemoteConfig, customSignals: CustomSignals): Promise<void>;

// @public
export function setLogLevel(remoteConfig: RemoteConfig, logLevel: LogLevel): void;

Expand Down
2 changes: 2 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,8 @@ toc:
- title: remote-config
path: /docs/reference/js/remote-config.md
section:
- title: CustomSignals
path: /docs/reference/js/remote-config.customsignals.md
- title: RemoteConfig
path: /docs/reference/js/remote-config.remoteconfig.md
- title: RemoteConfigSettings
Expand Down
23 changes: 23 additions & 0 deletions docs-devsite/remote-config.customsignals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# 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>

<b>Signature:</b>

```typescript
export interface CustomSignals
```
23 changes: 23 additions & 0 deletions docs-devsite/remote-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm
| [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>. |
| [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>. |
| [getValue(remoteConfig, key)](./remote-config.md#getvalue_476c09f) | Gets the [Value](./remote-config.value.md#value_interface) for the given key. |
| [setCustomSignals(remoteConfig, customSignals)](./remote-config.md#setcustomsignals_aeeb95e) | Sets the custom signals for the app instance. |
| [setLogLevel(remoteConfig, logLevel)](./remote-config.md#setloglevel_039a45b) | Defines the log level to use. |
| <b>function()</b> |
| [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. |
Expand All @@ -36,6 +37,7 @@ The Firebase Remote Config Web SDK. This SDK does not work in a Node.js environm

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

The value for the given key.

### setCustomSignals(remoteConfig, customSignals) {:#setcustomsignals_aeeb95e}

Sets the custom signals for the app instance.

<b>Signature:</b>

```typescript
export declare function setCustomSignals(remoteConfig: RemoteConfig, customSignals: CustomSignals): Promise<void>;
```

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| remoteConfig | [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) | The [RemoteConfig](./remote-config.remoteconfig.md#remoteconfig_interface) instance. |
| 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. |

<b>Returns:</b>

Promise&lt;void&gt;

### setLogLevel(remoteConfig, logLevel) {:#setloglevel_039a45b}

Defines the log level to use.
Expand Down
1 change: 1 addition & 0 deletions packages/firebase/compat/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,7 @@ declare namespace firebase.remoteConfig {
* Defines levels of Remote Config logging.
*/
export type LogLevel = 'debug' | 'error' | 'silent';

/**
* This method provides two different checks:
*
Expand Down
13 changes: 13 additions & 0 deletions packages/remote-config-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';
*/
export type LogLevel = 'debug' | 'error' | 'silent';

/**
* 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>
*/
export type CustomSignals = { [key: string]: string | number | null };

declare module '@firebase/component' {
interface NameServiceMapping {
'remoteConfig-compat': RemoteConfig;
Expand Down
64 changes: 62 additions & 2 deletions packages/remote-config/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@

import { _getProvider, FirebaseApp, getApp } from '@firebase/app';
import {
CustomSignals,
LogLevel as RemoteConfigLogLevel,
RemoteConfig,
Value
} from './public_types';
import { RemoteConfigAbortSignal } from './client/remote_config_fetch_client';
import { RC_COMPONENT_NAME } from './constants';
import {
RC_COMPONENT_NAME,
RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH,
RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH
} from './constants';
import { ErrorCode, hasErrorCode } from './errors';
import { RemoteConfig as RemoteConfigImpl } from './remote_config';
import { Value as ValueImpl } from './value';
Expand Down Expand Up @@ -114,11 +119,18 @@ export async function fetchConfig(remoteConfig: RemoteConfig): Promise<void> {
abortSignal.abort();
}, rc.settings.fetchTimeoutMillis);

const customSignals = rc._storageCache.getCustomSignals();
if (customSignals) {
rc._logger.debug(
`Fetching config with custom signals: ${JSON.stringify(customSignals)}`
);
}
// Catches *all* errors thrown by client so status can be set consistently.
try {
await rc._client.fetch({
cacheMaxAgeMillis: rc.settings.minimumFetchIntervalMillis,
signal: abortSignal
signal: abortSignal,
customSignals
});

await rc._storageCache.setLastFetchStatus('success');
Expand Down Expand Up @@ -258,3 +270,51 @@ export function setLogLevel(
function getAllKeys(obj1: {} = {}, obj2: {} = {}): string[] {
return Object.keys({ ...obj1, ...obj2 });
}

/**
* Sets the custom signals for the app instance.
*
* @param remoteConfig - The {@link RemoteConfig} instance.
* @param customSignals - 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.
*
* @public
*/
export async function setCustomSignals(
remoteConfig: RemoteConfig,
customSignals: CustomSignals
): Promise<void> {
const rc = getModularInstance(remoteConfig) as RemoteConfigImpl;
if (Object.keys(customSignals).length === 0) {
return;
}

// eslint-disable-next-line guard-for-in
for (const key in customSignals) {
if (key.length > RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH) {
rc._logger.error(
`Custom signal key ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH}.`
);
return;
}
const value = customSignals[key];
if (
typeof value === 'string' &&
value.length > RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH
) {
rc._logger.error(
`Value supplied for custom signal ${key} is too long, max allowed length is ${RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH}.`
);
return;
}
}

try {
await rc._storageCache.setCustomSignals(customSignals);
} catch (error) {
rc._logger.error(
`Error encountered while setting custom signals: ${error}`
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* limitations under the License.
*/

import { CustomSignals } from '../public_types';

/**
* Defines a client, as in https://en.wikipedia.org/wiki/Client%E2%80%93server_model, for the
* Remote Config server (https://firebase.google.com/docs/reference/remote-config/rest).
Expand Down Expand Up @@ -99,6 +101,12 @@ export interface FetchRequest {
* <p>Comparable to passing `headers = { 'If-None-Match': <eTag> }` to the native Fetch API.
*/
eTag?: string;

/** The custom signals stored for the app instance.
*
* <p>Optional in case no custom signals are set for the instance.
*/
customSignals?: CustomSignals;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/remote-config/src/client/rest_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* limitations under the License.
*/

import { CustomSignals } from '../public_types';
import {
FetchResponse,
RemoteConfigFetchClient,
Expand All @@ -41,6 +42,7 @@ interface FetchRequestBody {
app_instance_id_token: string;
app_id: string;
language_code: string;
custom_signals?: CustomSignals;
/* eslint-enable camelcase */
}

Expand Down Expand Up @@ -92,7 +94,8 @@ export class RestClient implements RemoteConfigFetchClient {
app_instance_id: installationId,
app_instance_id_token: installationToken,
app_id: this.appId,
language_code: getUserLanguage()
language_code: getUserLanguage(),
custom_signals: request.customSignals
/* eslint-enable camelcase */
};

Expand Down
3 changes: 3 additions & 0 deletions packages/remote-config/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
*/

export const RC_COMPONENT_NAME = 'remote-config';
export const RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 100;
export const RC_CUSTOM_SIGNAL_KEY_MAX_LENGTH = 250;
export const RC_CUSTOM_SIGNAL_VALUE_MAX_LENGTH = 500;
8 changes: 6 additions & 2 deletions packages/remote-config/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const enum ErrorCode {
FETCH_THROTTLE = 'fetch-throttle',
FETCH_PARSE = 'fetch-client-parse',
FETCH_STATUS = 'fetch-status',
INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable'
INDEXED_DB_UNAVAILABLE = 'indexed-db-unavailable',
CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS = 'custom-signal-max-allowed-signals'
}

const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
Expand Down Expand Up @@ -67,7 +68,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
[ErrorCode.FETCH_STATUS]:
'Fetch server returned an HTTP error status. HTTP status: {$httpStatus}.',
[ErrorCode.INDEXED_DB_UNAVAILABLE]:
'Indexed DB is not supported by current browser'
'Indexed DB is not supported by current browser',
[ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS]:
'Setting more than {$maxSignals} custom signals is not supported.'
};

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

export const ERROR_FACTORY = new ErrorFactory<ErrorCode, ErrorParams>(
Expand Down
17 changes: 17 additions & 0 deletions packages/remote-config/src/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,23 @@ export type FetchStatus = 'no-fetch-yet' | 'success' | 'failure' | 'throttle';
*/
export type LogLevel = 'debug' | 'error' | 'silent';

/**
* 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>
*
* @public
*/
export interface CustomSignals {
[key: string]: string | number | null;
}

declare module '@firebase/component' {
interface NameServiceMapping {
'remote-config': RemoteConfig;
Expand Down
Loading

0 comments on commit 7bf2aec

Please sign in to comment.