mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
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:
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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<void>
|
||||
|
||||
### setLogLevel(remoteConfig, logLevel) {:#setloglevel_039a45b}
|
||||
|
||||
Defines the log level to use.
|
||||
|
||||
Vendored
+1
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user