From 7dba661f15fd690d7e6486bf8009b728be09b9bc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 12 Nov 2026 19:23:57 -0800 Subject: [PATCH 1/2] chore(analytics): remove Freebuff ad tracking and the Beluga short-link rewrite --- apps/sim/app/(auth)/signup/signup-form.tsx | 4 +- .../cookie-policy/cookie-policy-content.tsx | 12 +--- .../app/(landing)/privacy/privacy-content.tsx | 17 ++--- .../app/_shell/consent/consent-provider.tsx | 2 - .../consent/freebuff-click-id-guard.tsx | 21 ------ apps/sim/lib/analytics/attribution.ts | 1 - apps/sim/lib/analytics/freebuff.server.ts | 67 ------------------- apps/sim/lib/analytics/freebuff.ts | 49 -------------- apps/sim/lib/auth/auth.ts | 19 ------ apps/sim/lib/consent/scripts.ts | 14 ---- apps/sim/lib/core/config/env.ts | 1 - apps/sim/lib/core/security/csp.ts | 4 -- apps/sim/next.config.ts | 4 -- 13 files changed, 7 insertions(+), 208 deletions(-) delete mode 100644 apps/sim/app/_shell/consent/freebuff-click-id-guard.tsx delete mode 100644 apps/sim/lib/analytics/freebuff.server.ts delete mode 100644 apps/sim/lib/analytics/freebuff.ts diff --git a/apps/sim/app/(auth)/signup/signup-form.tsx b/apps/sim/app/(auth)/signup/signup-form.tsx index af6506485e3..d74c61ddb88 100644 --- a/apps/sim/app/(auth)/signup/signup-form.tsx +++ b/apps/sim/app/(auth)/signup/signup-form.tsx @@ -5,7 +5,6 @@ import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile' import { createLogger } from '@sim/logger' import { useRouter, useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' -import { trackFreebuffConversion } from '@/lib/analytics/freebuff' import { trackGoogleEvent } from '@/lib/analytics/google' import { client, useSession } from '@/lib/auth/client' import { DISPOSABLE_EMAIL_ERROR_CODE, DISPOSABLE_EMAIL_MESSAGE } from '@/lib/auth/constants' @@ -112,7 +111,7 @@ function SignupFormContent({ const searchParams = useSearchParams() const { refetch: refetchSession } = useSession() const posthog = usePostHog() - const { measurement, marketing } = useTrackingConsent() + const { measurement } = useTrackingConsent() useCaptureWhenReady('signup_page_viewed', {}) const [isLoading, setIsLoading] = useState(false) @@ -352,7 +351,6 @@ function SignupFormContent({ } if (measurement) trackGoogleEvent('sign_up', { method: 'email' }) - if (marketing) trackFreebuffConversion('signup_completed', response.data.user.id) try { await refetchSession() diff --git a/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx b/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx index b887ca7b617..b5004de6944 100644 --- a/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx +++ b/apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx @@ -44,7 +44,7 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = { title: 'Cookie Policy', description: 'What cookies Sim sets, why, how long they last, and how to change your choice at any time.', - lastUpdated: 'October 13, 2026', + lastUpdated: 'November 12, 2026', intro: [ { kind: 'paragraph', @@ -238,12 +238,6 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = { ], ['personalization_id', 'X (Twitter)', 'Personalizes the ads shown on X.', '13 months'], ['muc_ads', 'X (Twitter)', 'Measures ad conversions across X domains.', '13 months'], - [ - 'bfcid', - 'Freebuff', - 'Stores the click identifier from a Freebuff ad that brought you to sim.ai, so a sign-up can be credited to that ad. Deleted if you withdraw Marketing consent.', - '30 days', - ], ]), ], }, @@ -280,8 +274,7 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = { , Google Ads,{' '} X (Twitter), - and PostHog. For Freebuff,{' '} - {CHANGE_CHOICES} to withdraw Marketing consent. + and PostHog. ), }, @@ -303,7 +296,6 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = { Google{' '} (Analytics and Ads),{' '} X (Twitter),{' '} - Freebuff,{' '} Ahrefs,{' '} PostHog, and{' '} Cloudflare. diff --git a/apps/sim/app/(landing)/privacy/privacy-content.tsx b/apps/sim/app/(landing)/privacy/privacy-content.tsx index c90897a0c81..8ac239179f5 100644 --- a/apps/sim/app/(landing)/privacy/privacy-content.tsx +++ b/apps/sim/app/(landing)/privacy/privacy-content.tsx @@ -44,7 +44,7 @@ function richText(content: string): ReactNode { export const PRIVACY_CONFIG: LegalPageConfig = { title: 'Privacy Policy', description: 'Sim Studio, Inc. · Operating the Sim platform (sim.ai)', - lastUpdated: 'November 4, 2026', + lastUpdated: 'November 12, 2026', intro: [ { kind: 'paragraph', @@ -263,7 +263,7 @@ export const PRIVACY_CONFIG: LegalPageConfig = { ], [ 'Behavioral remarketing', - 'Cookie and pixel identifiers, ad click identifiers, browser and Device data, campaign attribution, website interaction data, and, for Freebuff sign-up conversions, a pseudonymous Sim Account identifier and the sign-up time', + 'Cookie and pixel identifiers, ad click identifiers, browser and Device data, campaign attribution, and website interaction data', 'Consent — Article 6(1)(a)', 'Marketing cookies and ad personalization require Marketing consent. Google Ads may load earlier with ad storage denied and send limited cookieless consent signals. Consent may be changed or withdrawn through the cookie preferences link.', ], @@ -577,7 +577,7 @@ export const PRIVACY_CONFIG: LegalPageConfig = { { kind: 'paragraph', content: richText( - 'Sim advertises through Google Ads, X, and Freebuff, and measures which ads lead to sign-ups and demo bookings. These services use non-essential Cookies, pixels, and similar technologies. Marketing Cookies and ad personalization are enabled only after You consent to the Marketing category in the cookie banner.' + 'Sim advertises through Google Ads and X, and measures which ads lead to sign-ups and demo bookings. These services use non-essential Cookies, pixels, and similar technologies. Marketing Cookies and ad personalization are enabled only after You consent to the Marketing category in the cookie banner.' ), }, { @@ -592,12 +592,6 @@ export const PRIVACY_CONFIG: LegalPageConfig = { "**X.** The X pixel loads on Sim's marketing pages only after Marketing consent. It reports page views and demo bookings." ), }, - { - kind: 'paragraph', - content: richText( - '**Freebuff.** After Marketing consent, the Freebuff tag stores the click identifier from a Freebuff ad. When You sign up, Sim sends Freebuff that click identifier, the event type, a pseudonymous Sim Account identifier, and the sign-up time so the sign-up can be credited to the ad. Sim does not send Your name or email address.' - ), - }, { kind: 'paragraph', content: richText( @@ -621,9 +615,6 @@ export const PRIVACY_CONFIG: LegalPageConfig = { richText( 'X: [Personalization and data settings](https://x.com/settings/privacy_and_safety)' ), - richText( - 'Freebuff: withdraw Marketing consent through the cookie preferences link; Sim then deletes the Freebuff click identifier cookie' - ), ], }, ], @@ -987,7 +978,7 @@ export const PRIVACY_CONFIG: LegalPageConfig = { { kind: 'paragraph', content: richText( - 'We do not sell Personal Information for money. When Marketing Cookies are enabled, Sim makes identifiers and internet activity on sim.ai available to Google, X, and Freebuff for advertising and ad measurement. This includes cookie and click identifiers, pages visited, and conversion events. California law may treat this as a "sale" or "sharing" of Personal Information. You may opt out at any time through the cookie preferences link, by sending a Global Privacy Control signal, or by emailing privacy@sim.ai. Sim does not knowingly sell or share the Personal Information of consumers under 16.' + 'We do not sell Personal Information for money. When Marketing Cookies are enabled, Sim makes identifiers and internet activity on sim.ai available to Google and X for advertising and ad measurement. This includes cookie and click identifiers, pages visited, and conversion events. California law may treat this as a "sale" or "sharing" of Personal Information. You may opt out at any time through the cookie preferences link, by sending a Global Privacy Control signal, or by emailing privacy@sim.ai. Sim does not knowingly sell or share the Personal Information of consumers under 16.' ), }, { kind: 'subheading', text: 'Global Privacy Control' }, diff --git a/apps/sim/app/_shell/consent/consent-provider.tsx b/apps/sim/app/_shell/consent/consent-provider.tsx index d274ff4e101..8037a8548eb 100644 --- a/apps/sim/app/_shell/consent/consent-provider.tsx +++ b/apps/sim/app/_shell/consent/consent-provider.tsx @@ -5,7 +5,6 @@ import { TrackingConsentProvider } from '@/lib/consent/tracking-consent' import { AttributionCookieGuard } from '@/app/_shell/consent/attribution-cookie-guard' import { ConsentBanner } from '@/app/_shell/consent/consent-banner' import { ConsentStoreProvider } from '@/app/_shell/consent/consent-store-provider' -import { FreebuffClickIdGuard } from '@/app/_shell/consent/freebuff-click-id-guard' import { GoogleAnalyticsPageViewTracker } from '@/app/_shell/consent/google-analytics-page-view-tracker' interface ConsentProviderProps { @@ -24,7 +23,6 @@ export function ConsentProvider({ children }: ConsentProviderProps) { {children} - diff --git a/apps/sim/app/_shell/consent/freebuff-click-id-guard.tsx b/apps/sim/app/_shell/consent/freebuff-click-id-guard.tsx deleted file mode 100644 index bc221be8fb0..00000000000 --- a/apps/sim/app/_shell/consent/freebuff-click-id-guard.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client' - -import { useEffect } from 'react' -import { clearFreebuffClickId } from '@/lib/analytics/freebuff' -import { useTrackingConsent } from '@/lib/consent/tracking-consent' - -/** - * Drops a stored Freebuff click id once consent resolves without marketing, so - * a withdrawn or expired grant can never be attributed by the server postback, - * which only sees the cookie. Withdrawal reloads the page, so this runs before - * any later signup. - */ -export function FreebuffClickIdGuard() { - const { isResolved, marketing } = useTrackingConsent() - - useEffect(() => { - if (isResolved && !marketing) clearFreebuffClickId() - }, [isResolved, marketing]) - - return null -} diff --git a/apps/sim/lib/analytics/attribution.ts b/apps/sim/lib/analytics/attribution.ts index 82fa37b720e..0b6c62412cc 100644 --- a/apps/sim/lib/analytics/attribution.ts +++ b/apps/sim/lib/analytics/attribution.ts @@ -49,7 +49,6 @@ const CLICK_ID_PARAMETERS = [ 'ttclid', 'twclid', 'rdt_cid', - 'bfcid', ] as const /** diff --git a/apps/sim/lib/analytics/freebuff.server.ts b/apps/sim/lib/analytics/freebuff.server.ts deleted file mode 100644 index 3db98deb40a..00000000000 --- a/apps/sim/lib/analytics/freebuff.server.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { createLogger } from '@sim/logger' -import { sleep } from '@sim/utils/helpers' -import { backoffWithJitter } from '@sim/utils/retry' -import type { FreebuffConversionEvent } from '@/lib/analytics/freebuff' -import { env } from '@/lib/core/config/env' - -const logger = createLogger('FreebuffConversions') - -const FREEBUFF_CONVERSIONS_URL = 'https://freebuff.com/api/advertisers/conversions' -const MAX_ATTEMPTS = 4 -const REQUEST_TIMEOUT_MS = 10_000 - -/** Mirrors the tag's own click-id check, so a tampered cookie is never forwarded. */ -const CLICK_ID_SHAPE = /^bfc_[A-Za-z0-9._-]{1,508}$/ - -interface FreebuffConversion { - clickId: string - eventType: FreebuffConversionEvent - /** Idempotency key, shared with the tag call for the same conversion. */ - eventId: string - occurredAt: Date -} - -/** - * Server-to-server conversion postback. Network failures and 5xx responses are - * retried with the same `eventId` and `occurredAt`; every 4xx is terminal. A - * `deduped` answer means the tag already reported it and is a success. Never - * throws, so a caller can fire and forget. - */ -export async function reportFreebuffConversion(conversion: FreebuffConversion): Promise { - const apiKey = env.FREEBUFF_API_KEY - if (!apiKey || !CLICK_ID_SHAPE.test(conversion.clickId)) return - - const body = JSON.stringify({ - clickId: conversion.clickId, - eventType: conversion.eventType, - eventId: conversion.eventId, - occurredAt: conversion.occurredAt.toISOString(), - }) - const context = { eventType: conversion.eventType, eventId: conversion.eventId } - - for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { - let status: number | undefined - try { - const response = await fetch(FREEBUFF_CONVERSIONS_URL, { - method: 'POST', - headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, - body, - signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), - }) - status = response.status - const result = await response.text().catch(() => '') - if (response.ok) { - logger.info('Freebuff conversion recorded', { ...context, result }) - return - } - if (status < 500) { - logger.warn('Freebuff conversion rejected', { ...context, status, result }) - return - } - } catch (error) { - logger.warn('Freebuff conversion request failed', { ...context, attempt, error }) - } - if (attempt < MAX_ATTEMPTS) await sleep(backoffWithJitter(attempt, null)) - else logger.error('Freebuff conversion postback gave up', { ...context, status }) - } -} diff --git a/apps/sim/lib/analytics/freebuff.ts b/apps/sim/lib/analytics/freebuff.ts deleted file mode 100644 index 6f994523d14..00000000000 --- a/apps/sim/lib/analytics/freebuff.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Freebuff Ads conversion tracking. A Freebuff ad click lands with a signed - * `?bfcid=` click id; the hosted tag stores it in a first-party `bfcid` cookie, - * and each conversion is reported twice with the same `eventId` — once by the - * tag and once by the server postback — so Freebuff dedupes them into one. - * - * @see https://freebuff.com/docs/advertisers/conversions - */ - -export const FREEBUFF_TAG_SRC = 'https://freebuff.com/freebuff-tag.js' as const - -/** First-party cookie the tag writes the captured click id to. */ -export const FREEBUFF_CLICK_ID_COOKIE = 'bfcid' as const - -export type FreebuffConversionEvent = 'signup_completed' - -type FreebuffCommand = ( - command: 'conversion', - eventType: FreebuffConversionEvent, - options?: { eventId?: string } -) => void - -declare global { - interface Window { - freebuff?: FreebuffCommand & { q?: unknown[][] } - } -} - -/** Queues commands until the async tag loads and replays them. */ -export function installFreebuffStub(): void { - if (window.freebuff) return - const queue: unknown[][] = [] - window.freebuff = Object.assign((...args: unknown[]) => void queue.push(args), { q: queue }) -} - -/** Deletes the tag's click-id cookie, which it writes host-only on `Path=/`. */ -export function clearFreebuffClickId(): void { - if (!document.cookie.includes(`${FREEBUFF_CLICK_ID_COOKIE}=`)) return - document.cookie = `${FREEBUFF_CLICK_ID_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax` -} - -/** - * Reports a conversion from the page. Call only after the caller has verified - * marketing consent; the tag is a no-op for visitors who did not arrive from an - * ad. `eventId` must match the one the server postback sends. - */ -export function trackFreebuffConversion(event: FreebuffConversionEvent, eventId: string): void { - window.freebuff?.('conversion', event, { eventId }) -} diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 0b492da2045..6fb31d22b92 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -36,8 +36,6 @@ import { renderWelcomeEmail, } from '@/components/emails' import { readAttributionProperties } from '@/lib/analytics/attribution' -import { FREEBUFF_CLICK_ID_COOKIE } from '@/lib/analytics/freebuff' -import { reportFreebuffConversion } from '@/lib/analytics/freebuff.server' import { getAccessControlConfig, isEmailBlockedByAccessControl } from '@/lib/auth/access-control' import { enqueueAdminRemovedChatCleanup } from '@/lib/auth/admin-removal-chat-cleanup' import { holdAdminRemovalStops, stopAdminRemovedRuns } from '@/lib/auth/admin-removal-stops' @@ -373,23 +371,6 @@ export const auth = betterAuth({ userId: user.id, }) - /** - * Only the marketing-consent-gated Freebuff tag writes the `bfcid` - * cookie, and `FreebuffClickIdGuard` deletes it once marketing consent - * is withdrawn or expires. Not awaited: the postback - * retries on its own and must never delay signup. The browser tag - * reports the same `eventId` on email signup and Freebuff dedupes. - */ - const freebuffClickId = context?.getCookie(FREEBUFF_CLICK_ID_COOKIE) - if (freebuffClickId) { - void reportFreebuffConversion({ - clickId: freebuffClickId, - eventType: 'signup_completed', - eventId: user.id, - occurredAt: user.createdAt, - }) - } - try { PlatformEvents.userSignedUp({ userId: user.id, diff --git a/apps/sim/lib/consent/scripts.ts b/apps/sim/lib/consent/scripts.ts index a75a4a7110a..6bdc867894d 100644 --- a/apps/sim/lib/consent/scripts.ts +++ b/apps/sim/lib/consent/scripts.ts @@ -1,7 +1,6 @@ import { ahrefsAnalytics } from '@c15t/scripts/ahrefs-analytics' import { gtag } from '@c15t/scripts/google-tag' import { xPixel } from '@c15t/scripts/x-pixel' -import { FREEBUFF_TAG_SRC, installFreebuffStub } from '@/lib/analytics/freebuff' import { getGooglePageLocation, updateGooglePageContext } from '@/lib/consent/google-context' import { isPathOrDescendant } from '@/lib/navigation/paths' @@ -53,19 +52,6 @@ const GLOBAL_CONSENT_SCRIPTS = [ }, }, ahrefsAnalytics({ key: AHREFS_ANALYTICS_KEY }), - /** - * Global rather than landing-only: the ad lands on a marketing page but the - * conversion fires from `/signup`. The tag recovers `?bfcid=` from the - * original navigation entry, so a client-side route change before consent - * resolves does not lose the click id. - */ - { - id: 'freebuff-tag', - src: FREEBUFF_TAG_SRC, - category: 'marketing', - async: true, - onBeforeLoad: installFreebuffStub, - }, ] as const /** Excludes embedded extension pages and restricts Google's loader to production origins. */ diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index b87c640130d..d2039aaefa1 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -322,7 +322,6 @@ export const env = createEnv({ // Monitoring & Analytics TELEMETRY_ENDPOINT: z.string().url().optional(), // Custom telemetry/analytics endpoint - FREEBUFF_API_KEY: z.string().min(1).optional(), // Freebuff Ads key for server-side conversion postbacks (unset disables them) COST_MULTIPLIER: z.number().optional(), // Multiplier for cost calculations LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(), // Minimum log level to display (defaults to ERROR in production, DEBUG in development) GRAFANA_OTLP_ENDPOINT: z.string().url().optional(), // Grafana Cloud OTLP HTTP gateway base URL (e.g., https://otlp-gateway-prod-us-east-0.grafana.net/otlp). Trigger.dev exporters append /v1/traces, /v1/logs, /v1/metrics. diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index bb880026487..77c90decc36 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -119,8 +119,6 @@ const STATIC_SCRIPT_SRC = [ // X (Twitter) conversion pixel (landing pages) — the base code injects // uwt.js as a