payload-ab
Version:
Payload CMS plugin for A/B testing with PostHog
99 lines (98 loc) • 4.66 kB
JavaScript
import merge from 'lodash.merge';
import { PostHog } from 'posthog-node';
// --- Initialize PostHog server-side client ---
const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY || '', {
host: process.env.POSTHOG_HOST || 'https://app.posthog.com'
});
/**
* Server-side helper to determine which A/B test variant to serve.
* It reads cookies but DOES NOT set them directly. Cookie setting is delegated
* to a Server Action or Route Handler.
*
* @param document The original Payload CMS document.
* @param cookies The cookies object from Next.js `cookies()`.
* @param context Optional request context for proper feature flag evaluation with release conditions.
* @returns The content to display (either the variant or the original), augmented with PostHog details for client-side cookie setting.
*/ export const getServerSideABVariant = async (document, cookies, context)=>{
// If A/B testing is not enabled, return the original document
if (!document?.enableABTesting || !document.abVariant) {
return document;
}
const featureFlagKey = document.posthogFeatureFlagKey || `ab_test_${String(document.id)}`;
let assignedVariantKey = 'control';
let finalDocument = document;
let distinctId = cookies.get('_ph_id')?.value;
let newDistinctIdGenerated;
try {
if (!distinctId) {
newDistinctIdGenerated = crypto.randomUUID();
distinctId = newDistinctIdGenerated // Use the newly generated ID for flag evaluation
;
}
// Build person properties with request context for proper release condition evaluation
const personProperties = {};
// Add request context properties that PostHog uses for release conditions
if (context) {
// Add URL-related properties
if (context.url) {
const urlObj = new URL(context.url);
personProperties['$current_url'] = context.url;
personProperties['$host'] = urlObj.hostname;
personProperties['$pathname'] = urlObj.pathname;
} else {
if (context.host) personProperties['$host'] = context.host;
if (context.pathname) personProperties['$pathname'] = context.pathname;
}
// Add any custom headers that might be used in release conditions
if (context.headers) {
Object.entries(context.headers).forEach(([key, value])=>{
personProperties[`$header_${key.toLowerCase().replace(/-/g, '_')}`] = value;
});
}
}
console.log(`[A/B Plugin] Server-side: Evaluating flag "${featureFlagKey}" for distinct ID "${distinctId}" with context:`, {
pathname: personProperties['$pathname'],
host: personProperties['$host']
});
// Pass person properties to PostHog for proper release condition evaluation
const flagResponse = await posthogClient.getFeatureFlag(featureFlagKey, distinctId, {
personProperties,
groups: {}
});
console.log('Raw flag response:', flagResponse, 'Type:', typeof flagResponse);
// Handle both boolean and string variants
// Handle all possible response types
if (flagResponse === false || flagResponse === null || flagResponse === undefined) {
assignedVariantKey = 'control';
} else if (flagResponse === true) {
assignedVariantKey = 'variant';
} else if (typeof flagResponse === 'string') {
// Use the exact string returned by PostHog
assignedVariantKey = flagResponse;
} else {
assignedVariantKey = 'control' // fallback
;
}
if (assignedVariantKey !== 'control') {
finalDocument = merge({}, document, document.abVariant);
} else {
finalDocument = document;
}
// console.log(
// `[A/B Plugin] Server-side: Flag "${featureFlagKey}" assigned "${assignedVariantKey}" for distinct ID "${distinctId}".`,
// )
} catch (error) {
// console.error(`[A/B Plugin] Server-side error for flag "${featureFlagKey}":`, error)
assignedVariantKey = 'control';
finalDocument = document;
if (!distinctId) distinctId = crypto.randomUUID();
}
return {
...finalDocument,
posthogAssignedVariantKey: assignedVariantKey,
posthogFeatureFlagKeyUsed: featureFlagKey,
posthogServerDistinctId: distinctId,
posthogNewDistinctIdGenerated: newDistinctIdGenerated
};
};
//# sourceMappingURL=server.js.map