87 lines
3.2 KiB
TypeScript
87 lines
3.2 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { Analytics } from './analytics';
|
|
|
|
/**
|
|
* Leichtgewichtiges A/B-Testing ohne PostHog-Provider.
|
|
*
|
|
* Warum lokal und nicht PostHog Feature Flags: der PostHog-Client laeuft hier
|
|
* bewusst ohne Provider (siehe services/analytics.ts). Flags waeren damit erst
|
|
* nach einem Netzwerk-Roundtrip verfuegbar — der Welcome-Screen rendert aber
|
|
* sofort. Eine lokal gewuerfelte, persistierte Zuweisung ist deterministisch,
|
|
* funktioniert offline und wird als Event + Person-Property an PostHog gemeldet,
|
|
* sodass sich jeder Funnel-Schritt bis zum Kauf nach Variante segmentieren laesst.
|
|
*/
|
|
|
|
const STORAGE_PREFIX = 'greenlens_experiment_';
|
|
|
|
export const EXPERIMENTS = {
|
|
/** Welcome-Headline: Symptom-Einstieg vs. Abgrenzung zu Gratis-Identifikations-Apps. */
|
|
welcomeHeadline: {
|
|
key: 'welcome_headline_v1',
|
|
variants: ['symptom', 'contrast'] as const,
|
|
},
|
|
} as const;
|
|
|
|
export type WelcomeHeadlineVariant = (typeof EXPERIMENTS.welcomeHeadline.variants)[number];
|
|
|
|
const memoryCache = new Map<string, string>();
|
|
|
|
const pick = <T extends readonly string[]>(variants: T): T[number] =>
|
|
variants[Math.floor(Math.random() * variants.length)];
|
|
|
|
/**
|
|
* Liefert die Variante fuer einen Nutzer. Beim ersten Aufruf wird gewuerfelt,
|
|
* persistiert und `experiment_assigned` an PostHog geschickt. Alle weiteren
|
|
* Aufrufe geben dieselbe Variante zurueck.
|
|
*/
|
|
export const getVariant = async <T extends readonly string[]>(
|
|
experimentKey: string,
|
|
variants: T,
|
|
): Promise<T[number]> => {
|
|
const cached = memoryCache.get(experimentKey);
|
|
if (cached && (variants as readonly string[]).includes(cached)) {
|
|
return cached as T[number];
|
|
}
|
|
|
|
try {
|
|
const stored = await AsyncStorage.getItem(STORAGE_PREFIX + experimentKey);
|
|
if (stored && (variants as readonly string[]).includes(stored)) {
|
|
memoryCache.set(experimentKey, stored);
|
|
return stored as T[number];
|
|
}
|
|
} catch {
|
|
// Storage nicht verfuegbar — wir wuerfeln neu, statt den Screen zu blockieren.
|
|
}
|
|
|
|
const variant = pick(variants);
|
|
memoryCache.set(experimentKey, variant);
|
|
|
|
try {
|
|
await AsyncStorage.setItem(STORAGE_PREFIX + experimentKey, variant);
|
|
} catch {
|
|
// Nicht persistierbar: die Variante gilt dann nur fuer diese Session.
|
|
}
|
|
|
|
// Event fuer die Zuweisung selbst ...
|
|
Analytics.capture('experiment_assigned', { experiment: experimentKey, variant });
|
|
// ... plus Person-Property, damit sich JEDER spaetere Funnel-Schritt
|
|
// (onboarding_started, paywall_opened, purchase_completed) in PostHog nach
|
|
// Variante aufteilen laesst, ohne die Property ueberall mitschicken zu muessen.
|
|
Analytics.identifyProperties({ [`experiment_${experimentKey}`]: variant });
|
|
|
|
return variant;
|
|
};
|
|
|
|
/** Nur fuer QA/Debug: erzwingt eine Variante. */
|
|
export const forceVariant = async (experimentKey: string, variant: string): Promise<void> => {
|
|
memoryCache.set(experimentKey, variant);
|
|
try {
|
|
await AsyncStorage.setItem(STORAGE_PREFIX + experimentKey, variant);
|
|
} catch {
|
|
// ignorieren
|
|
}
|
|
};
|
|
|
|
export const getWelcomeHeadlineVariant = (): Promise<WelcomeHeadlineVariant> =>
|
|
getVariant(EXPERIMENTS.welcomeHeadline.key, EXPERIMENTS.welcomeHeadline.variants);
|