feat: Dub App integration (#21321)
Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
co-authored by
Peer Richelsen
sean-brydon
parent
324ac691eb
commit
357c82129c
@@ -0,0 +1,51 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { AnalyticsService, AnalyticsServiceClass } from "@calcom/types/AnalyticsService";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import appStore from "..";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["AnalyticsManager"] });
|
||||
|
||||
interface AnalyticsApp {
|
||||
lib: {
|
||||
AnalyticsService: AnalyticsServiceClass;
|
||||
};
|
||||
}
|
||||
|
||||
const isAnalyticsService = (x: unknown): x is AnalyticsApp =>
|
||||
!!x &&
|
||||
typeof x === "object" &&
|
||||
"lib" in x &&
|
||||
typeof x.lib === "object" &&
|
||||
!!x.lib &&
|
||||
"AnalyticsService" in x.lib;
|
||||
|
||||
export const getAnalyticsService = async ({
|
||||
credential,
|
||||
}: {
|
||||
credential: CredentialPayload;
|
||||
}): Promise<AnalyticsService | null> => {
|
||||
if (!credential || !credential.key) return null;
|
||||
const { type: analyticsType } = credential;
|
||||
|
||||
const analyticsName = analyticsType.split("_")[0];
|
||||
|
||||
const analyticsAppImportFn = appStore[analyticsName as keyof typeof appStore];
|
||||
|
||||
if (!analyticsAppImportFn) {
|
||||
log.warn(`analytics app not implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const analyticsApp = await analyticsAppImportFn();
|
||||
|
||||
if (!isAnalyticsService(analyticsApp)) {
|
||||
log.warn(`Analytics is not implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const AnalyticsService = analyticsApp.lib.AnalyticsService;
|
||||
log.info("Got analyticsApp", AnalyticsService);
|
||||
|
||||
return new AnalyticsService(credential);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import deel_config_json from "./deel/config.json";
|
||||
import demodesk_config_json from "./demodesk/config.json";
|
||||
import dialpad_config_json from "./dialpad/config.json";
|
||||
import discord_config_json from "./discord/config.json";
|
||||
import dub_config_json from "./dub/config.json";
|
||||
import eightxeight_config_json from "./eightxeight/config.json";
|
||||
import element_call_config_json from "./element-call/config.json";
|
||||
import elevenlabs_config_json from "./elevenlabs/config.json";
|
||||
@@ -127,6 +128,7 @@ export const appStoreMetadata = {
|
||||
demodesk: demodesk_config_json,
|
||||
dialpad: dialpad_config_json,
|
||||
discord: discord_config_json,
|
||||
dub: dub_config_json,
|
||||
eightxeight: eightxeight_config_json,
|
||||
"element-call": element_call_config_json,
|
||||
elevenlabs: elevenlabs_config_json,
|
||||
|
||||
@@ -21,6 +21,7 @@ export const apiHandlers = {
|
||||
demodesk: import("./demodesk/api"),
|
||||
dialpad: import("./dialpad/api"),
|
||||
discord: import("./discord/api"),
|
||||
dub: import("./dub/api"),
|
||||
eightxeight: import("./eightxeight/api"),
|
||||
"element-call": import("./element-call/api"),
|
||||
elevenlabs: import("./elevenlabs/api"),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpeg
|
||||
- 2.jpeg
|
||||
- 3.jpeg
|
||||
- 4.jpeg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
## How to Get Started
|
||||
|
||||
1. **Log in** to your [Dub.co](https://dub.co) account by installing the app.
|
||||
2. **Set up the [Client SDK](https://dub.co/docs/sdks/client-side/introduction)** on your website:
|
||||
- Add `app.cal.com` to your [Outbound Domains](https://dub.co/docs/sdks/client-side/features/cross-domain-tracking).
|
||||
3. Once a **booking event** occurs, your tracking data will be captured automatically.
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler } from "@calcom/lib/server/defaultHandler";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
|
||||
import getParsedAppKeysFromSlug from "../../_utils/getParsedAppKeysFromSlug";
|
||||
import { dubAppKeysSchema, scopeString } from "../lib/utils";
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const loggedInUser = req.session?.user;
|
||||
|
||||
if (!loggedInUser) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
// Ideally this should never happen, as email is there in session user but typings aren't accurate it seems
|
||||
// TODO: So, confirm and later fix the typings
|
||||
if (!loggedInUser.email) {
|
||||
throw new HttpError({ statusCode: 400, message: "Session user must have an email" });
|
||||
}
|
||||
|
||||
const { client_id, redirect_uris } = await getParsedAppKeysFromSlug("dub", dubAppKeysSchema);
|
||||
|
||||
const url = new URL("https://app.dub.co/oauth/authorize");
|
||||
url.searchParams.append("client_id", client_id);
|
||||
url.searchParams.append("redirect_uri", redirect_uris);
|
||||
url.searchParams.append("response_type", "code");
|
||||
url.searchParams.append("scope", scopeString);
|
||||
const oauthUrl = url.toString();
|
||||
|
||||
return res.status(200).json({ url: oauthUrl });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import getParsedAppKeysFromSlug from "../../_utils/getParsedAppKeysFromSlug";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import { dubAppKeysSchema } from "../lib/utils";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
if (typeof code !== "string") {
|
||||
if (state?.onErrorReturnTo || state?.returnTo) {
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state.onErrorReturnTo) ??
|
||||
getSafeRedirectUrl(state?.returnTo) ??
|
||||
`${WEBAPP_URL}/apps/installed`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new HttpError({ statusCode: 400, message: "`code` must be a string" });
|
||||
}
|
||||
|
||||
if (!req.session?.user?.id) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const { client_id, redirect_uris, client_secret } = await getParsedAppKeysFromSlug("dub", dubAppKeysSchema);
|
||||
|
||||
const codeExchangeUrl = `https://api.dub.co/oauth/token`;
|
||||
|
||||
const result = await fetch(codeExchangeUrl, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id,
|
||||
redirect_uri: redirect_uris,
|
||||
client_secret,
|
||||
grant_type: "authorization_code",
|
||||
}).toString(),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status !== 200) {
|
||||
let errorMessage = "Something wrong with Dub Api";
|
||||
try {
|
||||
const responseBody = await result.json();
|
||||
if (typeof responseBody?.error?.message === "string") {
|
||||
errorMessage = responseBody.error.message;
|
||||
}
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
return res.status(400).json({ message: errorMessage });
|
||||
}
|
||||
|
||||
const responseBody = await result.json();
|
||||
|
||||
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
|
||||
delete responseBody.expires_in;
|
||||
|
||||
await createOAuthAppCredential({ appId: "dub", type: "dub" }, responseBody, req);
|
||||
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "analytics", slug: "dub" })
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Dub",
|
||||
"slug": "dub",
|
||||
"type": "dub_analytics",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://dub.co",
|
||||
"variant": "analytics",
|
||||
"categories": ["analytics"],
|
||||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"description": "Dub is the modern link attribution platform for you to create short links, track conversion analytics, and run affiliate programs.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Dub } from "dub-package";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
import type { AnalyticsService, SendEventProps } from "@calcom/types/AnalyticsService";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import type { DubOAuthToken } from "./type";
|
||||
|
||||
export default class DubService implements AnalyticsService {
|
||||
private dubClient?: Dub;
|
||||
private client_id = "";
|
||||
private client_secret = "";
|
||||
private log = logger.getSubLogger({ prefix: ["[[lib]] dub"] });
|
||||
private credential: CredentialPayload;
|
||||
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.credential = credential;
|
||||
this.client_id = "";
|
||||
this.client_secret = "";
|
||||
}
|
||||
|
||||
private async initClient() {
|
||||
const appKeys = await getAppKeysFromSlug("dub");
|
||||
|
||||
const { client_id, client_secret } = appKeys;
|
||||
|
||||
if (!client_id || !client_secret) {
|
||||
this.log.error("Dub.co app keys missing!");
|
||||
return;
|
||||
}
|
||||
|
||||
this.client_id = client_id as string;
|
||||
this.client_secret = client_secret as string;
|
||||
|
||||
let token = this.credential.key as unknown as DubOAuthToken | undefined;
|
||||
|
||||
if (!token) return;
|
||||
|
||||
const isTokenExpired = (token: DubOAuthToken) => {
|
||||
if (!token || !token.access_token) return true;
|
||||
if (token.expiry_date) {
|
||||
return token.expiry_date < Date.now();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (isTokenExpired(token)) {
|
||||
token = await this.refreshAccessToken(token.refresh_token);
|
||||
if (!token) return;
|
||||
}
|
||||
|
||||
this.dubClient = new Dub({ token: token.access_token });
|
||||
}
|
||||
|
||||
private async refreshAccessToken(refreshToken: string): Promise<DubOAuthToken | undefined> {
|
||||
try {
|
||||
if (!refreshToken) return;
|
||||
const newToken: DubOAuthToken = await refreshOAuthTokens(
|
||||
async () => {
|
||||
const response = await fetch(`https://api.dub.co/oauth/token`, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
client_id: this.client_id,
|
||||
client_secret: this.client_secret,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
}).toString(),
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
"dub",
|
||||
this.credential.userId
|
||||
);
|
||||
|
||||
newToken.expiry_date = Date.now() + newToken.expires_in * 1000;
|
||||
|
||||
await CredentialRepository.updateCredentialById({
|
||||
id: this.credential.id,
|
||||
data: { key: newToken as any },
|
||||
});
|
||||
|
||||
return newToken;
|
||||
} catch (err) {
|
||||
this.log.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async sendEvent({ name, email, eventName, id, externalId }: SendEventProps): Promise<void> {
|
||||
await this.initClient();
|
||||
|
||||
if (!this.dubClient) return;
|
||||
|
||||
await this.dubClient.track.lead({
|
||||
clickId: id,
|
||||
customerName: name,
|
||||
customerEmail: email,
|
||||
externalId: externalId ?? email,
|
||||
eventName: eventName ?? "Cal.com lead",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AnalyticsService } from "./AnalyticsService";
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface DubOAuthToken {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: "Bearer";
|
||||
expires_in: number;
|
||||
expiry_date?: number;
|
||||
scope: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const dubAppKeysSchema = z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string(),
|
||||
redirect_uris: z.string(),
|
||||
});
|
||||
|
||||
const dubScope = ["workspaces.read"];
|
||||
|
||||
export const scopeString = dubScope.join(",");
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/dub",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"dub-package": "npm:dub@^0.61.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Dub is the modern link attribution platform for you to create short links, track conversion analytics, and run affiliate programs."
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -4,6 +4,7 @@ const appStore = {
|
||||
caldavcalendar: createCachedImport(() => import("./caldavcalendar")),
|
||||
closecom: createCachedImport(() => import("./closecom")),
|
||||
dailyvideo: createCachedImport(() => import("./dailyvideo")),
|
||||
dub: createCachedImport(() => import("./dub")),
|
||||
googlecalendar: createCachedImport(() => import("./googlecalendar")),
|
||||
googlevideo: createCachedImport(() => import("./googlevideo")),
|
||||
hubspot: createCachedImport(() => import("./hubspot")),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dub } from "dub";
|
||||
import { Dub } from "dub-package";
|
||||
|
||||
export const dub = new Dub({
|
||||
token: process.env.DUB_API_KEY,
|
||||
|
||||
@@ -1053,7 +1053,7 @@ export const getOptions = ({
|
||||
dub.track.lead({
|
||||
clickId,
|
||||
eventName: "Sign Up",
|
||||
customerId: user.id.toString(),
|
||||
externalId: user.id.toString(),
|
||||
customerName: user.name,
|
||||
customerEmail: user.email,
|
||||
customerAvatar: user.image,
|
||||
|
||||
@@ -60,6 +60,7 @@ export const mapBookingToMutationInput = ({
|
||||
const _isDryRun = isBookingDryRun(searchParams);
|
||||
const _cacheParam = searchParams?.get("cal.cache");
|
||||
const _shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined;
|
||||
const dub_id = searchParams?.get("dub_id");
|
||||
|
||||
return {
|
||||
...values,
|
||||
@@ -91,6 +92,7 @@ export const mapBookingToMutationInput = ({
|
||||
reroutingFormResponses: reroutingFormResponses ? JSON.parse(reroutingFormResponses) : undefined,
|
||||
_isDryRun,
|
||||
_shouldServeCache,
|
||||
dub_id,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import EventManager from "@calcom/lib/EventManager";
|
||||
import { handleAnalyticsEvents } from "@calcom/lib/analyticsManager/handleAnalyticsEvents";
|
||||
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import {
|
||||
@@ -2160,6 +2161,18 @@ async function handler(
|
||||
loggerWithEventDetails.error("Error while scheduling no show triggers", JSON.stringify({ error }));
|
||||
}
|
||||
|
||||
if (!isDryRun) {
|
||||
await handleAnalyticsEvents({
|
||||
credentials: allCredentials,
|
||||
rawBookingData,
|
||||
bookingInfo: {
|
||||
name: fullName,
|
||||
email: bookerEmail,
|
||||
eventName: "Cal.com lead",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Refactor better so this booking object is not passed
|
||||
// all around and instead the individual fields are sent as args.
|
||||
const bookingResponse = {
|
||||
|
||||
@@ -20,6 +20,7 @@ type TaskPayloads = {
|
||||
createCRMEvent: z.infer<typeof import("./tasks/crm/schema").createCRMEventSchema>;
|
||||
sendWorkflowEmails: z.infer<typeof import("./tasks/sendWorkflowEmails").ZSendWorkflowEmailsSchema>;
|
||||
scanWorkflowBody: z.infer<typeof import("./tasks/scanWorkflowBody").scanWorkflowBodySchema>;
|
||||
sendAnalyticsEvent: z.infer<typeof import("./tasks/analytics/schema").sendAnalyticsEventSchema>;
|
||||
};
|
||||
export type TaskTypes = keyof TaskPayloads;
|
||||
export type TaskHandler = (payload: string) => Promise<void>;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import z from "zod";
|
||||
|
||||
export const sendAnalyticsEventSchema = z.object({
|
||||
credentialId: z.number(),
|
||||
info: z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
id: z.string(),
|
||||
eventName: z.string(),
|
||||
externalId: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import AnalyticsManager from "@calcom/lib/analyticsManager/analyticsManager";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { CredentialRepository } from "@calcom/lib/server/repository/credential";
|
||||
|
||||
import { sendAnalyticsEventSchema } from "./schema";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: [`[[tasker] sendAnalyticsEvent]`] });
|
||||
|
||||
export async function sendAnalyticsEvent(payload: string): Promise<void> {
|
||||
try {
|
||||
const parsedPayload = sendAnalyticsEventSchema.safeParse(JSON.parse(payload));
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error(`malformed payload in sendAnalyticsEvent: ${parsedPayload.error}`);
|
||||
}
|
||||
|
||||
const { credentialId, info } = parsedPayload.data;
|
||||
const credential = await CredentialRepository.findFirstByIdWithKeyAndUser({ id: credentialId });
|
||||
|
||||
if (!credential) {
|
||||
throw new Error("Invalid credential");
|
||||
}
|
||||
const manager = new AnalyticsManager(credential);
|
||||
|
||||
if (!manager) return;
|
||||
|
||||
await manager.sendEvent(info);
|
||||
} catch (err) {
|
||||
log.error(
|
||||
`[Will retry] Error creating analytics event: error: ${safeStringify(err)} payload: ${safeStringify({
|
||||
payload,
|
||||
})}`
|
||||
);
|
||||
// Intentional rethrow to trigger retry
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ const tasks: Record<TaskTypes, () => Promise<TaskHandler>> = {
|
||||
createCRMEvent: () => import("./crm/createCRMEvent").then((module) => module.createCRMEvent),
|
||||
sendWorkflowEmails: () => import("./sendWorkflowEmails").then((module) => module.sendWorkflowEmails),
|
||||
scanWorkflowBody: () => import("./scanWorkflowBody").then((module) => module.scanWorkflowBody),
|
||||
sendAnalyticsEvent: () =>
|
||||
import("./analytics/sendAnalyticsEvent").then((module) => module.sendAnalyticsEvent),
|
||||
};
|
||||
|
||||
export const tasksConfig = {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getAnalyticsService } from "@calcom/app-store/_utils/getAnalytics";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { AnalyticsService, SendEventProps } from "@calcom/types/AnalyticsService";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["AnalyticsManager"] });
|
||||
export default class AnalyticsManager {
|
||||
analyticsService: AnalyticsService | null | undefined = null;
|
||||
credential: CredentialPayload;
|
||||
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
private async getAnalyticsService(credential: CredentialPayload) {
|
||||
if (this.analyticsService) return this.analyticsService;
|
||||
const analyticsService = await getAnalyticsService({ credential });
|
||||
this.analyticsService = analyticsService;
|
||||
|
||||
if (!this.analyticsService) {
|
||||
log.error("Analytics service initialization failed");
|
||||
}
|
||||
|
||||
return analyticsService;
|
||||
}
|
||||
|
||||
public async sendEvent(props: SendEventProps) {
|
||||
const analyticsService = await this.getAnalyticsService(this.credential);
|
||||
if (!analyticsService) return;
|
||||
|
||||
return await analyticsService.sendEvent(props);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import tasker from "@calcom/features/tasker";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
|
||||
interface HandleAnalyticsEventsProps {
|
||||
credentials: CredentialForCalendarService[];
|
||||
rawBookingData: Record<string, any>;
|
||||
bookingInfo: {
|
||||
email: string;
|
||||
name: string;
|
||||
eventName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const handleAnalyticsEvents = async ({
|
||||
credentials,
|
||||
rawBookingData,
|
||||
bookingInfo,
|
||||
}: HandleAnalyticsEventsProps) => {
|
||||
const { dub_id } = await rawBookingData;
|
||||
|
||||
if (!dub_id || typeof dub_id !== "string") return;
|
||||
|
||||
const dubCredential = credentials.find((cred) => cred.appId === "dub");
|
||||
|
||||
if (!dubCredential) return;
|
||||
|
||||
try {
|
||||
await tasker.create("sendAnalyticsEvent", {
|
||||
credentialId: dubCredential.id,
|
||||
info: {
|
||||
id: dub_id,
|
||||
...bookingInfo,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error sending dub lead: ", err);
|
||||
}
|
||||
};
|
||||
@@ -12,6 +12,14 @@ type CredentialCreateInput = {
|
||||
delegationCredentialId?: string | null;
|
||||
};
|
||||
|
||||
type CredentialUpdateInput = {
|
||||
type?: string;
|
||||
key?: any;
|
||||
userId?: number;
|
||||
appId?: string;
|
||||
delegationCredentialId?: string | null;
|
||||
};
|
||||
|
||||
export class CredentialRepository {
|
||||
static async create(data: CredentialCreateInput) {
|
||||
const credential = await prisma.credential.create({ data: { ...data } });
|
||||
@@ -64,6 +72,13 @@ export class CredentialRepository {
|
||||
await prisma.credential.delete({ where: { id } });
|
||||
}
|
||||
|
||||
static async updateCredentialById({ id, data }: { id: number; data: CredentialUpdateInput }) {
|
||||
await prisma.credential.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
static async deleteAllByDelegationCredentialId({
|
||||
delegationCredentialId,
|
||||
}: {
|
||||
|
||||
@@ -46,6 +46,7 @@ export const bookingCreateBodySchema = z.object({
|
||||
utm_term: z.string().optional(),
|
||||
utm_content: z.string().optional(),
|
||||
}).optional(),
|
||||
dub_id: z.string().nullish()
|
||||
});
|
||||
|
||||
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
|
||||
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
export interface SendEventProps {
|
||||
name: string;
|
||||
email: string;
|
||||
id: string;
|
||||
eventName: string;
|
||||
externalId?: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsService {
|
||||
sendEvent(props: SendEventProps): Promise<void>;
|
||||
}
|
||||
|
||||
export type AnalyticsServiceClass = Class<AnalyticsService>;
|
||||
Reference in New Issue
Block a user