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:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
285f6d54a3
commit
36713fc12e
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+15
@@ -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),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user