feat(capi): server-side Meta Conversions API for signup (CompleteRegistration)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
The browser pixel under-counts signups badly: ~96% of paid traffic is mobile in-app browsers where iOS ITP / ad-blockers drop client events, and the OAuth path never fired it reliably. Meta recorded ~0 registrations for a 7.5K-spend campaign while PostHog saw 98 facebook signups — so Meta could neither optimize toward nor attribute signups, which is the main driver of the low signup rate. This adds a server-side CAPI CompleteRegistration: - MetaCapiService + @Global module. Fail-open: no-ops unless META_CAPI_PIXEL_ID + META_CAPI_ACCESS_TOKEN are set; never throws (signup must not break). SHA-256 hashed email + fbp/fbc/IP/UA. - Fired from the better-auth user.create.after hook for ALL signups (reliable, covers Google OAuth which the browser pixel missed entirely). - A session-gated POST /analytics/meta/complete-registration endpoint adds fbp/fbc/IP/UA (ad-click attribution) for the email path. - The browser pixel now passes a shared event_id (signup_<userId>); the premature Google client-pixel fire (fired on click, before completion) is removed. - All sources dedupe via event_id=signup_<userId>. Activate by setting META_CAPI_PIXEL_ID + META_CAPI_ACCESS_TOKEN (Events Manager) in the api env; META_CAPI_TEST_EVENT_CODE routes to Test Events for verification. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import { HealthController } from "./health.controller";
|
|||||||
import { EmexModule } from "./integrations/emex/emex.module";
|
import { EmexModule } from "./integrations/emex/emex.module";
|
||||||
import { InternalAdminModule } from "./internal-admin/internal-admin.module";
|
import { InternalAdminModule } from "./internal-admin/internal-admin.module";
|
||||||
import { JobsModule } from "./jobs/jobs.module";
|
import { JobsModule } from "./jobs/jobs.module";
|
||||||
|
import { MetaCapiModule } from "./meta-capi/meta-capi.module";
|
||||||
import { NotificationsModule } from "./notifications/notifications.module";
|
import { NotificationsModule } from "./notifications/notifications.module";
|
||||||
import { PartsModule } from "./parts/parts.module";
|
import { PartsModule } from "./parts/parts.module";
|
||||||
import { PaymentsModule } from "./payments/payments.module";
|
import { PaymentsModule } from "./payments/payments.module";
|
||||||
@@ -95,6 +96,7 @@ import { VehiclesModule } from "./vehicles/vehicles.module";
|
|||||||
ChatwootModule,
|
ChatwootModule,
|
||||||
BlogModule,
|
BlogModule,
|
||||||
ContactModule,
|
ContactModule,
|
||||||
|
MetaCapiModule,
|
||||||
PostHogModule,
|
PostHogModule,
|
||||||
TelemetryModule,
|
TelemetryModule,
|
||||||
InternalAdminModule,
|
InternalAdminModule,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module, type OnModuleInit } from "@nestjs/common";
|
import { Module, type OnModuleInit } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { EmailService } from "../email/email.service";
|
import { EmailService } from "../email/email.service";
|
||||||
|
import { MetaCapiService } from "../meta-capi/meta-capi.service";
|
||||||
import { NovuService } from "../notifications/novu.service";
|
import { NovuService } from "../notifications/novu.service";
|
||||||
import { ReferralsModule } from "../referrals/referrals.module";
|
import { ReferralsModule } from "../referrals/referrals.module";
|
||||||
import { ReferralsService } from "../referrals/referrals.service";
|
import { ReferralsService } from "../referrals/referrals.service";
|
||||||
@@ -20,6 +21,7 @@ export class AuthModule implements OnModuleInit {
|
|||||||
private emailService: EmailService,
|
private emailService: EmailService,
|
||||||
private novuService: NovuService,
|
private novuService: NovuService,
|
||||||
private referralsService: ReferralsService,
|
private referralsService: ReferralsService,
|
||||||
|
private metaCapiService: MetaCapiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
@@ -36,6 +38,7 @@ export class AuthModule implements OnModuleInit {
|
|||||||
emailService: this.emailService,
|
emailService: this.emailService,
|
||||||
novu: this.novuService,
|
novu: this.novuService,
|
||||||
onEmailVerified: (userId) => this.referralsService.qualifyReferral(userId),
|
onEmailVerified: (userId) => this.referralsService.qualifyReferral(userId),
|
||||||
|
metaCapi: this.metaCapiService,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ interface AuthOptions {
|
|||||||
novu?: NovuService;
|
novu?: NovuService;
|
||||||
/** Called after a user's email is verified (referral qualification, etc.). */
|
/** Called after a user's email is verified (referral qualification, etc.). */
|
||||||
onEmailVerified?: (userId: string) => Promise<void>;
|
onEmailVerified?: (userId: string) => Promise<void>;
|
||||||
|
/** Server-side Meta CAPI sender — reliable signup backstop for BOTH email and
|
||||||
|
* Google OAuth (the browser pixel misses OAuth and is ad-blocked on mobile). */
|
||||||
|
metaCapi?: {
|
||||||
|
sendCompleteRegistration: (input: { userId: string; email?: string | null }) => Promise<void>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuth(
|
export function createAuth(
|
||||||
@@ -155,6 +160,14 @@ export function createAuth(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
after: async (user) => {
|
after: async (user) => {
|
||||||
|
// Server-side Meta CAPI signup event — reliable for BOTH email and
|
||||||
|
// Google OAuth (the browser pixel misses OAuth and is ad-blocked on
|
||||||
|
// mobile in-app). Deduped with the pixel/endpoint via
|
||||||
|
// event_id=signup_<id>. Fire-and-forget; never break signup.
|
||||||
|
void options?.metaCapi?.sendCompleteRegistration({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
// Fire-and-forget lifecycle e-mails on signup (covers password +
|
// Fire-and-forget lifecycle e-mails on signup (covers password +
|
||||||
// OAuth). Never await / never let a notification failure surface
|
// OAuth). Never await / never let a notification failure surface
|
||||||
// into the signup response. `referral` is delay-stepped in Novu
|
// into the signup response. `referral` is delay-stepped in Novu
|
||||||
|
|||||||
57
apps/api/src/meta-capi/meta-capi.controller.ts
Normal file
57
apps/api/src/meta-capi/meta-capi.controller.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Body, Controller, HttpCode, Post, Req } from "@nestjs/common";
|
||||||
|
import type { Request } from "express";
|
||||||
|
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
||||||
|
import { MetaCapiService } from "./meta-capi.service";
|
||||||
|
|
||||||
|
@Controller("analytics/meta")
|
||||||
|
export class MetaCapiController {
|
||||||
|
constructor(private readonly metaCapi: MetaCapiService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-match-quality `CompleteRegistration` for the email signup path. Called by
|
||||||
|
* the client right after a successful signup (session cookie present), so it can
|
||||||
|
* attach `fbp`/`fbc`/IP/UA that the server-side auth hook can't see — `fbc`
|
||||||
|
* (from the `fbclid` ad-click) is what ties the signup back to the ad.
|
||||||
|
*
|
||||||
|
* Session-gated: the user id/email come from the session, never the client.
|
||||||
|
* Deduped with the browser pixel + the auth hook via `event_id=signup_<userId>`.
|
||||||
|
*/
|
||||||
|
@Post("complete-registration")
|
||||||
|
@HttpCode(204)
|
||||||
|
async completeRegistration(
|
||||||
|
@CurrentUser() user: { id: string; email?: string } | undefined,
|
||||||
|
@Req() req: Request,
|
||||||
|
@Body() body: { eventSourceUrl?: string },
|
||||||
|
): Promise<void> {
|
||||||
|
if (!user?.id) return;
|
||||||
|
const cookies = parseCookies(req.headers.cookie);
|
||||||
|
await this.metaCapi.sendCompleteRegistration({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
fbp: cookies._fbp,
|
||||||
|
fbc: cookies._fbc,
|
||||||
|
clientIp: clientIp(req),
|
||||||
|
userAgent: req.headers["user-agent"],
|
||||||
|
eventSourceUrl: body?.eventSourceUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal Cookie-header parser (no cookie-parser middleware in this app). */
|
||||||
|
function parseCookies(header?: string): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
if (!header) return out;
|
||||||
|
for (const part of header.split(";")) {
|
||||||
|
const eq = part.indexOf("=");
|
||||||
|
if (eq < 0) continue;
|
||||||
|
const key = part.slice(0, eq).trim();
|
||||||
|
if (key) out[key] = decodeURIComponent(part.slice(eq + 1).trim());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientIp(req: Request): string | undefined {
|
||||||
|
const xff = req.headers["x-forwarded-for"];
|
||||||
|
if (typeof xff === "string" && xff.length > 0) return xff.split(",")[0]?.trim();
|
||||||
|
return req.ip;
|
||||||
|
}
|
||||||
13
apps/api/src/meta-capi/meta-capi.module.ts
Normal file
13
apps/api/src/meta-capi/meta-capi.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { MetaCapiController } from "./meta-capi.controller";
|
||||||
|
import { MetaCapiService } from "./meta-capi.service";
|
||||||
|
|
||||||
|
// @Global so the auth module's databaseHook can inject MetaCapiService without
|
||||||
|
// an import cycle.
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
controllers: [MetaCapiController],
|
||||||
|
providers: [MetaCapiService],
|
||||||
|
exports: [MetaCapiService],
|
||||||
|
})
|
||||||
|
export class MetaCapiModule {}
|
||||||
103
apps/api/src/meta-capi/meta-capi.service.ts
Normal file
103
apps/api/src/meta-capi/meta-capi.service.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
const GRAPH_VERSION = "v21.0";
|
||||||
|
|
||||||
|
export interface CapiRegistrationInput {
|
||||||
|
userId: string;
|
||||||
|
email?: string | null;
|
||||||
|
/** _fbp cookie (browser pixel id). */
|
||||||
|
fbp?: string;
|
||||||
|
/** _fbc cookie (derived from the fbclid ad-click param). Key for ad attribution. */
|
||||||
|
fbc?: string;
|
||||||
|
clientIp?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
eventSourceUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta Conversions API — server-side signup tracking.
|
||||||
|
*
|
||||||
|
* The browser pixel alone under-counts: ~96% of paid traffic is mobile in-app
|
||||||
|
* browsers where iOS ITP / ad-blockers drop client events, and the OAuth path
|
||||||
|
* never fires it reliably. This sends `CompleteRegistration` server-side so Meta
|
||||||
|
* can actually optimize toward (and attribute) signups.
|
||||||
|
*
|
||||||
|
* Fail-open: no-ops unless both pixel id and access token are configured, and
|
||||||
|
* never throws — signup must never break because of analytics.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MetaCapiService {
|
||||||
|
private readonly logger = new Logger(MetaCapiService.name);
|
||||||
|
private readonly pixelId?: string;
|
||||||
|
private readonly accessToken?: string;
|
||||||
|
private readonly testEventCode?: string;
|
||||||
|
private readonly enabled: boolean;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
this.pixelId = this.configService.get<string>("META_CAPI_PIXEL_ID") || undefined;
|
||||||
|
this.accessToken = this.configService.get<string>("META_CAPI_ACCESS_TOKEN") || undefined;
|
||||||
|
this.testEventCode = this.configService.get<string>("META_CAPI_TEST_EVENT_CODE") || undefined;
|
||||||
|
this.enabled = Boolean(this.pixelId && this.accessToken);
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.logger.warn(
|
||||||
|
"Meta CAPI disabled — set META_CAPI_PIXEL_ID + META_CAPI_ACCESS_TOKEN to enable",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a server-side `CompleteRegistration`. The deterministic
|
||||||
|
* `event_id = signup_<userId>` deduplicates against the browser pixel and any
|
||||||
|
* other CAPI source (e.g. the auth hook) for the same user, so multiple
|
||||||
|
* redundant sends collapse to one event with the best merged signals.
|
||||||
|
*/
|
||||||
|
async sendCompleteRegistration(input: CapiRegistrationInput): Promise<void> {
|
||||||
|
if (!this.enabled) return;
|
||||||
|
try {
|
||||||
|
const userData: Record<string, unknown> = {};
|
||||||
|
if (input.email) userData.em = [sha256(input.email.trim().toLowerCase())];
|
||||||
|
if (input.fbp) userData.fbp = input.fbp;
|
||||||
|
if (input.fbc) userData.fbc = input.fbc;
|
||||||
|
if (input.clientIp) userData.client_ip_address = input.clientIp;
|
||||||
|
if (input.userAgent) userData.client_user_agent = input.userAgent;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
event_name: "CompleteRegistration",
|
||||||
|
event_time: Math.floor(Date.now() / 1000),
|
||||||
|
event_id: `signup_${input.userId}`,
|
||||||
|
action_source: "website",
|
||||||
|
...(input.eventSourceUrl ? { event_source_url: input.eventSourceUrl } : {}),
|
||||||
|
user_data: userData,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...(this.testEventCode ? { test_event_code: this.testEventCode } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`https://graph.facebook.com/${GRAPH_VERSION}/${this.pixelId}/events?access_token=${this.accessToken}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => "");
|
||||||
|
this.logger.warn(
|
||||||
|
`Meta CAPI CompleteRegistration failed (${res.status}): ${text.slice(0, 300)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(`Meta CAPI CompleteRegistration error: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(value: string): string {
|
||||||
|
return createHash("sha256").update(value).digest("hex");
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ function ensureStub(): Fbq | null {
|
|||||||
if (typeof window === "undefined") return null;
|
if (typeof window === "undefined") return null;
|
||||||
if (window.fbq) return window.fbq;
|
if (window.fbq) return window.fbq;
|
||||||
|
|
||||||
const n: Fbq = function (...args: FbqArgs) {
|
const n: Fbq = (...args: FbqArgs) => {
|
||||||
if (n.callMethod) {
|
if (n.callMethod) {
|
||||||
n.callMethod(...args);
|
n.callMethod(...args);
|
||||||
} else {
|
} else {
|
||||||
@@ -63,7 +63,9 @@ export function trackPageView(): void {
|
|||||||
window.fbq?.("track", "PageView");
|
window.fbq?.("track", "PageView");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function track(event: string, params?: Record<string, unknown>): void {
|
export function track(event: string, params?: Record<string, unknown>, eventId?: string): void {
|
||||||
if (!_initialized) return;
|
if (!_initialized) return;
|
||||||
window.fbq?.("track", event, params);
|
// `eventID` lets Meta dedupe this browser event against the server-side CAPI
|
||||||
|
// event for the same action — both must use the same id.
|
||||||
|
window.fbq?.("track", event, params, eventId ? { eventID: eventId } : undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,13 @@ function RegisterPage() {
|
|||||||
// onboarding modal on the search page is the single place that applies it
|
// onboarding modal on the search page is the single place that applies it
|
||||||
// (covers both email and Google OAuth signups).
|
// (covers both email and Google OAuth signups).
|
||||||
capture("user_signed_up", { method: "email" });
|
capture("user_signed_up", { method: "email" });
|
||||||
trackMeta("CompleteRegistration", { method: "email" });
|
// CompleteRegistration: browser pixel + server-side CAPI (adds fbp/fbc/IP/UA
|
||||||
|
// for ad-click attribution), deduped via the shared event id `signup_<userId>`.
|
||||||
|
const metaEventId = data?.user ? `signup_${data.user.id}` : undefined;
|
||||||
|
trackMeta("CompleteRegistration", { method: "email" }, metaEventId);
|
||||||
|
api
|
||||||
|
.post("/analytics/meta/complete-registration", { eventSourceUrl: window.location.href })
|
||||||
|
.catch(() => {});
|
||||||
if (plan) localStorage.setItem(PENDING_PLAN_KEY, plan);
|
if (plan) localStorage.setItem(PENDING_PLAN_KEY, plan);
|
||||||
toast.success("Hesap oluşturuldu!");
|
toast.success("Hesap oluşturuldu!");
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
@@ -237,7 +243,10 @@ function RegisterPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
startAction("register", { method: "google" });
|
startAction("register", { method: "google" });
|
||||||
capture("user_signed_up", { method: "google" });
|
capture("user_signed_up", { method: "google" });
|
||||||
trackMeta("CompleteRegistration", { method: "google" });
|
// Meta CompleteRegistration for Google is sent SERVER-SIDE on actual
|
||||||
|
// account creation (auth databaseHook). The browser pixel here fired on
|
||||||
|
// click (before the user even finished Google auth) and would double-
|
||||||
|
// count, so it's intentionally not sent client-side for OAuth.
|
||||||
if (plan) localStorage.setItem(PENDING_PLAN_KEY, plan);
|
if (plan) localStorage.setItem(PENDING_PLAN_KEY, plan);
|
||||||
signIn.social({ provider: "google", callbackURL: redirectUrl });
|
signIn.social({ provider: "google", callbackURL: redirectUrl });
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ services:
|
|||||||
# "false" on non-prod → evaluate flags but DON'T emit analytics to prod project.
|
# "false" on non-prod → evaluate flags but DON'T emit analytics to prod project.
|
||||||
- POSTHOG_CAPTURE_ENABLED=${POSTHOG_CAPTURE_ENABLED:-}
|
- POSTHOG_CAPTURE_ENABLED=${POSTHOG_CAPTURE_ENABLED:-}
|
||||||
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
|
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
|
||||||
|
# Meta Conversions API — server-side signup tracking. Empty → CAPI no-ops.
|
||||||
|
- META_CAPI_PIXEL_ID=${META_CAPI_PIXEL_ID:-}
|
||||||
|
- META_CAPI_ACCESS_TOKEN=${META_CAPI_ACCESS_TOKEN:-}
|
||||||
|
- META_CAPI_TEST_EVENT_CODE=${META_CAPI_TEST_EVENT_CODE:-}
|
||||||
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
||||||
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
|
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
|
||||||
@@ -162,6 +166,10 @@ services:
|
|||||||
# "false" on non-prod → evaluate flags but DON'T emit analytics to prod project.
|
# "false" on non-prod → evaluate flags but DON'T emit analytics to prod project.
|
||||||
- POSTHOG_CAPTURE_ENABLED=${POSTHOG_CAPTURE_ENABLED:-}
|
- POSTHOG_CAPTURE_ENABLED=${POSTHOG_CAPTURE_ENABLED:-}
|
||||||
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
|
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
|
||||||
|
# Meta Conversions API — server-side signup tracking. Empty → CAPI no-ops.
|
||||||
|
- META_CAPI_PIXEL_ID=${META_CAPI_PIXEL_ID:-}
|
||||||
|
- META_CAPI_ACCESS_TOKEN=${META_CAPI_ACCESS_TOKEN:-}
|
||||||
|
- META_CAPI_TEST_EVENT_CODE=${META_CAPI_TEST_EVENT_CODE:-}
|
||||||
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
||||||
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
|
- OTEL_EXPORTER_OTLP_HEADERS=${OTEL_EXPORTER_OTLP_HEADERS:-}
|
||||||
|
|||||||
@@ -109,6 +109,14 @@ export const envSchema = z.object({
|
|||||||
// set "false" on dev. (Plain string — read as `!== "false"`.)
|
// set "false" on dev. (Plain string — read as `!== "false"`.)
|
||||||
POSTHOG_CAPTURE_ENABLED: z.string().optional(),
|
POSTHOG_CAPTURE_ENABLED: z.string().optional(),
|
||||||
|
|
||||||
|
// Meta Conversions API — server-side signup tracking (CompleteRegistration) so
|
||||||
|
// Meta can optimize toward / attribute signups despite the unreliable browser
|
||||||
|
// pixel. Empty → CAPI no-ops. Pixel id is public; the access token is a secret
|
||||||
|
// from Events Manager. Test event code routes to Events Manager > Test Events.
|
||||||
|
META_CAPI_PIXEL_ID: z.string().optional(),
|
||||||
|
META_CAPI_ACCESS_TOKEN: z.string().optional(),
|
||||||
|
META_CAPI_TEST_EVENT_CODE: z.string().optional(),
|
||||||
|
|
||||||
// Sentry — error tracking
|
// Sentry — error tracking
|
||||||
SENTRY_DSN: z.string().url().optional(),
|
SENTRY_DSN: z.string().url().optional(),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user