Repository navigation
feat(analytics): report account signups to X Ads #9212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import type { Metadata } from 'next' | ||
| import { Suspense } from 'react' | ||
| import { isHosted } from '@/lib/core/config/env-flags' | ||
| import { SocialSignUpCompletion } from '@/app/(auth)/signup/complete/social-sign-up-completion' | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Finishing Sign Up', | ||
| } | ||
|
|
||
| export default function SocialSignUpCompletePage() { | ||
| return ( | ||
| <Suspense fallback={null}> | ||
| <SocialSignUpCompletion consentRequired={isHosted} /> | ||
| </Suspense> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,62 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useRef } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useSearchParams } from 'next/navigation' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useSession } from '@/lib/auth/client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { SocialSignInProvider } from '@/lib/auth/social-sign-in' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { trackSignUpCompletionConversions } from '@/lib/auth/social-sign-up-conversion' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useTrackingConsent } from '@/lib/consent/tracking-consent' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { validateCallbackUrl } from '@/lib/core/security/input-validation' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DEFAULT_POST_AUTH_ROUTE } from '@/app/(auth)/auth-redirect' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface SocialSignUpCompletionProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| consentRequired: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isSignUpConversionMethod(value: string | null): value is SocialSignInProvider | 'email' { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value === 'email' || value === 'github' || value === 'google' || value === 'microsoft' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function SocialSignUpCompletion({ consentRequired }: SocialSignUpCompletionProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const redirectStartedRef = useRef(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const searchParams = useSearchParams() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: session, isPending } = useSession() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { isResolved, measurement, marketing } = useTrackingConsent() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const callbackURL = searchParams.get('callbackURL') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const destination = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| callbackURL && validateCallbackUrl(callbackURL) ? callbackURL : DEFAULT_POST_AUTH_ROUTE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const marker = searchParams.get('conversion') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const provider = searchParams.get('provider') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (redirectStartedRef.current || isPending || (consentRequired && !isResolved)) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| redirectStartedRef.current = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const finishSignUp = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (session?.user && isSignUpConversionMethod(provider)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await trackSignUpCompletionConversions({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| marker, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: provider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: session.user.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| measurement: consentRequired && measurement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| marketing: consentRequired && marketing, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = destination | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Comment on lines
+37
to
+46
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Put the redirect in a Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void finishSignUp() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| consentRequired, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| destination, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPending, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isResolved, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| marker, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| measurement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| marketing, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| session, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return <p className='text-center text-[var(--text-secondary)] text-sm'>Finishing signup…</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import type { ReactNode } from 'react' | ||
| import { isHosted } from '@/lib/core/config/env-flags' | ||
| import { SignupConsentTracking } from '@/app/(auth)/signup/signup-consent-tracking' | ||
|
|
||
| interface SignupLayoutProps { | ||
| children: ReactNode | ||
| } | ||
|
|
||
| export default function SignupLayout({ children }: SignupLayoutProps) { | ||
| return ( | ||
| <> | ||
| {children} | ||
| {isHosted && <SignupConsentTracking />} | ||
| </> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| 'use client' | ||
|
|
||
| import { useConsentScript } from '@c15t/nextjs/headless' | ||
| import { X_PIXEL_SCRIPT } from '@/lib/consent/scripts' | ||
|
|
||
| export function SignupConsentTracking() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Context Used: Component patterns and structure for React components (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| useConsentScript({ script: X_PIXEL_SCRIPT, unmountBehavior: 'keep' }) | ||
| return null | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * @vitest-environment jsdom | ||
| * @vitest-environment-options {"url":"https://www.sim.ai/signup"} | ||
| */ | ||
| import { afterEach, describe, expect, it, vi } from 'vitest' | ||
| import { trackSignUpConversions } from '@/lib/analytics/sign-up' | ||
|
|
||
| afterEach(() => { | ||
| Reflect.deleteProperty(window, 'twq') | ||
| Reflect.deleteProperty(window, 'freebuff') | ||
| Reflect.deleteProperty(window, 'gtag') | ||
| }) | ||
|
|
||
| describe('email signup conversions', () => { | ||
| it('reports the X signup event when marketing consent is granted', async () => { | ||
| const trackXEvent = vi.fn() | ||
| window.twq = trackXEvent | ||
|
|
||
| await trackSignUpConversions({ | ||
| method: 'email', | ||
| userId: 'user-1', | ||
| measurement: false, | ||
| marketing: true, | ||
| }) | ||
|
|
||
| expect(trackXEvent).toHaveBeenCalledOnce() | ||
| expect(trackXEvent).toHaveBeenCalledWith('event', 'tw-q5xbl-rhis8', {}) | ||
| }) | ||
|
|
||
| it('does not report the X signup event without marketing consent', async () => { | ||
| const trackXEvent = vi.fn() | ||
| window.twq = trackXEvent | ||
|
|
||
| await trackSignUpConversions({ | ||
| method: 'email', | ||
| userId: 'user-1', | ||
| measurement: true, | ||
| marketing: false, | ||
| }) | ||
|
|
||
| expect(trackXEvent).not.toHaveBeenCalled() | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { sleep } from '@sim/utils/helpers' | ||
| import { trackFreebuffConversion } from '@/lib/analytics/freebuff' | ||
| import { trackGoogleEvent } from '@/lib/analytics/google' | ||
| import { X_SIGN_UP_EVENT_ID } from '@/lib/consent/scripts' | ||
|
|
||
| const X_PIXEL_STUB_WAIT_INTERVAL_MS = 50 | ||
| const X_PIXEL_STUB_WAIT_ATTEMPTS = 20 | ||
|
|
||
| interface TrackSignUpConversionsOptions { | ||
| method: string | ||
| userId: string | ||
| measurement: boolean | ||
| marketing: boolean | ||
| } | ||
|
|
||
| async function trackXSignUpConversion(): Promise<void> { | ||
| for (let attempt = 0; attempt < X_PIXEL_STUB_WAIT_ATTEMPTS; attempt += 1) { | ||
| if (typeof window.twq === 'function') { | ||
| window.twq('event', X_SIGN_UP_EVENT_ID, {}) | ||
| return | ||
| } | ||
| await sleep(X_PIXEL_STUB_WAIT_INTERVAL_MS) | ||
| } | ||
| } | ||
|
|
||
| /** Reports a completed signup only to vendors allowed by the settled consent state. */ | ||
| export async function trackSignUpConversions({ | ||
|
waleedlatif1 marked this conversation as resolved.
|
||
| method, | ||
| userId, | ||
| measurement, | ||
| marketing, | ||
| }: TrackSignUpConversionsOptions): Promise<void> { | ||
| if (measurement) trackGoogleEvent('sign_up', { method }) | ||
| if (!marketing) return | ||
|
|
||
| trackFreebuffConversion('signup_completed', userId) | ||
| await trackXSignUpConversion() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| /** | ||
| * @vitest-environment jsdom | ||
| * @vitest-environment-options {"url":"https://www.sim.ai/signup"} | ||
| */ | ||
| import { afterEach, describe, expect, it, vi } from 'vitest' | ||
| import { | ||
| createEmailSignUpCallbackURL, | ||
| createSocialSignUpCallbackURL, | ||
| trackSignUpCompletionConversions, | ||
| } from '@/lib/auth/social-sign-up-conversion' | ||
|
|
||
| afterEach(() => { | ||
| sessionStorage.clear() | ||
| Reflect.deleteProperty(window, 'twq') | ||
| Reflect.deleteProperty(window, 'freebuff') | ||
| Reflect.deleteProperty(window, 'gtag') | ||
| }) | ||
|
|
||
| function getConversionMarker(callbackURL: string): string { | ||
| return new URL(callbackURL, window.location.origin).searchParams.get('conversion') ?? '' | ||
| } | ||
|
|
||
| describe('signup completion conversions', () => { | ||
| it('reports a new social user once', async () => { | ||
| vi.stubEnv('NODE_ENV', 'production') | ||
| const trackXEvent = vi.fn() | ||
| const trackGoogleEvent = vi.fn() | ||
| const trackFreebuffEvent = vi.fn() | ||
| window.twq = trackXEvent | ||
| window.gtag = trackGoogleEvent | ||
| window.freebuff = trackFreebuffEvent | ||
|
Comment on lines
+26
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The social signup test replaces every vendor function with a mock and checks their calls. Context Used: CLAUDE.md (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| const marker = getConversionMarker(createSocialSignUpCallbackURL('google', '/workspace')) | ||
|
|
||
| const firstReport = await trackSignUpCompletionConversions({ | ||
| marker, | ||
| method: 'google', | ||
| userId: 'user-1', | ||
| measurement: true, | ||
| marketing: true, | ||
| }) | ||
| const duplicateReport = await trackSignUpCompletionConversions({ | ||
| marker, | ||
| method: 'google', | ||
| userId: 'user-1', | ||
| measurement: true, | ||
| marketing: true, | ||
| }) | ||
|
|
||
| expect(firstReport).toBe(true) | ||
| expect(duplicateReport).toBe(false) | ||
| expect(trackXEvent).toHaveBeenCalledOnce() | ||
| expect(trackGoogleEvent).toHaveBeenCalledWith( | ||
| 'event', | ||
| 'sign_up', | ||
| expect.objectContaining({ method: 'google' }) | ||
| ) | ||
| expect(trackFreebuffEvent).toHaveBeenCalledOnce() | ||
| }) | ||
|
|
||
| it('does not report a returning social login that lacks the new-user callback marker', async () => { | ||
| const trackXEvent = vi.fn() | ||
| window.twq = trackXEvent | ||
| createSocialSignUpCallbackURL('github', '/workspace') | ||
|
|
||
| const reported = await trackSignUpCompletionConversions({ | ||
| marker: 'not-the-pending-new-user-marker', | ||
| method: 'github', | ||
| userId: 'user-1', | ||
| measurement: true, | ||
| marketing: true, | ||
| }) | ||
|
|
||
| expect(reported).toBe(false) | ||
| expect(trackXEvent).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('does not report a duplicate email signup with a synthetic user ID', async () => { | ||
| const trackXEvent = vi.fn() | ||
| window.twq = trackXEvent | ||
| const marker = getConversionMarker(createEmailSignUpCallbackURL('synthetic-user', '/workspace')) | ||
|
|
||
| const reported = await trackSignUpCompletionConversions({ | ||
| marker, | ||
| method: 'email', | ||
| userId: 'existing-user', | ||
| measurement: true, | ||
| marketing: true, | ||
| }) | ||
|
|
||
| expect(reported).toBe(false) | ||
| expect(trackXEvent).not.toHaveBeenCalled() | ||
| }) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.