feat: add cloud gtm injection (#8311)

## Summary

Add GTM injection for cloud distribution builds and push SPA page view +
signup events.

## Changes

- **What**: Inject GTM script into head-prepend and noscript iframe into
body-prepend for cloud builds
- **What**: Push `page_view` to `dataLayer` on cloud route changes
(page_location + page_title)
- **What**: Push `sign_up` to `dataLayer` after successful account
creation (email/google/github)
- **Dependencies**: None

## Review Focus

- Placement order for head-prepend/body-prepend and cloud-only gating
- Route-change page_view payload shape
- Signup event emission only for new users

## Screenshots (if applicable)

<img width="1512" height="860" alt="Screenshot 2026-01-26 at 11 38
11 AM"
src="https://github.com/user-attachments/assets/03fb61db-5ca4-4432-9704-bbdcc4c6c1b7"
/>

<img width="1512" height="862" alt="Screenshot 2026-01-26 at 11 38
26 AM"
src="https://github.com/user-attachments/assets/6e46c855-a552-4e52-9800-17898a512d4d"
/>
This commit is contained in:
Benjamin Lu
2026-01-27 12:44:15 -08:00
committed by GitHub
parent 75fd4f0e67
commit 788f50834c
9 changed files with 276 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import {
useFirebaseAuthStore
} from '@/stores/firebaseAuthStore'
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
import { startSubscriptionPurchaseTracking } from '@/platform/cloud/subscription/utils/subscriptionPurchaseTracker'
import type { BillingCycle } from './subscriptionTierRank'
type CheckoutTier = TierKey | `${TierKey}-yearly`
@@ -78,6 +79,7 @@ export async function performSubscriptionCheckout(
const data = await response.json()
if (data.checkout_url) {
startSubscriptionPurchaseTracking(tierKey, currentBillingCycle)
if (openInNewTab) {
window.open(data.checkout_url, '_blank')
} else {

View File

@@ -0,0 +1,78 @@
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
import type { BillingCycle } from './subscriptionTierRank'
type PendingSubscriptionPurchase = {
tierKey: TierKey
billingCycle: BillingCycle
timestamp: number
}
const STORAGE_KEY = 'pending_subscription_purchase'
const MAX_AGE_MS = 24 * 60 * 60 * 1000 // 24 hours
const VALID_TIERS: TierKey[] = ['standard', 'creator', 'pro', 'founder']
const VALID_CYCLES: BillingCycle[] = ['monthly', 'yearly']
const safeRemove = (): void => {
try {
localStorage.removeItem(STORAGE_KEY)
} catch {
// Ignore storage errors (e.g. private browsing mode)
}
}
export function startSubscriptionPurchaseTracking(
tierKey: TierKey,
billingCycle: BillingCycle
): void {
if (typeof window === 'undefined') return
try {
const payload: PendingSubscriptionPurchase = {
tierKey,
billingCycle,
timestamp: Date.now()
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload))
} catch {
// Ignore storage errors (e.g. private browsing mode)
}
}
export function getPendingSubscriptionPurchase(): PendingSubscriptionPurchase | null {
if (typeof window === 'undefined') return null
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return null
const parsed = JSON.parse(raw) as PendingSubscriptionPurchase
if (!parsed || typeof parsed !== 'object') {
safeRemove()
return null
}
const { tierKey, billingCycle, timestamp } = parsed
if (
!VALID_TIERS.includes(tierKey) ||
!VALID_CYCLES.includes(billingCycle) ||
typeof timestamp !== 'number'
) {
safeRemove()
return null
}
if (Date.now() - timestamp > MAX_AGE_MS) {
safeRemove()
return null
}
return parsed
} catch {
safeRemove()
return null
}
}
export function clearPendingSubscriptionPurchase(): void {
if (typeof window === 'undefined') return
safeRemove()
}