Revert "fix: handle null/undefined tracking IDs in analytics schemas (#27625)"

This reverts commit 04e828358a.
This commit is contained in:
Alex van Andel
2026-02-05 01:00:42 +00:00
parent aef96e214f
commit a8e1cbeeb5
10 changed files with 39 additions and 133 deletions
+17 -25
View File
@@ -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}`;
-95
View File
@@ -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();
});
});
});
+2 -1
View File
@@ -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 }),
})
);
+1
View File
@@ -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(
+1
View File
@@ -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(
+2 -2
View File
@@ -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)",
})
+5 -3
View File
@@ -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();
+3 -2
View File
@@ -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",
})
+2 -2
View File
@@ -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",
})
+6 -3
View File
@@ -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();