Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/landing/components/Analytics/Conversion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { sendFacebookEvent } from "@/lib/analytics";
import { IAnalytics } from "@litespace/types";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";

const Conversion: React.FC<{ eventName?: IAnalytics.EventName }> = ({
eventName = IAnalytics.EventName.PageView,
}) => {
const searchParams = useSearchParams();
const path = usePathname();

useEffect(() => {
const fbclid = searchParams.get("fbclid");
if (!fbclid) return;

sendFacebookEvent({
page: path,
eventName,
fbclid,
});
}, [path, eventName, searchParams]);

return null;
};

export default Conversion;
3 changes: 3 additions & 0 deletions apps/landing/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import cn from "classnames";
import Navbar from "@/components/Layout/Navbar";
import Sidebar from "@/components/Layout/Sidebar";
import Footer from "@/components/Layout/Footer";
import Conversion from "@/components/Analytics/Conversion";
import clarity, { getCustomeId, sessionId } from "@/lib/clarity";
import { usePathname } from "next/navigation";
import { IAnalytics } from "@litespace/types";

const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [showSidebar, setShowSidebar] = useState(false);
Expand All @@ -27,6 +29,7 @@ const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
{showSidebar ? <Sidebar hide={() => setShowSidebar(false)} /> : null}
{children}
<Footer />
<Conversion eventName={IAnalytics.EventName.PageView} />
</body>
);
};
Expand Down
22 changes: 22 additions & 0 deletions apps/landing/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { formatFbc } from "@litespace/utils/analytics";
import { api } from "@/lib/api";
import { IAnalytics } from "@litespace/types";

export function sendFacebookEvent({
page,
eventName,
fbclid,
}: {
page: string;
fbclid?: string | null;
eventName: IAnalytics.EventName;
}) {
return api.analytics.trackFacebookEvents({
eventName: eventName,
eventSourceUrl: window.location.href,
fbc: formatFbc(fbclid),
customData: {
page: page,
},
});
}
13 changes: 13 additions & 0 deletions packages/atlas/src/api/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Base } from "@/lib/base";
import { IAnalytics } from "@litespace/types";

export class Analytics extends Base {
async trackFacebookEvents(
payload: IAnalytics.ConversionEventPayload
): Promise<void> {
return this.post({
route: "/api/v1/analytics/fb/track",
payload,
});
}
}
3 changes: 3 additions & 0 deletions packages/atlas/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import { Fawry } from "@/api/fawry";
import { ConfirmationCode } from "@/api/confirmationCode";
import { Subscription } from "@/api/subscription";
import { Transaction } from "@/api/transaction";
import { Analytics } from "@/api/analytics";

export class Api {
public readonly user: User;
public readonly auth: Auth;
public readonly analytics: Analytics;
public readonly availabilitySlot: AvailabilitySlot;
public readonly contactRequest: ContactRequest;
public readonly plan: Plan;
Expand All @@ -49,6 +51,7 @@ export class Api {
const client = createClient("api", server, token);
this.user = new User(client);
this.auth = new Auth(client);
this.analytics = new Analytics(client);
this.availabilitySlot = new AvailabilitySlot(client);
this.contactRequest = new ContactRequest(client);
this.plan = new Plan(client);
Expand Down
29 changes: 29 additions & 0 deletions packages/types/src/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export type ConversionEventPayload = {
eventName: EventName;
userId?: string;
fbc?: string;
eventSourceUrl?: string;
customData?: Record<string, string | number>;
};

export type ConversionEvent = {
event_name: EventName;
event_time: number;
event_source_url?: string;
custom_data?: Record<string, string | number>;
action_source: "website";
user_id?: string;
user_data: {
client_user_agent: string;
client_ip_address?: string;
fbc?: string;
};
};

export enum EventName {
PageView = "page-view",
Register = "register",
Login = "login",
Depth = "depth",
Engagement = "engagement",
}
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * as IAnalytics from "@/analytics";
export * as IContactRequest from "@/contactRequest";
export * as IConfirmationCode from "@/confirmationCode";
export * as ISessionEvent from "@/sessionEvent";
Expand Down
4 changes: 4 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@
"import": "./dist/esm/plan.js",
"require": "./dist/cjs/plan.js"
},
"./analytics": {
"import": "./dist/esm/analytics.js",
"require": "./dist/cjs/analytics.js"
},
"./filterQuery": {
"import": "./dist/esm/filterQuery/index.js",
"require": "./dist/cjs/filterQuery/index.js"
Expand Down
17 changes: 17 additions & 0 deletions packages/utils/src/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { dayjs } from "@/dayjs";

/**
* see docs for fbc: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
* @param fbclid parameter that is passed with the URL of an advertiser's website when a user
* clicks an ad on Facebook and/or Instagram. You get that from url search params
* @returns fbc which is a unique id for each user
*/
export function formatFbc(fbclid?: string | null) {
if (!fbclid) return undefined;

const host = window.location.hostname;
const hostIndex = host.startsWith("www.") ? 2 : 1;
const now = dayjs().valueOf();

return `fb.${hostIndex}.${now}.${fbclid}`;
}
3 changes: 3 additions & 0 deletions services/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ MESSENGER_PASSWORD='litespace'

LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secret

CONVERSION_API_URL="https://graph.facebook.com/v22.0/1149846573218428/events"
CONVERSION_API_ACCESS_TOKEN="<token>"
3 changes: 3 additions & 0 deletions services/server/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ MESSENGER_PASSWORD='litespace'

LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secret

CONVERSION_API_URL="https://graph.facebook.com/v22.0/1149846573218428/events"
CONVERSION_API_ACCESS_TOKEN="<token>"
5 changes: 4 additions & 1 deletion services/server/.env.test.ci
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ MESSENGER_USERNAME='litespace'
MESSENGER_PASSWORD='litespace'

LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secret
LIVEKIT_API_SECRET=secret

CONVERSION_API_URL="https://graph.facebook.com/v22.0/1149846573218428/events"
CONVERSION_API_ACCESS_TOKEN="<token>"
5 changes: 5 additions & 0 deletions services/server/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,8 @@ export const INTRO_VIDEO_EXPIRY_MONTHS = 3;
* max size in megabytes
*/
export const INTRO_VIDEO_MAX_FILE_SIZE = 400;

export const conversionApiConfig = {
apiUrl: zod.string().url().trim().parse(process.env.CONVERSION_API_URL),
token: zod.string().trim().parse(process.env.CONVERSION_API_ACCESS_TOKEN),
};
55 changes: 55 additions & 0 deletions services/server/src/handlers/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import safeRequest from "express-async-handler";
import { IAnalytics } from "@litespace/types";
import { Request, Response } from "express";
import zod from "zod";
import dayjs from "@/lib/dayjs";
import axios from "axios";
import { conversionApiConfig } from "@/constants";

// Pixel Docs:
// https://developers.facebook.com/docs/marketing-api/conversions-api/using-the-api/
// Parameters:
// https://developers.facebook.com/docs/marketing-api/conversions-api/parameters

const trackConversionEventPayload = zod.object({
eventName: zod.nativeEnum(IAnalytics.EventName),
userId: zod.string().optional(),
fbc: zod.string().optional(),
eventSourceUrl: zod.string().optional(),
customData: zod.object({}).optional(),
});

async function trackFacebookEvents(req: Request, res: Response) {
const body = trackConversionEventPayload.parse(req.body);
const ip = req.ip || req.socket.remoteAddress || "Unknown IP";

const event: IAnalytics.ConversionEvent = {
event_name: body.eventName,
user_data: {
client_user_agent: req.headers["user-agent"] || "Unknown",
client_ip_address: ip,
fbc: body.fbc,
},
event_time: dayjs.utc().unix(),
event_source_url: body.eventSourceUrl,
custom_data: body.customData,
action_source: "website",
};

await axios.post(
conversionApiConfig.apiUrl,
{ data: [event] },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${conversionApiConfig.token}`,
},
}
);

res.sendStatus(200);
}

export default {
trackFacebookEvents: safeRequest(trackFacebookEvents),
};
1 change: 1 addition & 0 deletions services/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ app.use(cors({ credentials: true, origin: isAllowedOrigin }));
app.use(json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(authMiddleware({ jwtSecret }));
app.use("/api/v1/analytics", routes.analytics);
app.use("/api/v1/auth", routes.auth);
app.use("/api/v1/contact-request", routes.contactRequest);
app.use("/api/v1/user", routes.user(context));
Expand Down
8 changes: 8 additions & 0 deletions services/server/src/routes/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Router } from "express";
import analytics from "@/handlers/analytics";

const router = Router();

router.post("/fb/track", analytics.trackFacebookEvents);

export default router;
2 changes: 2 additions & 0 deletions services/server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import confirmationCode from "@/routes/confirmationCode";
import report from "@/routes/report";
import demoSession from "@/routes/demoSession";
import introVideo from "@/routes/introVideo";
import analytics from "@/routes/analytics";

export default {
user,
Expand All @@ -42,6 +43,7 @@ export default {
fawry,
transaction,
subscription,
analytics,
confirmationCode,
report,
demoSession,
Expand Down
Loading