Add hero CTA A/B test (hero_cta: control vs free_emphasis)
Sticky 50/50 variant assignment via gw_ab_hero cookie, server-rendered so no flicker. Tracks exposures, CTA clicks, and booking conversions to ab_events (table self-creates on first POST). Bot UAs are dropped; exposures/clicks dedupe per session. - ?ab=control / ?ab=free_emphasis forces and persists a variant - /owner/experiments shows per-variant CVR and relative lift - AB only runs on the marketing surface Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,14 @@
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import type { BookingContent } from '$lib/types';
|
||||
import { trackAb, type AbContext } from '$lib/ab';
|
||||
|
||||
type SuccessModalComponentType = typeof import('$lib/components/SuccessModal.svelte').default;
|
||||
type ErrorModalComponentType = typeof import('$lib/components/ErrorModal.svelte').default;
|
||||
|
||||
export let booking: BookingContent;
|
||||
export let pagePath = '';
|
||||
export let ab: AbContext | undefined = undefined;
|
||||
$: isCompactContactPage = pagePath === '/contact-us';
|
||||
|
||||
const defaultServices = ['Tiny Gang Pack Walks', 'Solo Walks', 'Puppy Visits', 'Other Services'];
|
||||
@@ -285,6 +287,7 @@
|
||||
}
|
||||
|
||||
submitted = true;
|
||||
if (ab) trackAb({ ...ab, event_type: 'conversion', meta: { surface: 'booking_submit' } });
|
||||
} catch (err: unknown) {
|
||||
submitErrorDetail = err instanceof Error ? err.message : String(err);
|
||||
showErrorModal = true;
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { CallToAction, HeroContent } from '$lib/types';
|
||||
import { trackAb, type AbContext } from '$lib/ab';
|
||||
|
||||
export let hero: HeroContent;
|
||||
export let reviewCta: CallToAction | undefined = undefined;
|
||||
export let primaryCtaOverride: CallToAction | undefined = undefined;
|
||||
export let ab: AbContext | undefined = undefined;
|
||||
|
||||
$: primaryCta = primaryCtaOverride ?? hero.primaryCta;
|
||||
|
||||
onMount(() => {
|
||||
if (ab) trackAb({ ...ab, event_type: 'exposure', meta: { surface: 'hero' } });
|
||||
});
|
||||
|
||||
function handlePrimaryCtaClick() {
|
||||
if (ab) trackAb({ ...ab, event_type: 'cta_click', meta: { surface: 'hero_primary' } });
|
||||
}
|
||||
|
||||
$: titleParts = splitTitle(hero.title);
|
||||
$: mobileTitle = hero.mobileTitle?.trim() || `${hero.title} ${hero.highlight}`.trim();
|
||||
@@ -133,12 +147,13 @@
|
||||
|
||||
<div class="hero-buttons">
|
||||
<a
|
||||
href={hero.primaryCta.href}
|
||||
target={linkTarget(hero.primaryCta.external)}
|
||||
rel={linkRel(hero.primaryCta.external)}
|
||||
href={primaryCta.href}
|
||||
target={linkTarget(primaryCta.external)}
|
||||
rel={linkRel(primaryCta.external)}
|
||||
class="btn btn-yellow btn-with-arrow btn-hide-arrow-mobile"
|
||||
on:click={handlePrimaryCtaClick}
|
||||
>
|
||||
{hero.primaryCta.label}
|
||||
{primaryCta.label}
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
<a
|
||||
|
||||
Reference in New Issue
Block a user