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)
This commit is contained in:
Ashish Kothari
2025-01-08 15:27:02 +05:30
committed by GitHub
parent 4e87280452
commit c69962d1a5
18 changed files with 411 additions and 14 deletions
+7
View File
@@ -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
View File
@@ -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>;
@@ -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;
+2
View File
@@ -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
@@ -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
View File
@@ -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. |
@@ -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. |
@@ -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.
+1
View File
@@ -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:
*
+13
View File
@@ -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;
+62 -2
View File
@@ -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';
@@ -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');
@@ -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}`
);
}
}
@@ -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).
@@ -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;
}
/**
@@ -15,6 +15,7 @@
* limitations under the License.
*/
import { CustomSignals } from '../public_types';
import {
FetchResponse,
RemoteConfigFetchClient,
@@ -41,6 +42,7 @@ interface FetchRequestBody {
app_instance_id_token: string;
app_id: string;
language_code: string;
custom_signals?: CustomSignals;
/* eslint-enable camelcase */
}
@@ -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 */
};
+3
View File
@@ -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;
+6 -2
View File
@@ -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 } = {
@@ -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
@@ -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>(
@@ -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;
+85 -7
View File
@@ -15,12 +15,13 @@
* limitations under the License.
*/
import { FetchStatus } from '@firebase/remote-config-types';
import { FetchStatus, CustomSignals } from '@firebase/remote-config-types';
import {
FetchResponse,
FirebaseRemoteConfigObject
} from '../client/remote_config_fetch_client';
import { ERROR_FACTORY, ErrorCode } from '../errors';
import { RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS } from '../constants';
import { FirebaseError } from '@firebase/util';
/**
@@ -70,7 +71,8 @@ type ProjectNamespaceKeyFieldValue =
| 'last_successful_fetch_timestamp_millis'
| 'last_successful_fetch_response'
| 'settings'
| 'throttle_metadata';
| 'throttle_metadata'
| 'custom_signals';
// Visible for testing.
export function openDatabase(): Promise<IDBDatabase> {
@@ -181,10 +183,64 @@ export class Storage {
return this.delete('throttle_metadata');
}
async get<T>(key: ProjectNamespaceKeyFieldValue): Promise<T | undefined> {
getCustomSignals(): Promise<CustomSignals | undefined> {
return this.get<CustomSignals>('custom_signals');
}
async setCustomSignals(customSignals: CustomSignals): Promise<CustomSignals> {
const db = await this.openDbPromise;
const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
const storedSignals = await this.getWithTransaction<CustomSignals>(
'custom_signals',
transaction
);
const combinedSignals = {
...storedSignals,
...customSignals
};
// Filter out key-value assignments with null values since they are signals being unset
const updatedSignals = Object.fromEntries(
Object.entries(combinedSignals)
.filter(([_, v]) => v !== null)
.map(([k, v]) => {
// Stringify numbers to store a map of string keys and values which can be sent
// as-is in a fetch call.
if (typeof v === 'number') {
return [k, v.toString()];
}
return [k, v];
})
);
// Throw an error if the number of custom signals to be stored exceeds the limit
if (
Object.keys(updatedSignals).length > RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS
) {
throw ERROR_FACTORY.create(ErrorCode.CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS, {
maxSignals: RC_CUSTOM_SIGNAL_MAX_ALLOWED_SIGNALS
});
}
await this.setWithTransaction<CustomSignals>(
'custom_signals',
updatedSignals,
transaction
);
return updatedSignals;
}
/**
* Gets a value from the database using the provided transaction.
*
* @param key The key of the value to get.
* @param transaction The transaction to use for the operation.
* @returns The value associated with the key, or undefined if no such value exists.
*/
async getWithTransaction<T>(
key: ProjectNamespaceKeyFieldValue,
transaction: IDBTransaction
): Promise<T | undefined> {
return new Promise((resolve, reject) => {
const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly');
const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
const compositeKey = this.createCompositeKey(key);
try {
@@ -210,10 +266,20 @@ export class Storage {
});
}
async set<T>(key: ProjectNamespaceKeyFieldValue, value: T): Promise<void> {
const db = await this.openDbPromise;
/**
* Sets a value in the database using the provided transaction.
*
* @param key The key of the value to set.
* @param value The value to set.
* @param transaction The transaction to use for the operation.
* @returns A promise that resolves when the operation is complete.
*/
async setWithTransaction<T>(
key: ProjectNamespaceKeyFieldValue,
value: T,
transaction: IDBTransaction
): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
const objectStore = transaction.objectStore(APP_NAMESPACE_STORE);
const compositeKey = this.createCompositeKey(key);
try {
@@ -237,6 +303,18 @@ export class Storage {
});
}
async get<T>(key: ProjectNamespaceKeyFieldValue): Promise<T | undefined> {
const db = await this.openDbPromise;
const transaction = db.transaction([APP_NAMESPACE_STORE], 'readonly');
return this.getWithTransaction<T>(key, transaction);
}
async set<T>(key: ProjectNamespaceKeyFieldValue, value: T): Promise<void> {
const db = await this.openDbPromise;
const transaction = db.transaction([APP_NAMESPACE_STORE], 'readwrite');
return this.setWithTransaction<T>(key, value, transaction);
}
async delete(key: ProjectNamespaceKeyFieldValue): Promise<void> {
const db = await this.openDbPromise;
return new Promise((resolve, reject) => {
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { FetchStatus } from '@firebase/remote-config-types';
import { FetchStatus, CustomSignals } from '@firebase/remote-config-types';
import { FirebaseRemoteConfigObject } from '../client/remote_config_fetch_client';
import { Storage } from './storage';
@@ -31,6 +31,7 @@ export class StorageCache {
private lastFetchStatus?: FetchStatus;
private lastSuccessfulFetchTimestampMillis?: number;
private activeConfig?: FirebaseRemoteConfigObject;
private customSignals?: CustomSignals;
/**
* Memory-only getters
@@ -47,6 +48,10 @@ export class StorageCache {
return this.activeConfig;
}
getCustomSignals(): CustomSignals | undefined {
return this.customSignals;
}
/**
* Read-ahead getter
*/
@@ -55,6 +60,7 @@ export class StorageCache {
const lastSuccessfulFetchTimestampMillisPromise =
this.storage.getLastSuccessfulFetchTimestampMillis();
const activeConfigPromise = this.storage.getActiveConfig();
const customSignalsPromise = this.storage.getCustomSignals();
// Note:
// 1. we consistently check for undefined to avoid clobbering defined values
@@ -78,6 +84,11 @@ export class StorageCache {
if (activeConfig) {
this.activeConfig = activeConfig;
}
const customSignals = await customSignalsPromise;
if (customSignals) {
this.customSignals = customSignals;
}
}
/**
@@ -99,4 +110,8 @@ export class StorageCache {
this.activeConfig = activeConfig;
return this.storage.setActiveConfig(activeConfig);
}
async setCustomSignals(customSignals: CustomSignals): Promise<void> {
this.customSignals = await this.storage.setCustomSignals(customSignals);
}
}
@@ -42,7 +42,8 @@ import {
getString,
getValue,
setLogLevel,
fetchConfig
fetchConfig,
setCustomSignals
} from '../src/api';
import * as api from '../src/api';
import { fetchAndActivate } from '../src';
@@ -93,6 +94,48 @@ describe('RemoteConfig', () => {
loggerLogLevelSpy.restore();
});
describe('setCustomSignals', () => {
beforeEach(() => {
storageCache.setCustomSignals = sinon.stub();
storage.setCustomSignals = sinon.stub();
logger.error = sinon.stub();
});
it('call storage API to store signals', async () => {
await setCustomSignals(rc, { key: 'value' });
expect(storageCache.setCustomSignals).to.have.been.calledWith({
key: 'value'
});
});
it('logs an error when supplied with a custom signal key greater than 250 characters', async () => {
const longKey = 'a'.repeat(251);
const customSignals = { [longKey]: 'value' };
await setCustomSignals(rc, customSignals);
expect(storageCache.setCustomSignals).to.not.have.been.called;
expect(logger.error).to.have.been.called;
});
it('logs an error when supplied with a custom signal value greater than 500 characters', async () => {
const longValue = 'a'.repeat(501);
const customSignals = { 'key': longValue };
await setCustomSignals(rc, customSignals);
expect(storageCache.setCustomSignals).to.not.have.been.called;
expect(logger.error).to.have.been.called;
});
it('empty custom signals map does nothing', async () => {
await setCustomSignals(rc, {});
expect(storageCache.setCustomSignals).to.not.have.been.called;
});
});
// Adapts getUserLanguage tests from packages/auth/test/utils_test.js for TypeScript.
describe('setLogLevel', () => {
it('proxies to the FirebaseLogger instance', () => {
@@ -449,6 +492,7 @@ describe('RemoteConfig', () => {
.stub()
.returns(Promise.resolve({ status: 200 } as FetchResponse));
storageCache.setLastFetchStatus = sinon.stub();
storageCache.getCustomSignals = sinon.stub();
timeoutStub = sinon.stub(window, 'setTimeout');
});
@@ -517,5 +561,11 @@ describe('RemoteConfig', () => {
'failure'
);
});
it('sends custom signals', async () => {
await fetchConfig(rc);
expect(storageCache.getCustomSignals).to.have.been.called;
});
});
});
@@ -117,4 +117,58 @@ describe('Storage', () => {
expect(actualMetadata).to.be.undefined;
});
it('sets and gets custom signals', async () => {
const customSignals = { key: 'value', key1: 'value1', key2: 1 };
const customSignalsInStorage = {
key: 'value',
key1: 'value1',
key2: '1'
};
await storage.setCustomSignals(customSignals);
const storedCustomSignals = await storage.getCustomSignals();
expect(storedCustomSignals).to.deep.eq(customSignalsInStorage);
});
it('upserts custom signals when key is present in storage', async () => {
const customSignals = { key: 'value', key1: 'value1' };
const updatedSignals = { key: 'value', key1: 'value2' };
await storage.setCustomSignals(customSignals);
await storage.setCustomSignals({ key1: 'value2' });
const storedCustomSignals = await storage.getCustomSignals();
expect(storedCustomSignals).to.deep.eq(updatedSignals);
});
it('deletes custom signal when value supplied is null', async () => {
const customSignals = { key: 'value', key1: 'value1' };
const updatedSignals = { key: 'value' };
await storage.setCustomSignals(customSignals);
await storage.setCustomSignals({ key1: null });
const storedCustomSignals = await storage.getCustomSignals();
expect(storedCustomSignals).to.deep.eq(updatedSignals);
});
it('throws an error when supplied with excess custom signals', async () => {
const customSignals: { [key: string]: string } = {};
for (let i = 0; i < 101; i++) {
customSignals[`key${i}`] = `value${i}`;
}
await expect(
storage.setCustomSignals(customSignals)
).to.eventually.be.rejectedWith(
'Remote Config: Setting more than 100 custom signals is not supported.'
);
});
});
@@ -37,6 +37,7 @@ describe('StorageCache', () => {
const status = 'success';
const lastSuccessfulFetchTimestampMillis = 123;
const activeConfig = { key: 'value' };
const customSignals = { 'key': 'value' };
storage.getLastFetchStatus = sinon
.stub()
@@ -47,12 +48,16 @@ describe('StorageCache', () => {
storage.getActiveConfig = sinon
.stub()
.returns(Promise.resolve(activeConfig));
storage.getCustomSignals = sinon
.stub()
.returns(Promise.resolve(customSignals));
await storageCache.loadFromStorage();
expect(storage.getLastFetchStatus).to.have.been.called;
expect(storage.getLastSuccessfulFetchTimestampMillis).to.have.been.called;
expect(storage.getActiveConfig).to.have.been.called;
expect(storage.getCustomSignals).to.have.been.called;
expect(storageCache.getLastFetchStatus()).to.eq(status);
expect(storageCache.getLastSuccessfulFetchTimestampMillis()).to.deep.eq(
@@ -81,4 +86,26 @@ describe('StorageCache', () => {
expect(storage.setActiveConfig).to.have.been.calledWith(activeConfig);
});
});
describe('setCustomSignals', () => {
const customSignals = { key: 'value' };
beforeEach(() => {
storage.setCustomSignals = sinon
.stub()
.returns(Promise.resolve(customSignals));
});
it('writes to memory cache', async () => {
await storageCache.setCustomSignals(customSignals);
expect(storageCache.getCustomSignals()).to.deep.eq(customSignals);
});
it('writes to persistent storage', async () => {
await storageCache.setCustomSignals(customSignals);
expect(storage.setCustomSignals).to.have.been.calledWith(customSignals);
});
});
});