mixpanel-react-native
Version:
Official React Native Tracking Library for Mixpanel Analytics
191 lines (167 loc) • 5.17 kB
JavaScript
import { MixpanelLogger } from './mixpanel-logger';
import { getPersistedVariantsKey } from './mixpanel-constants';
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
const VariantLookupPolicy = Object.freeze({
NETWORK_ONLY: 'networkOnly',
NETWORK_FIRST: 'networkFirst',
PERSISTENCE_UNTIL_NETWORK_SUCCESS: 'persistenceUntilNetworkSuccess',
});
const VALID_POLICIES = [
VariantLookupPolicy.NETWORK_ONLY,
VariantLookupPolicy.NETWORK_FIRST,
VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS,
];
/**
* Storage and retrieval of persisted feature flag variants for the JS-fallback
* implementation. Uses the AsyncStorage adapter; payloads are serialized to a
* string via JSON.stringify / JSON.parse.
*/
export class MixpanelFlagPersistence {
constructor(persistence, token, storage) {
this.persistence = persistence;
this.persistedVariantsKey = getPersistedVariantsKey(token);
this.storage = storage;
this.token = token;
}
getPolicy() {
if (!this._isConfigValid()) {
return VariantLookupPolicy.NETWORK_ONLY;
}
return this.persistence.variantLookupPolicy;
}
getTtlMs() {
if (!this._isConfigValid()) {
return DEFAULT_TTL_MS;
}
const configuredTtl = this.persistence.persistenceTtlMs;
return configuredTtl === undefined || configuredTtl === null
? DEFAULT_TTL_MS
: configuredTtl;
}
_isConfigValid() {
const config = this.persistence;
if (!config) {
return false;
}
if (VALID_POLICIES.indexOf(config.variantLookupPolicy) === -1) {
MixpanelLogger.error(
this.token,
'Invalid variantLookupPolicy:',
config.variantLookupPolicy
);
return false;
}
if (
config.persistenceTtlMs !== undefined &&
config.persistenceTtlMs !== null &&
config.persistenceTtlMs <= 0
) {
MixpanelLogger.error(
this.token,
'If provided, persistenceTtlMs must be a positive number. Provided value:',
config.persistenceTtlMs
);
return false;
}
return true;
}
/**
* Load persisted variants if they match the current context and haven't
* expired. Returns `null` for any miss (no persisted blob, TTL expired,
* distinct_id mismatch, or unreadable storage). Stale entries are NOT
* auto-deleted on read — the next successful fetch overwrites them.
*/
async loadFlagsFromStorage(context) {
if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
await this.clear();
return null;
}
const ttlMs = this.getTtlMs();
let data;
try {
const raw = await this.storage.getItem(this.persistedVariantsKey);
data = raw ? JSON.parse(raw) : null;
} catch (error) {
MixpanelLogger.error(
this.token,
'Failed to load persisted variants from storage, so clearing',
error
);
await this.clear();
return null;
}
if (!data) {
return null;
}
if (ttlMs && Date.now() - data.persistedAt >= ttlMs) {
return null;
}
if (!context || data.distinctId !== context.distinct_id) {
await this.clear();
return null;
}
const persistedFlags = new Map();
const flagVariants = data.flagVariants || {};
Object.keys(flagVariants).forEach((key) => {
const variantData = flagVariants[key];
persistedFlags.set(key, {
key: variantData.variant_key,
value: variantData.variant_value,
experiment_id: variantData.experiment_id,
is_experiment_active: variantData.is_experiment_active,
is_qa_tester: variantData.is_qa_tester,
variant_source: 'persistence',
persisted_at_in_ms: data.persistedAt,
});
});
return {
flags: persistedFlags,
pendingFirstTimeEvents: data.pendingFirstTimeEvents || {},
persistedAtMs: data.persistedAt,
ttlMs: ttlMs,
};
}
async save(context, flagsMap, pendingFirstTimeEvents) {
if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
return;
}
const flagVariants = {};
flagsMap.forEach((variant, key) => {
flagVariants[key] = {
variant_key: variant.key,
variant_value: variant.value,
experiment_id: variant.experiment_id,
is_experiment_active: variant.is_experiment_active,
is_qa_tester: variant.is_qa_tester,
};
});
const data = {
persistedAt: Date.now(),
distinctId: context && context.distinct_id,
context: context,
flagVariants: flagVariants,
pendingFirstTimeEvents: pendingFirstTimeEvents || {},
};
try {
await this.storage.setItem(this.persistedVariantsKey, JSON.stringify(data));
} catch (error) {
MixpanelLogger.error(
this.token,
'Failed to persist variants to storage:',
error
);
}
}
async clear() {
try {
await this.storage.removeItem(this.persistedVariantsKey);
} catch (error) {
MixpanelLogger.error(
this.token,
'Failed to clear persisted variants from storage:',
error
);
}
}
}
export { VariantLookupPolicy, DEFAULT_TTL_MS };