From a8e1cbeeb5df4c54bc6b0301cbd6882847bd264f Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 5 Feb 2026 01:00:42 +0000 Subject: [PATCH] Revert "fix: handle null/undefined tracking IDs in analytics schemas (#27625)" This reverts commit 04e828358a5703555b95e7ac12d7c88381978377. --- packages/app-store/_lib/analytics-schemas.ts | 42 ++++----- packages/app-store/analytics-apps.test.ts | 95 -------------------- packages/app-store/gtm/zod.ts | 3 +- packages/app-store/insihts/zod.ts | 1 + packages/app-store/matomo/zod.ts | 1 + packages/app-store/metapixel/zod.ts | 4 +- packages/app-store/plausible/zod.ts | 8 +- packages/app-store/posthog/zod.ts | 5 +- packages/app-store/twipla/zod.ts | 4 +- packages/app-store/umami/zod.ts | 9 +- 10 files changed, 39 insertions(+), 133 deletions(-) diff --git a/packages/app-store/_lib/analytics-schemas.ts b/packages/app-store/_lib/analytics-schemas.ts index 16b5edbf20..5bc8466fcf 100644 --- a/packages/app-store/_lib/analytics-schemas.ts +++ b/packages/app-store/_lib/analytics-schemas.ts @@ -1,15 +1,5 @@ import { z } from "zod"; -/** - * Helper to create schemas that accept nullish values but always output string. - * Used for tracking IDs that may be null/undefined in the database. - */ -const nullishString = () => - z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => (typeof val === "string" ? val.trim() : "")); - -// URL schema: string input, validates http/https (kept as original to maintain type compatibility) export const safeUrlSchema = z .string() .transform((val) => val.trim()) @@ -26,19 +16,23 @@ export const safeUrlSchema = z { message: "Invalid URL format. Must be a valid http or https URL" } ); -// Alphanumeric ID schema - accepts nullish for tracking IDs -export const alphanumericIdSchema = nullishString().refine( - (val) => !val || /^[A-Za-z0-9_-]+$/.test(val), - { message: "Invalid ID format. Expected alphanumeric characters, underscores, or hyphens" } -); +// Schema for tracking IDs that should only contain letters, numbers, underscores, and hyphens +export const alphanumericIdSchema = z + .string() + .transform((val) => val.trim()) + .refine((val) => !val || /^[A-Za-z0-9_-]+$/.test(val), { + message: "Invalid ID format. Expected alphanumeric characters, underscores, or hyphens", + }); -// Numeric ID schema - accepts nullish for tracking IDs -export const numericIdSchema = nullishString().refine( - (val) => !val || /^[0-9]+$/.test(val), - { message: "Invalid ID format. Expected a numeric ID" } -); +// Schema for tracking IDs that should only contain digits +export const numericIdSchema = z + .string() + .transform((val) => val.trim()) + .refine((val) => !val || /^[0-9]+$/.test(val), { + message: "Invalid ID format. Expected a numeric ID", + }); -// Factory for prefixed ID schemas (GTM-, G-, etc.) +// Factory for creating prefixed ID schemas (GTM, GA4, Fathom, etc.) export const createPrefixedIdSchema = (options: { prefix?: string; addPrefixIfMissing?: boolean; @@ -47,11 +41,9 @@ export const createPrefixedIdSchema = (options: { const { prefix = "", addPrefixIfMissing = false, allowEmpty = true } = options; return z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => { - if (typeof val !== "string") return ""; + .string() + .transform((val) => { let result = val.trim().toUpperCase(); - if (!result) return result; if (prefix && addPrefixIfMissing) { const clean = result.replace(new RegExp(`^${prefix}`, "i"), ""); result = `${prefix}${clean}`; diff --git a/packages/app-store/analytics-apps.test.ts b/packages/app-store/analytics-apps.test.ts index 0fe43255ca..7b0b6fea95 100644 --- a/packages/app-store/analytics-apps.test.ts +++ b/packages/app-store/analytics-apps.test.ts @@ -252,99 +252,4 @@ describe("Analytics Apps - Input Validation", () => { } }); }); - - // Null/undefined handling for embed endpoint (PR 26976) - // Schemas accept nullish input and always output string - describe("Null/Undefined Handling", () => { - it("GTM converts null to empty string", () => { - expect(gtmSchema.parse({ trackingId: null }).trackingId).toBe(""); - }); - - it("GA4 converts null to empty string", () => { - expect(ga4Schema.parse({ trackingId: null }).trackingId).toBe(""); - }); - - it("Meta Pixel converts null to empty string", () => { - expect(metapixelSchema.parse({ trackingId: null }).trackingId).toBe(""); - }); - - it("PostHog converts null to empty string", () => { - expect(posthogSchema.parse({ TRACKING_ID: null }).TRACKING_ID).toBe(""); - }); - - it("Fathom converts null to empty string", () => { - expect(fathomSchema.parse({ trackingId: null }).trackingId).toBe(""); - }); - - it("Plausible converts null to empty string", () => { - expect(plausibleSchema.parse({ trackingId: null }).trackingId).toBe(""); - }); - - it("Matomo converts null to empty string", () => { - expect(matomoSchema.parse({ SITE_ID: null }).SITE_ID).toBe(""); - }); - - it("Umami converts null to empty string", () => { - expect(umamiSchema.parse({ SITE_ID: null }).SITE_ID).toBe(""); - }); - - it("Twipla converts null to empty string", () => { - expect(twiplaSchema.parse({ SITE_ID: null }).SITE_ID).toBe(""); - }); - - it("Insihts converts null to empty string", () => { - expect(insihtsSchema.parse({ SITE_ID: null }).SITE_ID).toBe(""); - }); - - it("Databuddy converts null to empty string", () => { - expect(databuddySchema.parse({ CLIENT_ID: null }).CLIENT_ID).toBe(""); - }); - }); - - // Missing key handling - fields should be optional - describe("Missing Key Handling", () => { - it("GA4 accepts missing trackingId", () => { - expect(() => ga4Schema.parse({})).not.toThrow(); - }); - - it("GTM accepts missing trackingId", () => { - expect(() => gtmSchema.parse({})).not.toThrow(); - }); - - it("Meta Pixel accepts missing trackingId", () => { - expect(() => metapixelSchema.parse({})).not.toThrow(); - }); - - it("Fathom accepts missing trackingId", () => { - expect(() => fathomSchema.parse({})).not.toThrow(); - }); - - it("Plausible accepts missing trackingId", () => { - expect(() => plausibleSchema.parse({})).not.toThrow(); - }); - - it("PostHog accepts missing TRACKING_ID", () => { - expect(() => posthogSchema.parse({})).not.toThrow(); - }); - - it("Matomo accepts missing fields", () => { - expect(() => matomoSchema.parse({})).not.toThrow(); - }); - - it("Umami accepts missing SITE_ID", () => { - expect(() => umamiSchema.parse({})).not.toThrow(); - }); - - it("Twipla accepts missing SITE_ID", () => { - expect(() => twiplaSchema.parse({})).not.toThrow(); - }); - - it("Insihts accepts missing fields", () => { - expect(() => insihtsSchema.parse({})).not.toThrow(); - }); - - it("Databuddy accepts missing CLIENT_ID", () => { - expect(() => databuddySchema.parse({})).not.toThrow(); - }); - }); }); diff --git a/packages/app-store/gtm/zod.ts b/packages/app-store/gtm/zod.ts index 3e5a311676..71ee5440ab 100644 --- a/packages/app-store/gtm/zod.ts +++ b/packages/app-store/gtm/zod.ts @@ -1,11 +1,12 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + import { createPrefixedIdSchema } from "@calcom/app-store/_lib/analytics-schemas"; export const appDataSchema = eventTypeAppCardZod.merge( z.object({ - trackingId: createPrefixedIdSchema({ prefix: "GTM-", addPrefixIfMissing: true, allowEmpty: true }), + trackingId: createPrefixedIdSchema({ prefix: "GTM-", addPrefixIfMissing: true, allowEmpty: false }), }) ); diff --git a/packages/app-store/insihts/zod.ts b/packages/app-store/insihts/zod.ts index 7c5431e483..d8f0b3a24b 100644 --- a/packages/app-store/insihts/zod.ts +++ b/packages/app-store/insihts/zod.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + import { alphanumericIdSchema, safeUrlSchema } from "@calcom/app-store/_lib/analytics-schemas"; export const appDataSchema = eventTypeAppCardZod.merge( diff --git a/packages/app-store/matomo/zod.ts b/packages/app-store/matomo/zod.ts index 734829eebc..ee39ec1697 100644 --- a/packages/app-store/matomo/zod.ts +++ b/packages/app-store/matomo/zod.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + import { numericIdSchema, safeUrlSchema } from "@calcom/app-store/_lib/analytics-schemas"; export const appDataSchema = eventTypeAppCardZod.merge( diff --git a/packages/app-store/metapixel/zod.ts b/packages/app-store/metapixel/zod.ts index e4399565cd..532f72d529 100644 --- a/packages/app-store/metapixel/zod.ts +++ b/packages/app-store/metapixel/zod.ts @@ -4,8 +4,8 @@ import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; // Meta Pixel IDs are numeric strings of 15-16 digits const metaPixelIdSchema = z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => (typeof val === "string" ? val.trim() : "")) + .string() + .transform((val) => val.trim()) .refine((val) => val === "" || /^[0-9]{15,16}$/.test(val), { message: "Invalid Meta Pixel ID format. Expected a numeric ID (e.g., 1234567890123456)", }) diff --git a/packages/app-store/plausible/zod.ts b/packages/app-store/plausible/zod.ts index fbdc284322..df2c16b8bf 100644 --- a/packages/app-store/plausible/zod.ts +++ b/packages/app-store/plausible/zod.ts @@ -5,12 +5,14 @@ import { safeUrlSchema } from "@calcom/app-store/_lib/analytics-schemas"; // Domain schema for Plausible tracking (e.g., example.com, sub.example.com) const domainSchema = z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => (typeof val === "string" ? val.trim().toLowerCase() : "")) + .string() + .transform((val) => val.trim().toLowerCase()) .refine( (val) => val === "" || /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(val), - { message: "Invalid domain format. Expected format: example.com" } + { + message: "Invalid domain format. Expected format: example.com", + } ) .optional(); diff --git a/packages/app-store/posthog/zod.ts b/packages/app-store/posthog/zod.ts index b440b94cc0..26ccc8cbba 100644 --- a/packages/app-store/posthog/zod.ts +++ b/packages/app-store/posthog/zod.ts @@ -1,12 +1,13 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + import { safeUrlSchema } from "@calcom/app-store/_lib/analytics-schemas"; // PostHog Project API Keys (typically start with phc_) - allow alphanumeric to not break legacy data const posthogIdSchema = z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => (typeof val === "string" ? val.trim() : "")) + .string() + .transform((val) => val.trim()) .refine((val) => !val || /^[A-Za-z0-9_]+$/.test(val), { message: "Invalid PostHog Project API Key format. Expected alphanumeric characters or underscores", }) diff --git a/packages/app-store/twipla/zod.ts b/packages/app-store/twipla/zod.ts index c497d0c5a5..b337018986 100644 --- a/packages/app-store/twipla/zod.ts +++ b/packages/app-store/twipla/zod.ts @@ -4,8 +4,8 @@ import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; // Twipla Site IDs can be UUID or alphanumeric strings const twiplaSiteIdSchema = z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => (typeof val === "string" ? val.trim() : "")) + .string() + .transform((val) => val.trim()) .refine((val) => !val || /^[A-Za-z0-9-]+$/.test(val), { message: "Invalid Twipla Site ID format. Expected alphanumeric characters or UUID", }) diff --git a/packages/app-store/umami/zod.ts b/packages/app-store/umami/zod.ts index 5b39b25402..10b654ce66 100644 --- a/packages/app-store/umami/zod.ts +++ b/packages/app-store/umami/zod.ts @@ -1,18 +1,21 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; + import { safeUrlSchema } from "@calcom/app-store/_lib/analytics-schemas"; // Umami Website IDs: UUID in v2 (e.g., 4fb7fa4c-5b46-438d-94b3-3a8fb9bc2e8b) or numeric in v1 const umamiSiteIdSchema = z - .union([z.string(), z.null(), z.undefined()]) - .transform((val): string => (typeof val === "string" ? val.trim().toLowerCase() : "")) + .string() + .transform((val) => val.trim().toLowerCase()) .refine( (val) => !val || /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(val) || /^[0-9]+$/.test(val), - { message: "Invalid Umami Website ID format. Expected UUID or numeric ID" } + { + message: "Invalid Umami Website ID format. Expected UUID or numeric ID", + } ) .optional();