Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/blog/[slug]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import { useEffect } from 'react';
import { trackEvent } from '../../../lib/analytics';

export default function BlogPostNotFound() {
useEffect(() => {
trackEvent('blog_post_not_found', {

Check notice on line 8 in app/blog/[slug]/not-found.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: blog_post_not_found

Tracks blog detail not-found render.
page_path: window.location.pathname,
});
}, []);

return (
<section className="page-top-offset bg-light-grey px-6 pb-16 text-default-grey md:px-8 md:pb-20">
<div className="mx-auto max-w-3xl rounded-lg bg-white p-8 text-center shadow-md">
<h1 className="font-headline text-3xl font-semibold">Post not found</h1>
<p className="mt-4 text-default-grey/75">
The article you requested does not exist or is no longer available.
</p>
</div>
</section>
);
}
2 changes: 2 additions & 0 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import JsonLd from '../../../components/JsonLd';
import ScrollToTopButton from '../../../components/blog/ScrollToTopButton';
import BlogPostBreadcrumbs from '../../../components/blog/BlogPostBreadcrumbs';
import BlogPostHero from '../../../components/blog/BlogPostHero';
import BlogPostViewTracker from '../../../components/blog/BlogPostViewTracker';
import SimilarPostsSection from '../../../components/blog/SimilarPostsSection';
import {
getBlogBreadcrumbStructuredData,
Expand Down Expand Up @@ -86,6 +87,7 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
<JsonLd data={structuredData} />
<div className="mx-auto max-w-[1000px]">
<BlogPostBreadcrumbs />
<BlogPostViewTracker slug={post.slug} category={post.category} />
<BlogPostHero post={post} />
<div
className="blog-content mt-10"
Expand Down
4 changes: 3 additions & 1 deletion components/BookingCta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface BookingCtaProps {
location: string;
className?: string;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}

const variantClasses = {
Expand All @@ -16,14 +17,15 @@ const variantClasses = {

const BOOKING_CTA_LABEL = 'Book a Strategy Call';

export default function BookingCta({ location, className, variant = 'primary' }: BookingCtaProps) {
export default function BookingCta({ location, className, variant = 'primary', onClick }: BookingCtaProps) {
return (
<TrackedCtaLink
href="https://calendly.com/michaelzick/45min"
className={className || variantClasses[variant]}
location={location}
label={BOOKING_CTA_LABEL}
eventName="book_free_session_click"
onClick={onClick}
>
<span>{BOOKING_CTA_LABEL}</span>
<OpenInNewWindowIcon className="ml-2 h-4 w-4 shrink-0" aria-hidden="true" />
Expand Down
28 changes: 28 additions & 0 deletions components/ContactForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import { trackEvent } from '../lib/analytics';
import { loadRecaptchaV2, resetRecaptchaV2Widget } from '../lib/client/recaptcha-v2';

interface FormData {
Expand All @@ -20,6 +21,16 @@
workbookOptIn: true,
};

function getContactFailureReason(message: string) {
const lower = message.toLowerCase();
if (lower.includes('too many requests')) return 'rate_limited';
if (lower.includes('captcha')) return 'captcha_failed';
if (lower.includes('email service not configured')) return 'service_configuration_error';
if (lower.includes('failed to send email')) return 'email_delivery_failed';
if (lower.includes('request failed')) return 'request_failed';
return 'unknown';
}

export default function ContactForm() {
const [formData, setFormData] = useState<FormData>(initialFormData);
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
Expand Down Expand Up @@ -145,11 +156,19 @@

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
trackEvent('contact_form_submit_started', {

Check notice on line 159 in components/ContactForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: contact_form_submit_started

Marks the start of contact submission handling.
workbook_opt_in: formData.workbookOptIn,
page_path: window.location.pathname,
});
setStatus('submitting');
setErrorMessage(null);
setCaptchaError(null);

if (!RECAPTCHA_SITE_KEY) {
trackEvent('contact_form_submit_failed', {

Check notice on line 168 in components/ContactForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: contact_form_submit_failed

Tracks immediate failure when captcha configuration is unavailable.
failure_reason: 'captcha_configuration_missing',
page_path: window.location.pathname,
});
setCaptchaError('Contact form is temporarily unavailable. Please try again later.');
setStatus('idle');
return;
Expand All @@ -168,13 +187,22 @@
}
setSubmittedEmail(formData.email);
setSubmittedWorkbook(formData.workbookOptIn);
trackEvent('contact_form_submit_succeeded', {

Check notice on line 190 in components/ContactForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: contact_form_submit_succeeded

Confirms successful contact submission outcome after API success.
workbook_opt_in: formData.workbookOptIn,
page_path: window.location.pathname,
});
setStatus('success');
setFormData(initialFormData);
setCaptchaError(null);
discardCaptchaWidget();
} catch (err) {
console.error(err);
const message = err instanceof Error ? err.message : 'Request failed';
trackEvent('contact_form_submit_failed', {

Check notice on line 201 in components/ContactForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: contact_form_submit_failed

Tracks async submit failure for captcha, request, or delivery errors.
failure_reason: getContactFailureReason(message),
error_message: message,
page_path: window.location.pathname,
});
resetCaptcha();
if (message.toLowerCase().includes('captcha')) {
setCaptchaError(message);
Expand Down
28 changes: 27 additions & 1 deletion components/NguCouponSignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
onSuccess?: () => void;
};

function getNguCouponFailureReason(message: string) {
const lower = message.toLowerCase();
if (lower.includes('too many requests')) return 'rate_limited';
if (lower.includes('captcha')) return 'captcha_failed';
if (lower.includes('not configured')) return 'service_configuration_error';
if (lower.includes('failed to send coupon email')) return 'email_delivery_failed';
if (lower.includes('request failed')) return 'request_failed';
return 'unknown';
}

export default function NguCouponSignupForm({
intro,
formClassName = 'space-y-5',
Expand Down Expand Up @@ -135,7 +145,7 @@

const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
trackEvent('ngu_coupon_submit_click', {

Check notice on line 148 in components/NguCouponSignupForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: ngu_coupon_submit_click

Tracks the NGU coupon submit click before captcha and API processing.
location: successCtaLocation,
label: 'Email me the coupon',
page_path: window.location.pathname,
Expand All @@ -144,6 +154,11 @@
setErrorMessage(null);

if (!NGU_RECAPTCHA_SITE_KEY) {
trackEvent('ngu_coupon_signup_failed', {

Check notice on line 157 in components/NguCouponSignupForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: ngu_coupon_signup_failed

Tracks immediate failure when captcha configuration is missing.
location: successCtaLocation,
failure_reason: 'captcha_configuration_missing',
page_path: window.location.pathname,
});
setStatus('error');
setErrorMessage('Signup is temporarily unavailable. Please try again later.');
return;
Expand All @@ -163,14 +178,25 @@
}

onSuccess?.();
trackEvent('ngu_coupon_signup_succeeded', {

Check notice on line 181 in components/NguCouponSignupForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: ngu_coupon_signup_succeeded

Confirms coupon signup completion after API success.
location: successCtaLocation,
page_path: window.location.pathname,
});
setStatus('success');
setEmail('');
setErrorMessage(null);
discardCaptchaWidget();
} catch (error) {
console.error(error);
const message = error instanceof Error ? error.message : 'Request failed';
trackEvent('ngu_coupon_signup_failed', {

Check notice on line 192 in components/NguCouponSignupForm.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: ngu_coupon_signup_failed

Tracks async failure from captcha, API rejection, or email delivery issues.
location: successCtaLocation,
failure_reason: getNguCouponFailureReason(message),
error_message: message,
page_path: window.location.pathname,
});
setStatus('error');
setErrorMessage(error instanceof Error ? error.message : 'Request failed');
setErrorMessage(message);
resetCaptcha();
}
};
Expand Down
44 changes: 42 additions & 2 deletions components/Questionnaire.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@
}, {});
}

function getQuestionnaireFailureReason(message: string) {
const lower = message.toLowerCase();
if (lower.includes('too many requests')) return 'rate_limited';
if (lower.includes('missing required fields')) return 'missing_required_fields';
if (lower.includes('input exceeds') || lower.includes('response exceeds') || lower.includes('too many responses')) {
return 'input_validation_failed';
}
if (lower.includes('system busy') || lower.includes('honeypot')) return 'bot_protection_rejected';
if (lower.includes('configuration')) return 'service_configuration_error';
if (lower.includes('failed to analyze')) return 'analysis_provider_error';
return 'unknown';
}

export default function Questionnaire() {
const [stepIndex, setStepIndex] = useState(0);
const [formData, setFormData] = useState<FormData>({
Expand All @@ -58,6 +71,13 @@

const currentStep = STEPS[stepIndex];

useEffect(() => {
trackEvent('questionnaire_page_viewed', {

Check notice on line 75 in components/Questionnaire.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: questionnaire_page_viewed

Tracks questionnaire route arrival on client mount.
location: 'questionnaire-page',
page_path: window.location.pathname,
});
}, []);

useEffect(() => {
const storedDuration = window.localStorage.getItem(ANALYSIS_DURATION_STORAGE_KEY);
const parsedDuration = Number(storedDuration);
Expand Down Expand Up @@ -126,7 +146,17 @@
};

const nextStep = () => {
if (!isStepValid()) return;
if (!isStepValid()) {
const failureReason = currentStep.id === 'intake'
? (!consented ? 'consent_missing' : 'required_fields_missing')
: 'required_answers_missing';
trackEvent('questionnaire_submission_blocked', {

Check notice on line 153 in components/Questionnaire.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: questionnaire_submission_blocked

Captures client-side step validation failures before progression or submit.
step_id: currentStep.id,
failure_reason: failureReason,
page_path: window.location.pathname,
});
return;
}

if (stepIndex < STEPS.length - 1) {
setStepIndex(stepIndex + 1);
Expand All @@ -149,7 +179,7 @@
setSecondsRemaining(Math.ceil(estimatedDurationMs / 1000));
setIsSubmitting(true);
setError(null);
trackEvent('questionnaire_submit', { email: formData.email });

Check notice on line 182 in components/Questionnaire.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: questionnaire_submit

Tracks questionnaire submit intent when the analysis request is initiated.
let submissionSucceeded = false;

try {
Expand All @@ -170,9 +200,19 @@
setAnalysisProgress(100);
setSecondsRemaining(0);
submissionSucceeded = true;
trackEvent('questionnaire_analysis_succeeded', {

Check notice on line 203 in components/Questionnaire.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: questionnaire_analysis_succeeded

Captures successful analysis response after async submit resolves.
answer_count: Object.keys(orderedAnswers).length,
page_path: window.location.pathname,
});
setAnalysis(data.analysis);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
const message = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
trackEvent('questionnaire_analysis_failed', {

Check notice on line 210 in components/Questionnaire.tsx

View check run for this annotation

Amplitude / Amplitude / instrumentation

Event: questionnaire_analysis_failed

Tracks analysis failure shown in the questionnaire catch branch.
failure_reason: getQuestionnaireFailureReason(message),
error_message: message,
page_path: window.location.pathname,
});
setError(message);
} finally {
if (!submissionSucceeded) {
setAnalysisProgress(0);
Expand Down
4 changes: 3 additions & 1 deletion components/QuestionnaireCta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ interface QuestionnaireCTAProps {
location: string;
className?: string;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}

const variantClasses = {
primary: 'btn cta-unified',
secondary: 'btn-secondary cta-unified',
};

export default function QuestionnaireCta({ location, className, variant = 'secondary' }: QuestionnaireCTAProps) {
export default function QuestionnaireCta({ location, className, variant = 'secondary', onClick }: QuestionnaireCTAProps) {
return (
<TrackedCtaLink
href="/questionnaire"
Expand All @@ -22,6 +23,7 @@ export default function QuestionnaireCta({ location, className, variant = 'secon
label="Take the Assessment"
eventName="questionnaire_click"
target="_self"
onClick={onClick}
>
Take the Assessment
</TrackedCtaLink>
Expand Down
3 changes: 3 additions & 0 deletions components/TrackedCtaLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type TrackedCtaLinkProps = {
rel?: string;
section?: string;
smoothScroll?: boolean;
onClick?: () => void;
children: ReactNode;
};

Expand Down Expand Up @@ -54,6 +55,7 @@ export default function TrackedCtaLink({
rel,
section = 'cta',
smoothScroll = false,
onClick,
children,
}: TrackedCtaLinkProps) {
const isInternal = href.startsWith('/') && !href.startsWith('//');
Expand All @@ -74,6 +76,7 @@ export default function TrackedCtaLink({
href,
page_path: window.location.pathname,
});
onClick?.();

const hashTargetId = getHashTargetId(href);
if (
Expand Down
3 changes: 3 additions & 0 deletions components/TrackedLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type TrackedLinkProps = {
className?: string;
target?: string;
rel?: string;
onClick?: () => void;
children: ReactNode;
};

Expand All @@ -25,6 +26,7 @@ export default function TrackedLink({
className,
target,
rel,
onClick,
children,
}: TrackedLinkProps) {
const isInternal = href.startsWith('/') && !href.startsWith('//');
Expand All @@ -39,6 +41,7 @@ export default function TrackedLink({
variant,
pagePath: window.location.pathname,
});
onClick?.();
};

if (isInternal && (!target || target === '_self')) {
Expand Down
Loading
Loading