Telemetry improvements (#2935)

* next-collect initial setup

* naming changes

* WIP

* WIP: added cookie name

* telemetry update

* fixes

* telemetry eventTypes fix

* tmp jitsu config for testing

* tmp jitsu key update (for tests)

* deploy commit

* cookieName fixes

* telemetry credentials update

* NextCollect updated to latest canary; added TELEMETRY_KEY to config

* TELEMETRY_KEY fix

removed browser_user_agent field from event

* removed _middleware.ts for test deploy

* _middleware.ts restored

* next-collect version bump

* yarn.lock fix

* Added license consent property, set default telemetry endpoint

* Switched to stable version of next-collect; restored LicenseProvider accidentally deleted during merge

* Switched to stable version of next-collect; restored LicenseProvider accidentally deleted during merge

* Updated to latest version of next-collect

* - Updated to latest version of next-collect
- Few improvements in event collection: isTeamBooking for all events, page_url for all events
- Do not send second page event on re-render of /team/[slug]

* Revert booking confirmed tracking

* Applied prettier + fix lint

Co-authored-by: Art Sk <kirsan007@gmail.com>
This commit is contained in:
Vladimir KlimontovichandArt Sk authored and GitHub committed 2022-06-02 10:19:01 -06:00
1 parent 60d6c7be51
commit 456a4ba581
22 files changed
+186 -1344

No files matched your search

+2 -2
View File
@@ -53,8 +53,8 @@ NEXTAUTH_SECRET=
# Used for cross-domain cookie authentication
NEXTAUTH_COOKIE_DOMAIN=.example.com
# Remove this var if you don't want Cal to collect anonymous usage
NEXT_PUBLIC_TELEMETRY_KEY=js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r
# Remove those vars if you don't want Cal to collect anonymous usage
TELEMETRY_KEY=s2s.2pvs2bbpqq1zxna97wcml.3jdhd69y09dzku3x243ive
# ApiKey for cronjobs
CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0'
-9
View File
@@ -37,7 +37,6 @@ import classNames from "@lib/classNames";
import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
import useMeQuery from "@lib/hooks/useMeQuery";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
@@ -415,16 +414,8 @@ type LayoutProps = {
};
export default function Shell(props: LayoutProps) {
const router = useRouter();
const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) => {
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath));
});
}, [telemetry, router.asPath]);
const query = useMeQuery();
const user = query.data;
+1 -3
View File
@@ -40,9 +40,7 @@ export default function SAMLLogin(props: Props) {
event.preventDefault();
// track Google logins. Without personal data/payload
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
);
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
if (!props.hostedCal) {
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
@@ -78,9 +78,7 @@ export default function CancelBooking(props: Props) {
reason: cancellationReason,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
@@ -143,12 +143,13 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
useEffect(() => {
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
)
);
);
}
}, [telemetry]);
const changeDate = useCallback(
@@ -107,12 +107,13 @@ const BookingPage = ({
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
)
);
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -332,13 +333,10 @@ const BookingPage = ({
}
const bookEvent = (booking: BookingFormValues) => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
)
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
);
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
@@ -74,7 +74,7 @@ export default function SAMLConfiguration({
const rawMetadata = samlConfigRef.current.value;
// track Google logins. Without personal data/payload
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.samlConfig, collectPageParameters()));
telemetry.event(telemetryEventTypes.samlConfig, collectPageParameters());
mutation.mutate({
encodedRawMetadata: Buffer.from(rawMetadata).toString("base64"),
+8 -11
View File
@@ -7,7 +7,6 @@ import DynamicHelpscoutProvider from "@ee/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic";
import usePublicPage from "@lib/hooks/usePublicPage";
import { createTelemetryClient, TelemetryProvider } from "@lib/telemetry";
import { trpc } from "./trpc";
@@ -51,17 +50,15 @@ const AppProviders = (props: AppPropsWithChildren) => {
<CustomI18nextProvider {...props}>{props.children}</CustomI18nextProvider>
</SessionProvider>
);
const telemetryClient = useMemo(createTelemetryClient, []);
if (isPublicPage) {
return RemainingProviders;
}
return (
<TelemetryProvider value={telemetryClient}>
{isPublicPage ? (
RemainingProviders
) : (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
)}
</TelemetryProvider>
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
);
};
+62 -86
View File
@@ -1,17 +1,13 @@
import { jitsuClient, JitsuClient } from "@jitsu/sdk-js";
import React, { useContext } from "react";
import { NextApiRequest, NextApiResponse } from "next";
import { EventSinkOpts } from "next-collect";
import { useCollector } from "next-collect/client";
// it's ok to do this since we're importing only types which are harmless
// eslint-disable-next-line @next/next/no-server-import-in-page
import type { NextRequest, NextResponse } from "next/server";
declare global {
// eslint-disable-next-line no-var
var jitsu: JitsuClient | undefined;
}
/**
* Enumeration of all event types that are being sent
* to telemetry collection.
*/
export const telemetryEventTypes = {
pageView: "page_view",
apiCall: "api_call",
bookingConfirmed: "booking_confirmed",
bookingCancelled: "booking_cancelled",
importSubmitted: "import_submitted",
@@ -23,93 +19,73 @@ export const telemetryEventTypes = {
embedBookingConfirmed: "embed_booking_confirmed",
};
/**
* Telemetry client
*/
export type TelemetryClient = {
/**
* Use it as: withJitsu((jitsu) => {return jitsu.track()}). If telemetry is disabled, the callback will ignored
*
* ATTENTION: always return the value of jitsu.track() or id() call. Otherwise unhandled rejection can happen,
* which is handled in Next.js with a popup.
*/
withJitsu: (callback: (jitsu: JitsuClient) => void | Promise<void>) => void;
};
const emptyClient: TelemetryClient = {
withJitsu: () => {
// empty
},
};
function useTelemetry(): TelemetryClient {
return useContext(TelemetryContext);
}
function isLocalhost(host: string) {
return "localhost" === host || "127.0.0.1" === host;
}
/**
* Collects page parameters and makes sure no sensitive data made it to telemetry
* @param route current next.js route
*/
export function collectPageParameters(
route?: string,
extraData: Record<string, unknown> = {}
): Record<string, unknown> {
const host = document.location.hostname;
const maskedHost = isLocalhost(host) ? "localhost" : "masked";
//starts with ''
const host = document.location.host;
const docPath = route ?? "";
return {
page_url: route,
page_title: "",
source_ip: "",
doc_encoding: document.characterSet,
url: document.location.protocol + "//" + host + (docPath ?? ""),
doc_host: maskedHost,
doc_search: "",
doc_path: docPath,
referer: "",
...extraData,
};
}
function createTelemetryClient(): TelemetryClient {
if (process.env.NEXT_PUBLIC_TELEMETRY_KEY) {
return {
withJitsu: (callback) => {
if (!process.env.NEXT_PUBLIC_TELEMETRY_KEY) {
//telemetry is disabled
return;
}
if (!window) {
console.warn("Jitsu has been called during SSR, this scenario isn't supported yet");
return;
} else if (!window["jitsu"]) {
window["jitsu"] = jitsuClient({
log_level: "ERROR",
tracking_host: "https://t.calendso.com",
key: process.env.NEXT_PUBLIC_TELEMETRY_KEY,
cookie_name: "__clnds",
capture_3rd_party_cookies: false,
});
}
const res = callback(window["jitsu"]);
if (res && typeof res["catch"] === "function") {
res.catch((e) => {
console.debug("Unable to send telemetry event", e);
});
}
export const nextCollectBasicSettings: EventSinkOpts = {
drivers: [
process.env.TELEMETRY_KEY && {
type: "jitsu",
opts: {
key: process.env.TELEMETRY_KEY,
server: "https://t.calendso.com",
},
};
} else {
return emptyClient;
}
}
},
process.env.TELEMETRY_DEBUG && { type: "echo", opts: { disableColor: true } },
],
eventTypes: [
{ "*.ttf": null },
{ "*.webmanifest": null },
{ "*.json": null },
{ "*.svg": null },
{ "*.map": null },
{ "*.png": null },
{ "*.gif": null },
{ "/api/collect-events": null },
{ "/api*": null },
{ "/img*": null },
{ "/favicon*": null },
{ "/*": telemetryEventTypes.pageView },
],
};
const TelemetryContext = React.createContext<TelemetryClient>(emptyClient);
export const extendEventData = (
req: NextRequest | NextApiRequest,
res: NextResponse | NextApiResponse,
original: any
) => {
const onVercel =
typeof req.headers?.get === "function"
? !!req.headers.get("x-vercel-id")
: !!(req.headers as any)?.["x-vercel-id"];
const pageUrl = original?.page_url || (req as any)?.page?.name || undefined;
return {
title: "",
ipAddress: "",
queryString: "",
page_url: pageUrl,
licenseConsent: !!process.env.NEXT_PUBLIC_LICENSE_CONSENT,
isTeamBooking:
original?.isTeamBooking === undefined
? pageUrl?.includes("team/") || undefined
: original?.isTeamBooking,
referrer: "",
onVercel,
isAuthorized:
!!req.cookies["next-auth.session-token"] || !!req.cookies["__Secure-next-auth.session-token"],
utc_time: new Date().toISOString(),
};
};
const TelemetryProvider = TelemetryContext.Provider;
export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry };
export const useTelemetry = useCollector;
+1 -1
View File
@@ -41,7 +41,6 @@
"@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.8.9",
"@jitsu/sdk-js": "^2.2.4",
"@metamask/providers": "^8.1.1",
"@next-auth/prisma-adapter": "^1.0.3",
"@next/bundle-analyzer": "12.1.6",
@@ -81,6 +80,7 @@
"mime-types": "^2.1.35",
"next": "^12.1.6",
"next-auth": "^4.3.4",
"next-collect": "^0.1.0",
"next-i18next": "^11.0.0",
"next-mdx-remote": "^4.0.3",
"next-seo": "^4.26.0",
+6 -7
View File
@@ -122,13 +122,12 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
collectPageParameters("/[user]")
)
);
}, [telemetry]);
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
}
}, [telemetry, router.asPath]);
return (
<>
<Theme />
+20 -16
View File
@@ -1,3 +1,4 @@
import { EventCollectionProvider } from "next-collect/client";
import { DefaultSeo } from "next-seo";
import Head from "next/head";
import superjson from "superjson";
@@ -30,23 +31,26 @@ function MyApp(props: AppProps) {
pageStatus = "500";
}
return (
<ContractsProvider>
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Head>
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{Component.requiresLicense ? (
<LicenseRequired>
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<ContractsProvider>
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Head>
<script
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{Component.requiresLicense ? (
<LicenseRequired>
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
)}
</AppProviders>
</ContractsProvider>
)}
</AppProviders>
</ContractsProvider>
</EventCollectionProvider>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { collectEvents } from "next-collect/server";
import { extendEventData, nextCollectBasicSettings } from "@lib/telemetry";
export default collectEvents({
...nextCollectBasicSettings,
cookieName: "__clnds",
extend: extendEventData,
});
+9
View File
@@ -0,0 +1,9 @@
import { nextEventsCollectApi } from "next-collect/server";
import { extendEventData, nextCollectBasicSettings } from "@lib/telemetry";
export default nextEventsCollectApi({
...nextCollectBasicSettings,
cookieName: "__clnds",
extend: extendEventData,
});
+2 -4
View File
@@ -107,7 +107,7 @@ export default function Login({
className="space-y-6"
handleSubmit={async (values) => {
setErrorMessage(null);
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.login, collectPageParameters()));
telemetry.event(telemetryEventTypes.login, collectPageParameters());
const res = await signIn<"credentials">("credentials", {
...values,
callbackUrl,
@@ -177,9 +177,7 @@ export default function Login({
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
);
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
+1 -3
View File
@@ -114,9 +114,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
reason: cancellationReason,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
+4 -6
View File
@@ -322,12 +322,10 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
className="flex"
onSubmit={formMethods.handleSubmit(async (values) => {
// track the number of imports. Without personal data/payload
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.importSubmitted, {
...collectPageParameters(),
selectedImport,
})
);
telemetry.event(telemetryEventTypes.importSubmitted, {
...collectPageParameters(),
selectedImport,
});
setSubmitting(true);
const response = await fetch(`/api/import/${selectedImport}`, {
method: "POST",
+4 -6
View File
@@ -179,12 +179,10 @@ export default function Success(props: SuccessProps) {
const isCancelled = status === "CANCELLED" || status === "REJECTED";
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
collectPageParameters("/success")
)
);
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/success"));
}
}, [telemetry]);
useEffect(() => {
+7 -8
View File
@@ -3,6 +3,7 @@ import { UserPlan } from "@prisma/client";
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
@@ -32,17 +33,15 @@ function TeamPage({ team }: TeamPageProps) {
useExposePlanGlobally("PRO");
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
const router = useRouter();
useEffect(() => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
telemetryEventTypes.pageView,
collectPageParameters("/team/[slug]", {
isTeamBooking: true,
})
)
telemetry.event(
telemetryEventTypes.pageView,
collectPageParameters("/team/[slug]", { isTeamBooking: true })
);
}, [telemetry]);
}, [telemetry, router.asPath]);
const eventTypes = (
<ul className="space-y-3">
{team.eventTypes.map((type) => (
+1 -1
View File
@@ -22,7 +22,7 @@ declare namespace NodeJS {
/** @deprecated use `NEXT_PUBLIC_WEBSITE_URL` */
readonly NEXT_PUBLIC_APP_URL: string | undefined;
readonly NEXTAUTH_SECRET: string | undefined;
readonly NEXT_PUBLIC_TELEMETRY_KEY: string | undefined;
readonly TELEMETRY_KEY: string | undefined;
readonly MS_GRAPH_CLIENT_ID: string | undefined;
readonly MS_GRAPH_CLIENT_SECRET: string | undefined;
readonly ZOOM_CLIENT_ID: string | undefined;
+1 -1
View File
@@ -48,7 +48,6 @@
"$NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE",
"$NEXT_PUBLIC_STRIPE_PRO_PLAN_PRODUCT",
"$NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
"$NEXT_PUBLIC_TELEMETRY_KEY",
"$NEXT_PUBLIC_WEBAPP_URL",
"$NEXT_PUBLIC_WEBSITE_URL",
"$NEXT_PUBLIC_ZENDESK_KEY",
@@ -66,6 +65,7 @@
"$TANDEM_BASE_URL",
"$TANDEM_CLIENT_ID",
"$TANDEM_CLIENT_SECRET",
"$TELEMETRY_KEY",
"$ZOOM_CLIENT_ID",
"$ZOOM_CLIENT_SECRET"
],
+31 -1160
View File
File diff suppressed because it is too large. Load diff