feat: add UTM parameter data to insights bookings CSV download (#27854)

* feat: add UTM parameter data to insights bookings CSV download

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

* test: add UTM data tests and update snapshots for csvDataTransformer

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

* fix: use Tracking relation for UTM data instead of flat fields on Booking

Co-Authored-By: carina@cal.com <c.wollendorfer@me.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Carina Wollendorfer
2026-02-11 15:28:54 +05:30
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 285f6d54a3
commit 36713fc12e
4 changed files with 206 additions and 1 deletions
@@ -588,6 +588,15 @@ export class InsightsBookingBaseService {
bookingFields: true,
},
},
tracking: {
select: {
utm_source: true,
utm_medium: true,
utm_campaign: true,
utm_term: true,
utm_content: true,
},
},
},
});
@@ -251,6 +251,11 @@ exports[`csvDataTransformer > transformBookingsForCsv > snapshot: complete trans
"uid": "booking-1",
"userEmail": "sales@company.com",
"userUsername": "sales",
"utm_campaign": "spring-sale",
"utm_content": "banner-ad",
"utm_medium": "cpc",
"utm_source": "google",
"utm_term": "scheduling",
},
{
"Budget": null,
@@ -286,6 +291,11 @@ exports[`csvDataTransformer > transformBookingsForCsv > snapshot: complete trans
"uid": "booking-2",
"userEmail": "trainer@company.com",
"userUsername": "trainer",
"utm_campaign": "",
"utm_content": "",
"utm_medium": "",
"utm_source": "",
"utm_term": "",
},
{
"Budget": null,
@@ -321,6 +331,11 @@ exports[`csvDataTransformer > transformBookingsForCsv > snapshot: complete trans
"uid": null,
"userEmail": "user@company.com",
"userUsername": "user",
"utm_campaign": "",
"utm_content": "",
"utm_medium": "",
"utm_source": "",
"utm_term": "",
},
]
`;
@@ -9,6 +9,7 @@ import {
processBookingsForCsv,
formatCsvRow,
transformBookingsForCsv,
getUtmDataForBooking,
type BookingWithAttendees,
type BookingTimeStatusData,
} from "../csvDataTransformer";
@@ -212,6 +213,7 @@ describe("csvDataTransformer", () => {
seatsReferences: [],
responses: {},
eventType: { bookingFields: [] },
tracking: null,
...overrides,
});
@@ -365,6 +367,7 @@ describe("csvDataTransformer", () => {
seatsReferences: [],
responses: {},
eventType: { bookingFields: [] },
tracking: null,
},
{
uid: "booking-2",
@@ -377,6 +380,7 @@ describe("csvDataTransformer", () => {
seatsReferences: [],
responses: {},
eventType: { bookingFields: [] },
tracking: null,
},
];
@@ -397,6 +401,7 @@ describe("csvDataTransformer", () => {
eventType: {
bookingFields: [{ name: "company", type: "text", label: "Company" }],
},
tracking: null,
},
{
uid: "booking-2",
@@ -407,6 +412,7 @@ describe("csvDataTransformer", () => {
eventType: {
bookingFields: [{ name: "department", type: "text", label: "Department" }],
},
tracking: null,
},
];
@@ -438,6 +444,7 @@ describe("csvDataTransformer", () => {
{ name: "customPhone", type: "phone", label: "Contact Phone" },
],
},
tracking: null,
},
{
uid: "seated-booking",
@@ -456,6 +463,7 @@ describe("csvDataTransformer", () => {
{ name: "dietaryRestrictions", type: "text", label: "Dietary Restrictions" },
],
},
tracking: null,
},
];
@@ -689,6 +697,13 @@ describe("csvDataTransformer", () => {
{ name: "timeline", type: "text", label: "Timeline" },
],
},
tracking: {
utm_source: "google",
utm_medium: "cpc",
utm_campaign: "spring-sale",
utm_term: "scheduling",
utm_content: "banner-ad",
},
},
{
uid: "booking-2",
@@ -707,6 +722,7 @@ describe("csvDataTransformer", () => {
{ name: "emergencyContact", type: "phone", label: "Emergency Contact" },
],
},
tracking: null,
},
];
@@ -753,6 +769,7 @@ describe("csvDataTransformer", () => {
seatsReferences: [],
responses: {},
eventType: { bookingFields: [] },
tracking: null,
},
];
@@ -760,5 +777,143 @@ describe("csvDataTransformer", () => {
expect(result).toHaveLength(0);
});
it("should include UTM data in transformed rows", () => {
const csvData: BookingTimeStatusData[] = [
{
id: 1,
uid: "booking-with-utm",
title: "UTM Test",
createdAt: new Date("2024-01-01T00:00:00Z"),
timeStatus: "completed",
eventTypeId: 1,
eventLength: 30,
startTime: new Date("2024-01-01T10:00:00Z"),
endTime: new Date("2024-01-01T10:30:00Z"),
paid: false,
userEmail: "test@test.com",
userUsername: "test",
rating: null,
ratingFeedback: null,
noShowHost: false,
},
{
id: 2,
uid: "booking-no-utm",
title: "No UTM",
createdAt: new Date("2024-01-02T00:00:00Z"),
timeStatus: "completed",
eventTypeId: 1,
eventLength: 30,
startTime: new Date("2024-01-02T10:00:00Z"),
endTime: new Date("2024-01-02T10:30:00Z"),
paid: false,
userEmail: "test@test.com",
userUsername: "test",
rating: null,
ratingFeedback: null,
noShowHost: false,
},
];
const bookings: BookingWithAttendees[] = [
{
uid: "booking-with-utm",
eventTypeId: 1,
attendees: [{ name: "A", email: "a@test.com", phoneNumber: null, noShow: false }],
seatsReferences: [],
responses: {},
eventType: { bookingFields: [] },
tracking: {
utm_source: "google",
utm_medium: "cpc",
utm_campaign: "spring-sale",
utm_term: "scheduling",
utm_content: "banner-ad",
},
},
{
uid: "booking-no-utm",
eventTypeId: 1,
attendees: [{ name: "B", email: "b@test.com", phoneNumber: null, noShow: false }],
seatsReferences: [],
responses: {},
eventType: { bookingFields: [] },
tracking: null,
},
];
const result = transformBookingsForCsv(csvData, bookings, "UTC");
expect(result[0].utm_source).toBe("google");
expect(result[0].utm_medium).toBe("cpc");
expect(result[0].utm_campaign).toBe("spring-sale");
expect(result[0].utm_term).toBe("scheduling");
expect(result[0].utm_content).toBe("banner-ad");
expect(result[1].utm_source).toBe("");
expect(result[1].utm_medium).toBe("");
expect(result[1].utm_campaign).toBe("");
expect(result[1].utm_term).toBe("");
expect(result[1].utm_content).toBe("");
});
});
describe("getUtmDataForBooking", () => {
it("should return UTM data from booking", () => {
const booking: BookingWithAttendees = {
uid: "test",
eventTypeId: 1,
attendees: [],
seatsReferences: [],
responses: {},
eventType: null,
tracking: {
utm_source: "google",
utm_medium: "cpc",
utm_campaign: "spring-sale",
utm_term: "scheduling",
utm_content: "banner-ad",
},
};
expect(getUtmDataForBooking(booking)).toEqual({
utm_source: "google",
utm_medium: "cpc",
utm_campaign: "spring-sale",
utm_term: "scheduling",
utm_content: "banner-ad",
});
});
it("should return empty strings for null UTM values", () => {
const booking: BookingWithAttendees = {
uid: "test",
eventTypeId: 1,
attendees: [],
seatsReferences: [],
responses: {},
eventType: null,
tracking: null,
};
expect(getUtmDataForBooking(booking)).toEqual({
utm_source: "",
utm_medium: "",
utm_campaign: "",
utm_term: "",
utm_content: "",
});
});
it("should return empty strings for undefined booking", () => {
expect(getUtmDataForBooking(undefined)).toEqual({
utm_source: "",
utm_medium: "",
utm_campaign: "",
utm_term: "",
utm_content: "",
});
});
});
});
@@ -44,6 +44,13 @@ export type BookingWithAttendees = {
eventType: {
bookingFields: unknown;
} | null;
tracking: {
utm_source: string | null;
utm_medium: string | null;
utm_campaign: string | null;
utm_term: string | null;
utm_content: string | null;
} | null;
};
export type ProcessedBookingData = {
@@ -279,6 +286,18 @@ export function formatCsvRow(
return result;
}
export function getUtmDataForBooking(
booking: BookingWithAttendees | undefined
): Record<string, string> {
return {
utm_source: booking?.tracking?.utm_source || "",
utm_medium: booking?.tracking?.utm_medium || "",
utm_campaign: booking?.tracking?.utm_campaign || "",
utm_term: booking?.tracking?.utm_term || "",
utm_content: booking?.tracking?.utm_content || "",
};
}
export function transformBookingsForCsv(
csvData: BookingTimeStatusData[],
bookings: BookingWithAttendees[],
@@ -286,14 +305,21 @@ export function transformBookingsForCsv(
): Record<string, unknown>[] {
const { bookingMap, maxAttendees, allBookingQuestionLabels } = processBookingsForCsv(bookings);
const bookingsByUid = new Map(bookings.map((b) => [b.uid, b]));
return csvData.map((bookingTimeStatus) => {
const processedData = bookingTimeStatus.uid ? bookingMap.get(bookingTimeStatus.uid) : null;
return formatCsvRow(
const booking = bookingTimeStatus.uid ? bookingsByUid.get(bookingTimeStatus.uid) : undefined;
const row = formatCsvRow(
bookingTimeStatus,
processedData || null,
maxAttendees,
allBookingQuestionLabels,
timeZone
);
return {
...row,
...getUtmDataForBooking(booking),
};
});
}