abmeter
Version:
ABMeter browser SDK — feature flags and A/B experiments with server-side pre-evaluated assignments
139 lines (131 loc) • 4.8 kB
text/typescript
interface PlatformStorage {
getItem(key: string): Promise<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
}
interface PlatformLifecycle {
/**
* Fires when the app enters "background" — visibility hidden + pagehide in
* browsers, AppState 'active'→'background'/'inactive' in React Native.
* Handler is invoked once per transition. Returns an unsubscribe.
*/
onBackground(handler: () => void): () => void;
}
interface PlatformAdapter {
/** 'browser' | 'react-native' | ... — for logs. */
readonly name: string;
storage: PlatformStorage;
lifecycle: PlatformLifecycle;
}
interface UserInput {
userId?: string;
email?: string;
}
interface User {
userId: string;
email?: string;
}
type Logger = (message: string, payload?: unknown) => void;
type ErrorCallback = (error: unknown) => void;
interface ConfigInput {
apiKey: string;
baseUrl?: string;
user?: UserInput;
/** Milliseconds between background flushes. */
flushInterval?: number;
logger?: Logger;
errorCallback?: ErrorCallback;
/** Storage + lifecycle seam for non-browser runtimes; defaults to the browser adapter. */
platform?: PlatformAdapter;
}
interface Config {
apiKey: string;
baseUrl: string;
flushIntervalMs: number;
logger: Logger;
errorCallback?: ErrorCallback;
platform: PlatformAdapter;
}
interface ExposureRecord {
parameter_id: number;
space_id: number;
resolved_value: unknown;
user_id: string;
exposable_type: 'Experiment';
exposable_id: number;
audience_id: number;
resolved_at: string;
}
/**
* Initialize the SDK singleton. Synchronous, but init is not: identity loads
* from the platform storage, then the cache hydrates and refreshes — all
* awaited by ready(), the documented gate. A resolveParameter before ready()
* returns undefined even when a cached map exists. Throws on
* misconfiguration — a bad apiKey must be loud, not error-safe.
*/
declare function configure(input: ConfigInput): void;
/** Resolves when init (identity, cache hydrate, first refresh) has settled. */
declare function ready(): Promise<void>;
/**
* Resolved value for this user, or undefined when unknown/unconfigured. Queues
* an exposure lazily — only experiment resolutions carry exposure metadata,
* and repeats inside the dedup window are not re-queued.
*/
declare function resolveParameter(slug: string): unknown;
/** The exposure record resolveParameter would submit, or null — without queueing anything. */
declare function getExposure(slug: string): ExposureRecord | null;
/**
* Queue an event for the configured user.
*
* Deliberately takes no user id, unlike the server-side SDKs: there one
* process serves every user, so each call must say who it is for, while a
* page has exactly the one user configure() established. An override would
* only ever detach the event — results attribute events to a visitor by
* matching the id their exposure was recorded under, so an event under any
* other id is stored, counted, and never joined.
*/
declare function trackEvent(eventSlug: string, customFields?: Record<string, unknown>): void;
/** Drain the queue now (e.g. on SPA route changes). */
declare function flush(): Promise<void>;
/**
* Drain fully and tear down timers/listeners; configure() again to restart.
* force drops the queue instead of draining it.
*/
declare function reset(options?: {
force?: boolean;
}): Promise<void>;
declare const VERSION = "0.3.0";
interface ExposureMetadata {
exposable_type: 'Experiment';
exposable_id: number;
audience_id: number;
}
interface Assignment {
value: unknown;
parameter_id: number;
space_id: number;
/** null for feature-flag and default resolutions — never report those as exposures. */
exposure: ExposureMetadata | null;
}
interface AssignmentsPayload {
user: {
user_id: string;
email?: string | null;
};
assignments: Record<string, Assignment>;
}
interface ErrorBody {
error?: unknown;
details?: {
failures?: unknown[];
invalid_count?: number;
};
}
declare class ApiError extends Error {
readonly status: number;
readonly details: ErrorBody['details'];
constructor(status: number, body: unknown);
get retryable(): boolean;
get partialFailure(): boolean;
}
export { ApiError, type Assignment, type AssignmentsPayload, type Config, type ConfigInput, type ErrorCallback, type ExposureMetadata, type ExposureRecord, type Logger, type PlatformAdapter, type PlatformLifecycle, type PlatformStorage, type User, type UserInput, VERSION, configure, flush, getExposure, ready, reset, resolveParameter, trackEvent };