4.2.2 - tracking across email, fixes to dark mode.

This commit is contained in:
2026-05-06 15:50:01 +12:00
parent a7ce4c74b5
commit 2f4001b8af
11 changed files with 323 additions and 41 deletions
+87 -12
View File
@@ -9,6 +9,9 @@
export let booking: BookingContent;
export let allowGeneralEnquiry = false;
type EnquiryType = 'booking' | 'general';
const visitStartedStorageKey = 'goodwalk_visit_started_at';
const journeyStorageKey = 'goodwalk_journey';
const maxJourneyEntries = 8;
let step = 1;
$: headingParts = splitBookingTitle(booking.title);
@@ -23,6 +26,12 @@
let selectedServices: string[] = [];
let website = '';
let formStartedAt = 0;
let visitStartedAt = 0;
let pageEnteredAt = 0;
let firstInteractionAt = 0;
let sendClickedAt = 0;
let stepChanges = 0;
let journey: string[] = [];
let fullNameInput: HTMLInputElement;
let emailInput: HTMLInputElement;
@@ -84,7 +93,11 @@
$: successPetName = petName.trim() || 'your dog';
onMount(() => {
formStartedAt = Date.now();
const now = Date.now();
formStartedAt = now;
pageEnteredAt = now;
visitStartedAt = readOrCreateVisitStartedAt(now);
journey = updateJourneySnapshot(window.location.pathname, window.location.search);
});
function splitBookingTitle(title: string) {
@@ -107,7 +120,58 @@
}
}
function readOrCreateVisitStartedAt(fallback: number) {
try {
const raw = window.sessionStorage.getItem(visitStartedStorageKey);
const parsed = raw ? Number(raw) : NaN;
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
window.sessionStorage.setItem(visitStartedStorageKey, String(fallback));
} catch {
return fallback;
}
return fallback;
}
function updateJourneySnapshot(pathname: string, search: string) {
const nextEntry = `${pathname}${search}`;
try {
const raw = window.sessionStorage.getItem(journeyStorageKey);
const previous = raw ? (JSON.parse(raw) as string[]) : [];
const cleaned = previous.filter((value) => typeof value === 'string' && value.trim());
const deduped = cleaned[cleaned.length - 1] === nextEntry ? cleaned : [...cleaned, nextEntry];
const nextJourney = deduped.slice(-maxJourneyEntries);
window.sessionStorage.setItem(journeyStorageKey, JSON.stringify(nextJourney));
return nextJourney;
} catch {
return [nextEntry];
}
}
function noteInteraction() {
if (!firstInteractionAt) {
firstInteractionAt = Date.now();
}
}
function setStep(nextStep: number, trackTransition = false) {
if (step !== nextStep && trackTransition) {
stepChanges += 1;
}
step = nextStep;
errors = {};
}
function toggleService(service: string, checked: boolean) {
noteInteraction();
if (checked) {
selectedServices = [...selectedServices, service];
return;
@@ -117,6 +181,7 @@
}
function setEnquiryType(nextType: EnquiryType) {
noteInteraction();
enquiryType = nextType;
if (nextType === 'general') {
petName = '';
@@ -186,9 +251,9 @@
}
function goToOwnerStep() {
noteInteraction();
if (!validateDetailsStep()) return;
errors = {};
step = 2;
setStep(2, true);
}
async function handleSubmit(event: SubmitEvent) {
@@ -204,6 +269,8 @@
}
errors = {};
noteInteraction();
sendClickedAt = Date.now();
submitting = true;
submitErrorDetail = '';
showErrorModal = false;
@@ -223,6 +290,12 @@
services: isGeneralEnquiry ? [] : selectedServices,
website,
formStartedAt,
visitStartedAt,
pageEnteredAt,
firstInteractionAt,
sendClickedAt,
stepChanges,
journey,
referrer: document.referrer,
page: window.location.href
})
@@ -279,10 +352,7 @@
type="button"
class:active={step === 1}
class="booking-step"
on:click={() => {
step = 1;
errors = {};
}}
on:click={() => setStep(1, step !== 1)}
>
<span class="booking-step-number">1</span>
<span class="booking-step-label">{detailsStepLabel}</span>
@@ -300,7 +370,15 @@
</div>
</div>
<form class="booking-form" id="bookingForm" novalidate on:submit={handleSubmit}>
<form
class="booking-form"
id="bookingForm"
novalidate
on:submit={handleSubmit}
on:focusin={noteInteraction}
on:input={noteInteraction}
on:change={noteInteraction}
>
<div class="booking-honeypot" aria-hidden="true">
<label for="website">Website</label>
<input
@@ -569,10 +647,7 @@
<button
type="button"
class="btn btn-outline btn-outline-green"
on:click={() => {
step = 1;
errors = {};
}}
on:click={() => setStep(1, true)}
>
Back
</button>
+11 -2
View File
@@ -36,6 +36,7 @@ async function moveToOwnerStep(container: HTMLElement) {
describe('BookingSection', () => {
beforeEach(() => {
window.sessionStorage.clear();
Object.defineProperty(document, 'referrer', {
configurable: true,
value: 'https://www.google.com/'
@@ -104,9 +105,15 @@ describe('BookingSection', () => {
message: 'Loves small group walks.',
services: ['Pack Walks', 'Other Services'],
website: '',
referrer: 'https://www.google.com/'
referrer: 'https://www.google.com/',
stepChanges: 1,
journey: [window.location.pathname]
});
expect(payload.formStartedAt).toEqual(expect.any(Number));
expect(payload.visitStartedAt).toEqual(expect.any(Number));
expect(payload.pageEnteredAt).toEqual(expect.any(Number));
expect(payload.firstInteractionAt).toEqual(expect.any(Number));
expect(payload.sendClickedAt).toEqual(expect.any(Number));
expect(screen.getByRole('dialog', { name: /Booking confirmed/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /on our radar/i })).toBeInTheDocument();
@@ -156,7 +163,9 @@ describe('BookingSection', () => {
petName: '',
location: '',
message: 'I would like to discuss a business partnership.',
services: []
services: [],
stepChanges: 1,
journey: [window.location.pathname]
});
expect(screen.getByRole('dialog', { name: /Enquiry confirmed/i })).toBeInTheDocument();