Files
Greenlens/services/experiments.ts
2026-07-27 16:42:04 +02:00

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);